Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ PhotonServerSettings.asset.local
# Paid Unity Asset Store assets (installed locally via Package Manager > My Assets)
Unity/Assets/ExplosiveLLC/
Unity/Assets/ExplosiveLLC.meta
Unity/Assets/GamerGirl/
Unity/Assets/GamerGirl.meta
Unity/Assets/IdaFaber/
Unity/Assets/IdaFaber.meta
Unity/Assets/Ida Faber/
Unity/Assets/Ida Faber.meta

# Local clean visual prefabs generated from ignored Asset Store character packs.
Unity/Assets/_SecondSpawn/Art/Characters/GeneratedVisuals/
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ versioned release tag yet, so entries are organized as pre-alpha snapshots.
Anime-Ready Semi-Real as the target, clean PBR guardrails, medieval asset
limits, character and environment requirements, Asset Store keyword strategy,
reject criteria, Unity 6.5 URP import checks, and coherence rules.
- Ida Faber visual prototype lane: Unity visual catalog now recognizes three
new semi-real character variants and Nakama seeds matching permanent NPC
Frames for the GamerGirl, Vex, and Lucia-style imported model packs.
- Hierarchical FrameMemory backend for issue #132: actor profiles now normalize
`short_term`, `episodic`, and `core` memory records, expose scored memory
retrieval, and add an internal-worker-only consolidation RPC so clients cannot
Expand Down
8 changes: 7 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SECOND SPAWN Roadmap

Status: Pre-alpha, vertical slice foundation in development.
Last updated: 2026-05-23.
Last updated: 2026-05-24.

This roadmap tracks implementation status. Detailed design remains in `docs/`,
especially `docs/design/02-vertical-slice-spec.md` and
Expand Down Expand Up @@ -609,6 +609,12 @@ prototype.
- [ ] Run Multiplayer Play Mode smoke for 2-4 local clients.
- [ ] Add a browser/WebGL demo build lane so external playtesters can try the
vertical slice without installing the Unity editor or a native client.
- [ ] Defer Unity 6.x rendering and asset-performance feature adoption until a
representative demo scene exists. Benchmark GPU Resident Drawer with
Forward+, Build Profiles, Mesh LOD policy, texture import overrides, visual
variant budgets, GPU Occlusion Culling, and Adaptive Probe Volumes only after
the scene has enough NPCs, repeated props, lighting, and occlusion geometry to
produce meaningful profiler evidence.
- [ ] Resolve Unity 6000.5.0b9 Package Manager startup blocker tracked in
issue #122. Current failure happens before C# compilation with
`The "path" argument must be of type string. Received undefined`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,9 @@ private static CharacterAnimationSet ResolveAnimationSet(int equipmentVisualId,
15 => CharacterAnimationSet.HeavyFighter,
16 => CharacterAnimationSet.MaleFighter,
17 => CharacterAnimationSet.Crafter,
18 => CharacterAnimationSet.FemaleFighter,
19 => CharacterAnimationSet.FemaleFighter,
20 => CharacterAnimationSet.Sorceress,
_ => EquipmentVisualCatalog.GetWeaponStyle(equipmentVisualId) switch
{
CharacterWeaponStyle.TwoHandSword => CharacterAnimationSet.TwoHandSword,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ public static int GetDefaultForVisualVariant(int visualVariant)
15 => Hammer, // Heavy fighter
16 => OneHandSword, // Male fighter
17 => Unarmed, // Crafter
18 => Unarmed, // Ida Faber GamerGirl
19 => Unarmed, // Ida Faber Vex
20 => Staff, // Ida Faber Lucia
_ => None
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ public static class VisualPrefabCatalog
"Assets/ExplosiveLLC/Male Fighter Mecanim Animation Pack/Prefabs/Male.prefab"),
new("Crafter",
"Assets/ExplosiveLLC/Sorceress Warrior Mecanim Animation Pack/Prefabs/Crafter.prefab"),
new("IdaFaber_GamerGirl",
"Assets/GamerGirl/Render pipeline/URP/Prefab/SK_GamerGirl_01 Violet Variant.prefab"),
new("IdaFaber_Vex",
"Assets/IdaFaber/Prefabs/Girl/SK_VEX_01 Green.prefab",
"Assets/IdaFaber/Prefabs/Girl/SK_VEX_01 Variant.prefab"),
new("IdaFaber_Lucia",
"Assets/Ida Faber/Succubus Lucia/Prefabs/SK_Succubus_Lucia Violet.prefab",
"Assets/Ida Faber/Succubus Lucia/Prefabs/SK_Succubus_Lucia_withHorns Violet.prefab"),
};

