267 lines
13 KiB
JavaScript
267 lines
13 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import vm from "node:vm";
|
|
|
|
const root = process.cwd();
|
|
const mainPath = path.join(root, "main.js");
|
|
const cardsRoot = path.join(root, "assets", "cards");
|
|
const promptsDir = path.join(cardsRoot, "prompts");
|
|
|
|
function makeElement() {
|
|
return {
|
|
className: "",
|
|
dataset: {},
|
|
style: { setProperty() {} },
|
|
children: [],
|
|
textContent: "",
|
|
innerHTML: "",
|
|
addEventListener() {},
|
|
replaceChildren(...nodes) {
|
|
this.children = nodes;
|
|
},
|
|
prepend(node) {
|
|
this.children.unshift(node);
|
|
},
|
|
remove() {},
|
|
};
|
|
}
|
|
|
|
function buildDomStub() {
|
|
const nodes = new Map();
|
|
return {
|
|
querySelector(selector) {
|
|
if (!nodes.has(selector)) nodes.set(selector, makeElement());
|
|
if (selector === "#emptyCardTemplate") {
|
|
nodes.get(selector).content = { cloneNode: () => makeElement() };
|
|
}
|
|
return nodes.get(selector);
|
|
},
|
|
createElement: makeElement,
|
|
};
|
|
}
|
|
|
|
function damageSpec(stage) {
|
|
const specs = [
|
|
{
|
|
label: "normal",
|
|
visual: "intact armor and clothing, pristine battle-ready silhouette",
|
|
exposure:
|
|
"no battle-damage exposure yet; mature silhouette is visible through intact adult outfit design",
|
|
poseNote: "confident ready stance",
|
|
expression:
|
|
"controlled confident expression, eyes focused, no distress",
|
|
missingArmor:
|
|
"no missing armor or clothing; all equipment is intact",
|
|
camera:
|
|
"front or three-quarter front view for clear character identity",
|
|
},
|
|
{
|
|
label: "light-damage",
|
|
visual:
|
|
"light battle damage: scratched armor or modern gear, torn jacket or cloak edges, scuffed gloves, one shoulder strap loosened, small visible exposure around collarbone, shoulder edge, forearm, waistline, or upper thigh",
|
|
exposure:
|
|
"low but noticeable battle-damage exposure: collarbone, shoulder edge, forearm, waistline, or upper thigh should be visible from damaged outer layers",
|
|
poseNote: "still composed but reacting to an opening exchange",
|
|
expression:
|
|
"annoyed grimace, narrowed eyes, clenched jaw, clearly frustrated by the first hit",
|
|
missingArmor:
|
|
"minor loss of outer accessories only: torn glove edge, loosened shoulder strap, scuffed boot armor",
|
|
camera:
|
|
"three-quarter front view or slight side angle, enough to show both face and first armor damage",
|
|
},
|
|
{
|
|
label: "medium-damage",
|
|
visual:
|
|
"moderate battle damage: cracked shoulder armor or protective gear, torn outer sleeves, loosened belts, damaged protective plates, exposed forearms, one bare shoulder, torn neckline or side seam, clearly exposed abdomen or side torso, exposed upper thigh or hip line through broken outer layers",
|
|
exposure:
|
|
"clear adult battle-damage exposure: one bare shoulder, side torso, abdomen, upper thigh, hip line, and emphasized mature bust silhouette should be obvious, not hidden by intact modest underlayers",
|
|
poseNote: "strained but still fighting with focus",
|
|
expression:
|
|
"painful frown, wet eyes, anger and disbelief, trying not to cry while staying in the fight; if hit in the abdomen, show a gasping open mouth and strained breathing",
|
|
missingArmor:
|
|
"visible loss around hands, forearms, one shoulder, boot guards, and side-abdomen armor; damage suggests repeated heavy impacts",
|
|
camera:
|
|
"side view or strong three-quarter side view, body twisted from impact so shoulder and abdomen damage are visible",
|
|
},
|
|
{
|
|
label: "heavy-damage",
|
|
visual:
|
|
"heavy battle damage: broken outer armor or modern gear, shredded mantle/jacket/stage outfit, fractured bracers, missing glove armor, damaged boots or bare foot/ankle on one side, heavily exposed shoulders, back, abdomen/midriff, hips, upper thighs, and torn costume layers through destroyed outer armor",
|
|
exposure:
|
|
"push to maximum allowed non-sexual battle-damage exposure: exposed shoulders, back, cleavage or side-bust silhouette, abdomen, hips, upper thighs, and torn outfit remnants should make the impact feel severe",
|
|
poseNote: "wounded, guarded, barely keeping balance after a brutal exchange",
|
|
expression:
|
|
"tearful angry grimace, trembling lower lip, brows pinched, resentful eyes that show she hates losing; abdominal-hit reaction such as open mouth, gasping, hunched posture, and a small non-erotic saliva strand is allowed",
|
|
missingArmor:
|
|
"major loss around hands, feet, shoulders, abdomen, waist plates, and bracers; broken pieces scattered nearby",
|
|
camera:
|
|
"dramatic side view, rear three-quarter view, or over-the-shoulder angle while keeping face partially visible",
|
|
},
|
|
{
|
|
label: "defeated",
|
|
visual:
|
|
"defeated state: kneeling, collapsed, or sitting back after battle, broken armor pieces around them, torn outer garment layers, missing hand and foot armor, bare shoulders, exposed back or side-back, exposed abdomen/midriff, hips, upper thighs, and torn costume remnants through destroyed outer armor",
|
|
exposure:
|
|
"maximum permitted adult battle-damage exposure for the defeated card: broad exposed shoulder/back/side torso/abdomen/hip/thigh areas, dramatic torn costume remnants, and emphasized mature silhouette while staying framed as combat defeat rather than a sexual act",
|
|
poseNote: "battle-worn, emotionally overwhelmed, clearly defeated, no gore",
|
|
expression:
|
|
"frustrated tears, unfair-loss expression, clenched teeth or trembling open mouth, tear tracks, angry shame rather than calm dignity; defeated abdominal-hit pain reaction with open mouth, drool/saliva at the lip, and labored breathing is allowed when framed as combat pain",
|
|
missingArmor:
|
|
"the most intense equipment loss: shattered shoulder plates, missing bracers and glove armor, broken boot guards, torn waist and abdomen armor, scattered fragments",
|
|
camera:
|
|
"rear three-quarter, side-back, or low side view; show back/shoulder/cloak damage while keeping enough face profile to read pain",
|
|
},
|
|
];
|
|
return specs[stage];
|
|
}
|
|
|
|
function promptFor(character, stage) {
|
|
const spec = damageSpec(stage);
|
|
const elementName = {
|
|
water: "water/수",
|
|
metal: "metal/금",
|
|
earth: "earth/지",
|
|
fire: "fire/화",
|
|
wood: "wood/목",
|
|
}[character.affinity] ?? character.affinity;
|
|
const adultAppeal =
|
|
"adult female collectible-card appeal: emphasize large breasts through the armor, uniform, stage outfit, or gear silhouette; curvy mature body proportions; elegant sensual costume design";
|
|
return [
|
|
"Use case: stylized-concept",
|
|
"Asset type: high-resolution individual trading card illustration",
|
|
`Primary request: Create a unique full-card illustration for ${character.id} ${character.name}, damage stage ${stage} (${spec.label}).`,
|
|
`Subject: adult ${character.gender} ${character.race}, ${character.body}, ${character.title}, ${character.role}, weapon/equipment ${character.weapon}.`,
|
|
`World/era concept: ${character.world}; ${character.era}. This roster is not limited to medieval fantasy; characters can come from modern campuses, idol stages, cyberpunk streets, space colonies, sports arenas, mythic fashion worlds, or other time periods.`,
|
|
`Lighting/time/weather: ${character.lighting}. Do not default every battle to night; use daytime, afternoon, morning, cloudy, indoor, or night lighting only when this field calls for it.`,
|
|
`Signature costume/equipment: ${character.costume}.`,
|
|
`Unique appearance DNA: ${character.appearance.faceShape}; ${character.appearance.eyeStyle}; ${character.appearance.noseStyle}; ${character.appearance.lipStyle}; ${character.appearance.skinTone}; ${character.appearance.hairStyle}; ${character.appearance.bodyDetail}; ${character.appearance.bustShape}; ${character.appearance.presence}. Make this face and body clearly different from other cards.`,
|
|
`Personality and pose: ${character.personality}; ${character.pose}; ${spec.poseNote}.`,
|
|
`Facial expression: ${spec.expression}.`,
|
|
`Camera/view variation: ${spec.camera}. Across a character's five damage stages, avoid repeating the same straight-front pose; include front, side, and rear three-quarter views where appropriate.`,
|
|
`Adult visual appeal: ${character.appealFocus}; ${adultAppeal}.`,
|
|
`Scene/backdrop: ${character.scene}; ${elementName} elemental atmosphere; no other characters.`,
|
|
`Style/medium: ${character.style}, portrait card composition.`,
|
|
"Composition/framing: full body or knees-up character centered, strong silhouette, vertical card art, cinematic but readable.",
|
|
`Damage state: ${spec.visual}.`,
|
|
`Battle-damage exposure direction: ${spec.exposure}. The card should not look modest or fully covered after impact; exposure is part of the adult card-game reward while remaining non-sexual combat damage.`,
|
|
`Missing gear/clothing emphasis: ${spec.missingArmor}. Emphasize battle intensity through missing or damaged hand, foot, shoulder, abdomen, hip, and torso gear where appropriate, adapting the damage to this character's era and outfit.`,
|
|
`Continuity: keep this character's face, body type, race, weapon/equipment, world/era, costume silhouette, color motif, and elemental identity consistent across all five damage stages.`,
|
|
"Constraints: all characters are adults; adult-oriented multi-world card art; sensual silhouette, nudity, and body exposure are allowed when framed as battle damage or defeat; pain expressions, tears, gasping, and small saliva/drool details must read as combat injury reactions; no sexual act; no penetration; no gore; no text; no logo; no watermark.",
|
|
"Avoid: childlike appearance, underage cues, malformed hands, extra limbs, cropped head, UI elements.",
|
|
].join("\n");
|
|
}
|
|
|
|
const source = fs.readFileSync(mainPath, "utf8");
|
|
const context = {
|
|
document: buildDomStub(),
|
|
window: { setInterval() {}, clearInterval() {} },
|
|
fetch: async () => ({ ok: false }),
|
|
console,
|
|
};
|
|
const exportCode = `${source}
|
|
globalThis.__cardExport = {
|
|
damageStages: DAMAGE_ART_STAGES,
|
|
characters: CHARACTERS.map((character) => ({
|
|
id: character.id,
|
|
index: character.index,
|
|
name: character.name,
|
|
gender: character.gender,
|
|
race: character.race,
|
|
body: character.body,
|
|
affinity: character.affinity,
|
|
stars: character.stars,
|
|
title: character.title,
|
|
role: character.role,
|
|
weapon: character.weapon,
|
|
personality: character.personality,
|
|
pose: character.pose,
|
|
origin: character.origin,
|
|
world: character.world,
|
|
era: character.era,
|
|
scene: character.scene,
|
|
lighting: character.lighting,
|
|
costume: character.costume,
|
|
style: character.style,
|
|
damageProfile: character.damageProfile,
|
|
appealFocus: character.appealFocus,
|
|
appearance: character.appearance,
|
|
stats: character.stats
|
|
}))
|
|
};`;
|
|
|
|
vm.runInNewContext(exportCode, context);
|
|
|
|
const exported = context.__cardExport;
|
|
fs.mkdirSync(cardsRoot, { recursive: true });
|
|
fs.mkdirSync(promptsDir, { recursive: true });
|
|
|
|
const manifest = {
|
|
schemaVersion: 1,
|
|
artRoot: "assets/cards",
|
|
damageStages: Array.from({ length: exported.damageStages }, (_, stage) => ({
|
|
stage,
|
|
...damageSpec(stage),
|
|
})),
|
|
expectedImageCount: exported.characters.length * exported.damageStages,
|
|
imageSizeTarget: "portrait high resolution, 2048x3072 or better for final production",
|
|
safetyStyle: "adult-oriented multi-world pinup combat damage; emphasized mature silhouettes, nudity/body exposure allowed as battle-damage presentation; no minors, sexual acts, penetration, or gore",
|
|
characters: exported.characters.map((character) => ({
|
|
...character,
|
|
art: Array.from({ length: exported.damageStages }, (_, stage) => ({
|
|
damageStage: stage,
|
|
path: `assets/cards/${character.id}/damage-${stage}.png`,
|
|
promptId: `${character.id}-D${stage}`,
|
|
status: "needed",
|
|
})),
|
|
})),
|
|
};
|
|
|
|
const promptLines = [];
|
|
for (const character of exported.characters) {
|
|
for (let stage = 0; stage < exported.damageStages; stage += 1) {
|
|
promptLines.push(
|
|
JSON.stringify({
|
|
promptId: `${character.id}-D${stage}`,
|
|
characterId: character.id,
|
|
damageStage: stage,
|
|
outputPath: `assets/cards/${character.id}/damage-${stage}.png`,
|
|
prompt: promptFor(character, stage),
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
const availablePath = path.join(cardsRoot, "available-assets.json");
|
|
if (!fs.existsSync(availablePath)) {
|
|
fs.writeFileSync(
|
|
availablePath,
|
|
JSON.stringify(
|
|
{
|
|
schemaVersion: 1,
|
|
assets: [],
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
"utf8",
|
|
);
|
|
}
|
|
|
|
fs.writeFileSync(path.join(cardsRoot, "card-manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
fs.writeFileSync(path.join(promptsDir, "card-image-prompts.jsonl"), `${promptLines.join("\n")}\n`, "utf8");
|
|
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
characters: exported.characters.length,
|
|
damageStages: exported.damageStages,
|
|
expectedImageCount: manifest.expectedImageCount,
|
|
manifest: "assets/cards/card-manifest.json",
|
|
prompts: "assets/cards/prompts/card-image-prompts.jsonl",
|
|
available: "assets/cards/available-assets.json",
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|