AGENTS.md

AlfFarmTale 项目专属的「小抄本」。AI Agent / 新人 / 未来的你进项目时,先读这一份。
最后更新:2026-06-21(第二轮整理:补全 npc4001~4003、修正 ID 段、补 MapCapture 路径、扩充 building/hold_effect 列表、修正 wood bug 说明)


0. 项目速览

  • 引擎:Godot 4.5 Forward Plus(Web 端切到 GL Compatibility)
  • viewport:480×270 像素风,window 1440×810,stretch/mode = canvas_items
  • 主场景res://scenes/main/main.tscn(标题/存档菜单)→ 点 NewGame / Continue / LoadGame 切到 Gamescenes/game/game.tscn
  • 导出平台:仅 Web(export_presets.cfg 一条 web preset)
  • 关键 Addons:Dialogic 2(对话)、PhantomCamera 2D(相机)、LimboAI(NPC 行为树)、imgui-godot(编辑器调试,已废弃)、awesome_custom_cursor、inventory_editor

Inventory ID 体系(按 Inventory.InventoryType enum 索引)

注意:enum 注释与实际 .tres 数据有出入,实际数据以下表「已用」列为准

Enum 索引千位 ID 段注释实际 .tres 数据.tres 数
Weapon (0)1000武器1001 sword1
Armor (1)2000防具0
Material (2)3000材料id 段有,但只有 medicine/wood.tres 占位(见 §13 bug)0
Seed (3)4000种子4001~40077
Food (4)5000食物5000~50078
Medicine (5)6000药品只有 wood.tres(id=3001,路径与 type 都不一致,见 §13 bug)1(错位)
Tool (6)7000工具7001 hoe / 7002 sprinkler / 7003 axe3
Crop (7)8000作物enum 注释说”作物 8000″,但实际所有 id=8001~8095 的物品 .tres 都是 type=8 (Building)0
Building (8)9000建筑实际用 8001~8095 段(type=8),共 92 个 .tres(id 8061/8062/8063 缺号)92

设计上的不一致inventory.gd 注释说”作物 8000 / 建筑 9000″,但 .tres 实际全用 8000 段(type=8 = Building)。改任何涉及 building 的代码前,先确认 id 段是 8000 不是 9000。详见 §13 BuildingResource.BuildingType enum 已废弃说明。

季节

  • TimeRecord.Season { Spring, Summer, Fall, Winter }
  • 月份 → 季节映射写在 time_record.gdSEASONS 常量里

NPC 类型(NPC.NpcType enum)

用途位置BT 树
wuning剧情 NPC(剧情内引用,无脚本类)
npc1001船夫(silver_mist ↔ village_01 转移)compoents/npc/npc1001/ai/trees/npc1001.tres
npc2001店主(village_01)compoents/npc/npc2001/ai/trees/npc2001.tres
npc4001家具店主(silver_mist)compoents/npc/npc4001/ai/trees/npc4001.tres
npc4002食材种子店主(silver_mist)compoents/npc/npc4002/ai/trees/npc4002.tres
npc4003武器杂货店主(silver_mist)compoents/npc/npc4003/ai/trees/npc4003.tres

⚠️ NPC.NPC_TYPE 字典(character.gd 的 to_shopNPC.NPC_TYPE.get(npc))必须含每个 NpcType 值,否则 to_shop 会 push_error。新增 NPC 务必同步。


1. 体力系统(energy)

体力分两种来源,只能追回其中一种

