- UI SFX v3: procedurally synthesized one-shots (Tools/synth_ui_sfx.py) wired to buttons, rewards, errors and red dots via UiSfx, gated by the SFX setting. - Five formal enemies (Hornlet, Dashlet, Shellhorn, HornCaptain, GreatHorn): Hunyuan 50k -> 2000-tri XPbr, DGXA TokenRig, Idle/Walk/Attack, nested per type in PrototypeArena; PrototypeBattle.Enemy gains presentation-only Moving/Strikes. - Formal partners for all ten pets in the arena and portraits, including new VineSprite, ThunderLeopard and TideWhale. - Pipeline fixes: reject local TokenRig fallback, drop loose mesh islands, configurable Unity run_script timeout, companion static stage. - Also includes concurrent work from another tool in this working tree (FlameLion, enemy combat FX, challenge mode, home explorer, server updates). - Resources registered in Doc/ResDesc_*.md; verified on isolated :18081. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
121 lines
7.5 KiB
Python
121 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
"""P0 enemy concept drafts via the LAN LLM3DGraphics local Turbo image API (DGX3D_AI调用指南.md §4.4).
|
|
|
|
All five enemies are one 'horn imp' family built as short-legged bipeds (biped_short), so the validated
|
|
RockBall enemy rig/motion adapter applies. Drafts go to Doc/Design/Character/Enemy/{id}/drafts/.
|
|
|
|
Usage:
|
|
python Tools/generate_enemy_designs.py # all enemies, 4 drafts each
|
|
python Tools/generate_enemy_designs.py Hornlet Boss # selected enemies only
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import requests
|
|
|
|
BASE = "http://192.168.130.16:8008"
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
DESIGN = ROOT / "Doc/Design/Character/Enemy"
|
|
STYLE_REFS = [ROOT / "Doc/Design/Character/FireCat/firecat.png", ROOT / "Doc/Design/Character/RockRhino/rockrhino.png"]
|
|
|
|
COMMON = (
|
|
"Image 1 and image 2 are STYLE ONLY: match their friendly polished chibi 3D mobile-game render, glossy expressive eyes, "
|
|
"rounded appealing volumes and clean readable shapes; do not copy their species, colors or features. "
|
|
"Subject family: small mischievous 'horn imp' monsters that invade a cozy meadow camp in a cute tower-defense game; "
|
|
"cheeky and naughty, never scary, gory or realistic. Base palette violet / lilac skin with cream-ivory horns. "
|
|
"Body plan for rigging: a SHORT-LEGGED BIPED standing upright, two short stubby legs with flat round feet both grounded, "
|
|
"two short separate arms held slightly away from the body, bilaterally symmetrical, neutral calm standing pose, "
|
|
"head facing the viewer. Composition: single full-body character centered, very shallow front three-quarter view "
|
|
"(about 15 degrees off frontal), whole body including horns and feet in frame with generous margin. "
|
|
"Pure solid white #FFFFFF background to all corners, soft studio light, tiny contact shadow under the feet. "
|
|
"Simple smooth matte surfaces suitable for a 2000-triangle mobile model with a 1024 texture. "
|
|
"Exactly one character; no text, no watermark, no UI, no props held in hands, no weapons, no floor base, no scenery, "
|
|
"no particles, no glow, no extra limbs, no wings."
|
|
)
|
|
|
|
ENEMIES = [
|
|
{"id": "Hornlet", "name": "小角怪", "role": "normal", "seed": 61001,
|
|
"subject": "Hornlet, the basic grunt: a small round violet imp, egg-shaped body, one short stubby cream horn on top of the head, "
|
|
"big round eyes and a toothy cheeky grin with one small fang, tiny pointed ears, short tail stub."},
|
|
{"id": "Dashlet", "name": "疾跑怪", "role": "fast", "seed": 62001,
|
|
"subject": "Dashlet, the fast runner: a slimmer, lighter lilac imp with a narrow teardrop body leaning slightly forward, "
|
|
"one sleek swept-back cream horn, long pointed ears swept back like it is in a hurry, eager grinning face, "
|
|
"slightly longer but still short legs with springy feet, thin tail; reads as quick and light."},
|
|
{"id": "Shellhorn", "name": "硬壳怪", "role": "armored", "seed": 63101,
|
|
# v1 (seeds 63001-63004) drifted into four-legged turtles; v2 forces an upright biped with a compact back plate.
|
|
"subject": "Shellhorn, the armored one: an UPRIGHT standing two-legged violet imp, same upright body plan as a small chubby person, "
|
|
"standing on its two short hind legs only, two short arms hanging at its sides, NOT on all fours, not a turtle, not a quadruped. "
|
|
"Stocky wide body; a compact rounded gray-brown stone plate covers its back like a small backpack-sized shell, staying behind the body "
|
|
"and not wider than the shoulders, plus a smooth stone brow plate on its forehead with one short blunt cream horn poking out of it; "
|
|
"stubborn frowning but cute face. The stone plates are solid attached armor with shallow seams, not floating rocks."},
|
|
{"id": "HornCaptain", "name": "角怪队长", "role": "elite", "seed": 64001,
|
|
"subject": "Horn Captain, the elite squad leader: a taller, chunkier deep-violet imp with TWO curved cream horns, a short red scarf "
|
|
"tied around the neck with the knot at the front, confident smug grin with two small fangs, small spiky tuft on the head, "
|
|
"broader shoulders and slightly bigger fists; still short-legged and cute."},
|
|
{"id": "GreatHorn", "name": "大角首领", "role": "boss", "seed": 65001,
|
|
"subject": "Great Horn, the boss: a big heavy dark plum-violet imp chief with a round belly, two large curved ram-like cream horns "
|
|
"and a small crown-like ridge of three tiny horns between them, thick bushy eyebrows, proud bossy grin with fangs, "
|
|
"a short dark purple cape draped over the shoulders, chunky short legs and big round feet; imposing but still cute."},
|
|
]
|
|
|
|
|
|
def status(session: requests.Session) -> dict:
|
|
r = session.get(f"{BASE}/api/local-images/status", timeout=20)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
def run(enemy: dict, session: requests.Session, design: Path = DESIGN, refs: list[Path] = STYLE_REFS, common: str = COMMON,
|
|
size: tuple[int, int] = (1024, 1024)) -> dict:
|
|
"""Generate four drafts for one character into design/{id}/drafts; enemy defaults, overridable for companions."""
|
|
out = design / enemy["id"] / "drafts"
|
|
out.mkdir(parents=True, exist_ok=True)
|
|
prompt = f"{common} {enemy['subject']}"
|
|
data = dict(model="turbo", profile="concept", prompt=prompt, width=size[0], height=size[1], count=4,
|
|
seed=enemy["seed"], transparent="false", negative_prompt="")
|
|
files = [("images", (p.name, p.open("rb"), "image/png")) for p in refs]
|
|
try:
|
|
r = session.post(f"{BASE}/api/local-images/jobs", data=data, files=files, timeout=60)
|
|
finally:
|
|
for _, (_, fh, _) in files:
|
|
fh.close()
|
|
r.raise_for_status()
|
|
job = r.json()
|
|
while job.get("status") not in ("completed", "failed"):
|
|
time.sleep(3)
|
|
job = session.get(f"{BASE}/api/local-images/jobs/{job['id']}", timeout=20).json()
|
|
saved = []
|
|
for img in job.get("images", []):
|
|
path = out / f"{enemy['id']}_{img['seed']}.png"
|
|
d = session.get(f"{BASE}{img['url']}" if img["url"].startswith("/") else img["url"], timeout=60)
|
|
d.raise_for_status()
|
|
path.write_bytes(d.content)
|
|
saved.append(str(path.relative_to(ROOT)).replace("\\", "/"))
|
|
record = {"id": enemy["id"], "name": enemy["name"], "role": enemy["role"], "jobId": job["id"], "status": job["status"],
|
|
"error": job.get("error"), "model": "Qwen/Qwen-Image-2.1-Turbo (LAN LLM3DGraphics local-images)", "profile": "concept",
|
|
"size": list(size), "seedStart": enemy["seed"], "references": [str(p.relative_to(ROOT)).replace("\\", "/") for p in refs],
|
|
"prompt": prompt, "drafts": saved, "generatedUtc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}
|
|
(design / enemy["id"] / "design_drafts.json").write_text(json.dumps(record, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
return record
|
|
|
|
|
|
def main() -> None:
|
|
wanted = set(sys.argv[1:])
|
|
with requests.Session() as session:
|
|
state = status(session)
|
|
if not (state.get("connected") and state.get("ready")):
|
|
raise SystemExit(f"local images not ready: {state}")
|
|
for enemy in ENEMIES:
|
|
if wanted and enemy["id"] not in wanted:
|
|
continue
|
|
rec = run(enemy, session)
|
|
print(json.dumps({"id": rec["id"], "status": rec["status"], "drafts": len(rec["drafts"]), "error": rec["error"]}, ensure_ascii=False))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|