From d93c6b049be8f2597f196e02752e4f15c282eebb Mon Sep 17 00:00:00 2001 From: Wickedness Date: Thu, 18 Jun 2026 00:39:03 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B0=95=ED=99=94:=20=ED=94=84=EB=A1=AC?= =?UTF-8?q?=ED=94=84=ED=8A=B8=20=EC=B9=B4=EB=A9=94=EB=9D=BC/=ED=8F=AC?= =?UTF-8?q?=EC=A6=88=20=EB=8B=A4=EC=96=91=EC=84=B1=20=EA=B7=9C=EC=B9=99=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/export-card-assets.mjs | 90 +++++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 11 deletions(-) diff --git a/tools/export-card-assets.mjs b/tools/export-card-assets.mjs index f471379..a70a5d3 100644 --- a/tools/export-card-assets.mjs +++ b/tools/export-card-assets.mjs @@ -331,15 +331,15 @@ const CAMERA_VARIANTS = { const CAMERA_ORIENTATION_RULES = { normal: - "Use one angle from side/rear/low/high per render. Low-angle and high-angle shots should be used at least 40% of the time across the deck.", + "Use only side/rear/low/high framing. Never use a pure front-on head-on lock. Across one character's 5 stages, make sure at least one low-angle and one high-angle shot appear.", "light-damage": - "Prioritize low-angle, rear, side, or high-angle framing. Frontal reads are discouraged unless the face identity would otherwise be lost.", + "Prioritize low-angle, rear, side, and high-angle framing. Frontal reads are disallowed unless strict identity continuity would otherwise fail.", "medium-damage": - "Use energetic side/rear/low/high framing; force low-angle and high-angle alternation across nearby cards.", + "Use energetic side/rear/low/high framing; force low-angle and high-angle alternation so adjacent stages do not share the same vertical framing.", "heavy-damage": - "Prioritize low-angle, side, high-angle, and rear framing to emphasize impact chaos; high/low angles must dominate this stage.", + "Prioritize low-angle, side, high-angle, and rear framing to emphasize impact chaos; high/low angles should dominate this stage.", defeated: - "Prioritize side/back/low/high framing for collapse and pain continuity; avoid frontal composition unless continuity breaks.", + "Prioritize side/rear/back/low/high framing for collapse and pain continuity; frontal composition only if face continuity cannot be preserved.", }; const POSE_ORIENTATION_VARIANTS = { @@ -581,10 +581,37 @@ function pickDiverseOption(list, seed, stage, step) { return list[index]; } +function normalizeToDirectionDistance(length, from, to) { + return (to - from + length) % length; +} + +function firstIndexForOrientation(plan, wanted) { + for (let i = 0; i < plan.length; i += 1) { + if (normalizeOrientation(plan[i]) === wanted) { + return i; + } + } + return -1; +} + +function nextDifferentIndex(plan, startIndex, bannedOrientation) { + for (let i = 1; i <= plan.length; i += 1) { + const index = (startIndex + i) % plan.length; + const candidate = normalizeOrientation(plan[index]); + if (candidate !== bannedOrientation && candidate !== "front") { + return index; + } + } + return startIndex; +} + function normalizeOrientation(orientation) { return orientation === "front" ? "side" : orientation; } +const cameraHistoryByCharacter = new Map(); +const poseHistoryByCharacter = new Map(); + function pickCameraForStage(spec, character, stage) { const orientationPlan = CAMERA_ORIENTATION_PLAN[spec.label] ?? CAMERA_ORIENTATION_PLAN.normal; const orientationSeed = charSeed(`${character.id}-${character.scene}-${spec.label}`); @@ -598,10 +625,36 @@ function pickCameraForStage(spec, character, stage) { } resolvedIndex = (resolvedIndex + 1) % orientationPlan.length; } - const orientation = normalizeOrientation(orientationPlan[resolvedIndex]); + const historyKey = character.id; + const history = cameraHistoryByCharacter.get(historyKey) ?? []; + const needLow = stage >= 3 && !history.includes("low"); + const needHigh = stage >= 4 && !history.includes("high"); + if (needLow) { + const lowIndex = firstIndexForOrientation(orientationPlan, "low"); + if (lowIndex !== -1) { + resolvedIndex = (resolvedIndex + normalizeToDirectionDistance(orientationPlan.length, resolvedIndex, lowIndex)) % orientationPlan.length; + } + } + if (needHigh) { + const highIndex = firstIndexForOrientation(orientationPlan, "high"); + if (highIndex !== -1) { + resolvedIndex = (resolvedIndex + normalizeToDirectionDistance(orientationPlan.length, resolvedIndex, highIndex)) % orientationPlan.length; + } + } + if (history.length > 0) { + resolvedIndex = nextDifferentIndex(orientationPlan, resolvedIndex, history[history.length - 1]); + } + + const finalOrientation = normalizeOrientation(orientationPlan[resolvedIndex]); + history.push(finalOrientation); + if (history.length > 8) { + history.shift(); + } + cameraHistoryByCharacter.set(historyKey, history); + const stagePool = - (CAMERA_VARIANTS[spec.label] ?? CAMERA_VARIANTS.normal)[orientation] ?? - CAMERA_VARIANTS.normal[orientation] ?? + (CAMERA_VARIANTS[spec.label] ?? CAMERA_VARIANTS.normal)[finalOrientation] ?? + CAMERA_VARIANTS.normal[finalOrientation] ?? CAMERA_VARIANTS.normal.side; const phrase = pickDiverseOption( stagePool, @@ -609,7 +662,7 @@ function pickCameraForStage(spec, character, stage) { stage, 17, ); - const angleAddonPool = LOW_HIGH_CAMERA_PHRASES[orientation]; + const angleAddonPool = LOW_HIGH_CAMERA_PHRASES[finalOrientation]; const extraPhrase = angleAddonPool ? pickDiverseOption( angleAddonPool, @@ -620,7 +673,7 @@ function pickCameraForStage(spec, character, stage) { : null; const anglePhrase = extraPhrase === null ? phrase : `${phrase} ${extraPhrase}`; return { - orientation, + orientation: finalOrientation, phrase: anglePhrase, }; } @@ -633,12 +686,24 @@ function pickPoseModifier(spec, character, stage, orientation) { POSE_ORIENTATION_VARIANTS.normal?.[normalizedOrientation] ?? []; const posePool = orientationBonus.concat(list); - return pickDiverseOption( + const pick = pickDiverseOption( posePool, charSeed(`${character.id}-${character.pose}-${character.weapon}-${orientation}`), stage, 19, ); + const historyKey = character.id; + const history = poseHistoryByCharacter.get(historyKey) ?? []; + let selected = pick; + if (history.length > 0 && selected === history[history.length - 1]) { + selected = posePool[(posePool.indexOf(pick) + 1) % posePool.length]; + } + history.push(selected); + if (history.length > 12) { + history.shift(); + } + poseHistoryByCharacter.set(historyKey, history); + return selected; } function promptFor(character, stage) { @@ -659,6 +724,8 @@ function promptFor(character, stage) { const camera = pickCameraForStage(spec, character, stage); const angleControl = "Angle strictness: this render must use side/rear/low/high framing first; direct frontal composition is disallowed unless the model absolutely cannot preserve face identity."; + const angleConstraint = + "Do not output front-facing framing for default action shots. Use side/rear/low/high angles as primary composition, and rotate the view family between shots so the same character does not repeat the exact same camera axis."; const poseModifier = pickPoseModifier(spec, character, stage, camera.orientation); return [ "Use case: stylized-concept", @@ -674,6 +741,7 @@ function promptFor(character, stage) { `Facial expression: ${spec.expression}.`, `Camera/view variation: ${camera.orientation} angle. Composition directive: ${camera.phrase}.`, angleControl, + angleConstraint, "Across the character's five damage stages, rotate between side, rear, low-angle, and high-angle views as the primary framing, and only use frontal composition when the identity cannot be preserved.", `Camera constraint: ${CAMERA_ORIENTATION_RULES[spec.label] ?? CAMERA_ORIENTATION_RULES.normal}`, `Adult visual appeal: ${character.appealFocus}; ${adultAppeal}.`,