[功能] NPC:把城镇 NPC 画出来并可对话(凯恩 / 杰海因 / 赫拉铁力 / 拉祖克 / 尼拉塞克…) #6
Loading…
Reference in New Issue
No description provided.
Delete Branch "%!s(<nil>)"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
现象
acts.html的城镇里没有任何 NPC:罗格营地没有凯恩、鲁高因没有杰海因、库拉斯特没有赫拉铁力、哈洛加斯没有拉祖克/德蕾雅、尼拉塞克神殿里也没有尼拉塞克。src/scene/act-scene.ts里既没有 NPC 实体、也没有对话/任务代码。排查:数据在哪、卡在哪
① 摆放数据已经在包里(被算进"无美术成员")
samples/d2-packs/**/scene.json的对象列表里有 17 个 NPC:DCJEHRXR/XS0J它们都出现在打包器的"无美术成员"名单里(
JE (act 2) has no art members、DC (act 1) has no art members…),而它们的坐标、深度、mode 都在包里,可以直接用。② 卡在"美术基址"
这些行在 act 对象表里带
:m标记,含义是Base = /Data/Global/Monsters而不是/Data/Global/Objects(src/game/object-lookup-data.ts文件头写明;reference/opendiablo2/d2core/d2records/object_lookup_record_data.go:12/259/311/569/570/572就是这几行,Type: ObjectTypeCharacter、Base: "/Data/Global/Monsters")。而
src/game/objects.ts:76的OBJECT_ROOT固定是data\global\objects\,objectSpriteMember/objectCofMember都基于它拼路径 →DC/JE/HR/XR/XS/0J全都解析不到美术。美术本身在归档里是有的(
samples/d2/listfile_113c.txt计数):结构与对象完全一样:
COF+TR/HD/…组件 DCC,lit装甲类、hth武器类、NU/WL模式。也就是说接上基址后,src/game/character.ts那套 COF+DCC 合成可直接复用。③ 另一条路:DS1 的"怪物/NPC 刷新点"被主动丢弃
src/game/objects.ts:461 resolveDs1Object():objectType !== OBJECT_TYPE_OBJECT(2)时直接返回{ kind: 'monster', artless: true };scripts/pack-act-assets.ts(对象循环里)if (resolved.kind === 'monster') continue—— 这类摆放根本不进包;scripts/port-object-lookup.ts:30注明只移植ObjectTypeItem,ObjectTypeCharacter(怪物/NPC 行)明确未移植。(当前 62 张图里的对象 DS1
type全是 2,所以 NPC 是以"对象"形态进来、再卡在基址上的;一旦以后烘随机迷宫/野外或别的关卡,type=1 的刷新点会走上面这条丢弃路径。)④ 对话/任务的地基在沙盒页,但没接到真实 NPC
src/scene/map-scene.ts(map.html沙盒)已经有一整套 NPC 脚手架:TALK_RADIUS = 80(第 105 行)、DEMO_NPCS回退 NPC(第 110 行,"Elder" + offer/progress/done 三阶段台词)、任务表DEMO_QUESTS、npcEntities在出生点附近找空地摆放(第 470-483 行)、T键取最近 NPC 对话(第 567-580 行)、把 NPC 按同一画家序入队绘制(第 755-767 行,用的是夹具美术actorGroups[2]+ 金色 tint)、并把state.npcs/state.npcsNear发布给自动化检查。缺的是:act 页完全没有这套东西,而且沙盒里那套用的是夹具精灵与自造台词,不是真实 NPC 的摆放/美术/名字。
⑤ 名字要另找来源
包里这些 NPC 的
name是Dummy(DC/HR/XR)或jerhyn(JE)——来自Objects.txt行,而 NPC 的名字其实在MonStats.txt/string.tbl(object_lookup_record_data.go的行里另有MonstatsTxtId字段)。另外HANDOVER.md已注明本仓库的.tbl解码器尚未对真文件校准(中文场景名是手工整理的level-names-zh.ts),所以 NPC 名字要么先走手工表,要么先把 tbl 修准。需要的工作
Base(/Data/Global/Objectsvs/Data/Global/Monsters)纳入成员解析(src/game/objects.ts的objectSpriteMember/objectCofMember/resolveObjectArt),并把:m标记从object-lookup-data.ts一路带到调用方;kind === 'monster'的分支要区分"NPC(有名字要画)"与"怪物刷新点(本项目暂无怪物)",前者进包;NU首帧 → 锚点 → 按depth入画家序),朝向按 #3 的方向口径;NPC 不参与hp/可破坏体系,也不该被攻击;map-scene.ts的TALK_RADIUS+T键 + 三阶段台词/任务钩子搬到 act 页并接到真实 NPC 上(真实台词/任务文本来自string.tbl与任务表);tbl修准后切真数据);scripts/browser/checks/增加断言(NPC 数、最近 NPC 距离、按 T 后对话行非空);发布状态里加npcs计数。(关联:#5 对象层渲染是前置;#3 决定 NPC 的朝向是否正确。)
Issue #6 深度排查分析与解决步骤全景方案
经过对数据表结构、资源解析管线、资产打包器以及场景渲染器的系统性排查,现将城镇 NPC(凯恩、杰海因、赫拉铁力、拉祖克、德蕾雅、尼拉塞克等)在
acts.html中缺失的根本原因分析与具体解决步骤汇报如下。一、根本原因排查与深度剖析
1. 美术资源基址硬编码(Root Path Hardcoding)
has no art members(例如DC (act 1) has no art members,JE (act 2) has no art members),导致输出到samples/d2-packs/**/scene.json中的这 17 个 NPC 对象member: null,frame: null。src/game/objects.ts:76中,根路径硬编码为: 无论是objectCofMember还是objectSpriteMember,均基于OBJECT_ROOT拼装路径。src/game/object-lookup-data.ts中,这 11 条 NPC 记录均带有:m后缀(如110:385:DC:NU:d1:m,16:121:JE:NU:d1:m等),表示其资源基址在data\global\monsters\而非data\global\objects\。src/game/object-lookup.ts的parseAct正确将:m解析为了baseIsMonsters: true,但该标志从未被传递至resolveObjectArt、objectCofMember或objectSpriteMember。scripts/pack-act-assets.ts:338中,打包器建立的objectMembers字典仅扫描了OBJECT_PREFIX = 'data\\global\\objects\\',完全漏掉了data\global\monsters\,导致 NPC 即使查找 token 也必定返回空(dirs === undefined)。2. 打包器与对象解析对 DS1 怪物刷新点的过滤丢弃
type = 2(作为 Object 摆放)的 NPC,而任何以type = 1(怪物/NPC 刷新点)摆放的实体被完全抹去。src/game/objects.ts:470 resolveDs1Object():当objectType !== OBJECT_TYPE_OBJECT (2)时直接返回{ kind: 'monster', artless: true }。scripts/pack-act-assets.ts:495:遇到resolved.kind === 'monster'直接continue略过。scripts/port-object-lookup.ts:30注明目前只移植了ObjectTypeItem(3,615 条),ObjectTypeCharacter尚未移植。若后续支持非预设关卡或野外地图,以type = 1放置的 NPC 会在此阶段直接丢失。3.
acts.html(src/scene/act-scene.ts) 渲染层与实体层断层acts.html画面中没有任何对象渲染,只有地面、墙体、屋顶和玩家。src/scene/act-scene.ts中,MapRuntime仅将objects: scene.objects.length作为一个只读数字存储用于 HUD 统计,完全没有将scene.objects解析为运行时实体。drawFrame中,仅绘制了:floors(地面)walls[0..insertAt](前层墙)character(玩家)walls[insertAt..length](后层墙)roofs(屋顶)根本没有对象(Objects)与 NPC 的绘制阶段。
4. 对话系统与交互 UI 仅停留在沙盒页
T无反应,页面没有#dialog容器。src/scene/map-scene.ts(沙盒)虽然已经构建了TALK_RADIUS = 80、键盘T键触发、QuestLog三阶段对话逻辑(offerLines,progressLines,doneLines),但该套逻辑完全未引入src/scene/act-scene.ts。actorGroups[2]+ 金色 tint)与DEMO_NPCS,并未对接真实 NPC 数据。5. NPC 名称与文本来源断层
Objects.txt中这些 NPC 的名字为占位符(Dummy)或内部小写代号(jerhyn)。MonStats.txt中,并通过名字字符串引用string.tbl。.tbl解码器尚未对官方真实.tbl文件进行全面校验(场景名也是通过level-names-zh.ts静态映射实现)。二、分步解决步骤与实现指南
为了在
acts.html中完整呈现城镇 NPC 并实现可交互对话,建议按以下 6 个步骤实施:步骤 1:修复美术资源基址分流(Objects vs Monsters)
src/game/objects.ts:objectCofMember与objectSpriteMember,接受baseIsMonsters?: boolean参数并使用对应前缀。ObjectArtRequest接口,增加readonly baseIsMonsters?: boolean。resolveObjectArt中,将request.baseIsMonsters传入objectCofMember与objectSpriteMember,并在备选模糊匹配中根据该标志选择prefix ={objectRoot(baseIsMonsters)}{token.toLowerCase()}\``。scripts/pack-act-assets.ts打包管线:data\global\objects\与data\global\monsters\的成员。objectMembers能够根据 token 和 mode 正确匹配到data\global\monsters\<TOKEN>\...下的 COF 与 DCC 文件。scene.json中保留baseIsMonsters、direction等必要元数据。步骤 2:建立静态 NPC 名称与身份字典
string.tbl校验完全成熟前,参照level-names-zh.ts在src/game/npc-names.ts中建立权威映射:步骤 3:NPC 实体独立建模与语义解耦
src/scene/act-scene.ts中,加载scene.objects时将 NPC 实体与普通物品分离:hp、破坏状态与开/关交互。kind: 'npc'invulnerable: true:不参与攻击判定,不受伤害,无法被玩家技能命中。NU(方向使用数据中的direction,如d1/d7)。步骤 4:集成深度排序与精灵绘制(Painter's Algorithm)
MapRuntime中增加npcs: readonly NpcDrawable[]。src/game/character.ts的 COF+DCC 合成渲染逻辑或加载预烘焙的 NPC 精灵帧。act-scene.ts的主渲染循环中:步骤 5:移植交互系统与对话 UI
acts.html对应的 HTML DOM 中添加对话框容器#dialog,并应用 Diablo II 风格半透明暗金边框样式。act-scene.ts中引入输入监听(T键 / 鼠标左键点击 NPC):Math.hypot(npc.x - player.x, npc.y - player.y) <= 80(TALK_RADIUS)时触发交互。QuestLog逻辑,分发对应 NPC 的引导与任务台词(offer / progress / done)。state.npcs、state.npcsNear、state.dialog供外部检测。步骤 6:编写自动化回归测试门禁
scripts/browser/checks/下新增verify-town-npcs.ts:T键能够呼出对话框并包含有效台词。npm run verify:all。已修复 —
feat(scene): 衔接 NPC 数据至引擎层及交互面板验证(7d8629c)src/game/npc.ts:token → 显示名解析(DC→Deckard Cain、JE→Jerhyn、HR→Hratli、XR→Larzuk、XS→Drehya、0J→Nihlathak)。scripts/pack-act-assets.ts此前在resolved.kind === 'monster'处直接continue,把 NPC 整个丢掉了;现已保留并单独存储其地图定位。src/game/objects.ts打通baseIsMonsters寻址 —— NPC 的美术资源根目录是monsters/而非objects/,这是之前渲染不出来的直接原因。scripts/browser/checks/*的断言(npcs、nearestNpcDistance、dialogOpen、KeyT)。验证(冷装): typecheck 0,全套件绿,shuffle 稳定。变异测试由我方另行选定(把
objects.ts:571与:593的 root 强制指回 monsters base),对应的非:m回归用例正确转红。追加修复:城镇 NPC 大面积缺失(罗格营地只显示凯恩)
现象
罗格营地只渲染出德卡·凯恩一人,瓦瑞夫、查希、卡夏、阿卡拉、基德以及罗格哨兵全部缺席。
根因(两层)
1.
scripts/pack-act-assets.ts中data\global\monsters\前缀长度被手写成 22,实际是 21。slice(22)把DC截成了C,导致所有 NPC 美术查找全部落空。改为从命名常量推导。2. DS1
type === 1(MonPreset.txt 索引的怪物/NPC 放置)在整个代码库中没有任何实现。resolveDs1Object对非 type-2 的条目一律返回kind:'monster'+ 空 token,打包脚本随即continue丢弃。实测 Act 1 城镇四个分区共 67 条 type-1 条目被静默丢弃(TownN1 15 / TownE1 18 / TownS1 16 / TownW1 18),只剩 103 条 type-2。今天能渲染出来的 6 个 NPC 恰好就是NPC_NAME_FALLBACKS里硬编码的那 6 个 type-2 对象——所以只看得到凯恩。修复
src/game/acts.ts:ActTables增加monstats/monpreset,loadActTables一并解析。src/game/objects.ts:导出MONSTER_ROOT,新增MonstersTable,resolveDs1Object增加 type-1 分支(按 act 过滤 MonPreset,数组下标即 DS1 id →Place→ monstatsCode美术 token)。Code在出厂表里大小写不一致(K9/k9、ja、6z、7i、7j),在解析源头统一规范化。scripts/pack-act-assets.ts:预建presetPlaceByAct/statsById索引;Place为空时抛错而非跳过(数组下标即 id,跳行会让后续每个 NPC 整体错位,与 22/21 是同一类 bug;实测五幕零空行,该分支仅作为响亮的失败护栏);无美术的放置点计数跳过而不是烤成空对象。src/game/npc.ts:补RG/CK/CW名称。验证
npx tsc --noEmitnpx vitest runnpm run pack:datanpm run verify:packs修复前基线:103 个实例 / 27 个场景。
罗格营地 townw1 实测名册(19 人):瓦瑞夫(WA)、卡夏(RC)、阿卡拉(PS)、查希(CI)、基德(GH)、凯恩(DC),外加母鸡 5、罗格哨兵 5、牛 3。四个分区分别为 16 / 19 / 17 / 19 人(修复前各 1 人)。
新增回归护栏:
tests/resolve-type1.test.ts——type-1 解析链路,含小写Code规范化用例。tests/packed-npc-art.test.ts——已打包的每个 NPC 都必须有member与frame,且同一 token 不得出现两种拼写。两个测试都做过变异验证:把
presets[objectId]改成presets[0]→ 前者 2 fail;把map.get(token.toUpperCase())退回map.get(token)→ 补测试前全绿(说明大小写修复原本零覆盖),补测试后转红。落地提交:
68c7890、ad7ced6、451816d、3cd4549、e180520,已推送main。追加:NPC 有立绘时不再叠加绿色占位方块
上一条修复把城镇 NPC 从 103 补到 889 之后暴露出一个渲染层面的旧问题:罗格营地一个分区会出现 19 个绿色方块糊在正常立绘上。
两套绘制路径各自独立判断——
buildPackRuntime用if (obj.frame)把 NPC 并入objectDrawables画真实立绘,而实体层对engine.npcEntities里每个 NPC 无条件再画一个 16×26 纯绿占位方块。只有凯恩一个 NPC 的年代看不出来,NPC 变多之后就藏不住了。没有在实体层加一个平行判断(那只会把同一个条件抄成两份、早晚漂移),而是抽出
hasPackedSprite放进src/game/npc.ts,两条路径共用同一个函数:立绘层为真时画图,占位层为假时画框,互斥由函数本身保证。该函数写成类型守卫,立绘层在同一次调用里就把frame收窄成非空,连多余的二次判空都一并消掉,不留第二个会漂移的判断。NpcEntity.hasSprite设为必填而非可选,将来新增放置点必须表态。GameEngine里那圈合成放置点显式标记false——它们没有预烘焙美术,方块对它们仍是唯一可见形式。金色名字标签两类都保留。验证:
tsc0 error;vitest433 passed / 2 skipped(24 文件),较上一条 429 增加 4 条。新增
tests/npc-placeholder.test.ts的变异验证:hasPackedSprite里的 null 判断 → 转红(exit 1)frame改成null→ 完备性断言转红(exit 1),还原后转绿诚实说明覆盖边界:
if (npc.hasSprite) continue这一行本身没有单元测试覆盖,act-scene.ts是浏览器入口模块,测试目录里没有任何文件 import 它。防线是结构性的——两条路径共用同一个断言函数,且hasSprite为必填字段。提交
29b3a4a,已推送main。用户已在浏览器中目视确认:罗格营地 NPC 完整显示,立绘正常,绿色占位方块已消失。
最终状态:NPC 实例 103 → 889,覆盖场景 27 → 117,未解析 0。
verify:packs1302/1302,vitest433 通过 / 2 跳过,tsc零错误。落地提交:
68c7890、ad7ced6、451816d、3cd4549、e180520、29b3a4a。关闭。