来源行为持久化字段
跑步(按住 Shift)消耗可自然回复energy_current 直接操作
工具操作(锄地/浇水/砍树/播种/建造)消耗永久扣除,不可自然回复energy_floor 累加
  • CharacterResource.energy_max = 40energy_current = 40.0energy_floor = 0.0
  • 跑步回复封顶:min(energy_max - energy_floor, current + RECOVER_RATE)
  • 跑步机制详细参数见 compoents/character/character.gd:9-18(RUN_COST_PER_SEC 等常量)
  • 喘气期:体力归零后 2 秒内 Shift 无效(_panting_timer

体力归零拦截(2026-06)

energy_current <= 0.0 时禁止所有消耗体力的工具操作(锄地/浇水/砍树),并取消 range 显示。 触发”累瘫”对话提示玩家吃东西/睡觉。

  • 判定阈值 <= 0.0:与 character.gd:99 触发喘气期的边界对齐,与 consume_food 满体力守卫风格一致
  • 单点拦截range_prompt.deal_hold_action() 在调 play_effect 前守卫一次即可,7001/7002/7003 一行不用改
  • HoldEffectResource.consumes_energy() -> bool:新虚方法,默认 false;锄地(7001)/浇水(7002)/砍树(7003)override 返回 true。种子/建造/剑/食物默认 false,不被拦截
  • 未来加新消耗工具(如 hammer / pickaxe):只需 override consumes_energy() -> true,不用改 range_prompt
  • range_prompt.update_visible() 加分支:体力 <= 0 且 current_effect.consumes_energy()visible = false
  • range_prompt.deal_hold_action() 加守卫:体力 <= 0 且 current_effect.consumes_energy()return(不播动画、不 add_farmland / to_watering / to_tool / breakable.hit)
  • 抛光(hold.gd:update_effect):切工具时若 current_effect.consumes_energy() && current <= 0不调 show_range,避免切到工具瞬间闪一帧 range
  • statusinfo._on_consume_energy*if current > 0 守卫保留:作为 last-line defense 防 bug,不依赖它做主拦截(它只能挡”扣能量”,挡不住”动作”)
  • 喘气期 vs 工具拦截独立:喘气期只挡 Shift 跑步(character.gd:138),工具拦截是另一条独立通道

累瘫对话(Alf_stamina_deplete,2026-06)

  • timelineres://dialogic/timeline/Alf_stamina_deplete.dtl(bubble 风格,两行提示:四肢软软的 + 肚子空空的)
  • 触发点statusinfo.gd_process 里检测 _last_energy > 0.0 && energy_current <= 0.0 边沿
  • 触发模式:从正数跌到 <= 0 那一帧触发一次;之后即使 current 仍 <= 0 也不再触发;要等 current 重新 > 0 后再次跌到 0 才再触发(避免喘气期反复弹)
  • 首次帧跳过_is_first_frame 守卫避免加载一个 current=0 的存档时误弹
  • 调用模式:跟 wuninghouse_cutscene 等剧情脚本一致 — Dialogic.start() + character.on_dialogue_timeline_started()(设 is_dialogue=true)+ await Dialogic.timeline_ended + character.on_dialogue_timeline_ended()
  • 对话期间不能操作:因 is_dialogue=truecharacter.gd:138 的 direction 被清空(sprint 失效)、distribute_input 的 right_mouse/left_mouse 不触发;玩家看完对话再恢复

能量条 UI(2026-06)

  • 旧版:体力 <= 0 时 EnergyInner.visible = false(外框在、内框整个隐藏),玩家看不到”累瘫”状态
  • 新版:始终显示 EnergyInner,width 按 current/max 撑开;<= 0modulate = Color(0.55, 0.35, 0.35)(暗红色)提示玩家没力气了
  • 仍在 compoents/character/children/attribute/children/statusinfo/statusinfo.gd 里维护

character_resource.gd:energy_floor 默认值坑(2026-06 修复)

历史 bugstructs/character_resource.gdenergy_floor 原本默认值是 1(不是 0.0),同时 resources/character_resource.tres 加载时未声明 energy 三个字段 → 全部走默认 → 开局就被 update_stamina 1 秒后 clamp 到 ceiling = max - floor = 40 - 1 = 39。UI 用 current / max = 39/40 = 0.975 显示,玩家看到能量条差一点点不满,以为是 bug。

修复(2026-06)

  • energy_floor 默认值改成 0.0
  • resources/character_resource.tres 显式声明 energy_max = 40 / energy_current = 40.0 / energy_floor = 0.0(之前三个字段全靠代码默认,加载顺序敏感)

为什么是 floor 模型设计缺陷energy_floor 表达”已永久扣除的体力”(AGENTS.md §1),开局应该是 0(玩家没做过任何消耗动作)。默认 1 意味着玩家凭空被扣了 1 点体力——没游戏逻辑支撑。

Web 模式额外注意WebSaveAdapter.SEED_RUNTIME_PATHS:29character_resource.tres cold-start 拷到 user://runtime_state/ 做镜像,只 seed 一次。改源 .tres 后旧镜像仍是 floor=1,必须调 WebSaveAdapter.reset_runtime_state() 清掉镜像才能让改动生效。

未来加「食物/睡觉恢复体力」时

  • 不要直接 +energy_current
  • energy_floor = max(0, energy_floor - N),让「自然回复上限」自动上移
  • 这样与现有封顶逻辑一致,UI 不会跳值
  • 完整范式character.gd:consume_food,2026-06 实现):
  1. energy_floor = max(0, energy_floor - restore_value)
  2. ceiling = energy_max - energy_floor(新封顶)
  3. energy_current = min(ceiling, energy_current + restore_value)(立刻拉到新封顶,给玩家即时反馈)
  4. 扣 1 个物品,count → 0 时 slot.current.clear() + slot.update_display()
  • 不变量energy_current ≤ energy_max - energy_floor(即 current ≤ ceiling)。floor 模型任何操作都维持这个
  • 满体力判定current >= ceiling不是 current == max)。floor > 0 时 current 永远到不了 max
  • 满体力守卫consume_food:411-4192026-06 修复):旧版用 if current >= ceiling: returnfloor > 0 / current = ceiling 时错误 return(玩家视觉看到能量条 12.5% 觉得”快没电”,右键 pumpkin 却被守卫拦掉,物品也不扣,体验上是”吃东西没反应”)。新版改为预测算法:吃完不能涨 current 就不吃。计算 future_floor / future_ceiling / future_current = min(future_ceiling, current + restore)if future_current <= current: return。涵盖:
  • floor=0 / current=ceiling=max:吃完 future_current == current → return → 不吃(满 max,没收益)
  • floor=35 / current=5 / ceiling=5:吃完 future_floor=25 / future_ceiling=15 / future_current=15 > 5 → 不 return → 吃 → current 涨到 15 ✓
  • floor=40 / current=0 / ceiling=0(累瘫对话期间):吃完 future_floor=30 / future_ceiling=10 / future_current=10 > 0 → 不 return → 吃 → current 涨到 10 ✓
  • 注意:对话期间 is_dialogue=truecharacter.gd:144 的 right_mouse 入口被 !is_dialogue 守卫拦掉,对话期间不能吃东西——必须等 Dialogic.timeline_ended 后才能右键
  • 辅助方法
  • character.get_energy_ceiling() -> float — 返回当前封顶 max - floor
  • character.clamp_energy_to_ceiling() — 把 current 强制 ≤ ceiling(防 stale current 溢出)
  • 未来加 max 变化时(提升/降低 max 的 buff 或升级):先调 clamp_energy_to_ceiling() 再改 max,否则 max 降后 current 可能 > ceiling 溢出
  • 恢复量写在 Inventory 上@export var restore_value: int = 0,跟 terrain_id 同款(AGENTS.md §6)。Food/Medicine 类型自己填值,其他类型保持 0
  • 入口:手持 Food 类物品按鼠标右键(right_mouse action),character.gd:distribute_inputconsume_food()。门禁同 walk:is_interfact_state() && !is_dialogue && !LoadingManager.is_loading && !UtilsManager.is_shop
  • food 现状resources/inventory/food/*.tres,已填 restore_value):
  • apple 5 / carrot 5 / tomato 6 / eggplant 6 / cabbage 8 / wheat 4 / pumpkin 10 / cucumber 7
  • 价格 2~4,恢复量 4~10,与价格正相关

各信号能量消耗(statusinfo.gd:_on_consume_energy*

信号触发动作扣 energy加 floor
hoe_target锄地 / 收获11
watering_target浇水11
axe_target砍树 / 砍建筑11
seed_target播种0.50.5
building_target建造0.50.5

floor 累加用 min(energy_max, energy_floor + N) 封顶,避免异常突破上限。


2. 剧情状态机:cutscene_trigger 边界

game_resource.cutscene_trigger 是核心剧情状态,目前已知边界

trigger含义影响
0初始/未进入剧情禁用 Shift(见 §3)
1~4剧情引导期禁用 Shift、雨声播放、wuning BGM (Silent Rain Narrative)
5incident 视频播放视频中 BGM + 雨声硬停(pause_bgm_immediately / pause_rain_immediately
6leave 对话(玩家在 wuning 出口附近)看完对话 → trigger=7
7玩家出门后进入 SilverMist 时 resume BGM(silver_mist 自己的音乐)
>7自由探索Shift 启用、正常 BGM

各 trigger 在哪里被改写(owner map)

文件改写时机
wuninghouse_alf.gd:emit_cutscene_trigger()→ 2cs_pr_pro00_alfroom AnimationPlayer call_method track 调
wuninghouse_cutscene.gd:_on_character_speak01_entered2 → 3wuning_house_pro00_02 对话结束后
wuninghouse_cutscene.gd:_finalize_video_playback3 → 4incident 视频结束/被 Skip
wuninghouse_cutscene.gd4 → 5wuning_house_pro00_04(chest key 在 wuning_e 区域)+ Sunny 动画 + 发布任务 101
wuninghouse_alf.gd _process5 → 6sleep area body_entered + Input.is_action_just_pressed("pick") + 「Sleep」动画播完 + 日期+1/7:00 + load_level(WuningHouseCorridor)
wuninghouse_cutscene.gd6 → 7wuning_house_pro00_06(leave area)+ pause_bgm_immediately()
silver_mist_cutscene.gd:_process7 → 8进入 over_area,显示 %Over 结局面板

WuningHouseAlf sleep area 流程细节(trigger 5→6)

  • wuninghouse_alf.gdvar in_sleep_area: bool = false
  • _on_sleep_area_2d_body_entered 仅在 cutscene_trigger == 5 时设为 true + %Sleep_f.visible = true
  • _processin_sleep_area && cutscene_trigger == 5 && Input.is_action_just_pressed("pick") 触发整段:
  1. character.is_dialogue = true
  2. animation_player.play("Sleep") + await animation_finished
  3. cutscene_trigger = 6
  4. 日期 +1(month wrap / 12 月 → Winter 重置)
  5. hour = 7 / minute = 0
  6. load_level(WuningHouseCorridor) + 恢复 UI(statusinfo / menu / time_record)
  7. character.is_dialogue = false

⚠️ 改 trigger 边界前必须先查完整剧情流程,不要随便改数字。


3. 角色输入限制

Sprint(Shift 奔跑)

  • 文件:compoents/character/character.gd:97
  • 判断:is_run = Input.is_action_pressed('run') and CutsceneManager.can_sprint() and _panting_timer <= 0.0
  • 业务规则集中:CutsceneManager.can_sprint()cutscene_trigger > 6
  • 改 sprint 启用条件时不要直接在 character.gd 加 if,加到 CutsceneManager

喘气期

  • 体力归零 → _panting_timer = 2.0,期间 Shift 无效
  • 喘气期过了就允许再按(不会因为体力低就死锁,因为 is_run=true 后会继续扣体力再触发)

输入动作清单(project.godot[input]

Action物理键用途
walk_up/down/left/rightW/S/A/D八向行走
runShift奔跑
bag_switchTab/B开关背包面板(Menu 互斥)
select_1..9数字键 1-9手持栏选格
pickF拾起 / 拾取对话 / 睡眠触发
left_mouse鼠标左键主操作
right_mouse鼠标右键吃手持 Food 类物品(consume_food
dialogue / chestE推进对话 / 开关宝箱
task_switchX开关任务面板
acceptSpace接受
discardQ丢弃
page_up / page_down鼠标侧键 4/5手持栏翻页(带 50ms 冷却)
shift_click_transferShift + 鼠标左键背包↔手持↔宝箱快速转移
escEsc暂停菜单

鼠标焦点(MouseFocus)

  • 每帧更新:indoor / InterfaceNode.above / 对话中时隐藏;否则 UtilsManager.transform_position_tile(get_global_mouse_position()) 网格吸附
  • 跑 + 点击放树(is_placing_tree)的 ImGui 入口已废弃(见 §12)

4. 音频管理约定

雨声(CutsceneManager)

  • _rain_active + _manually_paused 两个状态字段
  • 公开方法:
  • stop_rain() — 10 秒渐变停(自然)
  • pause_rain_immediately() — 立刻硬停 + 置 _manually_paused=true会被 _process 拉回去重启)
  • resume_rain() — 复位 _manually_paused + 渐入
  • fade_stop_rain(duration) — 自定义时长渐停
  • _process 早返if _manually_paused: return(line 20-21),必须保留

BGM(SoundManager)

  • 状态字段:_bgm_paused(手动暂停标志)、_current_level_name(当前关卡名)、_bgm_session_id(自增 session id)
  • 公开方法:
  • pause_bgm_immediately() — 停 BGM + 置 _bgm_paused=true + 自增 _bgm_session_id(让旧协程退出)
  • resume_bgm() — 复位 _bgm_paused + 调 play_level_audio_by_name(_current_level_name) 重新播放
  • play_level_audio_by_name 早返if _bgm_paused: ... 早返前必须记录 _current_level_name,否则 resume 会播错关卡
  • 特殊 BGM:wuninghouse / wuninghouse_alf / wuninghouse_corridor 三个 level_name 走 _play_silent_rain_narrative()(加载 assets/sound/bgm/Silent Rain Narrative.mp3

BGM 分发规则(按 level_name)

back_level_audio_list 配置在 managers/sound_manager/sound_manager.tscn,结构:{ audios: [{audio, volume}, ...], level_name: String }

场景类型BGM 来源
birth列表里注册了 birth,播 4 首专属 BGM
village_01列表里注册了 village_01,播 2 首专属 BGM
wuninghouse / wuninghouse_alf / wuninghouse_corridor_play_silent_rain_narrative() 循环
其他未注册场景(SilverMist / OldPostRd / AdventurerGuildRoom_01 等)fallback 到 _play_mixed_outdoor_audio_loop():从 village_01 + birth 的 audios 里合并随机选播(6 首)
  • 当前 back_level_audio_list 只配了 birth + village_01 两条 — 大部分场景走 fallback 混合池
  • 新增场景专属 BGM:在 .tscn 里照 birth 那条格式加一行 level_name: "<场景名>" + audios 数组即可,无需改 .gd
  • 核心循环已抽到 _play_audio_pool_loop(audio_pool: Array, session_id)play_level_audio_loop / _play_mixed_outdoor_audio_loop 都委托给它

BGM 协程生命周期(session id 防 race condition,2026-06)

  • bug 场景:旧设计用 is_back_audio_running 全局标志 + pending_level_name 接力机制。协程在 await timer(10-30 秒)期间,新协程启动 → is_back_audio_running 被新协程重设 True → 旧协程醒来不退出 → 两个协程抢同一个 back_audio_player → BGM 一首没播完就停或诡异切换
  • fix:每次 play_level_audio_by_name / pause_bgm_immediately / stop_level_audio 自增 _bgm_session_id。协程启动时记录 my_session = _bgm_session_id,每次 await 后检查 _bgm_session_id != session_id 则 return 退出
  • AudioStreamPlayer.stop() 不 emit finished(Godot 4 已知行为),所以 pause_bgm_immediately 必须自增 session id——否则协程卡在 await back_audio_player.finished 永远不会被唤醒,下次别的协程让 player emit finished 时旧协程也醒来 → race condition
  • pending_level_name 字段已删除:原本”等当前首播完再切换”的设计已被 session id 机制取代——切关直接走,新协程的淡入淡出会处理切换平滑度

触发点(game.gd load_level 异步加载完成时)

  • level_loaded.emit() 之后按 LevelType 分发:
  • LevelType.Birthtask.try_complete_by_signal(&"Reach_Birth")
  • LevelType.SilverMist && cutscene_trigger == 7SoundManager.resume_bgm()
  • LevelType.WuningHouseAlf && cutscene_trigger == 1 → 播 cs_pr_pro00_alfroom 开场过场

随机音效池

动作音效
锄地hoe_01 ~ hoe_03(3 档随机)
浇水watering_01 ~ watering_04(4 档随机)
砍树axe_01 ~ axe_04(4 档随机)+ camera.shake
走路(草地)walk_at_grass_01 ~ walk_at_grass_07(7 档随机,volume -16)
走路(石头/路)walk_at_stone_01 ~ walk_at_stone_05(5 档随机,volume -10)
宝箱开chest_open_01 ~ chest_open_03(3 档随机)
宝箱关chest_close_01 ~ chest_close_03(3 档随机)

5. 任务系统扩展

场景触发完成任务

  • TaskResource.done_signal: StringName — 任务自己填信号名(如 &"Reach_Birth"
  • TaskResource.complete() — 外部调,把任务标 done + emit done_triggered
  • Task.try_complete_by_signal(sig_name) — 遍历任务列表找匹配的,标完成 + 自动 over(调 to_over_task:发奖励 + 移到 over_list)。设计意图:done_signal 触发的任务没有 NPC 交接环节,场景到达 = 完成 + 立即发奖励。不要手动再调一次 to_over_task
  • 新增「场景到达完成任务」的标准做法
  1. game.gd level_loaded.emit() 之后加 if level == X: task.try_complete_by_signal(&"Y")
  2. 在对应 TaskResource.tresdone_signal = &"Y"
  3. 不要写在 _process 里(每帧触发会出问题)
  • 如果未来需要「场景到达标完成,但奖励由 NPC 后续交接才发」:那是另一种任务模型,不要走 done_signal,应该用 CollectTaskDetailResource + NPC dialogic signal over_task

Task UI 的两个 signal

  • Task.detail_closed — Detail 面板(右侧详情)关闭
  • Task.task_closed整个 Task 主面板关闭(包括 Detail + 列表)
  • 剧情脚本里要「展示任务给玩家看」就 await task.task_closed不要 await detail_closed

触发顺序坑

  • Task.menu.on_click_item("Task")switch() 是带 0.15s 动画的,不会 await
  • 紧接着调 on_click_task_item 选任务时,主面板还没完全显示,要加 await get_tree().create_timer(0.5).timeout 兜底

TaskState enum

enum TaskState { Un_Get, Un_Done, Done, Finish }
  • Un_Get:未接取(get_task_from_id 返回 null)
  • Un_Done:已接取,未完成(task.done == false
  • Done:完成未交接(task.done == true
  • Finish:完成并交接(over_list 里有 id)

任务刷新触发点

  • attribute.task.refresh_task_state(npc) 在对话开始/结束时调(character.gd:on_dialogue_timeline_started/ended),重新检测 CollectTaskDetailResourcehas/count

6. 项目结构性坑(已踩过的)

通用

  • _process 里放「持续条件触发的对话」是反模式 → 玩家在区域里一帧触发一次(前面 leave 对话踩过)。信号回调(_on_*_body_entered)才是「边沿触发」的正确写法
  • .tres 里 StringName 字段的标准写法:done_signal = &"Name"
  • Godot 4 用 UID 跟踪资源,重命名资源文件 UID 不变,引用方零改动
  • 找资源引用时必须覆盖 3 种形式:文件路径、UID、字符串/参数里的 ID(DTL 对话文件也得搜)
  • CanvasLayer 父节点 modulate 不影响子节点
  • Task.to_add_task(id) 不去重task.gd:201-204 直接 push_back),重复 add 会让任务列表里出现两个一样的任务。涉及「发完奖再启动 tips」这种二次触发的剧情脚本,必须用 if not get_task_from_id(id): to_add_task(id) 守卫

NPC

  • 新建/复制 NPC 子场景必须显式绑 interact = NodePath("Interact"):基类 compoents/npc/npc.tscninteract: Control 声明为 @export,子场景(npc<id>.tscn)的根节点必须带 node_paths=PackedStringArray("interact") + interact = NodePath("Interact") 赋值。漏了之后 BT 首帧跑「不在范围内」序列 → ai/tasks/set_interace_visible.gd:6 执行 npc.interact.visible = visibleinteract 是 null → 抛 “Invalid get/set on a null instance”,表现是「一进 level 立刻崩」。schedule_list_resource 是可选的npc.gd:39if !schedule_list_resource: return 守卫,NPC 站桩不移动时不挂也不会崩)。复制 NPC 时检查 4 件套:scene 根节点 interact 绑了、BT 的 dialogue_name 指向自己的 dtl、dtl 里 [style]npc<id>_speaker: 都换成新 NPC、npc.name 在 silver_mist 实例化时如非 npc<id> 不影响(speaker style 不按 name 注册 character,只有 bubble style 用 npc.name.to_lower() 拼 dch)
  • NPC.NPC_TYPE 字典必须含每个 NpcTypecharacter.gd:to_shopNPC.NPC_TYPE.get(npc),缺失会 push_error。加新 NpcType 时务必同步加映射

Input / 异步

  • Input.is_action_just_pressed 边沿会被「动画 finished 回调里的 await」接住误触发。例如 page_gift 关闭流程:玩家点 left_mouse 关闭 → tween 150ms → finished 回调里 await 监听输入 → 刚才那次的 left_mouse.just_pressed 状态还没被消费(_process 已跑过那一帧),await 第一帧就命中 → 立刻关掉后续 tips。修法:在新的 await 循环前 await get_tree().process_frame 等一帧,让那一帧的 just_pressed 边沿在 _process 阶段被消费掉

Inventory

  • InventoryNode.holding_item 是 static dict:跨所有 bag/hold/chest 槽共享当前手持物品;MAX_STACK_SIZE = 64,half-split 是右击一半;DOUBLE_CLICK_INTERVAL = 0.3s
  • 背包工具提示延迟 1.2s:太快划过不闪面板,但用了 cancel timer 防 race
  • 合并规则:merge same-id stacks → drop 到首个空槽 → 否则 swap 回 holding_item

Typo(保留以兼容旧代码)

  • BuildingResource 拼写是 colums(不是 columns),ChestResource 也是 colums: int
  • SeedResource.groth_time_state 拼写是 groth(不是 growth),数组长度 = 帧数 – 1
  • Level.TileMapLayerContianer 拼写是 Contianer(不是 Container),Level.set_character_postion 也是 typo(不是 position)
  • Level.load_soild_cell() 拼写是 soild(不是 solid)
  • FellableBase.highlight_are 拼写是 are(不是 area)
  • Chest.animtion_player 拼写是 animtion(不是 animation)
  • Chest.ToOpen / ToClose 大小写不规范(不是 to_open)
  • ai/tasks/set_interace_visible.gd 拼写是 interace(不是 interface)
  • wuninghouse_corridor_wuningromm.gd 文件名拼写是 wuningromm(不是 wuningroom)
  • get_astar_path_to(start, end, callback)callback 参数当前是 dead code,函数体没用
  • FellableBase 的 occluder 字段名occluder_normal / occluder_stump / occluder_seasonal,季节贴图字段是 collapse_altas_*(typo:altas 不是 atlas)

Inventory / Building

  • Building 放置的 terrain id 必须在 Inventory.terrain_id 里手动声明,不要靠 id - 8001levels/level.gd:485-489)。TileSet terrain_set_0 顺序是美术按家具分类硬排的(fence/chest/bed/bonsai/book 在 0~8,book03/bowl/candle/chair/foods/paintings/plates/sofa/toilet/tv 段是另起 id),跟 inventory id 完全脱钩。如果用 id - 8001 算:fence/chest/bed 凑巧对上(0~2),但 paintings07 (id 8066) 会算成 65 → 命中 paintings10 的图;paintings10 (id 8069) → 68 → plates01。每个 building .tres 自己声明 terrain_id = N(对应 used/tileset/building/building.tresterrain_set_0/terrain_N),新增 building 时必须查 TileSet 拿正确的 terrain id 填进去,不能复制 id - 8001
  • Inventory.terrain_id 缺省值是 -1,只有 add_building 才会用(且只在 < 0 时才兜底回 id - 8001),其他 InventoryType(weapon/material/seed/food/tool/crop)不需要填。Inventory 字段顺序:structs/inventory.gd 里新字段加在最后
  • Inventory.restore_value 缺省值是 0:只 Food/Medicine 生效,其他类型保持 0;走 floor -= N 模型(详见 §1)
  • BuildingResource.BuildingType enum 已废弃(Fence/Chest/Bed 三项),add_building 实际按 terrain_id 索引而非 enum 引用。新代码不写这个 enum——保留仅为旧逻辑兼容
  • silver_mist 场景有 Building TileMapLayersilver_mist.tscn:112 [node name="Building" parent="TileMapLayerContianer" index="7"])。Level.building_layer 不为 null,可以正常 add_building / 砍建筑
  • 可破坏建筑走 Breakable 通用入口(2026-06 重构):斧子 / 未来其他工具都走 Breakable.hit(tool, at)不要在 Level 上加新的特例方法。每个 level 实例维护 var breakables: Array[Breakable]add_building 时挂 Breakable,load_buildings 时按 cell_to_inventory_id 重建。配置表集中在 Level 顶部两个 const(BREAKABLE_SCRIPT_BY_INVENTORY_ID / BREAKABLE_HITS_NEEDED_BY_INVENTORY_ID)——加新特例改这两张表,不动 inventory 资源。fence_axe_count 字段已废弃,新代码不写(保留仅为旧 .tres 兼容)
  • 加新 building 物品务必同步登记 _INVENTORY_ID_TO_FILENAMElevels/level.gd):这是 load_buildings 存档重建 Breakable 的 cell_to_inventory_id → .tres 文件名 反查表。漏登记时 load_buildingspush_warning + skip 该 cell——后果是新放能砍、存档重载后就砍不动了(因为没挂 Breakable 子节点,get_breakable_at 找不到)。新 building .tres 加完后务必同步加一行(目前实际 92 项,缺号 8061/8062/8063;后续按需补。这是设计上的 hard-coded 映射——目前没有自动扫目录机制)
  • tellable 可破坏装饰 tile(2026-06 新增,2026-06 改为持久化):场景中 TileMapLayer 加 metadata/tellable = true(如 Afforest 层),Level._ready 扫描该层所有 tile 挂 BreakableTellable,锄头 1 下擦 tile。已存档BuildingResource.tellable_broken_cells 按 layer_key 分组存被擦 cell)——reload 时按存档列表反向 erase_cell 还原擦掉状态。只走 Breakable.hit() 通用入口,但只有锄头(_tool == &"hoe")能命中——BreakableTellable.hit() 自身做工具守卫,斧头走 _tool != &"hoe" silent skip
  • 美术辅助层约定(visible=false 的 TileMapLayer)(2026-06 修砍完树后不能耕地 bug):silver_mist / birth / village_01 / old_post_rd 四个 outdoor 场景都有一个 Tree2(或 Tree)TileMapLayer 当关卡设计的定位参考层,运行时 visible = false 不渲染。但 tile 数据仍在 —— 之前会阻挡锄地判定(get_current_tile_can_hoe 自顶向下遍历时先命中顶层 tree tile → 非 farmland → return false;表现是「砍完树后那块地不能耕地」)。修复在 levels/level.gd:_get_topdown_layers():遍历时 if not c.visible: continue,所有走这函数的查询(锄地判定、terrain_type 等)都自动跳过美术辅助层。新增 TileMapLayer 时务必遵守:是定位参考 / 装饰 / 调试用的 → 设 visible = false;是真正的游戏内逻辑层 → 留 visible = true不要给美术辅助层设 visible = true —— 会破坏锄地 / 地形判定
  • get_breakable_at 不查 tellable(2026-06 重构):原本 get_breakable_at 把 tellable 也算作 breakable 返回,导致斧头在 Afforest 上能 range 出来打掉。重构成 get_breakable_at 跳过 BreakableTellable(保持”building”语义),新增 get_tellable_at(at_position) -> BreakableTellable 给锄头专用。共享扫描逻辑抽到私有 _find_breakable_at(at_position, skip_tellable)
  • tellable cell = 永远可锄(2026-06,get_current_tile_can_hoe):tellable 元数据的 cell 不管 tile 在不在、TileSet 下层是什么 tile 类型——永远可锄(除非被农田/axe/building 占用)。由 Level.tellable_broken_cells: Dictionary<layer_key, Array<Vector2i>> 记录被擦的 cell(与 BuildingResource.tellable_broken_cells 同构——内存视图 + 持久化共享结构)。layer_key 格式 "<scene_file_path>:<layer_name>",玩家站在这些 cell 上即使 tile 已擦也始终算可锄。设计意图:Afforest 等装饰 tile 擦掉就解锁一块可耕地,绕开 TileSet 配置限制
  • .tres 字段缺失陷阱sword.tresplates03.tres 没显式声明 type 字段,加载后 type 是 enum 默认 0(Weapon)——这是个潜在 bug,新增 .tres 时务必显式声明 type 字段,不要依赖 enum 默认

Resource / Character

  • character_resource.gd:energy_floor 默认值必须是 0.0:floor 表达”已永久扣除的体力”,开局应该是 0。不要改回 1 或任何非零值——update_stamina 1 秒后会把 current clamp 到 ceiling = max - floor,非零默认值会让能量条一开局就显示不满。详见 AGENTS.md §1 “character_resource.gd:energy_floor 默认值坑”
  • .tres 里 Resource 子字段要显式声明,不要全靠代码默认resources/character_resource.tres 历史上完全没写 energy_max/current/floor 三个字段,加载时全用 character_resource.gd 里的默认。如果默认值改了(比如这次 floor 从 1 改 0),旧 .tres 没有”声明了什么 / 用了默认”的概念,加载顺序敏感容易踩坑。新建/改 .tres 时显式声明关键字段,避免隐性依赖代码默认值
  • Web 模式 .tres 改动不会自动同步到 user:// 镜像WebSaveAdapter.SEED_RUNTIME_PATHS 在 cold-start 把源 .tres 拷贝到 user://runtime_state/,只 seed 一次。改源 .tres 后浏览器里旧的镜像仍存在(值不变)。清镜像:WebSaveAdapter.reset_runtime_state()_seed_runtime_state_if_needed() 检查机制。改 character_resource.gd 字段默认值时尤其注意——浏览器用户跑的就是镜像版
  • floor 模型下 current 永远等于 ceiling(除非初始或吃食物让 ceiling 上升的瞬间):每次工具/建造/收获时 current -= N; floor += N,所以 ceiling = max - floor 也同步降 N。任何代码写 if current >= ceiling: return 都会被 floor > 0 时错误触发——视觉上玩家看到 12.5% 觉得”快没电”,守卫却当作”已满 ceiling”拦掉操作。正确守卫:预测 future_current = min(new_ceiling, current + delta)if future_current <= current: return。详见 AGENTS.md §1 “满体力守卫”和”完整范式”。这条是 2026-06 修 consume_food 时挖出来的 floor 模型设计陷阱

7. 关键文件索引

主题文件
任务资源定义structs/task_resource.gd
任务 UIcompoents/character/children/attribute/children/task/task.gd
体力资源structs/character_resource.gd
体力 UI / 工具消耗 / 累瘫对话触发compoents/character/children/attribute/children/statusinfo/statusinfo.gd
角色输入 + 跑步机制 + 吃食物compoents/character/character.gd
雨声managers/cutscene_manager/cutscene_manager.gd
BGMmanagers/sound_manager/sound_manager.gd
剧情 trigger 分发scenes/game/game.gdload_levellevel_loaded.emit() 之后)
wuninghouse cutscenelevels/wuninghouse/wuninghouse_cutscene.gd
wuninghouse 灯光闪烁levels/wuninghouse/wuninghouse.gd
wuninghouse_alf 灯光闪烁 + sleep arealevels/wuninghouse_alf/wuninghouse_alf.gd
wuninghouse_corridor 灯光闪烁 + TransferArea 偏移levels/wuninghouse_corridor/wuninghouse_corridor.gd
走廊 → wuningroom 单次对话门levels/wuninghouse_corridor/wuninghouse_corridor_wuningromm.gd
silver_mist 结局面板(trigger 7→8)levels/silver_mist/silver_mist_cutscene.gd
任务资源文件resources/task/*.tres(101 / 1001)
商店资源resources/shop/npc{2001,4001,4002,4003}_shop_resource.tres
范围提示(工具 range grid 显示 + 体力归零拦截)compoents/range_prompt/range_prompt.gd
工具效果基类(含 consumes_energy() 抽象)structs/hold_effect_resource.gd
锄地/浇水/砍树效果(override consumes_energy()=truecompoents/character/hold_effect/7001.gd / 7002.gd / 7003.gd
建造效果基类(BuildingHoldEffectResourceBasecompoents/character/hold_effect/8001.gd
种子效果基类(SeedHoldEffectResourceBasecompoents/character/hold_effect/4001.gd
剑攻击效果compoents/character/hold_effect/1001.gd
手持栏(含切工具抛光)compoents/character/children/attribute/children/hold/hold.gd
累瘫对话(体力归零触发)dialogic/timeline/Alf_stamina_deplete.dtl
NPC 1001(船夫)对话dialogic/timeline/npc1001_prompt.dtl
NPC 2001(店主)对话dialogic/timeline/npc2001_prompt.dtl
NPC 4001/4002/4003(silver_mist 三店主)对话dialogic/timeline/npc{4001,4002,4003}_prompt.dtl
WuningHouse 主剧情对话dialogic/timeline/wuning_house_pro00_*.dtl
@tool 编辑器 TileMap 导出工具levels/MapCapture.gd
跳官网临时按钮scenes/game/canvas_layer.gd

模块 / 资源 / API 速查(补全)

下面这张表是给新人和 Agent 用的「地图」:每个 autoload、struct、component、level 都能在这里定位。


8. Autoload 总览

注册顺序(project.godot[autoload],按引擎实例化顺序):

#名称来源职责
1PhantomCameraManageraddons/phantom_camera2D 相机管理(addon)
2Dialogicaddons/dialogic对话系统(addon)
3Cursoraddons/awesome_custom_cursor自定义光标(addon)
4CutsceneManagermanagers/cutscene_manager剧情状态 / 雨声 / sprint 门禁
5GameManagermanagers/game_manager全局游戏总控 / quit / save 流程
6SceneManagermanagers/scene_managerLoading 过渡切场景
7PromptManagermanagers/prompt_manager任务奖励弹窗 start_celebrate
8SoundManagermanagers/sound_managerBGM + 动作音效池
9UtilsManagermanagers/utils_manager坐标/朝向/drop_pickable/start_shop
10LoadingManagermanagers/loading_managerColorRect+AnimationPlayer 加载过渡
11ResourceManagermanagers/resource_managerResource 加载/保存统一入口
12WebSaveAdaptermanagers/web_save_adapterWeb/桌面存档路径适配 + swap_to_runtime_deep
13ImGuiRootaddons/imgui-godot编辑器 ImGui 调试(addon)

9. Manager API 速查

CutsceneManager(剧情状态 + 雨声)

  • 信号:无
  • 常量RAIN_DB=-8.0RAIN_FADE_OUT_SEC=10.0RAIN_FADE_IN_SEC=1.0RAIN_SILENCE_DB=-25.0
  • 公开方法
  • _process(_delta) — 根据 cutscene_trigger 自动播/停雨声(1~4 播放;5 淡出)
  • stop_rain() — 主动 10s 渐停
  • pause_rain_immediately() — 硬停 + _manually_paused=true(incident 视频用)
  • resume_rain() — 复位 _manually_paused + 渐入
  • fade_stop_rain(duration = 10.0) — 自定义时长渐停,可 await
  • can_sprint() -> boolcutscene_trigger > 6(角色 sprint 门禁)

GameManager(全局总控)

  • 信号quit
  • 导出game: Gamesetting_resource: SettingResourcearchive_resource_list: Array[String]
  • 公开方法
  • on_quit() — emit quit;save_setting;Web 走 WebSaveAdapter._persist_all,桌面走 _persist_runtime_state + get_tree().quit()
  • save_to_archives() — 兼容 pause_menu/main 的旧入口,内部走 _persist_runtime_state
  • get_archive_resource_list() -> Array[String] — 给 _persist_all
  • pause() / resume()get_tree().paused = true/false

SceneManager(场景切换)

  • 信号scene_changed(scene: Scene)
  • 枚举Scene { Main, Game, Prologue }
  • 常量SCENE_NAME = { Main, Game, Prologue } → 路径
  • 公开方法switch_scene(scene, center = Vector2(0.5,0.5)) — 内部走 LoadingManager.enter → 异步加载 → add_child → LoadingManager.leave,整个链条 await 在 autoload 内不被截断

PromptManager(奖励弹窗)

  • 字段is_celebrate: bool(任务弹窗显示期间 true)
  • 公开方法start_celebrate(label, inventory_list: Array[Dictionary]) — 先 character.add_inventory(item['inventory'], item['count']) 发奖励,再 instantiate Celebrate 节点 → await celebrate.finish → queue_free

SoundManager(BGM + 音效池)

  • 枚举AudioType(walk 草/石多档、chest 开/关多档、axe/hoe 各档、page_turner、unlocking、rain)、AudioGrade(None + 各 walk/chest 子集)
  • 导出audio_List: Array[AudioStream]pool_size: int = 10back_level_audio_list: Array[Dictionary](按 Game.LevelType
  • 公开方法
  • fill_pool() — 初始化 player pool
  • player_audio(audio_tpye, volume=0.0, from=0.0, grade=None) — 拿空闲 player 播放
  • stop_grade(grade) — 停掉该 grade 的所有 player
  • get_free_player() — pool 取空闲,无则 new 一个
  • play_level_audio(level) — 随机选首 → 旧 1s 淡出 → 新 2s 淡入
  • stop_level_audio()CutsceneManager.fade_stop_rain(2.0) + 停 back_audio
  • pause_bgm_immediately() / resume_bgm() — 见 §4
  • play_level_audio_loop(level) / play_level_audio_by_name(level_name) — 协程循环播 + 按名特化(wuninghouse 三个走 Silent Rain Narrative)

UtilsManager(杂项)

  • 内部类ScreenPositionResult { position, canvas_position, is_on_screen }
  • 公开方法
  • get_screen_position(node) -> ScreenPositionResult — 屏幕归一化坐标(offset -12px)
  • get_render_size() -> Vector2
  • transform_position_tile(at) -> Vector2i — 16px 网格吸附
  • get_direction_from_position(at, target) — 返回 Front/Back/Left/Right
  • drop_pickable(inventory, count, at_position) — 世界掉落物
  • start_shop(shop_resource)await shop.finishqueue_free

LoadingManager(加载过渡)

  • 公开方法
  • enter_force() / leave_force() — 直接设 shader_parameter/progress
  • enter(center, invent=false, callback) — 播 enter 动画 → await → callback
  • leave(center, callback) — 播 leave 动画 → await → is_loading=false → callback

ResourceManager(资源加载/保存)

  • 公开方法
  • load_resource(path) -> Resource — Web 优先查 WebSaveAdapter.runtime_path(path)
  • load_resource_async(path, callback, process) — 异步 + 进度回调
  • save_resource(resource, custom_path="") — 递归把 Resource 子属性一并 save
  • save_to_archive(resource, archive_name, prop_name) — 走 WebSaveAdapter.archive_file_path
  • save_runtime(resource) — 转发到 WebSaveAdapter.save_runtime

WebSaveAdapter(Web/桌面存档路径适配)

  • 常量WEB_RUNTIME_ROOT = "user://runtime_state"DESKTOP_ARCHIVE_ROOT = "res://archives"SEED_RUNTIME_PATHS(cold-start 镜像清单,31 个 .tres:5 个全局 + 3 个 character 内层 + 8 个关卡外层 + 15 个关卡内层 farmland/fellable_tree/building)
  • 字段is_web: bool_ready 时根据 OS.has_feature("web") 决定)
  • 公开方法
  • runtime_path(original_path) — Web 端 res://xxx.tresuser://runtime_state/<slot>/<basename>
  • save_runtime(resource) -> Error / load_runtime(path) -> Resource
  • has_runtime_state() -> bool — 桌面恒 true,Web 看 list_archive_names()
  • reset_runtime_state() / reset_slot_runtime_state(slot) — Web 端删运行时镜像
  • archive_root() / archive_dir_path(name) / archive_file_path(name, prop) / open_archive_root()
  • make_archive_dir(name) -> Error
  • list_archive_names() -> PackedStringArray
  • swap_to_runtime(original) -> Resource — 浅 swap(只换外层)
  • swap_to_runtime_deep(original) -> Resource — 深 swap(递归把 Resource 子属性也切到镜像版)
  • _persist_all() — quit 时遍历 archive_resource_listsave_resource
  • Web 端触发器:_beforeunload / pagehide / visibilitychange / 30s autosave_timer

10. Structs(持久化数据模型)

类名关键字段备注
GameResourcelevel: Game.LevelTypeseasonmonth/day/hour/minutecutscene_trigger全局游戏状态(剧情 trigger 主存档
CharacterResourcebag: BagResourcehold: HoldResourcecurrent_hold_selecthealth_max/currentenergy_max/current/floormoney体力 floor 永久扣除见 §1
BagResourcebag_inventory: Array[Dictionary] = [30 个空]{inventory, count}
HoldResourcehold_inventory: Array[Dictionary] = [9 个空]同 bag 格式
Inventoryid, texture, highlight_texture, name, description, type: InventoryType, price, stack, lift, terrain_id=-1, restore_value=0InventoryType 千位 ID 见 §0。terrain_id 仅 building 用,restore_value 仅 food/medicine 用
TaskResourceid, promupgator: NPC.NpcType, summary, describe, done, detail: Resource, detail_text, reward, done_signal: StringName方法 deal_done/pay/complete,信号 done_triggered
TaskListResourcelist: Array[TaskResource]over_list: Array[int]全局任务列表
CollectTaskDetailResourcelist: Array[{inventory, count, has}]方法 deal_done/pay
SeedResourcesequence_frame: AtlasTexture, groth_time_state: Array[int], fruit: Inventory生长帧 + 阶段时长(typo: groth)
FarmlandExistResourcelands: Array[{inventory, moisture, drown_time, cell, position}]持久化农田
FellableTreeResourcetree_type, whole/stump/collapse_altas_*(4 季贴图), times, destroy_times, woodget_whole/stump/collapse_atlas(season)
FellableTreeExistResourcetrees: Array[{fellable_tree, tool_count, position}]持久化可砍对象。兼容旧字段 axe_countfellable_base.gd:_ready 迁移到 tool_count
BuildingResourcebuildings: Dict<terrain_id, Array[Vector2i]>, chests: Dict<Vector2i, dict>, cell_to_inventory_id: Dict<Vector2i, int>, fence_axe_count: Dictionary, tellable_broken_cells: Dict<layer_key, Array<Vector2i>>BuildingType enum 已废弃。cell_to_inventory_id 存 Vector2i→inventory.id 用于重建 Breakable;fence_axe_count 已废弃保留兼容;tellable_broken_cells 持久化锄头擦掉的 tellable cell(layer_key = <scene_file_path>:<layer_name>),reload 时按存档反向 erase_cell 还原擦掉状态
ChestResourcecolums: int = 4(typo)、chest: Array[Dict]宝箱物品
ShopResourceinventory_list: Array[Inventory], columns = 5商店配置
ScheduleResourcetype: ScheduleType.Day, start_place, target_place, level: Game.LevelType, hour, end_hour子类重写 update_day_schedule/finish
ScheduleListResourcelist: Array[ScheduleResource]NPC 日程集合
HoldEffectResourcefalse_color, true_color + get_range/update_texture_limit/get_cell_limit/play_effect/has_range_effect/has_oblique_angle/has_origin/consumes_energy工具交互范围基类。consumes_energy 默认 false
InteractKeyboardResourcekeyboard_list: Array[{keyboard, label}]UI 按键提示
LevelResourcelast_position, farmland_exist_resource, fellable_tree_exist_resource, building_resource关卡存档
SettingResourcecurrent_existing_index, volume = 0.5全局设置(slot + 音量)

11. Component API 速查(按目录分组)

compoents/character/(玩家核心)

  • character.gdCharacter extends CharacterBody2D
  • 信号:hoe_targetwatering_targetsickle_targetaxe_targetseed_targetbuilding_target
  • 常量:RUN_COST_PER_SEC=2.0RUN_RECOVER_PER_SEC=1.0RECOVER_DELAY_SEC=1.0PANTING_DURATION_SEC=2.0MAX_STACK_SIZE=64
  • 枚举:ActionState{Default, Lift, OneShot}MovementState{Idle=-1, Walk, Run}FaceDirection{Front=-1, Back, Left}OneShotState{Collect, Hoe, Watering, Axe, Place, Sword}
  • 关键方法:is_current_level_indoor()is_interfact_state()get_input_values()distribute_input()movement()set_action_state()set_face_direction()set_movement_state()update_face_direction(p1)update_lift_visible()update_lights_visibility()update_stamina(delta)emit_*_target()on_pickable_enter/leave(p)on_pick() / pick_pickable()add_inventory(inv, count)find_exist_item_with_space/find_exist_item/find_can_use_item()on_dialogue_timeline_started/endedon_dialogue_signal_event(dict)on_npc1001_dialogue()to_shop(npc)to_transfer(level)to_buy_inventory(inv, count)selling_inventory(target, count)consume_food()get_energy_ceiling()clamp_energy_to_ceiling()play_walk_sound() / play_walkatgrass()
  • _relink_inventory_after_swap():Web 模式 deep swap 后把 BagItem/HoldItem.current 重新指向新 bag/hold 的字典,避免玩家改 current 写到旧 bag
  • children/interact/interact.gdInteract extends Control — 拾取提示 KeyboardType {None=-1, Pickable}
  • children/animation_state/animation_state.gdAnimationState extends Node2D — 写 BlendTree 参数 + start_one_shot(state) await animation_finished
  • children/attribute/attribute.gdAttribute extends CanvasLayer — UI 总枢纽:get_inventory_node(target)alluishow/alluihide
  • children/attribute/children/task/task.gdTask extends InterfaceNode
  • 信号:detail_closedtask_closed
  • 枚举:TaskState{Un_Get, Un_Done, Done, Finish}
  • 方法:save()draw_list()switch()reset()on_click_task_item(task)over_task_from_id/get_task_from_id/close_detail/get_task_state/to_add_task/refresh_task_state/to_over_task/try_complete_by_signal
  • children/attribute/children/statusinfo/statusinfo.gdStatusInfo extends InterfaceNode
  • 监听:hoe_target/watering_target/axe_target/seed_target/building_target(详见 §1 末尾表)
  • _process 维护 GoldCount / HealthBar / EnergyBar + 边沿触发累瘫对话
  • children/attribute/children/bag/bag.gdBag extends InterfaceNode
  • switch()handle_holding_item_on_close()(drop-back 逻辑:原槽 → 首个空槽 → spawn_pickable)、spawn_pickable(inv, count)reset()set_current_inventory(inv)
  • children/attribute/children/hold/hold.gdHold extends InterfaceNode
  • 字段:current_effect: HoldEffectResourceallow_click_select: boolallow_scroll: bool_scroll_cooldownSCROLL_COOLDOWN_MS=50
  • update_current_select(index)update_lift()(lift_sprite 缩放到 12px)、update_effect()(instantiate HoldEffectResource,含切工具抛光)、get_current_select()on_input(event)(1-9 / page_up/down 带 50ms 冷却)
  • children/attribute/children/closet/closet.gdCloset extends InterfaceNodeswitch() 与 Bag 互斥
  • children/attribute/children/menu/menu.gdMenu extends InterfaceNode
  • on_click_item(item) — Bag/Task 互斥(带 0.15s 延迟),Shop.is_opened 时 bail
  • children/attribute/children/task/children/task_item/task_item.gdTaskItem extends InterfaceNodeupdate_state()on_left_click()task.on_click_task_item
  • children/attribute/children/task/children/collect_item/collect_item.gdCollectItem extends InventoryNode — 显示 has/count
  • children/attribute/children/bag/children/bag_item/bag_item.gdBagItem extends InventoryNode — hover → bag.set_current_inventory
  • children/attribute/children/hold/children/hold_item/hold_item.gdHoldItem extends InventoryNodeon_left_click 禁用,on_inventory_click/right_clickhold.allow_click_select 控制
  • children/attribute/children/menu/children/menu_item/menu_item.gdMenuItem extends InterfaceNode — hover 切换 outline_color

hold_effect/ 完整列表(每个 = 一个 HoldEffectResource 子类)

ID 段文件class_name / extends行为
10011001.gdextends HoldEffectResource剑攻击(无 range gate,任意位置 OneShotState.Sword
40014001.gdclass_name SeedHoldEffectResourceBase种子基类(emit_seed_target
4002~40074002.gd ~ 4007.gdextends SeedHoldEffectResourceBase7 个种子物品全部复用 4001 基类
70017001.gdextends HoldEffectResource锄头(开垦/收割成熟作物/擦 Afforest tellable)→ emit_hoe_targetconsumes_energy()=true
70027002.gdextends HoldEffectResource洒水壶 → emit_watering_targetconsumes_energy()=true
70037003.gdextends HoldEffectResource斧头(砍树/砍栅栏)→ emit_axe_target + camera.shake,consumes_energy()=true
80018001.gdclass_name BuildingHoldEffectResourceBase建造基类emit_building_target,call add_building
8002~80248002.gd ~ 8024.gdextends BuildingHoldEffectResourceBase23 个 building 物品全部复用 8001 基类
8025~8055不存在id 段空缺,没有对应 .tres 也没有 hold_effect 脚本
8056~80958056.gd ~ 8095.gdextends BuildingHoldEffectResourceBase40 个 building 物品全部复用 8001 基类

全部 94 个 extends BuildingHoldEffectResourceBase 的脚本只是单行 extends(39 bytes),所有行为继承自 8001.gd新加 building .tres 时不需要新写 hold_effect,系统会按 id 自动 instantiate。

compoents/npc/(NPC 行为树宿主)

  • npc.gdNPC extends Node2D
  • 信号:move_finish
  • 枚举:NpcType { wuning, npc1001, npc2001, npc4001, npc4002, npc4003 }
  • 常量:NPC_TYPE = { npc1001, npc2001, npc4001, npc4002, npc4003 }必须包含每个 NpcType 值,否则 to_shop push_error)
  • 导出:machine: BTPlayergraphics: Node2Danimation_sprite2d: AnimatedSprite2Dinteract: Controlschedule_list_resource: ScheduleListResource(可选)
  • 方法:save()(保存 schedule)、to_update_schedule()_on_character_entered/exited
  • Web 模式:_ready 把 schedule_list_resource 走 deep swap
  • 6 个子类(全部空壳 extends NPC
  • npc/npc1001/npc1001.gd — 船夫
  • npc/npc2001/npc2001.gd — village_01 店主
  • npc/npc4001/npc4001.gd — silver_mist 家具店主
  • npc/npc4002/npc4002.gd — silver_mist 食材种子店主
  • npc/npc4003/npc4003.gd — silver_mist 武器杂货店主
  • 每个 NPC 子场景(.tscn)四件套:.gd + _spriteframes.tres + 主场景 npc.tscn 引用 + 自己的 BT tree

compoents/farmland/farmland.gd

  • Farmland extends Node2D
  • 关键方法:on_time_update()(moisture -= 0.01 + 仅当 moisture > 0.5 时 growth_time++,与 wet 视觉阈值对齐)、to_saw(inventory_node)(播种)、set_sf()(按 groth_time_state 切帧)、get_frame_from_sequence(at)to_watering()set_moisture()wet.visible = moisture > 0.5)、to_sickle()(收获:成熟 2x seed + 3x fruit,半熟 1x seed,<50% 1x seed)
  • 生长-浇水联动(2026-06):moisture > 0.5 才让 growth_time++,所见即所得——wet 视觉对应「在长」。不浇水 ~50 分钟后(moisture 1.0→0.5)生长暂停,再浇水恢复

compoents/fellable_tree/

  • fellable_base.gdFellableBase extends Node2D
  • 方法:_on_level_loaded()_on_time_update()(季节变更刷帧)、set_frame_from_sequence(season=null)(whole ↔ stump)、update_occluder(season=null)update_highlight_area()(stump 后 modulate.a=1.0 并 queue_free HighlightArea)、to_tool()(”rock” 动画 + 累加 tool_count + 到 times 掉 base_drop_count wood,到 destroy_times 掉 1 wood 并 remove_fellable_tree
  • 枚举:FellableType { FELLABLE_TREE, FELLABLE_MINE }
  • 字段兼容:_readyif current.has("axe_count") and not current.has("tool_count"): 迁移到 tool_count
  • fellable_tree_small.gdbase_drop_count = 1to_axe() → to_tool()to_axe() 是 deprecated alias,7003.gd 现在调 to_tool(),对齐 Breakable.hit() 抽象)
  • fellable_tree_medium.gdbase_drop_count = 2
  • fellable_tree_large.gdbase_drop_count = 3

compoents/fellable_house/

  • house.gdHouse extends Node2D(空壳)
  • adventurer_guild/adventurer_guild_house_01.gdAdventurerGuildHouse01 extends House(空壳)

compoents/chest/

  • chest.gdChest extends Node2D
  • 方法:save()is_dynamic 时跳过)、to_get_resource()to_load_items()ToOpen()(layer 可见 + 随机 SFX + tween fade-in + 动画 + hold.allow_click_select=true)、ToClose()(对称)、_on_character_entered/exited(退出自动关)
  • 字段:current_cell: Vector2i(2026-06 新增,add_chest / load_buildings 时写入;Level 监听 BreakableChest.broken 时按 cell 找对应 Chest 子节点 free)
  • children/chest_item/chest_item.gdChestItem extends InventoryNode(空壳)

compoents/breakable/(2026-06 新增:可破坏建筑通用抽象)

  • breakable.gdBreakable extends Node2D 通用基类
  • 字段:current: Dictionary(含 cell / layer / inventory / hits_needed / tool_count / drops / drops_count
  • 信号:broken(彻底破了)、progress_changed(tool_count 变,可挂进度条 UI)
  • 方法:hit(_tool: StringName, drop_position: Vector2) — 通用入口;累加 tool_count → 达 hits_needed 时 _on_pre_destroy() + drop_pickable + emit broken + queue_free
  • 默认行为:hits_needed=1drops=自身 inventorydrops_count=1_on_pre_destroy 空操作
  • breakable_fence.gdBreakableFence extends Breakable(空子类占位,未来 fence 加特殊逻辑时 override _on_pre_destroy
  • breakable_chest.gdBreakableChest extends Breakable
  • 字段:chest_resource_ref: Dictionary(引用 level_resource.building_resource.chests[cell]
  • override _on_pre_destroy():先把内部物品全部掉到地上
  • breakable_bed.gdBreakableBed extends Breakable(空子类占位)
  • breakable_tellable.gdBreakableTellable extends Breakable(2026-06 新增)
  • 用于带 metadata/tellable = true 的 TileMapLayer(当前是 Afforest)的 tile
  • override 整个 hit() 而非 _on_pre_destroy——基类 hit() 会在 hits_needed 达成时调 UtilsManager.drop_pickable(inventory, ...),tellable 没 inventory 必崩
  • 工具守卫hit(_tool, _drop) 第一行 if _tool != &"hoe": return——只有锄头能擦。其他工具 silent skip
  • 命中后只 layer.erase_cell(cell),不 drop 任何物品、不写存档
  • 临时机制,玩家 reload 后 tile 从场景恢复(Level._ready 时重新扫描挂回)
  • 95 个其他 inventory 都走默认基类——不要为每个 inventory 新建 .gd
  • 配置表集中在 Level 顶部 constBREAKABLE_SCRIPT_BY_INVENTORY_ID(inventory.id → 子类脚本路径)、BREAKABLE_HITS_NEEDED_BY_INVENTORY_ID(inventory.id → 几下碎,默认 1)、_INVENTORY_ID_TO_FILENAME(inventory.id → .tres 文件名,92 项 仅 load_buildings 用)
  • 新工具接入(如未来加 hammer):override Breakable.hit()_tool 分支,或 override can_break_with(tool) -> bool7003.gd 不用动

compoents/camera/camera.gd

  • Camera extends Node2D
  • 常量:ZOOM_INDOOR = 2.0ZOOM_OUTDOOR = 1.8
  • 方法:set_limit()(从 current_level_instance.get_bound())、set_follow_target(character)shake(amplitude=2.0, frequency=3.0, duration=0.1)_check_indoor_and_set_zoom()

compoents/mouse_focus/mouse_focus.gd

  • MouseFocus extends Node2D
  • 每帧:indoor / InterfaceNode.above / 对话中时隐藏;否则 UtilsManager.transform_position_tile(get_global_mouse_position()) 网格吸附

compoents/range_prompt/range_prompt.gd

  • RangePrompt extends Node2D
  • 方法:update_visible()(OneShot / 对话 / 无 current_effect 时隐藏;体力<=0 且 consumes_energy() 也隐藏)、distribute_input()on_left_click() / deal_hold_action()(按 has_range_effect() 分支,含体力守卫)、show_range(range_count)2n+1 网格)、set_shader_texture()update_position(force=false)

compoents/highlight_area/highlight_area.gd

  • HighlightArea extends Area2D(实际类名带 typo highlight_are 字段在 fellable_base 里引用)
  • _on_character_entered/exited → tween 父 modulate.a 0.3/1.0 + 玩家 sprite shader outline_color

compoents/transfer_area/transfer_area.gd

  • TransferArea extends Area2D
  • 字段:target_level: Game.LevelTypeis_enter: boolhas_finish: booltimer: Timer
  • 方法:_on_character_entered/exited(退出停 timer + 清 has_finish)、_process(delta)(player inside + 非零方向才 tick)、_on_timer_timeout()GameManager.game.load_level(target_level)

compoents/pickable/pickable.gd

  • Pickable extends Area2D
  • 方法:set_size()(12px tile)、play_animation()(0.4s 弹跳 + 垂直 sine arc)、_on_character_entered/exited(维护 character.pickable_list

compoents/global_light/global_light.gd

  • GlobalLight extends Node2D
  • 节点:canvas_modulate: CanvasModulatedirectional_light: DirectionalLight2Dshadow_polygon: Polygon2D
  • 常量(时间带):DAWN_START=4, DAWN_END=7, MORNING_START=7, MORNING_END=9, DAY_START=9, DAY_END=16, SUNSET_START=16, SUNSET_END=19, DUSK_START=19, DUSK_END=21, NIGHT_START=21, NIGHT_END=4
  • 角度常量:MORNING_3AM_ROTATION=-90°NOON_ROTATION=-180°EVENING_7PM_ROTATION=-270°MIDNIGHT_ROTATION=-360°
  • 颜色/能量 export:COLOR_DAWN/DAY/SUNSET/NIGHT (HSV)、ENERGY_DAWN=0.1/DAY=0.2/SUNSET=0.2/NIGHT=0.0
  • 方法:_physics_process(跟随角色)、_on_time_update()(非 indoor 才更新)、_check_indoor_status()(hide canvas_modulate + directional_light + shadow_polygon)、update_light_color(hour)(分段线性插值 + max_dist 写 shadow shader)、ease_in_out/is_between/interpolate_color

compoents/pause_menu/pause_menu.gd

  • PauseMenu extends CanvasLayer
  • 方法:on_click_pause()(淡入 + GameManager.pause())、_on_back_to_game_pressed()(淡出 + resume)、_on_save_game_pressed()quit.emit() + save_to_archives())、_on_setting_pressed()(占位)、_on_back_to_main_menu_pressed()(resume + 停 level audio + quit + save + SceneManager.switch_scene(Main)

compoents/inventory_node/inventory_node.gd

  • InventoryNode extends InterfaceNode所有 bag/hold/chest 槽的基类
  • 常量:MAX_STACK_SIZE = 64DOUBLE_CLICK_INTERVAL = 0.3
  • 静态字段:holding_item: Dictionarymode = "left_click"|"right_split"sourcecenter)、_drag_placed_slots_last_click_time_last_click_slot
  • 方法:static drop_holding_item()_gui_input(event)(处理 Shift+左键转移 / 单击 / 右击 / 双击)、on_shift_click_transfer()find_open_chest()transfer_from_bag_to_chest/from_hold_to_chest/from_bag_to_hold/from_hold_to_bagon_inventory_click(mouse_pos)merge_all_same_items()on_inventory_double_click()on_inventory_right_click()(半切)、on_inventory_drag_over()update_display()on_mouse_enter/exit/moveget_popup_position()place_item()static create_preview_from_data/close_preview/update_preview()
  • 合并规则:merge same-id stacks → drop 到首个空槽 → 否则 swap 回 holding_item

compoents/inventory_preview/inventory_preview.gd

  • InventoryPreview extends CanvasLayer — 跟随鼠标的浮动槽,每帧更新 inventory_node.center

compoents/inventory_info_popup/inventory_info_popup.gd

  • InventoryInfoPopup extends CanvasLayer
  • 方法:set_current_inventory(target)show_popup()(1.2s 延迟 + 淡入)、hide_popup()(取消 timer + 淡出)、update_popup(at)(clamp 到 viewport)

compoents/time_recorder/time_record.gd

  • TimeRecord extends Node2D
  • 信号:new_daynew_yeartime_updateday(06:00)、night(21:00)
  • 枚举:Season { Spring, Summer, Fall, Winter }
  • 常量:MONTH_DAYS(2 月=29 天)、SEASONS(月→季节)
  • 方法:handle_time()(level load 时同步 season)、_on_timer_update()(minute++ + 级联)、update_pointer_rotation()(写 label + 边界 emit)、reset_to_january_first()(年 wrap emit new_year)、uishow()/uihide()

12. Levels / Scenes API 速查

levels/level.gd(关卡基类,所有 Level 子类继承)

  • Level extends Node2D
  • 枚举:LevelLocation { outdoor, indoor }
  • 导出:level_resource: LevelResourcefarmland_layer: TileMapLayerlocation = outdoorbuilding_layer: TileMapLayer
  • 状态变量:farmlands: Array[Farmland]fellable_trees: Array[Node2D]breakables: Array[Breakable]tellable_broken_cells: Dictionary<layer_key, Array<Vector2i>>astar := AStarGrid2D.new()
  • 方法:
  • save()last_position + ResourceManager.save_resource
  • load_astar() / load_soild_cell()(typo) / mark_static_bodies_as_solid() / mark_circle_area_as_solid / mark_rectangle_area_as_solid
  • get_used_rect()(排除 meta("bound_exclude"))、get_all_tile_map_layers()(遍历 TileMapLayerContianer)、get_bound()(合并 layers 的 world-pixel Rect2i)
  • set_character_postion()(typo)— 放在 last_position$OriginPoint
  • tile 查询get_current_tile_can_hoe / can_sickle / can_axe / can_sow / can_building / has_buildingget_current_has_farmlandget_tellable_at(at)
  • 可破坏建筑(2026-06 新机制):get_breakable_at(at) -> Breakable / damage_breakable_at(at, tool) —— axe 和未来所有工具走 Breakable.hit(tool, at) 统一入口。axe_fence / get_current_tile_can_axe_fence 已废弃(保留签名只防”unknown method”误报)
  • get_terrain_type(at) → “grass”/”stone”/”road”/”dirt”/”unknown”
  • 农田:add_farmland / load_exist_farmland / get_farmland_from_position / get_all_farmland_cells
  • 可砍对象:load_exist_fellable_tree(按 tree_type instantiate)/ get_fellable_tree_from_position / remove_fellable_tree
  • 寻路:get_astar_path_to(start, end, callback) 返回 Array[Vector2] cell path(callback 当前是 dead code)
  • 建造:add_building / add_chest / create_empty_chest / load_buildings(add_building 同步挂 Breakable;load_buildings 遍历 cell_to_inventory_id 重建)
  • tellable 持久化(2026-06):load_tellables / _attach_tellable / _on_tellable_broken——扫描带 metadata/tellable 的 TileMapLayer 挂 BreakableTellable;锄头 1 下擦 tile + 写存档(BuildingResource.tellable_broken_cells[layer_key])。reload 时按存档反向 erase_cell 还原擦掉状态
  • tellable_broken_cells: Dictionary<layer_key, Array<Vector2i>>(2026-06):被锄头擦掉的 tellable cell 列表,按 layer 标识(<scene_file_path>:<layer_name>)分组;Level 内存视图与 BuildingResource 持久化字段同构。get_current_tile_can_hoe 按当前 layer 查对应 list 判断「曾被擦的 tellable cell」——玩家站在这些 cell 上永远可锄,绕开 TileSet 配置限制。Web 旧镜像兼容:缺失此字段 → 加载为 {} → 行为 = “没擦过任何 tile”(用户已擦的 tile 首次 reload 会复活一次,再擦就持久化生效)
  • 锄地 + tellable 联动(2026-06,get_current_tile_can_hoe):tellable cell 永远可锄——第一优先遍历所有 metadata/tellable = true 的 TileMapLayer:
  • 玩家位置的 cell 上 tellable tile 还在 → 可锄
  • 玩家位置的 cell 上 tellable tile 已被砍、且在 tellable_broken_cells 列表里 → 可锄
  • 否则走原始 farmland 类型判断(TileSet custom_data farmland = true
  • 设计意图:Afforest 等装饰 tile 砍掉就解锁一块可耕地,不依赖 TileSet 下层 tile 是 farmland
  • 可破坏查询get_breakable_at(at_position)Breakable(不假设 building_layer,按 current.layer + current.cell 反查,跳过 BreakableTellable——tellable 是锄头专用通道,由 get_tellable_at 独立查);跳过 tile 已被 erase 的 breakable(直接查 layer.get_cell_source_id(cell) == -1)——比 is_queued_for_deletion() 更精确(queue_free 窗口期 + tile 状态是真相之源)
  • Breakable 私有:_attach_breakable / _create_breakable_for / _on_breakable_broken / _on_breakable_tree_exiting / _on_tellable_broken
  • 常量:BREAKABLE_SCRIPT_BY_INVENTORY_ID(inventory.id → .gd 路径,4 项特例:8001 fence / 8002 chest / 8003-8004 bed)、BREAKABLE_HITS_NEEDED_BY_INVENTORY_ID(inventory.id → 几下碎,3 项特例:8002=3 / 8003-8004=2)、_INVENTORY_ID_TO_FILENAME(inventory.id → .tres 文件名,92 项 仅 load_buildings 重建用)

levels/ 子类一览

文件关键扩展
WuningHousewuninghouse/wuninghouse.gdPointLight2D flicker 系统(sin + jitter + 偶发 spike),TransferArea 偏移(trigger 1~5 时 y=30),$VideoStreamPlayer/$VideoStreamPlayer2 显隐
WuningHouseAlfwuninghouse_alf/wuninghouse_alf.gd同款 flicker;Sleep 区域按 pick 推进一日 + trigger 5→6 + load_level(WuningHouseCorridor)(流程见 §2);AnimationPlayer cs_pr_pro00_alfroom 触发开场过场;CutsceneUiShow/Hideplayalfspeakemit_cutscene_trigger
WuningHouseCorridorwuninghouse_corridor/wuninghouse_corridor.gd同款 flicker;trigger 5 时 $TransferArea.x = 30
WuningHouseCorridorWuningroomwuninghouse_corridor/wuninghouse_corridor_wuningromm.gdextends TransferArea,把传送替换成单次对话门(wuninghouse_corridor_wuningroom.dtl
SilverMistsilver_mist/silver_mist.gd空壳。场景实例化 npc4001/4002/4003(silver_mist 三店主)
SilverMistCutscenesilver_mist/silver_mist_cutscene.gdover_area → 显示 %Over 结局面板 → trigger 7→8
Village01village_01/village_01.gd空壳。场景实例化 npc1001(船夫)+ npc2001(店主)
OldPostRdold_post_rd/old_post_rd.gd空壳
AdventurerGuildRoom01adventurer_guild_room_01/adventurer_guild_room_01.gd空壳
Birthbirth/birth.gd空壳
WuningHouseCutscenewuninghouse/wuninghouse_cutscene.gd主剧情驱动:trigger 2→3→4→5、6→7、page 拾取、incident 视频、_populate_gift_container/_grant_gift_rewards_await_any_action
WuningHouseAlf.OpenClosetwuninghouse_alf/opencloset.gd衣柜触发器(按 chest 键开关)
GiftResourcewuninghouse/gift_resource.gdPage_gift 奖励 bundle(reward: Array[Dict]

scenes/main/main.gd(主菜单)

  • Main extends Node2D
  • 枚举:MainType { NewGame, Continue, LoadGame, Setting, About, Quit }
  • 导出:video: Node2D(背景视差)、mouse_sensitivity=0.15max_offset_x=20max_offset_y=12follow_speed=0.01ExistingList: Controlcontinue_buttonload_game_buttonexisting_buttons: Array[MainButton]
  • 方法:_process(delta)(视差)、_ready()(无存档时禁用 Continue/LoadGame)、_generate_archive_name()<seq>_MMDD_HHMM)、_on_new_game_pressed()(生成 slot + desktop 拷贝原始 .tres 到 archive)、_on_continue_pressed()(选最新 slot)、_on_load_game_pressed()(显示存档列表)、_on_back_pressed()(GameMain↔ExistingList)、to_load_existing(index, _new_game=false)_on_quit_pressed()
  • 调用的 autoload:GameManagerSceneManagerResourceManagerWebSaveAdapter

scenes/game/game.gd(主游戏循环)

  • Game extends Node2D
  • 枚举LevelType { Birth, Village_01, AdventurerGuildRoom_01, SilverMist, OldPostRd, WuningHouse, WuningHouseAlf, WuningHouseCorridor }
  • 常量LEVEL_TYPE: Dict[LevelType → "<folder>/<name>"]键是小写字符串,如 "wuninghouse_alf",不是 "WuningHouseAlf"
  • 导出:camera: Cameracharacter: Characterinventory_info_popuptime_recordgame_resource: GameResourceglobal_lightmouse_focusrange_promptpause_menu
  • 信号level_loaded(emit 时机:level add_child + character position + camera set_limit/set_follow + audio start + 一帧 await 之后)
  • 状态变量:is_placing_treecurrent_tree_typecurrent_level_instance: Level
  • 核心方法:load_level(level: LevelType)switch_season(season, use_loading=true)(更新 game_resource + 切 meta(“seasonal”)=true 的 TileMapLayer tile_set + set_frame_from_sequence
  • ImGui 调试面板已废弃(2026-06 全部代码被 # 注释):包括 Season switch / Pickable spawner / fellable_tree placement / Level Transfer / Hour setter / Cutscene trigger / dialogue tester。_inputWantCaptureMouse 守卫 + is_placing_tree 放置树逻辑也都在注释块里。不要恢复这些功能,除非有真实需求
  • load_level 内 LevelType 分发(在 level_loaded.emit() 之后):
  • LevelType.Birthtask.try_complete_by_signal(&"Reach_Birth")
  • LevelType.SilverMist && cutscene_trigger == 7SoundManager.resume_bgm()
  • LevelType.WuningHouseAlf && cutscene_trigger == 1animation_player.play("cs_pr_pro00_alfroom")(从 Cutscene/AnimationPlayer 节点取)

scenes/game/canvas_layer.gd(临时按钮

  • extends CanvasLayer
  • const URL = "https://alffarmtale.top/"
  • 子节点 TextureRect(Ver 按钮)点击 → 跳官网
  • Web 用 JavaScriptBridge.eval("window.open(...)"),桌面用 OS.shell_open()
  • 同时被 game.tscn 和 main.tscn 引用:两个场景的 CanvasLayer 节点都挂这个脚本,子节点 TextureRect(main 里位置/贴图一样,但用户加了不同 outline shader)。脚本是通用的,不需要每个场景各自复制
  • 未来删除时把 game.tscn + main.tscn 里 CanvasLayer 下的 Ver/TextureRect 子树和这个脚本一起删掉(脚本里有注释提醒)

levels/MapCapture.gd(@tool 编辑器工具

  • @tool extends ReferenceRect
  • 导出 TileMapLayer 整层为 PNG/JPG(保持原始像素)
  • 工具按钮:1. Setup Capture Area / 2. Export Original Pixels
  • 不是关卡类型——是辅助美术的编辑器工具,不在 LEVEL_TYPE / LevelType enum 里

13. 资源目录速查(Catalog)

物品(按 InventoryType 分组,以实际 .tres 数据为准

Weapon 1000(1 个)

ID名称价格
1001sword(剑)80

Material 3000(实际无 .tres

  • 该 ID 段实际没有 inventory 数据
  • wood.tres 在 medicine/ 目录,type=5(Medicine),id=3001(详见下条 bug)

⚠️ resources/inventory/medicine/wood.tres 数据不一致 bug

  • 文件路径medicine/(与 type 不一致)
  • id3001(Material 段,与 type 不一致)
  • type5(Medicine = 6000 段)
  • name:木材
  • description:原木原料,制作必备
  • price:3 / stack:true
  • 修复建议:把文件 mv 到 medicine/ 保留(路径对),改 id 为 6001 + name 不变。所有引用方用 ResourceManager.load_resource("res://resources/inventory/medicine/wood.tres") 走路径,路径不变 id 怎么改都能加载——这是 Uid/路径解耦的好处。task 1001 的 detail list 里用 &"res://resources/inventory/medicine/wood.tres" 引用所以路径改不得。

Seed 4000(7 个,生长阶段 [10,20,30] 秒)

Seed ID名称果实 ID
4001胡萝卜种子5001 carrot
4002番茄种子5002 tomato
4003茄子种子5003 eggplant
4004卷心菜种子5004 cabbage
4005小麦种子5005 wheat
4006南瓜种子5006 pumpkin
4007小黄瓜种子5007 cucumber

Food 5000(8 个)

ID名称价格restore_value
5000apple(苹果)25
5001carrot(胡萝卜)35
5002tomato(番茄)46
5003eggplant(茄子)46
5004cabbage(卷心菜)38
5005wheat(小麦)24
5006pumpkin(南瓜)410
5007cucumber(小黄瓜)47

价格 2~4,恢复量 4~10,与价格正相关。

Medicine 6000(实际只有 1 个错位的 wood)

  • 见上条 bug

Tool 7000(3 个)

ID名称价格stacklift
7001hoe(锄头)35falsefalse
7002sprinkler(洒水壶)30falsefalse
7003axe(斧头)60falsefalse

Crop 8000(实际无 .tres)

  • enum 注释说”作物 8000″,但实际所有 id=8001~8095 的 .tres 都是 type=8 (Building)
  • Crop 类型当前没有任何物品

Building 8000 段(实际 92 个 .tres,type=8)

按子类别分组(不全完整,仅列主要):

子类别id 段数量
fence / chest8001~80022
bed8003~80042
bonsai8005~80073
book8008~80114
bowl / candle / plate / pork / knife / clock / chinese_cabbage8012~8013, 8071, 8075, 8056, 8020, 80197
chair8014~80185
closet8021~80233
foods(盘子装食物)8024~805532
paintings8057~8060, 8064~807011(8061~8063 缺号
plates8072~80743
seasoner(调味罐)8076~80827
sofa8083~80886
table80891
toilet8090~80934
tv8094~80952

总计 92 项(缺号 8061/8062/8063)。Building .tres 的 terrain_id 字段必须手动查 used/tileset/building/building.tres 填正确值,不要复制 id - 8001(详见 §6)。

任务(TaskResource)

IDsummary触发方式奖励
101海鸥岛旧农场代管done_signal = &"Reach_Birth"(到达 Birth 关触发)10× cabbage_seed
1001船夫的请求对话内信号 over_task(收集 1 axe + 20 wood)4× carrot_seed + 4× tomato_seed

可砍对象(FellableTreeResource)

大小times(树桩)/ destroy_times(消失)每次掉落
small3 / 41 wood
medium4 / 62 wood
large6 / 93 wood

商店(ShopResource)— 共 4 个,按店主分类

文件店主 NPC内容备注
npc2001_shop_resource.tresnpc2001(village_01)axe, hoe, sprinkler, sword, carrot, carrot_seed通用工具店。columns=5
npc4001_shop_resource.tresnpc4001(silver_mist)全部 92 个 building 物品家具店。columns=5
npc4002_shop_resource.tresnpc4002(silver_mist)8 Food + 7 Seed食材种子店。columns=5
npc4003_shop_resource.tresnpc4003(silver_mist)sword + wood + axe/hoe/sprinkler武器杂货店。columns=5

卖价统一 = max(int(price * 0.8), 1)shop_item 卖出计算)

NPC 日程(ScheduleResource)

  • npc2001_work.tres:9~18 时,rest → work
  • npc2001_rest.tres:18~9 时,work → rest(覆盖到次日)
  • npc2001_schedule_list_resource.tres:当前激活的日程(动态追加 rest)
  • npc4001/4002/4003 暂无 schedule(silver_mist 场景里不移动,schedule_list_resource 可不挂)

对话(Dialogic timeline)

文件角色 / 风格内容
Alf_stamina_deplete.dtlAlf / bubble体力归零提示(详见 §1)
npc1001_prompt.dtlNPC 1001 / bubble船夫接任务 + 关卡转移(birth ↔ village_01)
npc2001_prompt.dtlNPC 2001 / speaker商店对话(”我要买点东西”开 Shop)
npc4001_prompt.dtlNPC 4001 / speakersilver_mist 家具店主(”我要买点东西”→ [signal {npc:"3", type:"shop"}]
npc4002_prompt.dtlNPC 4002 / speakersilver_mist 食材种子店主(→ [signal {npc:"4", type:"shop"}]
npc4003_prompt.dtlNPC 4003 / speakersilver_mist 武器杂货店主(→ [signal {npc:"5", type:"shop"}]
wuning_house_pro00_01.dtlAlf / bubble开头独白:窗开着,文件会被雨淋
wuning_house_pro00_02.dtlAlf / bubble收拾房间的誓言
wuning_house_pro00_02_1.dtlAlf / bubble“她还没准备好”
wuning_house_pro00_03.dtlAlf / bubble打碎盘子惊慌
wuning_house_pro00_04.dtlWuning / speaker海鸥岛失败开垦,委托 Alf 代管
wuning_house_pro00_05.dtlWuning / speaker雨停,明早 7 点东门出发
wuning_house_pro00_06.dtlAlf / speaker离家不舍
wuninghouse_corridor_wuningroom.dtlAlf / bubble走廊回忆被训斥的反思

Dialogic character(.dch

  • character_bubble.dch / character_speaker.dch — Alf
  • npc1001_bubble.dch — NPC 1001 船夫
  • npc2001_speaker.dch — NPC 2001 店主
  • wuning_bubble.dch / wuning_speaker.dch — Wuning
  • (npc4001/4002/4003 暂无独立 .dch,使用 speaker style 直接以 [signal] 触发商店)

14. AI / LimboAI 速查

行为树(ai/trees/

BTNPCSelector
npc1001.tres船夫不在范围:idle(back) + 隐藏 interact → 在范围:face → idle facing player → 显示 interact → await dialoguestart_bubble_dialogue("npc1001_prompt")
npc2001.tresvillage_01 店主寻路(move=true 隐藏 interact + move_to_destination(60, contrary=true)) → 不在范围:idle(back) + 隐藏 interact → 在范围:face → idle facing player → 显示 interact → await dialoguestart_speaker_dialogue("npc2001_prompt")
npc4001.tressilver_mist 家具店主同上但 start_speaker_dialogue("npc4001_prompt")
npc4002.tressilver_mist 食材种子店主同上但 start_speaker_dialogue("npc4002_prompt")
npc4003.tressilver_mist 武器杂货店主同上但 start_speaker_dialogue("npc4003_prompt")

自定义 BTAction(ai/tasks/

文件行为
await_input.gdSUCCESS if Input.is_action_just_pressed(input_key) else FAILURE
calculation_range_face_direction.gd算玩家↔NPC delta → 翻 npc.graphics.scale.x,支持 contrary
move_to_destination.gdcurrent_level_instance.get_astar_path_to() + 走 waypoints + 播 walk_<dir> + 到点 set move=false + emit npc.move_finish
play_direction_animation.gd<animation>_<face_direction>(黑板 face_direction 或 export)
set_interace_visible.gdtoggle npc.interact.visible(typo: interace)
start_bubble_dialogue.gd切 bubble style + 注册 npc.name.to_lower()+"_bubble.dch" + 启 timeline + await timeline_ended
start_speaker_dialogue.gd同上但 speaker 风格,不注册 character

15. Shop 流程细节(utils_manager/children/shop/

  • shop.gdShop extends CanvasLayer
  • 信号:finish
  • 静态字段:is_opened: bool
  • 字段:current_select: ShopItemcurrent_page=1total_pages=1is_sell_mode=falsesell_items: Array[Dict]items_per_page=20
  • 方法:load_items()(按 shop_resource.columns/%List 创建 ShopItem)、load_sell_items()(收集 bag + hold_item_list 非空项)、clear_items()close()(0.15s 同时 modulate.a→0 + scale→0.9 → await finished → is_opened=false → 恢复 hold.allow_scroll=true → emit finish)、update_price()(sell 时 max(int(price*0.8), 1))、update_current_select(item)set_current_inventory(inventory)_on_buylabel_button_pressed() / _on_selllabel_button_pressed() / _on_buy_pressed()character.to_buy_inventory(inv, count) / _on_sell_pressed()character.selling_inventory(target, count)_on_page_up_pressed / _on_page_down_pressedupdate_pages_text()update_items_visibility()calculate_total_pages()
  • shop_item.gdShopItem extends InventoryNodeupdate_display()reset()/set_current()on_left_click() 调 super + shop.update_current_select(self)on_inventory_click 空实现(不能被拿起)

16. 存档系统流程(Web vs 桌面)

桌面(Desktop)

  • 存档根目录:res://archives/<slot>/
  • 路径:res://archives/<slot>/<game_resource|bag_resource|hold_resource|...>.tres
  • ResourceManager.save_resource(resource) → 直接 ResourceSaver.save(resource.resource_path)
  • GameManager.on_quit()_persist_runtime_stateget_tree().quit()

Web(Web)

  • 存档根目录:user://runtime_state/<slot>/
  • cold-start:SEED_RUNTIME_PATHS 把 31 个核心 .tres 镜像到 user://
  • 运行时:WebSaveAdapter.swap_to_runtime_deep(original) 递归把 Resource 子属性切到镜像版
  • 触发保存:_beforeunload / pagehide / visibilitychange / 30s autosave_timer
  • GameManager.on_quit()WebSaveAdapter._persist_all() → 遍历 archive_resource_listsave_resource

存档 slot 格式

  • <seq>_MMDD_HHMMseqarchive_root 子目录数 + 1,MMDD_HHMM 是当前月日时分)
  • SettingResource.current_existing_index 保存当前 slot 名

Main 菜单存档操作

  • NewGame:生成 slot → desktop 创建 archive 目录 + 拷贝原始 .tres → to_load_existing
  • Continue:选最大 seq slot → to_load_existing
  • LoadGame:列出所有 slot,按 seq 倒序,点了再 to_load_existing
  • 无存档时 Continue/LoadGame 按钮自动 disable

17. 加载流程时序图(文字版)

SceneManager.switch_scene(Game)

  1. LoadingManager.enter(center, false, _start_scene_load.bind(Game)) — 播放 enter 动画
  2. await animation_finished → callback
  3. _start_scene_load(Game)
  • ResourceManager.load_resource_async("res://scenes/game/game.tscn", callback, process)
  • callback: instantiate → add_child(Game) → GameManager.game = self → WebSaveAdapter.swap_to_runtime_deep(game_resource) → connect GameManager.quit → save_runtime(game_resource)
  1. Game 节点 _ready → 首次 load_level(initial_level)
  • 首次走 LoadingManager.enter_force()
  • ResourceManager.load_resource_async(level.tscn, ...)
    • level_instance.instantiate() → if current_level_instance: save + queue_free → add_child(level_instance) → set_character_postion → camera.set_limit → camera.set_follow_target → SoundManager.play_level_audio_by_name → await process_frame → level_loaded.emit()
    • LevelType 分发(Birth / SilverMist+7 / WuningHouseAlf+1)
    • callback: await process_frame → camera.phantom_camera2D.teleport_position → await process_frame → LoadingManager.leave(center)
  1. 后续 load_level(other):走 enter 动画版(不带 force)
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