Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ memory.md

# Local asset drops (not wired into build yet)
npc-img/
npc-asset/

# Internal development docs (local only)
docs/ISSUE-LOG.md
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ Plan → scope → `pnpm agent:verify` → `pnpm agent:verify --e2e`(golden fl

| 路径 | 契约 | 回归 gate |
|------|------|-----------|
| `apps/web/src/game/entityLabels.ts` | Proximity 铭牌 12px + stroke 4px + shadow(ISSUE-038) | `pnpm verify:phase13` + UAT #6/#7 |
| `apps/web/src/game/entitySprites.ts` | `SPRITE_NAMEPLATE_Y` 在 sprite 头顶上方 | 同上 |
| `apps/web/src/game/entityLabels.ts` | Proximity 铭牌:宋体、`LABEL_SCALE` 字号、无描边/无底条、轻投影(Guardrail #57) | `pnpm verify:phase13` + UAT #6/#7 |
| `apps/web/src/game/entitySprites.ts` | `spriteNameplateY` + **全员 LPC**(Guardrail #106) | 同上 + `pnpm verify:phase6:move-only` |
| `apps/web/src/game/ProximityNameplate.ts` | ≤2 格 fade;speak/thinking 强制显示 | 同上 |
| `apps/web/src/hooks/useNpcChat.ts` | 方案 A composer busy;`onSpeakBusy` 清 `sendingNpcId`(ISSUE-037) | `uat:phase8` Test 4 + web test |

Expand Down
51 changes: 13 additions & 38 deletions apps/game-server/data/world/beginning-fields@v1/spawns.json
Original file line number Diff line number Diff line change
@@ -1,42 +1,17 @@
{
"defaultPlayerSpawn": { "lx": 34, "ly": 13 },
"npcSpawns": {
"npc-1": { "lx": 23, "ly": 10 },
"npc-2": { "lx": 9, "ly": 21 },
"npc-3": { "lx": 28, "ly": 27 }
},
"backgroundNpc": [
{
"id": "bg-villager-1",
"lx": 33,
"ly": 11,
"displayNameZh": "老张",
"wanderZoneId": "beginning-fields@v1:plaza",
"activityKey": "wandering"
},
{
"id": "bg-villager-2",
"lx": 30,
"ly": 15,
"displayNameZh": "小满",
"wanderZoneId": "beginning-fields@v1:plaza",
"activityKey": "wandering"
},
{
"id": "bg-villager-3",
"lx": 19,
"ly": 12,
"displayNameZh": "阿牛",
"wanderZoneId": "beginning-fields@v1:orchard",
"activityKey": "wandering"
},
{
"id": "bg-villager-4",
"lx": 25,
"ly": 25,
"displayNameZh": "巧娘",
"wanderZoneId": "beginning-fields@v1:pond",
"activityKey": "wandering"
}
"councilSpawns": [
{ "x": 9, "y": 21, "facing": "s", "maxRadius": 0 },
{ "x": 9, "y": 5, "facing": "s", "maxRadius": 0 },
{ "x": 23, "y": 11, "facing": "e", "maxRadius": 0 },
{ "x": 31, "y": 13, "facing": "w", "maxRadius": 0 },
{ "x": 17, "y": 13, "facing": "e", "maxRadius": 0 },
{ "x": 33, "y": 28, "facing": "n", "maxRadius": 0 },
{ "x": 20, "y": 26, "facing": "s", "maxRadius": 0 },
{ "x": 16, "y": 31, "facing": "n", "maxRadius": 0 },
{ "x": 27, "y": 27, "facing": "w", "maxRadius": 0 },
{ "x": 29, "y": 17, "facing": "s", "maxRadius": 0 },
{ "x": 5, "y": 9, "facing": "e", "maxRadius": 0 },
{ "x": 17, "y": 22, "facing": "s", "maxRadius": 0 }
]
}
3 changes: 1 addition & 2 deletions apps/game-server/data/world/village-plaza@v1/spawns.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
{
"defaultPlayerSpawn": { "lx": 10, "ly": 20 },
"npcSpawns": {}
"defaultPlayerSpawn": { "lx": 10, "ly": 20 }
}
17 changes: 17 additions & 0 deletions apps/game-server/src/ambient/intent-fallback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,21 @@ describe("pickIntentFallbackReasonZh", () => {
const c = pickIntentFallbackReasonZh("npc-3", zone, "patrol", "wander");
expect(new Set([a, b, c]).size).toBeGreaterThanOrEqual(2);
});

it("returns npc-7 voice pool not npc-1 default for extended seats", () => {
const reason = pickIntentFallbackReasonZh(
"npc-7",
"beginning-fields@v1:plaza",
"socializing",
"wander",
);
const npc1Default = pickIntentFallbackReasonZh(
"npc-1",
"beginning-fields@v1:plaza",
"socializing",
"wander",
);
expect(reason).not.toBe(npc1Default);
expect(reason).toMatch(/对话|折中|缓和|平衡/);
});
});
50 changes: 49 additions & 1 deletion apps/game-server/src/ambient/intent-fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,58 @@ const NPC_ZONE_POOLS: Record<string, Record<string, string[]>> = {
plaza: ["看看广场里有什么新鲜事", "有事就当面讲明白", "不想错过碰面的机会"],
default: ["直来直去省得误会", "先把想法说清楚"],
},
"npc-4": {
orchard: ["今天想搞点有趣的动静", "说不定能黑出点乐子", "灵感来了挡都挡不住"],
plaza: ["广场越热闹越有意思", "看看谁能被逗乐", "超级有趣的对吧"],
default: ["无聊才是最大Bug", "先找点好玩的点子"],
},
"npc-5": {
orchard: ["想听听风里的哭声", "若能以歌声换片刻安宁", "生灵也需要被倾听"],
plaza: ["出来走走心会软一点", "想替弱小的人说句话", "和平比争吵更珍贵"],
default: ["别让眼泪白流", "温柔也能守住底线"],
},
"npc-6": {
orchard: ["得先算算这笔账划不划算", "情报比情绪更值钱", "每一步都要有回报"],
plaza: ["看看有没有可交换的筹码", "人脉也是资源", "互惠条款得先谈好"],
default: ["账本永远要厚实", "风险收益得算清楚"],
},
"npc-7": {
orchard: ["不妨换个角度想想", "或许能找到折中办法", "先听听各方难处"],
plaza: ["出来聊聊也许能缓和", "大家都还想继续对话", "平衡点往往藏在中间"],
default: ["别让局面彻底破裂", "和谈比翻脸划算"],
},
"npc-8": {
orchard: ["得先确认大家还安全", "让我看看有没有隐患", "守护比冒进更重要"],
plaza: ["出来走走顺便照看一下", "同伴平安才安心", "喝口热茶冷静一下"],
default: ["家园安稳是底线", "让我先挡在前面"],
},
"npc-9": {
orchard: ["这里要是更美就好了", "舒服一点灵感才来", "生活品质不能将就"],
plaza: ["广场气氛得再浪漫些", "沉重的事先放一放", "美与快乐才值得回味"],
default: ["太丑太累就回家躺平", "先让自己舒服一点"],
},
"npc-10": {
orchard: ["今天得找点刺激的", "不动起来浑身难受", "碰撞才证明自己活着"],
plaza: ["广场里谁不服来战", "这地方还不够痛快", "引擎轰鸣才像活着"],
default: ["冒险比安稳带劲", "先热热身再说"],
},
"npc-11": {
orchard: ["细节还得再抠一抠", "这里还有微米级隐患", "再完善一点就完美了"],
plaza: ["广场布局也得讲究", "粗糙提案是对未来的侮辱", "精密才经得起时间"],
default: ["差不多可不行", "瑕疵必须当场修正"],
},
"npc-12": {
orchard: ["那边说不定有新世界", "封条是给胆小鬼的", "说走就走才像冒险"],
plaza: ["广场只是探险起点", "未知比安稳更诱人", "下一段旅程在招手"],
default: ["探索权不能丢", "自由灵魂不该被拴住"],
},
};

function poolFor(npcId: string, zoneId: string): string[] {
const byNpc = NPC_ZONE_POOLS[npcId] ?? NPC_ZONE_POOLS["npc-1"]!;
const byNpc = NPC_ZONE_POOLS[npcId];
if (!byNpc) {
return NPC_ZONE_POOLS["npc-1"]!.default ?? ["心里有点事"];
}
const suffix = ZONE_SUFFIX(zoneId);
return byNpc[suffix] ?? byNpc.default ?? byNpc.orchard ?? ["心里有点事"];
}
Expand Down
8 changes: 0 additions & 8 deletions apps/game-server/src/ambient/schedule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,6 @@ describe("validateNpcSchedulesAgainstRegistry (T-16-01)", () => {
zones: Array<{ id: string }>;
};
zones.zones = zones.zones.filter((z) => z.id !== "orchard");
const spawns = bundle.spawnsByRegionId[BEGINNING_FIELDS_ID] as {
backgroundNpc?: Array<{ wanderZoneId: string }>;
};
for (const bg of spawns.backgroundNpc ?? []) {
if (bg.wanderZoneId === "beginning-fields@v1:orchard") {
bg.wanderZoneId = "beginning-fields@v1:plaza";
}
}
const registry = loadWorldRegistry(bundle);
expect(() => validateNpcSchedulesAgainstRegistry(registry)).toThrow(
/unknown zoneId beginning-fields@v1:orchard/,
Expand Down
21 changes: 10 additions & 11 deletions apps/game-server/src/ambient/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,34 @@ import { syncColyseusFromMap } from "../colyseus/bridge.js";
import { GameRoomState } from "../colyseus/schema.js";

describe("GameRoomState ambient schema defaults", () => {
it("defaults gameMinute to 360 and activity keys to idle", () => {
it("defaults gameMinute to 360 and schemaVersion to 2", () => {
const state = new GameRoomState();
expect(state.gameMinute).toBe(360);
expect(state.npc1ActivityKey).toBe("idle");
expect(state.npc2ActivityKey).toBe("idle");
expect(state.npc3ActivityKey).toBe("idle");
expect(state.schemaVersion).toBe(2);
expect(state.npcs.size).toBe(0);
});
});

describe("syncColyseusFromMap activity slots", () => {
it("writes npc1ActivityKey from map.npcs activityKey", () => {
it("writes npc-1 activityKey from map.npcs activityKey", () => {
const map = createDefaultRoom("default");
map.npcs[0]!.activityKey = "reading";
map.npcs.find((n) => n.id === "npc-1")!.activityKey = "reading";

const colyseus = new GameRoomState();
syncColyseusFromMap(colyseus, map);

expect(colyseus.npc1ActivityKey).toBe("reading");
expect(colyseus.npc2ActivityKey).toBe("idle");
expect(colyseus.npc3ActivityKey).toBe("idle");
expect(colyseus.npcs.get("npc-1")!.activityKey).toBe("reading");
expect(colyseus.npcs.get("npc-2")!.activityKey).toBe("idle");
expect(colyseus.npcs.get("npc-3")!.activityKey).toBe("idle");
});

it("defaults missing activityKey to idle", () => {
const map = createDefaultRoom("default");
delete map.npcs[1]!.activityKey;
delete map.npcs.find((n) => n.id === "npc-2")!.activityKey;

const colyseus = new GameRoomState();
syncColyseusFromMap(colyseus, map);

expect(colyseus.npc2ActivityKey).toBe("idle");
expect(colyseus.npcs.get("npc-2")!.activityKey).toBe("idle");
});
});
Loading