Files
rthythm_archor_game/docs/decisions.md
T
2026-07-06 22:59:47 -07:00

697 lines
50 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Migration Decisions
## 2026-07-06 第八轮:视差语义定案(远景天幕,夕阳恒在画面内)
策划澄清视差意图:背景是**远景天幕**,夕阳要时刻在画面内——即背景应
大比例**跟随相机**(远景几乎不动的错觉),而非此前理解的"背景比世界慢
一点"。此前 0.92/0.8 的参数把背景几乎钉在世界上,方向整个反了,这才是
连续几轮"看不出视差"的真因。
1. **参数**`PARALLAX_SCROLL_SCALE := 0.2`(背景仅以 0.2 倍速随世界滚动,
80% 跟随相机);`PARALLAX_ANCHOR_X := 1734.5`(让夕阳——原画 x≈760、
世界 x≈1656——在出生点镜头可见中心 1340.8 恰好居中)。
2. **验算+截图验证**:相机可见中心全程 [1340.8, 2753.2],夕阳屏幕位置从
正中缓移到左 282px,始终在画面内(含光晕);两端背景覆盖充足
(极左窗⊂[581.5,2882.5]、极右窗⊂[1711.5,4012.5])。窗口化实测四点
截图确认:出生点夕阳居中、中段/封门处缓移仍在画面中央区域,未来
相位(红色天幕)同样成立;世界物件大幅滚动 vs 天幕缓动的对比即
视差观感本体。
3. **已知取舍加剧说明**:天幕语义下步道砖(烙在原画里)随镜头大幅跟随
(奔跑时脚下滑动 176px/s)——单张烙死图的固有代价;美术把"近景步道"
拆成独立 1:1 层后即可消除,其余层保持现参数。
4. 可视化验证配方(复用):manage_scene=false + 手动挂 main.tscn +
start_game()Title 态直接 _set_state 不行(树暂停 + 标题屏不透明底色)。
## 2026-07-06 第七轮修复(玩家与敌人重叠:冲刺穿人收尾排斥)
策划确认:冲刺穿人机制保留,其余时间玩家不得与小怪/Boss 重叠。
1. **调查结论**:身体碰撞矩阵完整且实测有效(玩家 layer32/mask65,敌人
layer64/mask33,平移互撞精确挡在半宽和 19px)。重叠的真实来源是
dash_through 的收尾策略:落点嵌在敌人体内时,恢复碰撞的逻辑"干等分离"
(07-04 定案,防止嵌入恢复被反推回入口侧)——嵌着就无限期保持幽灵态,
玩家可长期站在敌人身体里;期间敌人 AI 还会照常向玩家中心走位,把重叠
"续命"。叠加事实:Godot 的 move_and_slide 对已重叠的双方永不主动分离
safe_margin 0.001),所以一旦嵌入只能靠显式排斥。
2. **修复**MotionExecutor 记录冲刺方向;`_try_restore_collision` 嵌入时
不再干等,每物理帧沿冲刺方向 `move_and_collide` 推 4px240px/s,快于
小怪追击 120px/s),顶到边界墙推不动则反向从入口侧退出;推清后恢复
碰撞。幽灵掩码只剩 world 位,排斥不可能进入地形。"从远侧穿出"的
07-04 手感语义保留;cancel 的非物理帧即时恢复路径不变(有测试钉)。
3. **不动的部分**:敌人互相不碰撞(minion/boss mask 无 bit64,成群穿插是
现状设计);小怪尸体在死亡动画期间仍挡路(存量怪癖);Boss 战前
combat_enabled=false 非实体是有意豁免(test_level1_melee_contact 钉)。
4. 契约测试 test_dash_overlap_separation:嵌入落点 45 帧内推出+恢复碰撞+
从远侧穿出+间距 ≥ 半宽和;干净落点行为与旧版一致。
## 2026-07-06 第六轮修复(Boss→玩家击退 / 视差加强重启)
**语义澄清**:策划三轮反馈的"Boss 击退"实指 **Boss 攻击玩家时对玩家的击退**
(第三~五轮修的"玩家击退 Boss"方向保留不回退)。
1. **Boss→玩家击退不可见的三个根因**:①Boss 近战沿用默认基准 120,普通
连段倍率仅 0.4/0.5 → 滑行 2~3px(重击也只 20~37px);②敌方弹丸基准击退
恒为 (0,0)actors_container 只给敌弹注 speed),Boss 未来相位只用远程 →
整相位击退数学上为零(.tres 里的 0.3/0.5 倍率是死数值);③相机 1:1 跟随
玩家——玩家被击退时自己在屏幕上不动,只有背景平移,小位移完全无感。
2. **修复**boss.tscn DamageEmitter 覆写 `base_knockback = (360, 304.056)`
(初速 ×3 = 距离 ×9):连段 20/32px、重击 190/341px;敌方弹丸生成时注入
`base_knockback = (360, 0)`(不击飞):Boss 射击 11~32px、女巫 8~15px
强弱势归零规则照常生效。Boss 恒强势(is_strong_in_current_time_phase
恒 true),玩家 Hitstun 0.4s 内不能起手/移动,滑行不受自身动作干扰。
调参 knobboss.tscn 的 base_knockback 一处 + actors_container.gd 注入值。
3. **视差加强重启**:0.92 时代偏移峰值仅 ±56px、实战镜头段 ±20px,肉眼难辨
(策划"看不出视差"两轮反馈的真因;上一轮误判为"脚下打滑"而回退)。现
`PARALLAX_SCROLL_SCALE := 0.8`,峰值 ±141px、实战段 ±60px。已知取舍:
步道砖烙在原画里会随镜头轻微滑动(0.8 下奔跑时约 44px/s),策划若不接受
只能等美术拆层(无伤视差的唯一解,接线半小时)。
## 2026-07-06 第五轮修复(击退被反向突进吃掉 / 视差回退)
1. **Boss 实战零击退的真凶:自己的下一拍动作**。裸场景击退管线全通过,但
真实 stage 里 Boss 被击退 3 帧后就照常起手攻击(attack 态还冻结摩擦衰减),
随后动作自带的突进位移接管 velocity 以 -375px/s 冲回玩家——实测净位移
**-24px**(比不击退还近)。修复:**趔趄门禁**——击退残速期
`can_start_enemy_action` 返回 falseEnemyActionDriver 的 BT/谱面双路径
都走此钩子),滑行完整播完 AI 才恢复;小怪加同款钩子。Boss 受击链强制
撤退豁免:`_trigger_hit_chain_escape``clear_knockback_stray()`(新增
motor 公开方法)再起手 boss_retreat_dash,否则 4 连击逃脱被门禁拦死。
真实 stage 探针验证:位移 -24px → **+132px**(≈理论 129.6)。
2. **单层微视差回退**。可行走步道砖烙在关卡原画里,整图 0.92 倍速 =
角色脚下地砖打滑(策划反馈"视差有问题"即此)。逐像素核查后确认横向
分层缝也不可行:油灯底座 y495-520、栅栏柱脚 y485-495、锚徽 y495-555、
步道面 y495-550 相互咬合,任何水平缝都会把显眼物件剪成两截随视差错位;
左右边界石墙纵贯全高更无法归层。结论:**单张烙死的图做不了无伤视差**,
已回退到背景钉死,契约改为"背景不得随相机移动"。真视差待美术按
"天空/远景海船/中景码头道具/近景步道"拆层出带透明通道的 PNG(宽度均
≥2301+视差余量),接线约半小时(每层 Sprite2D + 相机可见中心 × 系数,
勿用 Parallax2D——历史上因 CanvasModulate/wipe 兼容性移除过)。
## 2026-07-05 第四轮修复(Boss 击退加码 ×9 / 小怪击退修复 / 单层微视差)
1. **Boss 击退距离 ×3 → ×9**:首轮 ×3(滑行 43px 级)试玩仍嫌短,策划要求再
×3。`boss.knockback_distance_taken_mult()` 3.0→9.0(初速 ×3)。斩波 Lv1
击退 Boss ≈130px、下砸 ≈634px;嫌多嫌少改这一个返回值即可。
2. **小怪击退修复(结构 bug,与 Boss 同源)**minion.tscn 挂着 MovementMotor
但 minion.gd 零引用——击退初速写入后下一帧就被 AI 接管/清零,且 motor 的
摩擦衰减和假高度积分从不执行。修复:新增 movement_motor 成员;
handle_air_time 无条件委托 motorboss 同款);handle_movement 在相位切换
hold **之后**加击退残速早退(换相位定身优先)。顺带修掉隐藏 bug:垂直击退
曾令小怪 ground_state 永久卡 Airborne,从此 allowed_ground_states 拒绝一切
出招——现在假高度正常积分、落地复位 Grounded。小怪不加距离倍率(基准
15~70px 滑行),策划试玩后如需加码再走 knockback_distance_taken_mult 钩子。
3. **关卡背景单层微视差**ground_background.gd 新增 _process——ArtLayer.x =
基准 + (相机可见中心 − 2047) × 0.08,即背景水平速度为相机的 0.92 倍
PARALLAX_SCROLL_SCALE)。锚点 2047 = GroundAnchor = battle_center =
贴图中心;极限位移 ±56.5px,已验算两端不露边(顺带修掉了原有的极右
16.5px 潜在露边)。用 get_screen_center_position()(含 limit 夹取)而非
camera.global_position,玩家贴墙时背景不跟滑;无相机(headless/菜单)
保持原位。手写每帧位移而非 Parallax2D:历史上 Parallax2D 因
CanvasModulate/wipe 兼容性已被移除过(见下方历史条目)。真·多层视差
(天空/远景/近景分速度)等美术拆层素材到位后再升级。
4. **修复存量 flake**test_rhythm_feedback_and_move_list 的特殊拍断言依赖
"启动后 0.46s 内跑到检查点"RhythmManager 墙钟自启动,_upcoming_beat_index
走实时钟),启动稍慢即越过第 1 拍。按测试纪律钉停时钟(stop_manager),
走确定性 _last_beat_index 兜底。6 连跑验证。
5. 契约测试 test_round4_fixes 钉住小怪水平滑行/残速自清/垂直击退落地恢复、
视差公式与无相机兜底;Boss ×9 更新在 test_round3_fixes。
## 2026-07-05 第三轮修复(标题背景重组 / 判定字样 1s 消失 / Boss 击退 ×3
1. **标题画面重组**:背景弃用烙有 Logo 的整图(start_background_past/future),
改用无 Logo 的干净关卡原画 `assets/art/ground/past/past.png`
`future/future.png`(与关卡内背景同资产),Logo 由独立悬浮 TextureRect
`TITLE_LOGO_RECT` = 390,140,616×300MOUSE_FILTER_IGNORE)叠加。背景轮换
周期改为策划口径"2 秒为单位渐入切换"`TITLE_BG_HOLD_SECONDS` 2.8→2.0、
`TITLE_BG_FADE_SECONDS` 1.6→0.8。渐变期间 Logo/菜单固定,顶部重叠感消除。
难度选择页同步换干净未来原画 + 悬浮 Logo。双背景节点名与 `_title_bg_tween`
变量契约保持不变(test_title_music_and_background 无需改动)。
2. **判定字样 1 秒自动消失**rhythm_track 新增 `JUDGEMENT_VISIBLE_SECONDS=1.0`
倒计时;每次 `_show_judgement_art`(普通判定/锚点 held/TIME SHIFT 三条路径)
重置计时("顶替"语义),到点把 JudgementArt/JudgementLabel 一并隐藏。倒计时
期间不动 modulate/text/scale,与脉冲动画及判定色互不干扰。
3. **Boss 击退距离 ×3**:调查发现 Boss(水平)击退位移实际恒为 0——boss.gd
`handle_movement()` 每物理帧无条件 `velocity.x = 0.0`,击退初速活不过一帧
headless 实测 61 帧位移 0.00px)。修复:MovementMotor 暴露
`has_knockback_stray()`,Boss 在击退残速期跳过清零、交给 480px/s² 摩擦自然
衰减(与玩家同路径,不用被 test_boss_integration 禁止的本地计时器 hack)。
距离 ×3 通过受击方鸭子钩子 `knockback_distance_taken_mult()=3.0` 实现,
CombatManager 按 距离∝初速² 换算为初速 ×√3;只放大水平滑行,击飞不变。
实测 120 初速滑行 14.4px→43.3px。小怪存在同类清零问题(minion.gd AI 每帧
接管 velocity.x),策划未提,本轮未动。
4. 契约测试 test_round3_fixes 钉住以上全部行为(背景资产路径、2s/0.8s 常量、
悬浮 Logo 几何与鼠标穿透、判定 1s 消失+顶替重置、Boss 击退倍率与残速存活)。
## 2026-07-05 第二轮需求落地(剑雨放大 / 敌弹降速 / 菜单组件化 / 结算重做 / 受击朝向修复)
依据策划当日九条答复的最终口径:
1. **剑雨放大一倍(伤害不变)**6 个 jian_yu .tres 的 `range` 140→280(三级判定
仍相同);落剑视觉 strike_count `1+level``2*(1+level)`4/6/8 把)、spread
`90+40*level``120+40*level`Lv3 最远落点 40+240=280 恰与新判定对齐,顺带修复
旧版"视觉铺 250px 但判定只有 140px"的空气剑);BLADE_RAIN_TILE_SPECS 四条目
scale 0.5→1.0raindrop 跟随)。damage_mult/base_cost 一字未动。
test_blade_rain_alignment 的 Lv3 数量断言 4→8。
2. **敌弹降速 80%(斩波不变)**actors_container.gd 新增
`ENEMY_PROJECTILE_SPEED := 416.0`(520×0.8),非玩家阵营弹丸生成时覆写 speed;
玩家弹保持 player_projectile.gd 默认 520。销毁按累计飞行距离,降速不缩射程。
3. **受击朝向修复(策划定案:保持受击前朝向)**:双根因双修——
① combat_manager._apply_knockback 按受击者与 result["from"]emitter 位置)的
x 差把水平击退方向化为"背离攻击者"dx=0 或缺 from 保留旧 +X);result dict
里的 knockback 数值保持 resolver 原值,纯数值契约测试不受影响。
② movement_motor 新增 `_knockback_stray_active` 标记:apply_knockback 置位、
残速衰减到 0 或自主移动接管时清除;set_heading 在标记存活或 life_state !=
Alive 时不改写 heading。玩家/敌人同语义(受击不转身)。
4. **菜单组件化(废弃整图+透明热区)**:新素材套件入库 assets/ui/menu2/
(面板 panel_tall + 按钮底两态 button_base_normal/hover + 文字贴图 13 张 +
Logo title_logo),中文字体 assets/fonts/fzzdhjw.ttf。game_flow_manager 全部
界面改为"面板 + StyleBoxTexture 双态按钮 + 文字贴图/字体"拼装:标题(Logo+
三键+当前难度页脚)、难度选择(简单/普通/困难+开始/回标题+页脚,选中=高亮橙
框)、暂停/确认框/胜负 splash(NinePatch 面板;确认框用 确认/取消 贴图)、
结算(整面板+游戏结算标题字)。按钮保持 Button 类型 + 旧点击矩形兼容坐标测试;
过去/未来场景背景轮换保留。素材里的"结束游戏"按策划答复弃用。
5. **结算重做**:统计标签中文化(最大连击/完美/良好/勉强/失误/锚点成功/相位
切换,fzzdhjw 字体渲染);RANK 只留等级("RANK S",去掉 "(83/100)" 分数),
失败仍不显示 RANK。胜负停留时间 ×3VICTORY_SCREEN_DELAY 1.2→3.6s、
DEFEAT_SCREEN_DELAY 1.6→4.8s;保留两段式(胜负 splash → 查看结算 → 结算)。
6. **胜负合成音效(策划定案:合成音,不引音频文件)**SfxManager 新增
&"victory"C5-E5-G5-C6 上行琶音 1.2s)与 &"defeat"G4-Eb4-C4-G3 分立下行
1.3s,与 &"death" 滑音区分);订阅 EventBus.flow_state_changed,进入
Victory/Defeat 态时播放(voice 继承 PROCESS_MODE_ALWAYS,暂停树可发声);
玩家死亡瞬间的 &"death" 保留叠加。
7. 新契约测试 test_round2_changes(剑雨数值、敌/我弹速、击退方向、朝向门禁、
延迟常量、结算中文与 RANK 格式、难度页脚联动)。
8. 顺带修复的存量测试问题:①四个对难度敏感的测试(startup_fix / time_phase_adapter /
ui_animation_regression / v2_core_contracts)此前隐性依赖 menu 测试残留在
settings.cfg 的 easy 难度,现已显式钉 easy;②三处过时断言改到现口径——相位强势
倍率实为 ×1.4(资源 07-04 校准)、谱面预告标记已有意停用(钉住"不生成")、combo
HUD 脉冲断言改抓命名槽位 Slot1;③test_stage4_movement_physics 的固定秒数等待
会被 ANCHOR_ACTIVE 起手拉伸打败(偶发 flake),改为物理帧轮询。
## 2026-07-05 new1/new2/new3 需求落地(判定门控 / 左侧出生 / 怪物与 Boss 行为)
依据 docs/new1-3.md 与策划当日答疑的最终口径(new1 原文的"零代价无效输入"
已被策划改为"MISS+锁定"混合方案):
1. **判定门控(new1 定案)**RhythmManager 新增 `gate_judged_input` /
`is_input_locked``input_lockout_seconds` 0.5、`consumed_repeat_grace` 1)。
规则:每个节奏点只能被一次有效输入消耗;同拍第二次按键按双击容错忽略;
第三次(或超出 BAD 窗的空按)判 MISS——保留原惩罚(无动作、清连击、四拍
槽写 ∅)并触发 0.5s 输入锁定,锁定期间按键完全无效(不判定、不留痕、不
刷新锁定)。纯判定接口 judge/get_rating_for_time 保持无副作用;门控只作
用于**实时判定**的输入(_ensure_judged 打 `live` 标记),测试/AI 的预置
判定原样绕过。接入点:ActionController.submit_intent 主路径 +
两个蓄力施放路径。锁定横跨时间锚点拍 = 锚点必破(相位切换),设计确认。
test_player_combo_input 顺带修复:每次按键前把时钟钉到新拍心,替代旧的
墙钟判定(~20% flake 消除)。新测试 test_input_gate。
2. **出生点左移(new2**stage.tscn 玩家 (2047,560)→(1180,560),相机初始
(1180,395);开场需向右推进约 1000px 才遇预置近战怪(2200 不变,策划确认
该段路用于熟悉操作)。LevelDirector 的战斗中心/增援锚点不变。标题固定+
背景轮播本就已实现,未改动。基线测试(x1_visual_baseline /
rhythm_ui_layout / boss_integration / level1_melee_contact /
minion_phase_system)中依赖旧出生点的断言改为按门线/停步距离取位。
3. **伤害体系(new3 定案)**:保留"基础攻击 × 相位倍率"。近战小怪基础攻击
翻倍 25→50LevelDirector 按 MinionBehavior.role 配置 emitter.damage
远程保持 25);Boss 近战招式倍率翻倍(combo 0.8/0.9→1.6/1.8;重击保守
放大 combo_3 1.4→2.2、lunging_stab 2.0→3.0,未按字面 ×2 以免单跳 120),
Boss 远程倍率下调 shoot 0.6→0.430×0.4=12 恰合 new3 推荐)、2way
0.7→0.45。
4. **距离参数(new3**:近战怪警戒 420→360、停步 110→120、出手距离
150→120、攻击判定 range 58~74→130(前方 reach≈140);远程怪危险距离
150→220、后撤 190→220、后撤完毕按拍停顿 1 拍;**射程上限压到视距内**
(攻击距离 430→450 ≤ 相机半宽 460——绝不屏幕外开火,new3 的 720 被策划
否决)。Boss melee_distance 135→180、ranged 260→360、dash 360→460。
5. **远程怪受击脱离(策划定案,取代 new3 §5.5 的单次受击)**:连续受击 4 次
(2s 链窗)触发闪烁无敌 0.4s + 跳跃形式脱离(escape_speed_scale 3.2
minion.gd 的 approach_direction 钳制放宽到 ±4);无敌走
FrameCollisionDriver.damage_receiver_enabled(驱动器每帧重申矩阵,直接改
Area2D 会被覆盖);3s 冷却防连续触发;贴墙时沿用穿过玩家身位的反向跳。
6. **Boss 霸体(策划定案,取代 new3 §6.5 韧性系统——明确不做韧性)**
CombatResolver 新增受击方 `shrugs_off_hit(action)` 钩子;Boss 对
base_cost≤0 的攻击恒霸体(伤害照常、打断与击退取消),耗能技能可压制。
所有耗能技能补上击退:dash_slash 1.5、combo_finisher 2.0、ground_smash
2.2(保留原 y1.8 击飞)、zhan_bo 1.0/1.3/1.6、jian_yu 0.8/y0.6;玩家弹丸
在 ActorsContainer 注入 base_knockback(120,304)(原为零)。保留"被连打
4 次后撤逃脱"作为防贴脸兜底。resolve_knockback 顺带修复为跟随伤害判定
通道(good 0.85),对齐 test_combat_manager_resolvers 既有契约。
7. **Boss 相位行为(new3 §6.4/6.7**:远程相位被近身(≤240px)优先
boss_retreat_dash 拉开(后撤无伤害、8 拍冷却);相位切换后停顿 1 拍再
决策(以总线最后拍号为基准,兼容测试合成拍号)。stage.tscn 的 Boss
stationary=true 保持——那是战前木桩状态,BossRoomGate 反锁时已会释放。
新测试 test_new3_enemy_rules(霸体规则/按职责基础攻击/受击脱离)。
## 2026-07-04 playtest pass 2 (dash pierce / anchor presentation / radial phase wipe)
1. **Dash-through actually pierces now.** Two compounding defects: (a) action
displacement speed was computed over the full action length while the
motion only runs from Active start to action end, so every action
under-travelled by the startup fraction — the 260px dash realized ~195px
and frequently ENDED inside the boss; (b) the collision mask snapped back
the moment the action finished, so ending overlapped meant depenetration
shoved the player back out the entry side ("did not pierce").
Fixes: `_action_motion_speed` divides by (action_beats - startup_beats);
MotionExecutor ghosts BOTH directions during dash_through (strips
player_body|enemy_body bits from mask AND layer so neither side's
move_and_slide recovery can shove anyone), and restores collision
deferred — each physics frame it shape-queries the ghosted body layers
and only snaps back once clear of overlap. Outside physics frames
(headless tests) restore is immediate, keeping the existing
test_stage4_movement_physics contract green.
2. **Time anchor presentation follows the upgrade semantics.** The mirrored
red pair converging every anchor beat read as "recolored beat balls",
and at high streak tiers (interval 1) it doubled screen elements. Now:
the beat movers THEMSELVES turn anchor-red when the beat they converge on
is an anchor (screen element count never exceeds the rhythm grid).
Follow-up: the extra "herald" ball that parked above the center emblem was
judged redundant in playtest (the recolored movers + the HUD "ANCHOR Nb"
countdown carry the information) and was removed entirely — time anchors
add zero elements to the track. Held/broken feedback unchanged.
3. **Phase switch is a player-centered radial wipe.** Scene tint now lives
solely on the CanvasModulate (stronger: past warm 1.0/0.94/0.82, future
cool 0.55/0.72/1.0; the color-mapped ground reads as the other map). On
time_phase_changed the world gets the NEW tint instantly and a fullscreen
blend_mul shader multiplies everything OUTSIDE an expanding circle
(centered on the player, radius 0 → screen diagonal in 0.5s) by
old_tint/new_tint — the old scene visibly dissolves outward from the
player, with a bright rim at the wavefront. Works symmetrically in both
switch directions; the wipe center follows the player each frame.
## 2026-07-04 action feel pass (mirroring / air slam / dash derive / charge gauge / dummy boss)
Fixes driven by playtest feedback against the player_tagged author sheets:
1. **Blade wave cast mirroring.** zhan_bo has no displacement and no left/right
tag, so the body never turned while the projectile aimed at the nearest
boss. `_play_action_animation` now faces projectile actions
(`hit_type == projectile` or `projectile` tag) toward
`projectile_direction()`; the whole Visual (body + FX overlay) mirrors
with heading as usual.
2. **Air combo is a ground slam.** The author's 12-Aerial Combo sheets are
byte-identical to 13-Plunging Strike (PREP / SWING START / FALLING [LOOP] /
LAND + FX2/3/4): the move is a forward slam, not an in-air slash. atk_air
gained the falling loop segment + FX3 hold; both atk_air and plunge_start
now arm `_pending_air_slam_land` at active start — the falling pose holds
until touchdown, then a shared `air_slam_land` animation (land.png 6f +
FX4) plays. FX overlay offsets follow the author's bottom-right anchor
rule `offset = (64 - cell_w, -cell_h)` → FX4 (135x166) = (-71, -166).
air_attack_l/r data: move_mult_y 0.2 → -1.6, active 0.5 / recovery 0.5.
3. **[A][sp] derive beats the A-charge.** With hold_threshold at 0.25 beats
(0.125s @120bpm), beat-cadenced [A] then [SP] with A still held had already
entered Charging, so SP was shelved and the A release cast blade rain.
New rule in ActionController.submit_intent: a pressed SP while an
on_release charge hold (A/D) is armed or charging breaks the charge and
falls through to normal resolution — the four-slot window still holds [A],
so [A][SP] resolves the dash (dash_through pierces the boss and hits while
passing). S-charge (on_secondary_key) is untouched.
4. **Blade rain composite.** Falling blades now fall for exactly
BLADE_RAIN_DROP_FALL_TIME (0.24s @1900px/s) and vanish at ground contact
as the landing tile starts (its first frames carry the streak + impact);
tiles are bottom-anchored to the ground line via per-sheet cell_height,
uniform 0.5 scale, spread grows with charge level (2/3/4 strikes).
5. **Charge gauge = cast level.** ChargeComponent's private 1.1s ramp had
nothing to do with the beat-quantized cast level in ActionController.
ActionController now exposes `charge_state()` (MAX_CHARGE_LEVEL 3,
progress_units 0..max-1) as the single source; the gauge renders
level-space (ready == top level), pulses the overlay on level-up, and the
HUD shows "CHARGE LV n / 3" with per-level fill colors. Legacy 1.1s ramp
remains only as a standalone fallback.
6. **Dummy boss for testing.** Boss export `stationary` (attacks but never
displaces: no BT dash/retreat — replaced with ranged pressure — no action
motion, no hit-chain escape). Enabled on the Stage instance only;
boss.tscn keeps mobile defaults so behavior-tree contract tests still
exercise retreat/escape (the point-blank test explicitly un-sets it).
## 2026-07-04 time anchor / dual time phase feature pass
Implemented the full AnchorV1.0 chapter 10-12/17/20 feature set following the
incremental plan in `Fighting_Rthythm_game/docs/时间锚点与双相位架构修改方案.md`
(all names below use its reserved terms: `time_anchor` / `time_phase` /
`attack_buff` / `streak`):
- **New autoloads** (registered after CombatManager): `TimePhaseManager`
(sole writer of `current_time_phase`, broadcasts `time_phase_changed`) and
`TimeAnchorSystem` (schedules chart-explicit + periodic anchors, resolves
held/broken from `judgement_made` facts only, routes `force_time_phase`).
Periodic candidates are computed lazily (origin + current streak tier) and
lock when they enter the 2-beat lead window; explicit chart anchors win
same-beat collisions and restart the periodic count.
- **EventBus** gained `time_phase_changed` / `time_anchor_scheduled` /
`time_anchor_resolved` / `streak_changed` / `attack_buff_changed`.
`judgement_made(quality, offset_ms, beat_index)` was already extended
during migration (7.1-A), so everything here is pure increment.
- **EffectContainer** two increments only: stack-aware aggregation for add
modifiers (`value x stacks`; all pre-existing resources are max_stacks 1,
audited by test_effect_container) and `set_effect_stacks` (plus a small
`effect_stacks` reader). Resolvers/CombatManager/RhythmManager untouched.
- **Player** gained `StreakCounter` (+1 on skill_executed, zero on miss /
chart_reset) and `AttackBuffComponent` (stack storage IS the
`effect_time_anchor_attack_buff.tres` Effect instance: +10% damage_mult per
stack, cap 20, halved floor on every actual phase switch, zeroed on
chart_reset). Damage rides the existing buffs multiplier slot in
`resolve_damage` — the design formula 1 + 0.10 x stacks emerges from the
stack-aware aggregation.
- **Boss** gained `TimePhaseAdapter` with the PastStrong profile
(`resources/time_phase/profile_past_strong_enemy.tres` — past: dmg x1.2 /
taken x0.8, future: x0.7 / x1.3 via `effect_tp_*` resources; the
FutureStrong mirror profile exists for future enemies). Damage-taken
multipliers ride DefenseModifier entries; both phases share all collision.
- **Chart layer**: `ChartEvent.time_phase_mask` (+`matches_time_phase`),
`ChartTrack.time_phase_mask` (authoring default pushed onto `both` events
at load), `BeatChart.initial_time_phase`, ChartRunner trigger-time final
ruling (skipped events are marked triggered and never re-dispatched) and
initial-phase application on ready/reset. stage9_boss_duel gained a
time_anchor track (beats 8 / 40).
- **Dual music layers**: `AudioLayerController` (scenes/audio/) runs two
always-synced players seeded from `RhythmManager.song_position`, crossfades
volumes on phase switch (0.35s), drift-corrects the *presentation* players
only (loop-wrap aware), and mutes the clock stream. Placeholder assets:
both layers use ev_past1.mp3; the future layer goes through a runtime low-pass
bus so the switch is audible until a real Future loop lands.
- **Presentation placeholders per request**: the second map is ground.png
color-mapped (Stage tints the ground sprite + a CanvasModulate, warm past /
cool future, 0.35s tween + white flash under the UI layer); the time anchor
ball is yellow_ball.png color-mapped crimson in RhythmTrack (mirrored pair
converging on the center, grows near the hit point, held → green lock
flash "ANCHOR HELD", broken → magenta "TIME SHIFT!").
- **HUD**: TimePhaseHud (phase name + next-anchor countdown, buff stacks
/20 with percent, streak) and DebugPanel lines for time phase state and
armed/last-resolved anchors.
- **Tooling/tests**: `tools/time_anchor_chart_tool.gd` (validate / generate /
simulate) and seven new SceneTree tests (`test_time_phase_manager`,
`test_time_anchor_resolution`, `test_time_anchor_scheduling`,
`test_streak_counter`, `test_attack_buff`, `test_time_phase_adapter`,
`test_chart_time_phase_mask`). No Godot binary is available on this
machine, so the suite is authored but not yet executed locally.
Open tuning items deferred (doc chapter 12): anchor-specific window, streak
clear on hurt, on_break punishment effects, conditional anchors (v1.5).
## 2026-07-03 body collision audit
Phase 0 keeps the base physics layer names from the migration manual:
`world`, `player_hurtbox`, `enemy_hurtbox`, `player_hitbox`, and `enemy_hitbox`.
The source project still uses root body collision to let the player and boss block each other:
the player body is on layer 2 with mask 5, while the boss body is on layer 4 with mask 3.
To preserve that feel without mixing body blocking into hurtbox or hitbox layers, the target
project reserves layer 6 as `player_body` and layer 7 as `enemy_body`.
## 2026-07-03 phase 4 character asset migration
Phase 4 copies character art only from the curated `tmp/new` source folders:
`player_tagged` is migrated to `res://assets/art/characters/player/`, and `boss_tagged`
is migrated to `res://assets/art/characters/boss/`. File names are normalized to ASCII
snake_case before animation paths are authored.
The player package README is preserved as `credits_blair_ceradsky.txt`; the package is
CC-BY 4.0 and must credit Blair Ceradsky / @MookaTheCaveman. The boss folder still has
no license file in the source material, so boss art is a development asset until the
license is identified and added.
The old jump-height test is superseded by a no-free-movement/no-jump-input regression:
player movement in this migration comes from ActionData-driven rhythm actions only.
## 2026-07-03 post-migration bug-fix pass
A full audit against `AnchorV1.0.md`, the migration plan/manual, and the character/boss
design doc fixed the following migration defects (tests updated in the same pass):
1. Energy scale: player max energy was 10 while skills cost 15-50, so no skill could
ever be paid for. Player/EnergyComponent max is now 100 and the energy bar fills
proportionally.
2. Melee reach: all player melee ActionData had `range = 0`, leaving a 48px centered
hitbox that body-blocking made unreachable. Every melee action now carries a range.
3. Projectile teams: the shared projectile scene was hard-wired to enemy-hitbox layer /
player-hurtbox mask, so the player's blade wave hit the player and never the boss.
`projectile_requested` now carries a context Dictionary (team, action, judgement,
range); ActorsContainer assigns collision layers per team and injects the ActionData
and judgement so damage multipliers resolve. Player gained muzzle spawn methods;
the boss muzzle height was lowered to 55 so bullets actually cross the player hurtbox.
4. Super armor: HealthComponent entered Hitstun on every damaging hit regardless of
`interrupts`; it now respects the defense resolution (death still always applies).
5. Boss behavior tree: retreat no longer has top priority at melee range (combo first,
2-way shot punish when melee is on cooldown, retreat only after both), the ranged
cycle no longer bypasses the 2-way cooldown, and beyond dash range the boss closes
in instead of shooting forever. Hit-chain escape now needs 4 hits, has a 4s cooldown
and only fires while grounded. Chart events take priority: an upcoming chart event
postpones BT decisions and a triggering chart event overrides a BT action.
6. Boss presentation: MotionExecutor is cancelled on action end (no more sliding across
the arena), animation speed_scale is aligned to the action's beat length, and the
boss no longer turns to track the player mid-action.
7. Charge levels: level was measured from the initial key press (always level 3);
it is now measured from time spent in the Charging phase, quantized by
`charge_level_beats` (1 beat per level). Holding A/D during a chain-cancelled combo
step now arms the charge gate too.
8. Combo HUD: enemy ComboWindows no longer broadcast to the EventBus, so the player's
four-slot HUD is not wiped whenever the boss is interrupted.
9. Direction semantics: displacement direction now follows the left/right action tags
(mirror pairs) and falls back to the current heading for forward actions (W launcher
no longer forces the player to face right); plunging strike now actually drives the
fake-height fall (negative move_mult_y maps to height_speed while airborne).
10. Player presentation: hit-stun and death now play (manual sheet animations from
14_hit_stun / 15_death), the plunge has its falling loop, and the charge FX overlay
uses the real charging_up_overlay sheet instead of leftover textures.
11. Rhythm track UI reads BPM from RhythmManager (was hard-coded 80 vs the actual 120).
12. Block: parry reduction is graded by the block judgement (perfect 100% / good 80% /
bad 60%) and only applies to attacks from the front, per AnchorV1.0 section 9.4;
blocked hits no longer interrupt. block_start active window widened to 0.5 beats
(total 0.8 beats = 0.4s at 120 BPM, matching the design sheet).
## 2026-07-03 playtest follow-up fixes (bullets / collision / latency / air lock)
Root causes found during the first playtest and their fixes:
1. **Stuck in the air after W then S.** MotionExecutor converted `move_mult_y` into a
screen-space body velocity; when landing flipped the presentation state to LAND the
attack-state cleanup never ran, so the plunge left `velocity.y = -220` on the
CharacterBody and the body ascended forever (vertical motion belongs to the fake
height system, the body must stay on the ground plane). MotionExecutor is now
horizontal-only, MovementMotor zeroes `velocity.y` on landing and decays stray
horizontal knockback velocity (KNOCKBACK_FRICTION 480) whenever no attack owns it.
2. **Bullets looked wrong.** effect_sheet.png is a 6x2 grid of 32px cells (verified by
alpha scan); the projectile sliced it as 5x2 (38.4px, non-integer) so every frame was
cut through the middle. The projectile now uses 6x2, loops the four flight frames at
16 fps, expires by travel range (default 460px, zhan_bo levels 380/460/540) instead
of a 0.3s animation timer, and flies at 520px/s instead of 1100 so it is readable.
3. **Collision boxes did not match the drawn bodies.** The new art draws both characters
with their feet ~40px above the node root (player idle rows 24-88 of 128; boss rows
16-47 with a -67 sprite offset). All body/hurtbox/hitbox shapes still sat at the old
feet-at-root heights, i.e. ~35px below the visible bodies. Player shapes moved to
y=-72, boss shapes to y=-70, projectile lanes to -70 on both sides. Player waves now
also clear enemy bullets (mask includes enemy_hitbox; enemy_projectiles are freed on
contact) implementing the design's projectile duel.
4. **Blade wave fired away from the boss.** The player heading defaults to left and only
directional attacks turn it; zhan_bo now aims at the nearest live member of the
"bosses" group with heading as fallback.
5. **Latency feel.** anchor_grid halved (0.5 -> 0.25 beats) so on-beat presses reach
Active with no artificial stretch; the four-slot clear display no longer swallows the
next input (it flushes and consumes immediately); agile input flushing enabled in
project.godot; player action displacement now follows the S6 rule (move_mult 1.0 =
100px over the action) instead of a flat 220px/s lunge.
test_boss_integration projectile assertions were rewritten for the new contract
(6x2 grid, flight loop, range expiry, torso-lane spawn heights, moved hurtboxes).
## 2026-07-03 player animation / FX audit vs player_tagged source pack
Audited all 17 tagged action folders against the runtime setup. Root problem: only the
three AnimationPlayer clips (atk_ground_1/2, rising_slash) had FX tracks; every action
running through the manual sheet-animation path had no FX channel at all, and looping
cast animations were being stomped by idle because attack_time_left used the natural
clip length (0.1s for the 2-frame blade rain loop).
- Manual animation system now supports per-segment fps and a parallel FX timeline
driving Visual/FxOverlay. All overlay offsets were computed by aligning the author's
embedded frame-0 idle reference against the idle sheet (method reproduces the two
hand-calibrated offsets exactly): 01/02/03/05 slash (-147,-191), 04 dash (-98,-128),
07 rising (-111,-156), 08 prep ring (-64,-305), 11 cast (-115,-172),
12/13 FX2/FX3 (-107,-198).
- FX wired: atk_ground_3 + combo_finisher slash overlays, dash_slash speed-line overlay
(frame 1 is the author's intentional blank), atk_air FX2, plunge FX2 + FX3 falling
hold, blade_wave cast overlay, blade_rain launch ring (prep overlay frames 17-19).
- Charge visuals split per entry: A/D charge plays the MAGIC CAST prep body animation
(hold last frame) with the sword-ring overlay forming (frames 1-16, hold); S charge
keeps idle stance with CHARGING UP overlay at the author's 50fps.
- Mode C: ContactFxSpawner script added (was a script-less placeholder node) — spawns
the per-attack ENEMY CONTACT bursts on the target for melee and the projectile
contact burst for blade waves. Mode D: ground_smash chains three SHOCKWAVE tiles
along the ground; jian_yu spawns raindrops plus 2-4 RAIN LANDING TILE pillars scaled
by charge level. One-shot world FX use scenes/combat/one_shot_fx.gd.
- Blade wave projectiles now use the author's PROJECTILE sheet (5 frames, 33fps loop);
enemy bullets keep the effect_sheet placeholder.
- Smoothness: attack presentation window extended to the action's beat length (idle no
longer cuts off casts), same-name animations restart on chain (player and boss), and
starting any new action clears stale FX overlay state. Idle now plays all 8 frames
(test updated to [0..7]).
## 2026-07-04 full UI reskin (ui_and_ground asset pack) + harbor stage backdrop
The placeholder rhythm-track art (rod/blue_ball stitching, center.png emblem,
yellow_ball movers) was replaced wholesale by the authored pack in ui_and_ground/,
and the stage gained a three-layer harbor backdrop. Map A (past phase) uses the
purple/gold set, map B (future phase) the red "d" set; the world itself still
renders map B via the existing CanvasModulate color mapping + radial wipe.
- RhythmTrack re-skins entirely from `TRACK_SKINS[time_phase]`
(rhythm_track.gd): background b/bd (single stretched piece — no more rod
stitching, the line sits centered in the frame), long axis line/lined, center
emblem anchor01/anchor01d, movers star/stard, special (time anchor) beats swap
the mover texture to sanchor/sanchord — still zero extra track elements.
- Hit feedback per the art spec: a judged normal beat flips the emblem to the
own-phase anchor02 variant and fires the own-phase c01 flash; a held special
beat flips the emblem to the CROSS-phase anchor02 variant with the own-phase
c02 flash (A: anchor02d + c02, B: anchor02 + c02d). The emblem reverts to
idle after 0.45s. A latch keeps the follow-up judgement readout from
overwriting anchor feedback (held resolutions arrive nested inside the same
judgement_made dispatch; deadline breaks pass an empty judgement dict and
don't latch).
- Beat ticks now pulse the flash at 0.35 peak alpha so full brightness reads as
"you hit something"; test_ui_animation_regression asserts visibility > 0.2.
- Stage backdrop: long.png (sunset sea, Parallax2D scroll 0.12, scaled 1.4378 to
exactly fill the 982px-tall view) and mid.png (lighthouse/ship/crane cluster,
scroll 0.4, scaled 0.92, bases tucked 17px below the sea horizon at y≈383)
render behind the ground scene; both are plain-canvas Parallax2D so the
CanvasModulate phase tint and the radial wipe cover them too.
- ground.png (stone dock with chain railing + end pillars) replaces the old
strip at uniform scale 1.7979: the walk surface in the art (texture y 570)
lands exactly on the gameplay ground plane y=366 and the art spans precisely
boundary-to-boundary (-21.5..4115.5, pillars at the walls). A dark
StreetUnderfill polygon backfills below the art so the bottom of the frame
never shows void, and Camera2D got limit_left/right (-22/4116) so the view
can no longer slide past the artwork at the arena edges.
## 2026-07-04 (later) disaster recovery + WYSIWYG authored view + panel HUD + per-frame collision matrices
Context: at ~05:11 an incomplete Mac zip (Baidu sync had not finished uploading
scenes/assets — the *.baiduyun.uploading.cfg markers) was extracted over the
tree and the merge wiped scenes/, scripts/, tools/, most of resources/ and
assets/. The repo had zero commits, so nothing was recoverable from git. The
tree was rebuilt from four sources: (1) the three session transcripts (every
file ever read/written, replayed Write+Edit chains, most files verified
byte-exact against the surviving .git/index SHA-1s), (2) the old project
(assets, addon, byte-identical UI scenes, the tagged author packs under
assets/art/characters/tmp/new/), (3) ui_and_ground/ + playbossui/ art packs,
(4) the three newer test files carried by the zip (test_rhythm_ui_layout,
test_boss_integration, test_stage4_player_scene), which numerically encode the
newer authored-view state and were treated as the binding spec. A survivor
backup was kept at ../Rthythm_archor_game_survivor_backup_0542. THIS commit
history now exists precisely so this can never happen again.
WYSIWYG (editor == game), per the newer tests:
- Stage exports use_authored_camera_view (default true): the editor-authored
Camera2D pose IS the runtime view — position (2229.63, 97.38), zoom 0.82,
no player follow, no zoom overwrite. false restores the legacy follow rig.
- MainUI moved out of CanvasLayer into the world canvas: Main/UI is a Control
positioned at the camera world-rect origin (1527.191, -297.742) at scale
1/0.82, sized 1152x648 — the editor shows UI over world exactly as rendered.
- Backgrounds are plain Node2D containers (names LongParallax/MidParallax kept
for test paths): long.png sunset at (791.6, -335) x1.25, mid.png regions as
moored ship (1230, -250) and lighthouse (2650, -303); CanvasModulate is
authored in stage.tscn so the phase tint previews in the editor.
- Ground sprite crops to region Rect2(0, 90, 2301, 593), offset (-1150.5,
-480) — walk line stays texture y570 = world y366. Actor grounding is
authored: Visual position (0, 32) with player.gd visual_ground_offset 32.
- Player Visual scale ±2, boss Visual scale ±4 (flip preserves magnitude);
all scaled sprites use TEXTURE_FILTER_LINEAR.
Panel HUD (playbossui pack): StatusBars at (20.74, 35.64) wraps playerui.png
(x1.5) with HealthBar/EnergyBar/ChargeBar overlaying the baked wells
(player_red_ui/player_blue_ui fills); BossStatus right edge 1123.2 wraps
boss_ui.png with BossHealthBar (boss_red_ui fill) + DREAD HARBINGER name;
ComboWindow centered at y 278.64; TimePhaseHud and DebugPanel ship hidden.
Per-frame collision/damage matrices (frame_collision_driver.gd, on player and
boss): every physics tick the driver re-derives all three matrices from the
frame actually displayed —
- body: base layer/mask re-asserted, composed with MotionExecutor ghost bits;
- damage-receive: DamageReceiver rect follows the opaque pixel bounds of the
current frame (per-sheet bounds cached from texture alpha), margins via
hurt_margin, mirrored automatically through the Visual transform;
- damage-send: DamageEmitter only monitors during ACTIVE-phase frames whose
native-forward extent reaches HIT_REACH_GATE (0.72) of the sheet's max —
the hit window is the weapon-extended frames, not the whole active phase;
the shape is rebuilt per frame from pose reach vs action range.
DamageEmitter gained per-swing dedup (_already_hit_ids) since per-frame
windows can re-trigger area_entered within one swing. New headless test:
tests/test_frame_collision_driver.gd (synthetic 4-frame sheet: pose-following
hurtbox, reach-gated hitbox, mirror flip, matrix re-assertion, swing dedup).
Verification pass (9 static-analysis agents over all 39 tests + cross-refs)
confirmed the rebuild and surfaced fixes that were applied:
- grounding: player CharacterSprite/FxOverlay moved to node position (0, 40)
(visible feet at world 398, camera-frame ratio 0.88; FX stays glued since
both sprites shift together and all test-pinned offsets are untouched);
boss.gd gained the set_sprite_height_position override so the authored
Visual (0, 32) survives physics ticks — boss visible feet land at 398 too.
- driver: forward reach is now facing-derived (boss art is native RIGHT-
facing, player native LEFT — the mirror sign of basis_x times heading picks
the correct native side); a late-ACTIVE fallback window guarantees a plain
range hit if no frame of a swing passes the reach gate (art timing can no
longer whiff a beat-anchored swing); frame bounds use per-frame
get_region().get_used_rect() (C++-side, no first-swing hitch).
- test isolation: chart_runner and TimeAnchorSystem resolve bus/manager/clock
via reverse root-child scan, and only the real TimeAnchorSystem autoload
self-drives from the global clock (test instances are inert).
- player.gd no longer contains get_parent() (architecture test contract).
Known pre-existing flake (left as-is, documented): test_player_combo_input
judges through the RhythmManager wall clock headless, so ~20% of beat phase
lands in the miss window and the run can abort; other suites pin judgements.
The `libraries/ =` AnimationPlayer serialization was flagged and verified
VALID (per-library subpath form, matches the editor-saved old project), and
hash-verified load_steps miscounts in old .tres files were intentionally kept
(historical bytes, self-healing on editor save).
## 2026-07-04 final Level 1 and menu UI migration baseline
The playable baseline was restored to the `rthythm_archor_game_player`
reference for camera, HUD canvas, actor/background scale, and authored stage
proportions. Level migration is scoped to Level 1 flow, spawn logic, gate state,
and combat sequencing; it must not resize the player, minions, Boss,
background, HUD, or camera.
- `GameFlowManager` now follows the `Rthythm_archor_game` menu implementation
byte-for-byte and uses the authored menu textures under `assets/ui/menu`.
Difficulty options apply immediately on click, the difficulty page exposes
the reference start/back commands, and no current-value label is drawn over
the command area.
- Stage runtime returns to the player reference view: `UILayer/UI` remains a
CanvasLayer-hosted HUD, the camera starts at `(2047, 395)` with zoom
`(1.25, 1.25)`, and camera follow is retained.
- Root ground anchors for player, the initial minion, and Boss stay at
`y = 560`; the visible sprite feet line is verified at `y = 520` for all
three actors. Boss uses `visual_ground_offset = -40` and the first active
minion uses `visual_ground_offset = -32` so the runtime walk/dash frame lines
up with the same visible baseline without changing actor scale.
## 2026-07-05 charge release facing, blade rain ground line, projectile hit circles
Three combat-feel fixes reported from play: the S-charge release auto-aimed at
the nearest enemy (waves went right whenever a target stood to the right), the
blade rain landing FX rendered 40px below where the player visibly stands, and
enemy bullets carried a hit circle far larger than the drawn bolt.
- Player `projectile_direction` now returns the current facing only (left
heading releases left); the nearest-enemy auto-aim helper was removed along
with the projectile-turn branch in `_play_action_animation` (a release no
longer flips the player toward a target behind them). Regression:
`tests/test_zhan_bo_release_facing.gd`.
- World-FX ground anchoring: actor origins sit on `y = 560` but the visible
feet line is `y = 520` (40px transparent margin under the authored player
frames; minions/boss match via `visual_ground_offset`). Blade rain raindrops
and landing tiles now anchor to `Player.VISIBLE_GROUND_LINE_OFFSET = -40` so
the lowest visible pixel of every landing tile shares the player's visible
ground line instead of sinking into the pier. `tests/test_blade_rain_alignment.gd`
asserts against the constant.
- Projectile collision must stay inside the visible art: `VISUAL_PROFILES`
gained `collision_radius` (bullet 9.0 — the effect_sheet bolt is ~32x18px at
scale 2; wave keeps 32.0, inside its ~66px smallest frame) and
`_apply_visual_profile` duplicates the scene's shared `CircleShape2D` before
resizing so bullet/wave instances never overwrite each other. Layer audit
found emitter/receiver/projectile team masks already correct (player
projectile also keeps enemy_hitbox in its mask on purpose — wave clears
enemy bullets). Melee hitboxes untouched: jian_yu keeps its 140px range,
which stays narrower than the FX spread at every level.
- Known stale test: `test_stage4_player_scene.gd` expects the old 2x-scaled
player Visual with a `(0, 32)` editor feet preview; it fails against the
current 1x `player.tscn` regardless of these changes (verified by reverting).