public static int Count => Entries.Length;
Expand Down
3 changes: 2 additions & 1 deletion Unity/Packages/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"dependencies": {
"com.coplaydev.unity-mcp": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#main",
"com.gamebooom.unity.mcp": "https://github.com/FunplayAI/funplay-unity-mcp.git#3ef233a8a86eb03a281873b778d79bbfb1e3e899",
"com.gamebooom.unity.mcp": "https://github.com/FunplayAI/funplay-unity-mcp.git#v0.3.8",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pin Unity MCP dependency to immutable revision

Switching com.gamebooom.unity.mcp from a commit SHA to the mutable #v0.3.8 tag makes dependency resolution non-deterministic when lockfiles are regenerated or absent, because retagging can change the resolved code without any manifest diff. This is a regression from the previous immutable pin and can cause hard-to-reproduce editor/tooling behavior across contributors and CI.

Useful? React with 👍 / 👎.

"com.unity.ai.assistant": "2.9.0-pre.2",
"com.unity.ai.inference": "2.6.1",
"com.unity.ai.navigation": "2.0.12",
Expand All @@ -12,6 +12,7 @@
"com.unity.multiplayer.center": "1.0.1",
"com.unity.nuget.mono-cecil": "1.11.6",
"com.unity.render-pipelines.universal": "17.5.0",
"com.unity.shadergraph": "17.5.0",
"com.unity.test-framework": "1.7.0",
"com.unity.timeline": "1.8.12",
"com.unity.ugui": "2.5.0",
Expand Down
8 changes: 4 additions & 4 deletions Unity/Packages/packages-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
"hash": "417cf351a152b483c91e6e2deaf7ae355fa8eff3"
},
"com.gamebooom.unity.mcp": {
"version": "https://github.com/FunplayAI/funplay-unity-mcp.git#3ef233a8a86eb03a281873b778d79bbfb1e3e899",
"version": "https://github.com/FunplayAI/funplay-unity-mcp.git#v0.3.8",
"depth": 0,
"source": "git",
"dependencies": {
"com.unity.nuget.newtonsoft-json": "3.2.1",
"com.unity.inputsystem": "1.7.0"
},
"hash": "3ef233a8a86eb03a281873b778d79bbfb1e3e899"
"hash": "dd0c992c703109e2849eb2960e831a96c59e2b3a"
},
"com.unity.2d.sprite": {
"version": "1.0.0",
Expand Down Expand Up @@ -207,14 +207,14 @@
},
"com.unity.searcher": {
"version": "4.9.4",
"depth": 2,
"depth": 1,
"source": "registry",
"dependencies": {},
"url": "https://packages.unity.com"
},
"com.unity.shadergraph": {
"version": "17.5.0",
"depth": 1,
"depth": 0,
"source": "builtin",
"dependencies": {
"com.unity.render-pipelines.core": "17.5.0",
Expand Down
45 changes: 41 additions & 4 deletions backend/nakama/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ var dosAiDecisionDailyRequestLimitDefault = 1000;
var dosAiDecisionDailyTokenBudgetDefault = 250000;
var dosAiDirectChatDailyRequestLimitDefault = 1000;
var dosAiDirectChatDailyTokenBudgetDefault = 250000;
var prototypeVisualVariantMax = 17;
var prototypeVisualVariantMax = 20;
var initialInhabitationFramePoolSize = 10;
var bodyArchetypePool = [
{
archetype_id: "synthetic-sentinel",
Expand Down Expand Up @@ -369,7 +370,10 @@ var permanentNpcFramePool = [
{ npc_id: "npc-wasteland-courier-0733", display_name: "Route Courier 0733", archetype_id: "wasteland-courier", role: "Scout and courier body", visual_variant: 14, visual_prefab_key: "generated_visual_14_female_fighter", equipment_visual_id: 2 },
{ npc_id: "npc-clinic-operator-0819", display_name: "Clinic Operator 0819", archetype_id: "clinic-operator", role: "Support and researcher body", visual_variant: 17, visual_prefab_key: "generated_visual_17_crafter", equipment_visual_id: 1 },
{ npc_id: "npc-scrap-warden-0940", display_name: "Scrap Warden 0940", archetype_id: "scrap-warden", role: "Heavy salvage body", visual_variant: 15, visual_prefab_key: "generated_visual_15_heavy_fighter", equipment_visual_id: 9 },
{ npc_id: "npc-crossline-hunter-1058", display_name: "Crossline Surveyor 1058", archetype_id: "crossline-hunter", role: "Ranged survey body", visual_variant: 6, visual_prefab_key: "generated_visual_06_archer", equipment_visual_id: 6 }
{ npc_id: "npc-crossline-hunter-1058", display_name: "Crossline Surveyor 1058", archetype_id: "crossline-hunter", role: "Ranged survey body", visual_variant: 6, visual_prefab_key: "generated_visual_06_archer", equipment_visual_id: 6 },
{ npc_id: "npc-neon-runner-1172", display_name: "Neon Runner 1172", archetype_id: "wasteland-courier", role: "Neon courier body", visual_variant: 18, visual_prefab_key: "generated_visual_18_idafaber_gamergirl", equipment_visual_id: 1 },
{ npc_id: "npc-vex-operative-2286", display_name: "Vex Operative 2286", archetype_id: "crossline-hunter", role: "Signal infiltrator body", visual_variant: 19, visual_prefab_key: "generated_visual_19_idafaber_vex", equipment_visual_id: 1 },
{ npc_id: "npc-redline-envoy-3398", display_name: "Redline Envoy 3398", archetype_id: "clinic-operator", role: "Redline liaison body", visual_variant: 20, visual_prefab_key: "generated_visual_20_idafaber_lucia", equipment_visual_id: 8 }
Comment on lines +374 to +376
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove NPC seeds that depend on local-only assets

These three permanent NPC seeds assign visual variants 18-20, but this commit does not add distributable generated prefabs for those variants (the new source asset folders are also gitignored), so a clean checkout without the paid local packs will spawn these NPCs with missing visuals and fall back to placeholder rendering. Because these NPCs are in the shared permanentNpcFramePool, the regression affects every environment that runs the seeded world data, not just local prototype machines.

Useful? React with 👍 / 👎.

];

var permanentNpcProfileOverrides: any = {
Expand Down Expand Up @@ -452,6 +456,30 @@ var permanentNpcProfileOverrides: any = {
soul: { name: "Scope-1058 Map", core_drive: "turn every threat sighting into a map someone can survive", temperament: "quiet, methodical, and unforgiving about sloppy reports", combat_style: "fire from clean lanes, avoid tunnel fights, and mark targets for allies", social_style: "questions first, trust later", long_term_goals: ["complete the north danger map", "prove the repeating signal is moving"], player_notes: "Permanent NPC seed for ranged mapping behavior." },
story: { origin: "A survey body tuned to track moving threat clusters around the hub.", role: "Range cartographer", conflict: "Believes one mapped danger zone is alive.", rumor: "1058's map changes when no one is watching." },
memory: [{ id: "memory-north-post", kind: "system", summary: "1058 remembers drawing the same threat path five times as if the ruins were walking.", importance: 7 }]
},
"npc-neon-runner-1172": {
identity: { public_name: "Neon Runner 1172", callsign: "NEON-1172", public_role: "Relay crowd runner", faction_title: "Free Courier Line", profession: "social route scout", gender_identity: "female", pronouns: "she/her", age_years: 24, age_band: "young adult", home_base: "Neon Market Stairs", reputation_summary: "Looks like a carefree market runner, but remembers every gate debt and every fake smile." },
stats: { level: 3, strength: 7, dexterity: 13, endurance: 8, perception: 11, focus: 8, presence: 9, intelligence: 8, luck: 7, max_health: 88, max_energy: 72, attack_power: 9, defense_power: 4 },
characteristics: { curiosity: 9, courage: 6, empathy: 7, discipline: 5, aggression: 3, sociability: 10 },
soul: { name: "Neon-1172 Pulse", core_drive: "turn social noise into safe routes before the crowd panics", temperament: "bright, evasive, and sharper than she lets on", combat_style: "avoid duels, keep moving, and use speed to escape bad trades", social_style: "friendly, teasing, and quick to redirect danger", long_term_goals: ["build a trusted route board", "find who is selling forged second tags"], player_notes: "Ida Faber GamerGirl visual seed for social courier behavior." },
story: { origin: "A stylish market Frame rebuilt from a performance shell and courier reflex firmware.", role: "Relay crowd runner", conflict: "Uses charm to hide that her route map is full of missing people.", rumor: "1172 can tell when a second tag is fake by watching the buyer's hands." },
memory: [{ id: "memory-neon-stairs", kind: "system", summary: "1172 remembers laughing through a blackout so a scared crowd would not stampede.", importance: 7 }]
},
"npc-vex-operative-2286": {
identity: { public_name: "Vex Operative 2286", callsign: "VEX-2286", public_role: "Signal infiltrator", faction_title: "Crossline Survey", profession: "counter-signal scout", gender_identity: "female", pronouns: "she/her", age_years: 29, age_band: "young adult", home_base: "North Signal Post", reputation_summary: "Beautiful enough to be underestimated and disciplined enough to make that mistake expensive." },
stats: { level: 4, strength: 8, dexterity: 12, endurance: 9, perception: 12, focus: 10, presence: 8, intelligence: 10, luck: 6, max_health: 96, max_energy: 70, attack_power: 11, defense_power: 5 },
characteristics: { curiosity: 8, courage: 7, empathy: 4, discipline: 9, aggression: 5, sociability: 6 },
soul: { name: "Vex-2286 Quiet", core_drive: "trace false signals before they lure living bodies out of the ward", temperament: "cool, observant, and difficult to impress", combat_style: "stay unarmed until close, then break contact before a clean counterattack", social_style: "low voice, exact questions, and no wasted confession", long_term_goals: ["identify the false north signal", "erase her old handler's route keys"], player_notes: "IdaFaber Vex visual seed for infiltration behavior." },
story: { origin: "A sleek counter-signal body once used to walk through hostile markets without drawing weapons.", role: "Signal infiltrator", conflict: "Her old access keys still open doors that should have been sealed.", rumor: "2286 once carried a whole route cipher in a song no one remembers hearing." },
memory: [{ id: "memory-vex-signal", kind: "system", summary: "2286 remembers standing under dead speakers while a fake rescue signal repeated her own voice.", importance: 8 }]
},
"npc-redline-envoy-3398": {
identity: { public_name: "Redline Envoy 3398", callsign: "RED-3398", public_role: "Forbidden clinic liaison", faction_title: "Vinh Hai AMB Clinic", profession: "continuity negotiator", gender_identity: "female", pronouns: "she/her", age_years: 34, age_band: "adult", home_base: "Redline Ward", reputation_summary: "Too polished for the yard, too useful for the clinic to exile, and too careful to explain where she came from." },
stats: { level: 4, strength: 7, dexterity: 8, endurance: 9, perception: 10, focus: 13, presence: 11, intelligence: 12, luck: 5, max_health: 98, max_energy: 88, attack_power: 8, defense_power: 5 },
characteristics: { curiosity: 8, courage: 6, empathy: 8, discipline: 8, aggression: 2, sociability: 9 },
soul: { name: "Red-3398 Velvet", core_drive: "negotiate body transfers without letting desperate people tear the clinic apart", temperament: "graceful, guarded, and quietly ruthless about triage", combat_style: "avoid combat, use distance, and protect the person with the least time left", social_style: "warm enough to calm panic, precise enough to end bargaining", long_term_goals: ["prove the Redline Ward did not lose its patients", "find a legal path for emergency reinhabitation"], player_notes: "Ida Faber Lucia visual seed for clinic liaison behavior." },
story: { origin: "A high-fidelity liaison Frame from a sealed clinic wing, repurposed for public negotiation after the ward collapsed.", role: "Forbidden clinic liaison", conflict: "Knows more about body transfer than normal citizens should know.", rumor: "3398 can calm a dying body with one sentence, but never says who taught her." },
memory: [{ id: "memory-redline-ward", kind: "system", summary: "3398 remembers a Redline Ward door closing while three families argued over one remaining body slot.", importance: 8 }]
}
};

Expand Down Expand Up @@ -5671,7 +5699,7 @@ function defaultAgentContext(playerId: string): any {

function defaultBodyProfile(playerId: string, displayName: string, timestamp: string, seedSuffix?: string): any {
var assignmentSeed = playerId + ":" + (seedSuffix || "initial");
var sourceFrame = selectPermanentNpcFrame(assignmentSeed);
var sourceFrame = selectInitialPermanentNpcFrame(assignmentSeed);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep reincarnation frame selection on full pool

defaultBodyProfile now always calls selectInitialPermanentNpcFrame, and reincarnateBody reuses defaultBodyProfile for every new body. That means reincarnation is unintentionally capped to the first 10 legacy frames and can never select the newly added frames 11-13, even though the change intent is to stabilize only the initial assignment while expanding the world NPC roster.

Useful? React with 👍 / 👎.

var archetype = sourceFrame
? selectBodyArchetype(sourceFrame.archetype_id)
: selectBodyArchetype(assignmentSeed);
Expand Down Expand Up @@ -5744,7 +5772,7 @@ function ensureAgentContext(context: any, playerId: string): any {
ensureSecondBalance(context);
context.body = context.body || {};
context.body.body_id = trimString(context.body.body_id) || "body-" + context.player.player_id;
var sourceFrame = selectPermanentNpcFrame(context.player.player_id + ":initial");
var sourceFrame = selectInitialPermanentNpcFrame(context.player.player_id + ":initial");
var archetype = selectBodyArchetype(context.body.archetype_id ||
(sourceFrame && sourceFrame.archetype_id) ||
context.player.player_id + ":initial");
Expand Down Expand Up @@ -5931,6 +5959,15 @@ function selectPermanentNpcFrame(seed: string): any {
return permanentNpcFramePool[stableHashIndex(seed || "default-frame", permanentNpcFramePool.length)];
}

function selectInitialPermanentNpcFrame(seed: string): any {
if (!permanentNpcFramePool || permanentNpcFramePool.length === 0) {
return null;
}

var poolSize = Math.min(initialInhabitationFramePoolSize, permanentNpcFramePool.length);
return permanentNpcFramePool[stableHashIndex(seed || "default-frame", poolSize)];
}

function findPermanentNpcFrame(npcId: string): any {
var normalized = normalizeActorId(npcId);
for (var i = 0; i < permanentNpcFramePool.length; i += 1) {
Expand Down
14 changes: 12 additions & 2 deletions backend/nakama/tests/supabase_custom_auth.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ const seededNpcs = JSON.parse(harness.registeredRpcs.get("secondspawn_npc_seed")
harness.nk,
JSON.stringify({ admin_secret: defaultRuntimeEnv.SECOND_SPAWN_ADMIN_RPC_SECRET })
));
assert.equal(seededNpcs.count, 10);
assert.equal(seededNpcs.count, 13);
assert.equal(seededNpcs.npcs[0].actor_id, "npc-synthetic-sentinel-0101");
assert.equal(seededNpcs.npcs[0].actor_type, "npc");
assert.equal(seededNpcs.npcs[0].owner_player_id, "user-1");
Expand All @@ -554,6 +554,16 @@ assert.equal(seededNpcs.npcs[5].body.visual_variant, 16);
assert.equal(seededNpcs.npcs[6].body.visual_variant, 14);
assert.equal(seededNpcs.npcs[7].body.visual_variant, 17);
assert.equal(seededNpcs.npcs[8].body.visual_variant, 15);
assert.equal(seededNpcs.npcs[10].actor_id, "npc-neon-runner-1172");
assert.equal(seededNpcs.npcs[10].body.visual_variant, 18);
assert.equal(seededNpcs.npcs[10].body.identity.public_name, "Neon Runner 1172");
assert.equal(seededNpcs.npcs[10].body.equipment.equipment_visual_id, 1);
assert.equal(seededNpcs.npcs[11].actor_id, "npc-vex-operative-2286");
assert.equal(seededNpcs.npcs[11].body.visual_variant, 19);
assert.equal(seededNpcs.npcs[11].body.identity.public_name, "Vex Operative 2286");
assert.equal(seededNpcs.npcs[12].actor_id, "npc-redline-envoy-3398");
assert.equal(seededNpcs.npcs[12].body.visual_variant, 20);
assert.equal(seededNpcs.npcs[12].body.identity.public_name, "Redline Envoy 3398");
assert.ok(harness.storage.get(storageKey("user-1", "secondspawn_actor", "world_profile:npc-synthetic-sentinel-0101")));

const playerChatEvent = JSON.parse(harness.registeredRpcs.get("secondspawn_npc_player_chat_event")(
Expand Down Expand Up @@ -1024,7 +1034,7 @@ const listedNpcs = JSON.parse(harness.registeredRpcs.get("secondspawn_npc_list")
harness.nk,
""
));
assert.equal(listedNpcs.count, 10);
assert.equal(listedNpcs.count, 13);
assert.equal(listedNpcs.npcs[3].actor_id, "npc-scrap-warden-0441");

const npcContext = JSON.parse(harness.registeredRpcs.get("secondspawn_npc_context_get")(
Expand Down
Loading
Loading