Wire the crowd variants into per-role pools and verify every seed resolves
This commit is contained in:
1 parent
4e8665e25b
commit
6052ea391b
3 files changed
+35
-28
No files matched your search
@@ -639,8 +639,12 @@ must inherit the reviewed face/hand/gear baseline instead of multiplying whateve
|
|||||||
- [x] Add the four specs to `CHARACTERS`, commit their GLBs/AO maps and manifest roles/tint data.
|
- [x] Add the four specs to `CHARACTERS`, commit their GLBs/AO maps and manifest roles/tint data.
|
||||||
(Note: this adds ~2 MB to the next release build via `export_filter=all_resources` — accepted
|
(Note: this adds ~2 MB to the next release build via `export_filter=all_resources` — accepted
|
||||||
for the crowd-variety roster.)
|
for the crowd-variety roster.)
|
||||||
- [ ] Replace the fixed `character_model_for()` mapping with deterministic per-role pools and add
|
- [x] Per-role pools: `character_model_for()` now picks from `CHARACTER_POOLS` by seed — the
|
||||||
tests for repeatability and duplicate avoidance (`godot-export` contract check included).
|
routine crowd roles gain the crowd variants (tourist → casual/crowd_a/crowd_d,
|
||||||
|
confused_visitor → casual/crowd_b, intoxicated → casual/crowd_c) while the special roles
|
||||||
|
keep their existing alternation (guest ⅓ jacket, trespasser/threat ½ hoodie). The test
|
||||||
|
now checks seed-stability plus that every pool entry resolves to a committed GLB
|
||||||
|
(277/277).
|
||||||
- [ ] Rebuild and verify gear fit for every new variant (`blender_assets.py fit` plus the runtime
|
- [ ] Rebuild and verify gear fit for every new variant (`blender_assets.py fit` plus the runtime
|
||||||
gear checks) and re-run `inspect` on the four new GLBs.
|
gear checks) and re-run `inspect` on the four new GLBs.
|
||||||
- [ ] Build the crowd-variety reference board (licensed street-crowd photos plus AI proposals) and
|
- [ ] Build the crowd-variety reference board (licensed street-crowd photos plus AI proposals) and
|
||||||
|
|||||||
+19
-23
@@ -195,32 +195,28 @@ const GEAR_ATTACH := {
|
|||||||
}
|
}
|
||||||
## Character variants from tools/blender_assets.py. The manifest is the source
|
## Character variants from tools/blender_assets.py. The manifest is the source
|
||||||
## of truth; this mapping keeps selection deterministic and testable offline.
|
## of truth; this mapping keeps selection deterministic and testable offline.
|
||||||
const CHARACTER_MODELS := {
|
const CHARACTER_POOLS := {
|
||||||
Rules.NpcType.TOURIST: "npc_casual",
|
Rules.NpcType.TOURIST: ["npc_casual", "npc_crowd_a", "npc_crowd_d"],
|
||||||
Rules.NpcType.CONFUSED_VISITOR: "npc_casual",
|
Rules.NpcType.CONFUSED_VISITOR: ["npc_casual", "npc_crowd_b"],
|
||||||
Rules.NpcType.INTOXICATED: "npc_casual",
|
Rules.NpcType.INTOXICATED: ["npc_casual", "npc_crowd_c"],
|
||||||
Rules.NpcType.AUTHORIZED_GUEST: "npc_jacket",
|
Rules.NpcType.AUTHORIZED_GUEST: ["npc_jacket", "npc_casual", "npc_casual"],
|
||||||
Rules.NpcType.AUTHORIZED_STAFF: "npc_jacket",
|
Rules.NpcType.AUTHORIZED_STAFF: ["npc_jacket"],
|
||||||
Rules.NpcType.PROTESTER: "npc_jacket",
|
Rules.NpcType.PROTESTER: ["npc_jacket"],
|
||||||
Rules.NpcType.INFLUENCER: "npc_coat",
|
Rules.NpcType.INFLUENCER: ["npc_coat"],
|
||||||
Rules.NpcType.TRESPASSER: "npc_coat",
|
Rules.NpcType.TRESPASSER: ["npc_hoodie", "npc_coat"],
|
||||||
Rules.NpcType.VIOLENT_THREAT: "npc_hoodie",
|
Rules.NpcType.VIOLENT_THREAT: ["npc_hoodie", "npc_coat"],
|
||||||
Rules.NpcType.MENTAL_CRISIS: "npc_hoodie",
|
Rules.NpcType.MENTAL_CRISIS: ["npc_hoodie"],
|
||||||
Rules.NpcType.PERFORMER: "npc_dress",
|
Rules.NpcType.PERFORMER: ["npc_dress"],
|
||||||
Rules.NpcType.DELIVERY: "npc_vest",
|
Rules.NpcType.DELIVERY: ["npc_vest"],
|
||||||
Rules.NpcType.RESPONDER: "npc_firefighter",
|
Rules.NpcType.RESPONDER: ["npc_firefighter"],
|
||||||
}
|
}
|
||||||
|
|
||||||
## Which library character represents this profile. Pure and seed-stable; some
|
## Which library character represents this profile. Pure and seed-stable; a role
|
||||||
## roles alternate between two silhouettes so crowds do not clone.
|
## picks from its pool by seed so crowds vary without cloning, and every pool
|
||||||
|
## entry resolves to a committed library GLB.
|
||||||
static func character_model_for(npc_type: int, seed_value: int) -> String:
|
static func character_model_for(npc_type: int, seed_value: int) -> String:
|
||||||
match npc_type:
|
var pool: Array = CHARACTER_POOLS.get(npc_type, ["npc_casual"])
|
||||||
Rules.NpcType.AUTHORIZED_GUEST:
|
return str(pool[absi(seed_value) % pool.size()])
|
||||||
return "npc_jacket" if seed_value % 3 == 0 else "npc_casual"
|
|
||||||
Rules.NpcType.TRESPASSER, Rules.NpcType.VIOLENT_THREAT:
|
|
||||||
return "npc_hoodie" if seed_value % 2 == 0 else "npc_coat"
|
|
||||||
_:
|
|
||||||
return str(CHARACTER_MODELS.get(npc_type, "npc_casual"))
|
|
||||||
|
|
||||||
const HAIR_STYLES := ["short", "long", "bun", "beard", "bald", "short"]
|
const HAIR_STYLES := ["short", "long", "bun", "beard", "bald", "short"]
|
||||||
const HAND_ITEMS := ["phone", "cup", "clipboard", "umbrella", "camera", "none"]
|
const HAND_ITEMS := ["phone", "cup", "clipboard", "umbrella", "camera", "none"]
|
||||||
|
|||||||
@@ -180,10 +180,12 @@ func test_pose_reaches_the_skeleton_bones() -> bool:
|
|||||||
return a and b and c and d
|
return a and b and c and d
|
||||||
|
|
||||||
func test_character_selection_is_deterministic_and_complete() -> bool:
|
func test_character_selection_is_deterministic_and_complete() -> bool:
|
||||||
|
# Seed 5 resolves each role to a fixed pool member; the routine crowd roles
|
||||||
|
# now pick from the crowd-variety pools instead of a single npc_casual.
|
||||||
var expected := {
|
var expected := {
|
||||||
Rules.NpcType.TOURIST: "npc_casual",
|
Rules.NpcType.TOURIST: "npc_crowd_d",
|
||||||
Rules.NpcType.CONFUSED_VISITOR: "npc_casual",
|
Rules.NpcType.CONFUSED_VISITOR: "npc_crowd_b",
|
||||||
Rules.NpcType.INTOXICATED: "npc_casual",
|
Rules.NpcType.INTOXICATED: "npc_crowd_c",
|
||||||
Rules.NpcType.AUTHORIZED_STAFF: "npc_jacket",
|
Rules.NpcType.AUTHORIZED_STAFF: "npc_jacket",
|
||||||
Rules.NpcType.PROTESTER: "npc_jacket",
|
Rules.NpcType.PROTESTER: "npc_jacket",
|
||||||
Rules.NpcType.INFLUENCER: "npc_coat",
|
Rules.NpcType.INFLUENCER: "npc_coat",
|
||||||
@@ -201,6 +203,11 @@ func test_character_selection_is_deterministic_and_complete() -> bool:
|
|||||||
problems.append("%s missing" % model)
|
problems.append("%s missing" % model)
|
||||||
if NpcAvatar.character_model_for(npc_type, 5) != model:
|
if NpcAvatar.character_model_for(npc_type, 5) != model:
|
||||||
problems.append("%d is not seed-stable" % npc_type)
|
problems.append("%d is not seed-stable" % npc_type)
|
||||||
|
# Every seed in a role's pool must resolve to a committed library model.
|
||||||
|
for seed_value in [0, 1, 2, 3, 4]:
|
||||||
|
var pooled := NpcAvatar.character_model_for(npc_type, seed_value)
|
||||||
|
if not ResourceLoader.exists(NpcAvatar.BLENDER_NPC_DIR + pooled + ".glb"):
|
||||||
|
problems.append("%d seed %d -> %s missing" % [npc_type, seed_value, pooled])
|
||||||
# Seed-alternating variants still resolve to a library model.
|
# Seed-alternating variants still resolve to a library model.
|
||||||
for seed_value in [1, 2, 3, 4, 5, 6]:
|
for seed_value in [1, 2, 3, 4, 5, 6]:
|
||||||
var variant := NpcAvatar.character_model_for(Rules.NpcType.TRESPASSER, seed_value)
|
var variant := NpcAvatar.character_model_for(Rules.NpcType.TRESPASSER, seed_value)
|
||||||
|
|||||||
Reference in new issue
Block a user