diff --git a/assets/cards/README.md b/assets/cards/README.md index dacf96c..ee8885a 100644 --- a/assets/cards/README.md +++ b/assets/cards/README.md @@ -47,7 +47,7 @@ The game art direction is adult-oriented multi-world pinup: - Medium through defeated stages may show clear combat-pain reactions from abdominal hits: gasping open mouth, hunched posture, labored breathing, tears, and small saliva/drool details. These must read as combat injury reactions, not sexual acts or fetish framing. - High-damage stages should make the combat feel intense through missing or destroyed hand, foot, shoulder, abdomen, hip, and torso armor/clothing. Strong adult damaged-costume presentation should be used wherever possible, and the composition should be built to show it clearly. - Adult pinup body language is strongly encouraged when it remains framed as battle damage, defeat, or damaged-costume card art. Use arched backs, twisted torsos, hip-forward stances, side-back views, exposed-leg kneeling, strained chest/shoulder posture, damaged-costume torso silhouettes, and defeated poses that emphasize mature curves. Do not depict intimate acts, penetration, minors, gore, or explicit closeups. -- Do not keep every card as a frontal shot. Use camera variety across stages: side and low/high combinations for baseline, twisted side or side-back for abdomen impacts, and rear/low/high angles for heavy damage and defeated art when it helps show shoulders, back, cloak, and armor destruction. +- Do not keep every card as a frontal shot. Use camera variety across stages: side and low/high combinations for baseline, twisted side or side-back for abdomen impacts, and rear/low/high angles for heavy damage and defeated art when it helps show shoulders, back, cloak, and armor destruction. Force at least one low-angle and one high-angle view in every 5-stage damage set, and avoid two consecutive front-facing reads by default. - For each character, force low-angle or high-angle framing somewhere in every consecutive damage-set. Stages should not read as a mirrored front-facing sequence even when same movement language is used. All damage-state art must use adult characters only. Intimate acts, penetration, childlike characters, and gore are out of scope; adult-only damaged-costume presentation, adult pinup posing, mature body language, and visible non-sensitive body areas are allowed and encouraged when framed as battle damage, defeat, or damaged-costume card art. Do not make the roster feel like a teen-safe or modest game; high-damage cards should read unmistakably as adult-only card art while remaining non-explicit. diff --git a/tools/export-card-assets.mjs b/tools/export-card-assets.mjs index a70a5d3..301aa1f 100644 --- a/tools/export-card-assets.mjs +++ b/tools/export-card-assets.mjs @@ -193,11 +193,6 @@ const CAMERA_ORIENTATION_PLAN = { const CAMERA_VARIANTS = { normal: { - front: [ - "low side-three-quarter composition with a shoulder twist that keeps the body angled", - "three-quarter profile composition with one foot forward and hips leading the movement", - "slanted side-front composition where the face stays readable through a short turn", - ], side: [ "strong profile side shot with one step forward and hips leading the movement", "45-degree profile turn with the leading shoulder pushed toward camera", @@ -220,11 +215,6 @@ const CAMERA_VARIANTS = { ], }, "light-damage": { - front: [ - "front-canted three-quarter read with shoulders turned against impact and one side stepping in", - "low-front slant with torso dropping into counter step and side-shifted hips", - "eye-level angled guard frame that keeps the strike angle visible in the shoulder and waist", - ], side: [ "side pivot camera with shoulder turn and counter-step body shift", "off-center side-frame with one foot forward and hips leading", @@ -247,11 +237,6 @@ const CAMERA_VARIANTS = { ], }, "medium-damage": { - front: [ - "front-angled three-quarter combat lock with shoulder and hip crossing against a force", - "low-front recovery turn with one leg absorbing impact and waist twisted for balance", - "angled forward body-lock pose as the torso rotates into a side-facing response", - ], side: [ "strong side profile, body twisted from force so shoulder, torso, and abdomen damage are obvious", "rearward three-quarter side-forward action frame while keeping enough face profile", @@ -274,11 +259,6 @@ const CAMERA_VARIANTS = { ], }, "heavy-damage": { - front: [ - "low-front collapse angle where one shoulder drops while body rotates around a failing knee", - "aggressive forward-lean collapse with side weight shift and heavy gear drag", - "frontal-to-side rebound pose with fractured guard and broken upper-body support", - ], side: [ "dramatic side view from near ground, torn shoulder and torso damage clearly visible", "wide extreme-side camera with body falling forward and equipment flying", @@ -301,11 +281,6 @@ const CAMERA_VARIANTS = { ], }, defeated: { - front: [ - "front-canted defeated collapse with one knee down and torso dropping into recoil", - "kneeling recoil frame with hip-heavy fall and waist opening", - "collapsed chest-forward posture with profile drift and controlled face read", - ], side: [ "low angle side-back shot with fallen armor pieces and defeated body posture", "rear three-quarter, side-back, or very low side view; show damaged back/shoulder/cloak with readable face profile", @@ -331,7 +306,7 @@ const CAMERA_VARIANTS = { const CAMERA_ORIENTATION_RULES = { normal: - "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.", + "Use only side/rear/low/high framing. Frontal-on faces are disallowed unless identity continuity absolutely requires it. Across one character's 5 stages, enforce at least one low-angle and one high-angle shot.", "light-damage": "Prioritize low-angle, rear, side, and high-angle framing. Frontal reads are disallowed unless strict identity continuity would otherwise fail.", "medium-damage": @@ -598,7 +573,7 @@ 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") { + if (candidate !== bannedOrientation) { return index; } } @@ -606,11 +581,29 @@ function nextDifferentIndex(plan, startIndex, bannedOrientation) { } function normalizeOrientation(orientation) { - return orientation === "front" ? "side" : orientation; + return orientation; } const cameraHistoryByCharacter = new Map(); const poseHistoryByCharacter = new Map(); +const poseFamilyHistoryByCharacter = new Map(); + +function poseFamily(phrase) { + const key = phrase.toLowerCase(); + if (/(drop|kneel|crouch|crawl|collapse|sit|ground|fall)/.test(key)) { + return "ground-recovery"; + } + if (/(jump|vault|leap|swing|spin|air|mid-air|rebound)/.test(key)) { + return "airborne"; + } + if (/(counter|parry|brace|rebound|recover|drag|pull|guard)/.test(key)) { + return "defensive"; + } + if (/(lunge|dash|step|stride|pivot|split|run|dodge|advance)/.test(key)) { + return "forward-motion"; + } + return "impact"; +} function pickCameraForStage(spec, character, stage) { const orientationPlan = CAMERA_ORIENTATION_PLAN[spec.label] ?? CAMERA_ORIENTATION_PLAN.normal; @@ -686,23 +679,36 @@ function pickPoseModifier(spec, character, stage, orientation) { POSE_ORIENTATION_VARIANTS.normal?.[normalizedOrientation] ?? []; const posePool = orientationBonus.concat(list); - 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; + const familyHistory = poseFamilyHistoryByCharacter.get(character.id) ?? []; + const avoidedFamilies = new Set(familyHistory.slice(-2)); + const familyFilteredPool = posePool.filter((pose) => !avoidedFamilies.has(poseFamily(pose))); + const finalPool = familyFilteredPool.length > 0 ? familyFilteredPool : posePool; + + const poolSeed = charSeed(`${character.id}-${character.pose}-${character.weapon}-${normalizedOrientation}`); + const index = (poolSeed + stage * 19) % finalPool.length; + const selectedCandidate = finalPool[index]; + + const history = poseHistoryByCharacter.get(character.id) ?? []; + let selected = selectedCandidate; if (history.length > 0 && selected === history[history.length - 1]) { - selected = posePool[(posePool.indexOf(pick) + 1) % posePool.length]; + const fallbackIndex = (finalPool.indexOf(selected) + 1) % finalPool.length; + selected = finalPool[fallbackIndex]; } + + const family = poseFamily(selected); + const familyHistoryKey = character.id; + const nextFamilyHistory = poseFamilyHistoryByCharacter.get(familyHistoryKey) ?? []; + nextFamilyHistory.push(family); + if (nextFamilyHistory.length > 8) { + nextFamilyHistory.shift(); + } + poseFamilyHistoryByCharacter.set(familyHistoryKey, nextFamilyHistory); + history.push(selected); if (history.length > 12) { history.shift(); } - poseHistoryByCharacter.set(historyKey, history); + poseHistoryByCharacter.set(character.id, history); return selected; } @@ -726,6 +732,8 @@ function promptFor(character, stage) { "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 poseDiversityConstraint = + "Pose diversity is mandatory: rotate through guard, movement, recoil, evasive, grounded, and collapse actions across the five stages; do not repeat the same motion family in multiple consecutive shots."; const poseModifier = pickPoseModifier(spec, character, stage, camera.orientation); return [ "Use case: stylized-concept", @@ -742,6 +750,7 @@ function promptFor(character, stage) { `Camera/view variation: ${camera.orientation} angle. Composition directive: ${camera.phrase}.`, angleControl, angleConstraint, + poseDiversityConstraint, "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}.`,