fix: repair parallax collisions and add play guide
This commit is contained in:
@@ -1,11 +0,0 @@
|
||||
节奏输入有效范围规则
|
||||
|
||||
节奏条的中心位置为核心判定锚点。
|
||||
|
||||
在核心判定锚点左右两侧,各存在一段有效输入范围。该范围可以理解为“中心判定区”。
|
||||
|
||||
只有当普通节奏点或时间锚点进入这个中心判定区时,玩家按键才会被视为有效输入,并进入后续的节奏判定流程。
|
||||
|
||||
如果当前中心判定区内没有任何普通节奏点或时间锚点,则玩家按键视为无效输入。
|
||||
|
||||
无效输入不会触发动作,不会进入 BAD / MISS / GOOD / PERFECT 判定流程,也不会消耗节奏点。
|
||||
@@ -1,7 +0,0 @@
|
||||
开始界面标题固定
|
||||
* 开始界面的背景可以进行循环切换。
|
||||
* 但游戏标题不参与切换,不跟随背景变化。
|
||||
* 标题位置始终固定,避免背景切换时导致标题位置发生变化。
|
||||
3. 玩家初始生成位置
|
||||
* 游戏正式开始后,玩家角色应生成在地图最左侧。
|
||||
* 该位置作为关卡起点,玩家从左向右推进关卡。
|
||||
-569
@@ -1,569 +0,0 @@
|
||||
怪物与 Boss 行为设定方案
|
||||
|
||||
1. 核心原则
|
||||
|
||||
* 近战怪:近距离、高伤害、主动压迫玩家。
|
||||
* 远程怪:远距离、低伤害、保持距离输出。
|
||||
* Boss:根据相位切换近战 / 远程行为组。
|
||||
* 强势相位怪物的攻击可以打断玩家动作。
|
||||
* 弱势相位怪物的攻击不能打断玩家动作。
|
||||
* 玩家动作带霸体时,不会被怪物攻击打断。
|
||||
|
||||
⸻
|
||||
|
||||
2. 通用行为状态
|
||||
|
||||
所有敌人使用以下基础状态:
|
||||
|
||||
待机
|
||||
追踪 / 调整距离
|
||||
攻击预备
|
||||
攻击触发
|
||||
攻击后摇
|
||||
受击
|
||||
脱离 / 后撤
|
||||
死亡
|
||||
|
||||
所有攻击必须有三段:
|
||||
|
||||
攻击预备 → 攻击触发 → 攻击后摇
|
||||
|
||||
状态切换尽量按节拍发生,避免怪物连续无间隔攻击。
|
||||
|
||||
⸻
|
||||
|
||||
3. 伤害设定
|
||||
|
||||
3.1 近战怪伤害
|
||||
|
||||
近战怪攻击伤害:高
|
||||
推荐值:20
|
||||
|
||||
特点:
|
||||
|
||||
* 攻击距离短。
|
||||
* 命中代价高。
|
||||
* 强势相位下可打断玩家。
|
||||
* 弱势相位下只造成伤害,不打断玩家。
|
||||
|
||||
3.2 远程怪伤害
|
||||
|
||||
远程怪攻击伤害:低
|
||||
推荐值:8
|
||||
|
||||
特点:
|
||||
|
||||
* 攻击距离远。
|
||||
* 命中频率较高。
|
||||
* 主要制造站位压力。
|
||||
* 强势相位下可打断玩家。
|
||||
* 弱势相位下只造成伤害,不打断玩家。
|
||||
|
||||
3.3 Boss 伤害
|
||||
|
||||
Boss 根据当前相位切换攻击倾向:
|
||||
|
||||
近战相位:攻击力高
|
||||
远程相位:攻击力低
|
||||
|
||||
推荐值:
|
||||
|
||||
Boss 近战攻击伤害:25
|
||||
Boss 远程攻击伤害:12
|
||||
|
||||
⸻
|
||||
|
||||
4. 近战怪行为逻辑
|
||||
|
||||
4.1 行为定位
|
||||
|
||||
近战怪负责贴身压迫玩家。
|
||||
|
||||
它不需要频繁后撤,也不需要复杂逃跑逻辑。
|
||||
|
||||
⸻
|
||||
|
||||
4.2 距离规则
|
||||
|
||||
警戒距离:360 px
|
||||
追踪停止距离:120 px
|
||||
攻击距离:120 px
|
||||
攻击判定范围:前方 140 px
|
||||
|
||||
⸻
|
||||
|
||||
4.3 行为流程
|
||||
|
||||
玩家进入警戒距离
|
||||
↓
|
||||
近战怪向玩家移动
|
||||
↓
|
||||
进入攻击距离
|
||||
↓
|
||||
停止移动
|
||||
↓
|
||||
攻击预备
|
||||
↓
|
||||
攻击触发
|
||||
↓
|
||||
攻击后摇
|
||||
↓
|
||||
重新判断距离
|
||||
|
||||
⸻
|
||||
|
||||
4.4 受击逻辑
|
||||
|
||||
弱势相位
|
||||
|
||||
受到高伤害
|
||||
可被普通攻击打出硬直
|
||||
不具备明显霸体
|
||||
可以被玩家快速击败
|
||||
|
||||
强势相位
|
||||
|
||||
受到伤害降低
|
||||
硬直减弱
|
||||
普通攻击不一定打断动作
|
||||
攻击触发阶段具备霸体
|
||||
|
||||
⸻
|
||||
|
||||
4.5 墙角逻辑
|
||||
|
||||
近战怪被逼到墙角时,不后撤。
|
||||
|
||||
处理方式:
|
||||
|
||||
如果玩家距离 < 120 px
|
||||
↓
|
||||
近战怪优先攻击
|
||||
↓
|
||||
强势相位下攻击可打断玩家
|
||||
↓
|
||||
命中后对玩家造成小幅击退
|
||||
|
||||
推荐击退距离:
|
||||
|
||||
玩家被击退:80–120 px
|
||||
|
||||
⸻
|
||||
|
||||
5. 远程怪行为逻辑
|
||||
|
||||
5.1 行为定位
|
||||
|
||||
远程怪负责远距离压制玩家。
|
||||
|
||||
它不能被玩家贴身无限连死,所以需要后撤和脱离机制。
|
||||
|
||||
⸻
|
||||
|
||||
5.2 距离规则
|
||||
|
||||
危险距离:0–220 px
|
||||
理想距离:320–480 px
|
||||
最大攻击距离:720 px
|
||||
|
||||
⸻
|
||||
|
||||
5.3 基础行为流程
|
||||
|
||||
玩家进入攻击范围
|
||||
↓
|
||||
远程怪寻找理想距离
|
||||
↓
|
||||
保持 320–480 px 距离
|
||||
↓
|
||||
攻击预备
|
||||
↓
|
||||
发射远程攻击
|
||||
↓
|
||||
攻击冷却
|
||||
↓
|
||||
重新判断距离
|
||||
|
||||
⸻
|
||||
|
||||
5.4 玩家过近时的后撤逻辑
|
||||
|
||||
触发条件:
|
||||
|
||||
玩家距离远程怪 < 220 px
|
||||
|
||||
执行流程:
|
||||
|
||||
停止攻击
|
||||
↓
|
||||
进入短暂脱离保护
|
||||
↓
|
||||
向远离玩家方向后撤
|
||||
↓
|
||||
恢复到 320–420 px 距离
|
||||
↓
|
||||
短暂停顿
|
||||
↓
|
||||
重新进入远程攻击循环
|
||||
|
||||
推荐数值:
|
||||
|
||||
单次后撤距离:220 px
|
||||
最小后撤后距离:320 px
|
||||
最大后撤后距离:420 px
|
||||
后撤后停顿:0.4 秒
|
||||
|
||||
⸻
|
||||
|
||||
5.5 被攻击后的脱离逻辑
|
||||
|
||||
触发条件满足任一即可:
|
||||
|
||||
远程怪被玩家攻击命中
|
||||
玩家距离远程怪 < 160 px
|
||||
远程怪在 1 秒内连续受击 2 次
|
||||
|
||||
执行流程:
|
||||
|
||||
受击
|
||||
↓
|
||||
短暂无敌
|
||||
↓
|
||||
后撤 / 脱离
|
||||
↓
|
||||
恢复到安全距离
|
||||
↓
|
||||
取消无敌
|
||||
↓
|
||||
重新进入攻击循环
|
||||
|
||||
推荐数值:
|
||||
|
||||
短暂无敌时间:0.35 秒
|
||||
后撤距离:220–260 px
|
||||
脱离冷却:3 秒
|
||||
|
||||
限制:
|
||||
|
||||
后撤过程中不攻击
|
||||
无敌时间只用于脱离
|
||||
不能连续触发无敌后撤
|
||||
|
||||
⸻
|
||||
|
||||
5.6 墙角脱离逻辑
|
||||
|
||||
触发条件:
|
||||
|
||||
玩家距离远程怪 < 220 px
|
||||
远程怪背后距离墙体 < 120 px
|
||||
脱离冷却已结束
|
||||
|
||||
执行流程:
|
||||
|
||||
短暂无敌
|
||||
↓
|
||||
穿越玩家 / 闪避到玩家另一侧
|
||||
↓
|
||||
落点与玩家保持约 320 px
|
||||
↓
|
||||
短暂停顿
|
||||
↓
|
||||
恢复攻击循环
|
||||
|
||||
推荐数值:
|
||||
|
||||
穿越距离:玩家身后 320 px
|
||||
无敌时间:0.35 秒
|
||||
墙角脱离冷却:4 秒
|
||||
落地停顿:0.5 秒
|
||||
|
||||
限制:
|
||||
|
||||
墙角脱离不造成伤害
|
||||
不能连续触发
|
||||
不能直接接攻击
|
||||
|
||||
⸻
|
||||
|
||||
6. Boss 行为逻辑
|
||||
|
||||
6.1 Boss 核心设定
|
||||
|
||||
Boss 拥有两套行为组:
|
||||
|
||||
近战相位:近战技能组
|
||||
远程相位:远程技能组
|
||||
|
||||
Boss 不采用小怪的弱势相位秒杀逻辑。
|
||||
|
||||
Boss 的相位切换主要改变:
|
||||
|
||||
攻击方式
|
||||
攻击距离
|
||||
攻击力
|
||||
追踪策略
|
||||
霸体强度
|
||||
|
||||
⸻
|
||||
|
||||
6.2 Boss 近战相位
|
||||
|
||||
定位
|
||||
|
||||
主动靠近玩家
|
||||
使用高伤害近战攻击
|
||||
压迫玩家站位
|
||||
|
||||
目标距离
|
||||
|
||||
目标距离:100–160 px
|
||||
攻击距离:180 px
|
||||
|
||||
行为流程
|
||||
|
||||
判断玩家距离
|
||||
↓
|
||||
如果距离 > 180 px,向玩家靠近
|
||||
↓
|
||||
如果距离 ≤ 180 px,进入近战攻击预备
|
||||
↓
|
||||
攻击触发
|
||||
↓
|
||||
攻击后摇
|
||||
↓
|
||||
重新判断距离
|
||||
|
||||
伤害
|
||||
|
||||
Boss 近战伤害:25
|
||||
|
||||
打断
|
||||
|
||||
近战相位攻击可打断玩家
|
||||
玩家霸体动作除外
|
||||
|
||||
⸻
|
||||
|
||||
6.3 Boss 远程相位
|
||||
|
||||
定位
|
||||
|
||||
主动拉开距离
|
||||
使用低伤害远程攻击
|
||||
制造空间压制
|
||||
|
||||
目标距离
|
||||
|
||||
目标距离:360–520 px
|
||||
最小安全距离:240 px
|
||||
最大攻击距离:760 px
|
||||
|
||||
行为流程
|
||||
|
||||
判断玩家距离
|
||||
↓
|
||||
如果玩家距离 < 240 px,Boss 后撤
|
||||
↓
|
||||
如果玩家距离在 360–520 px,使用远程攻击
|
||||
↓
|
||||
如果玩家距离 > 760 px,Boss 小幅靠近
|
||||
↓
|
||||
重新判断距离
|
||||
|
||||
伤害
|
||||
|
||||
Boss 远程伤害:12
|
||||
|
||||
打断
|
||||
|
||||
远程相位攻击可打断玩家
|
||||
玩家霸体动作除外
|
||||
|
||||
⸻
|
||||
|
||||
6.4 Boss 相位切换规则
|
||||
|
||||
当前为近战相位
|
||||
↓
|
||||
切换到远程相位
|
||||
↓
|
||||
Boss 拉开距离
|
||||
↓
|
||||
使用远程攻击组
|
||||
当前为远程相位
|
||||
↓
|
||||
切换到近战相位
|
||||
↓
|
||||
Boss 主动靠近玩家
|
||||
↓
|
||||
使用近战攻击组
|
||||
|
||||
相位切换时不立刻攻击。
|
||||
|
||||
推荐流程:
|
||||
|
||||
相位切换提示
|
||||
↓
|
||||
短暂停顿
|
||||
↓
|
||||
调整距离
|
||||
↓
|
||||
进入新相位攻击循环
|
||||
|
||||
推荐数值:
|
||||
|
||||
相位切换停顿:0.5 秒
|
||||
|
||||
⸻
|
||||
|
||||
6.5 Boss 受击逻辑
|
||||
|
||||
Boss 不应被普通连招无限压制。
|
||||
|
||||
普通受击
|
||||
|
||||
正常掉血
|
||||
播放受击反馈
|
||||
不进入完整硬直
|
||||
|
||||
韧性机制
|
||||
|
||||
推荐设定:
|
||||
|
||||
Boss 韧性值:100
|
||||
普通攻击削韧:10
|
||||
技能攻击削韧:20
|
||||
时间锚点强化攻击削韧:30
|
||||
|
||||
韧性归零时:
|
||||
|
||||
Boss 进入短暂硬直
|
||||
硬直后恢复韧性
|
||||
|
||||
推荐数值:
|
||||
|
||||
Boss 硬直时间:1.2 秒
|
||||
韧性恢复值:100
|
||||
|
||||
⸻
|
||||
|
||||
6.6 Boss 霸体逻辑
|
||||
|
||||
普通移动:轻霸体
|
||||
攻击预备:中霸体
|
||||
攻击触发:强霸体
|
||||
攻击后摇:霸体降低
|
||||
硬直状态:无霸体
|
||||
|
||||
玩家主要输出窗口:
|
||||
|
||||
Boss 攻击后摇
|
||||
Boss 韧性破防硬直
|
||||
Boss 相位切换后的短暂停顿
|
||||
|
||||
⸻
|
||||
|
||||
6.7 Boss 后撤逻辑
|
||||
|
||||
只在远程相位下启用。
|
||||
|
||||
触发条件:
|
||||
|
||||
Boss 当前为远程相位
|
||||
玩家距离 Boss < 240 px
|
||||
|
||||
执行流程:
|
||||
|
||||
停止攻击
|
||||
↓
|
||||
后撤
|
||||
↓
|
||||
恢复到 360–480 px 距离
|
||||
↓
|
||||
短暂停顿
|
||||
↓
|
||||
继续远程攻击
|
||||
|
||||
推荐数值:
|
||||
|
||||
Boss 后撤距离:280 px
|
||||
后撤后目标距离:360–480 px
|
||||
后撤冷却:4 秒
|
||||
后撤后停顿:0.5 秒
|
||||
|
||||
限制:
|
||||
|
||||
Boss 后撤过程中不攻击
|
||||
Boss 后撤不造成伤害
|
||||
Boss 后撤不能连续触发
|
||||
|
||||
⸻
|
||||
|
||||
6.8 Boss 墙角处理
|
||||
|
||||
触发条件:
|
||||
|
||||
Boss 当前为远程相位
|
||||
玩家距离 Boss < 240 px
|
||||
Boss 背后距离墙体 < 160 px
|
||||
后撤冷却已结束
|
||||
|
||||
处理方式:
|
||||
|
||||
Boss 短暂无敌
|
||||
↓
|
||||
穿越玩家 / 跳到另一侧
|
||||
↓
|
||||
与玩家重新拉开 400 px 左右距离
|
||||
↓
|
||||
短暂停顿
|
||||
↓
|
||||
恢复远程攻击循环
|
||||
|
||||
推荐数值:
|
||||
|
||||
Boss 墙角脱离冷却:6 秒
|
||||
短暂无敌时间:0.4 秒
|
||||
落点距离玩家:400 px
|
||||
落地停顿:0.6 秒
|
||||
|
||||
限制:
|
||||
|
||||
墙角脱离不造成伤害
|
||||
不能连续触发
|
||||
不能直接接攻击
|
||||
|
||||
⸻
|
||||
|
||||
7. 最终规则汇总
|
||||
|
||||
近战怪
|
||||
|
||||
短距离
|
||||
高伤害
|
||||
主动靠近
|
||||
强势相位可打断玩家
|
||||
弱势相位容易被击败
|
||||
被逼墙角时不逃跑,使用攻击反压
|
||||
|
||||
远程怪
|
||||
|
||||
长距离
|
||||
低伤害
|
||||
保持 320–480 px 距离
|
||||
被近身后后撤
|
||||
被连续攻击后短暂无敌脱离
|
||||
被逼墙角后穿越玩家脱离
|
||||
脱离动作不造成伤害
|
||||
|
||||
Boss
|
||||
|
||||
拥有近战 / 远程两套行为组
|
||||
随相位切换行为
|
||||
近战相位高伤害,主动压迫
|
||||
远程相位低伤害,主动拉开距离
|
||||
Boss 有韧性和阶段性霸体
|
||||
Boss 远程相位被近身时后撤
|
||||
Boss 被逼墙角时可脱离
|
||||
Boss 脱离动作不造成伤害
|
||||
+663
@@ -0,0 +1,663 @@
|
||||
# 《Anchor》试玩说明
|
||||
|
||||
这是一款节奏横板动作游戏。你不需要像传统动作游戏那样一直按方向键移动,角色的前进、后退、突进、起跳、下坠和攻击位移,主要都来自你在音乐节奏上的出招。
|
||||
|
||||
一句话理解玩法:
|
||||
|
||||
> 听节奏按键,用招式带动角色移动,在过去与未来两个时间相位之间寻找敌人的弱点,打完小怪后进入 Boss 战。
|
||||
|
||||
---
|
||||
|
||||
## 1. 基本目标
|
||||
|
||||
试玩关卡分为两个部分:
|
||||
|
||||
1. 前半段清理小怪。
|
||||
2. 进入右侧 Boss 区域,击败 Boss。
|
||||
|
||||
前场一共会出现 6 只小怪。全部击败后,通往 Boss 区域的道路打开。进入 Boss 区域后,Boss 战正式开始。
|
||||
|
||||
胜利条件:
|
||||
|
||||
- 击败 Boss。
|
||||
|
||||
失败条件:
|
||||
|
||||
- 玩家生命归零。
|
||||
|
||||
---
|
||||
|
||||
## 2. 基本操作
|
||||
|
||||
| 按键 | 用途 |
|
||||
| --- | --- |
|
||||
| A | 向左攻击,也可长按蓄力 |
|
||||
| D | 向右攻击,也可长按蓄力 |
|
||||
| W | 上挑,让敌人或自己进入空中连段节奏 |
|
||||
| S | 防御;在空中时可下砸;也可长按蓄力 |
|
||||
| Space | 特殊键,用来派生技能或释放部分蓄力招式 |
|
||||
| E | 显示或隐藏招式表 |
|
||||
| Esc | 暂停游戏 |
|
||||
|
||||
最重要的一点:
|
||||
|
||||
> A、D、W、S、Space 不是自由移动键。它们都是“节奏动作键”。
|
||||
|
||||
角色移动主要来自动作本身,比如向右攻击会带你向右推进,冲刺斩会穿过敌人,上挑和空中攻击会改变你的高度与落点。
|
||||
|
||||
---
|
||||
|
||||
## 3. 节奏判定
|
||||
|
||||
每次按键都会和音乐节拍比较,得到一个判定。
|
||||
|
||||
| 判定 | 含义 |
|
||||
| --- | --- |
|
||||
| Perfect | 非常准,效果最好 |
|
||||
| Good | 比较准,效果稳定 |
|
||||
| Bad | 勉强命中节奏,动作仍可成立,但收益下降 |
|
||||
| Miss | 没有踩中节奏,动作失败 |
|
||||
|
||||
判定会影响这些内容:
|
||||
|
||||
- 你是否成功出招。
|
||||
- 造成多少伤害。
|
||||
- 获得多少能量。
|
||||
- 使用技能时消耗多少能量。
|
||||
- 是否能稳住时间相位。
|
||||
|
||||
大致规则:
|
||||
|
||||
- Perfect:标准伤害,标准回能,技能消耗更低。
|
||||
- Good:伤害和回能略低。
|
||||
- Bad:伤害和回能明显降低,技能消耗更高。
|
||||
- Miss:不出招,连段被打断。
|
||||
|
||||
连续乱按不会带来优势。同一个节奏点被使用后,短时间内重复按键会被忽略;继续乱按会变成 Miss,并让你进入短暂的按键失效状态。这个设计是为了鼓励你跟着节奏打,而不是疯狂连点。
|
||||
|
||||
---
|
||||
|
||||
## 4. 招式记忆与连段
|
||||
|
||||
游戏会记住你最近几次有效输入,并根据输入顺序决定招式。
|
||||
|
||||
你可以把它理解成一小段“招式记忆”:
|
||||
|
||||
```text
|
||||
A → 普通左攻击
|
||||
A A → 第二段左攻击
|
||||
A A A → 第三段左攻击
|
||||
A Space → 左冲刺斩
|
||||
A A Space → 左终结技
|
||||
A A A Space → 左重击
|
||||
```
|
||||
|
||||
右方向同理:
|
||||
|
||||
```text
|
||||
D → 普通右攻击
|
||||
D D → 第二段右攻击
|
||||
D D D → 第三段右攻击
|
||||
D Space → 右冲刺斩
|
||||
D D Space → 右终结技
|
||||
D D D Space → 右重击
|
||||
```
|
||||
|
||||
当这段记忆里出现 Miss 时,连段会断开。你需要重新踩节奏开始下一轮输入。
|
||||
|
||||
---
|
||||
|
||||
## 5. 能量与技能
|
||||
|
||||
玩家有能量条。
|
||||
|
||||
能量主要通过基础攻击获得:
|
||||
|
||||
- 普通攻击命中节奏可以回复能量。
|
||||
- Perfect 回能最多。
|
||||
- Good 次之。
|
||||
- Bad 回能较少。
|
||||
|
||||
技能通常需要消耗能量:
|
||||
|
||||
- 冲刺斩消耗较少,适合突进、穿越敌人和调整位置。
|
||||
- 终结技消耗中等,伤害和击退更明显。
|
||||
- 重击消耗较高,伤害高,并且更容易把敌人打飞或击退。
|
||||
- 蓄力技能根据蓄力等级消耗不同,等级越高,威力越强,消耗也越高。
|
||||
|
||||
能量不足时,技能不会释放成功。
|
||||
|
||||
---
|
||||
|
||||
## 6. 常用招式
|
||||
|
||||
### 普通攻击
|
||||
|
||||
A 和 D 是最基础的攻击键。
|
||||
|
||||
- A:向左攻击。
|
||||
- D:向右攻击。
|
||||
- 连续按同方向可以打出多段攻击。
|
||||
- 普通攻击会带有轻微位移,可以用来接近敌人或调整站位。
|
||||
- 普通攻击是最稳定的回能方式。
|
||||
|
||||
### 冲刺斩
|
||||
|
||||
输入:
|
||||
|
||||
```text
|
||||
A Space
|
||||
D Space
|
||||
```
|
||||
|
||||
特点:
|
||||
|
||||
- 向对应方向快速突进。
|
||||
- 可以穿过敌人。
|
||||
- 适合躲开夹击、穿到敌人背后、靠近远程敌人。
|
||||
- 冲刺结束后,角色会重新恢复正常身体阻挡,不会长期站在敌人体内。
|
||||
|
||||
### 终结技
|
||||
|
||||
输入:
|
||||
|
||||
```text
|
||||
A A Space
|
||||
D D Space
|
||||
```
|
||||
|
||||
特点:
|
||||
|
||||
- 比普通攻击伤害更高。
|
||||
- 有更明显的击退效果。
|
||||
- 适合在敌人露出破绽时使用。
|
||||
|
||||
### 重击
|
||||
|
||||
输入:
|
||||
|
||||
```text
|
||||
A A A Space
|
||||
D D D Space
|
||||
```
|
||||
|
||||
特点:
|
||||
|
||||
- 消耗更高。
|
||||
- 伤害高。
|
||||
- 击退和打飞效果更强。
|
||||
- 出手和收招更重,需要找准时机。
|
||||
|
||||
### 上挑
|
||||
|
||||
输入:
|
||||
|
||||
```text
|
||||
W
|
||||
```
|
||||
|
||||
特点:
|
||||
|
||||
- 可把战斗带入空中节奏。
|
||||
- 可衔接空中攻击。
|
||||
- 适合打断地面节奏,制造新的攻击角度。
|
||||
|
||||
### 空中攻击
|
||||
|
||||
在空中按:
|
||||
|
||||
```text
|
||||
A
|
||||
D
|
||||
S
|
||||
```
|
||||
|
||||
效果:
|
||||
|
||||
- A / D:向左或向右进行空中攻击,并带有明显横向移动。
|
||||
- S:向下攻击,适合从空中压回地面。
|
||||
|
||||
### 防御
|
||||
|
||||
输入:
|
||||
|
||||
```text
|
||||
S
|
||||
```
|
||||
|
||||
特点:
|
||||
|
||||
- 地面状态下按 S 会进入防御动作。
|
||||
- 正面防御可以减轻或挡住伤害。
|
||||
- 背后受到攻击时,防御效果会变差。
|
||||
- 防御仍然需要跟节奏配合,乱按不会稳定保命。
|
||||
|
||||
---
|
||||
|
||||
## 7. 蓄力招式
|
||||
|
||||
部分按键可以长按蓄力。
|
||||
|
||||
### A / D 蓄力:剑雨
|
||||
|
||||
用法:
|
||||
|
||||
1. 按 A 或 D 打出一次对应方向的普通攻击。
|
||||
2. 继续按住不放,进入蓄力。
|
||||
3. 松开按键,释放剑雨。
|
||||
|
||||
特点:
|
||||
|
||||
- A 蓄力:向左释放剑雨。
|
||||
- D 蓄力:向右释放剑雨。
|
||||
- 蓄力越久,等级越高。
|
||||
- 等级越高,伤害越高,消耗能量越多。
|
||||
- 剑雨适合攻击一片区域,尤其适合压制小怪。
|
||||
|
||||
注意:
|
||||
|
||||
- A / D 蓄力过程中按 Space,会优先变成普通连段派生,例如冲刺斩。
|
||||
- 也就是说,如果你正在按住 A,途中按 Space,可能会打出 `A Space` 的冲刺斩,而不是剑雨。
|
||||
|
||||
### S 蓄力:斩波
|
||||
|
||||
用法:
|
||||
|
||||
1. 按住 S 进入蓄力。
|
||||
2. 蓄力过程中按 Space 释放斩波。
|
||||
|
||||
特点:
|
||||
|
||||
- 斩波是远程攻击。
|
||||
- 会朝角色当前面对方向发射。
|
||||
- 蓄力越久,等级越高。
|
||||
- 等级越高,射程、伤害和击退越强。
|
||||
- 适合攻击不方便接近的敌人。
|
||||
|
||||
---
|
||||
|
||||
## 8. 过去与未来
|
||||
|
||||
游戏中有两个时间相位:
|
||||
|
||||
- 过去
|
||||
- 未来
|
||||
|
||||
相位会影响:
|
||||
|
||||
- 背景与画面色调。
|
||||
- 音乐层次。
|
||||
- 敌人的强弱状态。
|
||||
- 界面提示。
|
||||
|
||||
相位不会改变地形。你能站的位置、墙和地面不会因为过去或未来而变化。
|
||||
|
||||
---
|
||||
|
||||
## 9. 时间锚点
|
||||
|
||||
战斗中会出现特殊节奏点,叫做时间锚点。
|
||||
|
||||
当时间锚点出现时:
|
||||
|
||||
- 如果你在窗口内按出 Perfect、Good 或 Bad,当前相位会保持。
|
||||
- 如果你 Miss,或者没有及时输入,相位会切换到另一边。
|
||||
|
||||
时间锚点不是单独的按键。你可以用当前任何合适的动作输入去稳住它。
|
||||
|
||||
举例:
|
||||
|
||||
- 时间锚点出现时,你按 D,既可能打出右攻击,也可能同时稳住相位。
|
||||
- 如果这次按键是 Miss,动作失败,相位也可能切换。
|
||||
|
||||
---
|
||||
|
||||
## 10. 攻击提升与回血
|
||||
|
||||
成功守住时间锚点会给你奖励。
|
||||
|
||||
### 攻击提升
|
||||
|
||||
- Perfect 或 Good 守住时间锚点:获得 1 层攻击提升。
|
||||
- Bad 守住时间锚点:只保持相位,不增加攻击提升。
|
||||
- 攻击提升最多 7 层。
|
||||
- 每层都会让你的攻击更强。
|
||||
- 相位切换时,会失去 2 层。
|
||||
- 受到伤害进入受击状态时,也会失去一部分层数。
|
||||
|
||||
### 回血
|
||||
|
||||
成功守住时间锚点时会回血。
|
||||
|
||||
- Perfect 回血最多。
|
||||
- Good 次之。
|
||||
- Bad 较少。
|
||||
- 如果你的血量很低,回血效果会更明显。
|
||||
|
||||
简单理解:
|
||||
|
||||
> 节奏越稳,越能维持当前相位,也越能把角色养强。
|
||||
|
||||
---
|
||||
|
||||
## 11. 敌人的强弱状态
|
||||
|
||||
小怪会随着过去与未来改变强弱。
|
||||
|
||||
### 近战小怪
|
||||
|
||||
近战小怪在过去更强,在未来更弱。
|
||||
|
||||
过去时:
|
||||
|
||||
- 攻击更危险。
|
||||
- 更容易打断你。
|
||||
- 承受你的伤害更低。
|
||||
|
||||
未来时:
|
||||
|
||||
- 攻击威胁下降。
|
||||
- 更容易被你击杀。
|
||||
- 更适合主动输出。
|
||||
|
||||
### 远程小怪
|
||||
|
||||
远程小怪在未来更强,在过去更弱。
|
||||
|
||||
过去时:
|
||||
|
||||
- 远程压力较低。
|
||||
- 更容易被处理。
|
||||
|
||||
未来时:
|
||||
|
||||
- 射击更危险。
|
||||
- 更容易打断你。
|
||||
- 需要优先压制或躲避。
|
||||
|
||||
### 推荐打法
|
||||
|
||||
- 过去:优先小心近战怪,可以趁机处理远程怪。
|
||||
- 未来:优先小心远程怪,可以趁机处理近战怪。
|
||||
- 如果场上两种怪都有,优先攻击当前较弱的那一类,能更快减少场面压力。
|
||||
|
||||
---
|
||||
|
||||
## 12. 小怪行为
|
||||
|
||||
### 近战小怪
|
||||
|
||||
近战小怪会:
|
||||
|
||||
- 在区域内巡逻。
|
||||
- 看到你后追击。
|
||||
- 靠近后按节奏出手。
|
||||
- 强势状态下攻击更容易打断你。
|
||||
|
||||
应对方式:
|
||||
|
||||
- 不要一直站在它正面吃招。
|
||||
- 用 A / D 普攻调整距离。
|
||||
- 用冲刺斩穿过它,换到另一侧。
|
||||
- 在它处于弱势相位时集中输出。
|
||||
|
||||
### 远程小怪
|
||||
|
||||
远程小怪会:
|
||||
|
||||
- 保持距离射击。
|
||||
- 被你贴近后尝试后撤。
|
||||
- 连续被打时,会短暂闪避脱离。
|
||||
|
||||
应对方式:
|
||||
|
||||
- 用冲刺斩快速接近。
|
||||
- 用斩波远距离反击。
|
||||
- 不要长期放任远程怪在屏幕边缘输出。
|
||||
- 在它处于弱势相位时尽快解决。
|
||||
|
||||
---
|
||||
|
||||
## 13. 第一关流程
|
||||
|
||||
第一关小怪部分一共 3 个阶段。
|
||||
|
||||
### 阶段一:单只近战怪
|
||||
|
||||
开局你会从地图偏左位置进入,向右推进后遇到第一只近战怪。
|
||||
|
||||
这个阶段主要让你熟悉:
|
||||
|
||||
- 节奏攻击。
|
||||
- 普通连段。
|
||||
- 近战怪追击。
|
||||
- 过去/未来对敌人强弱的影响。
|
||||
|
||||
### 阶段二:单只远程怪
|
||||
|
||||
击败第一只近战怪后,会出现一只远程怪。
|
||||
|
||||
这个阶段主要让你熟悉:
|
||||
|
||||
- 远程攻击压力。
|
||||
- 接近远程怪的方法。
|
||||
- 冲刺斩和斩波的使用时机。
|
||||
|
||||
### 阶段三:混合增援
|
||||
|
||||
击败远程怪后,会陆续出现 4 只增援:
|
||||
|
||||
- 近战怪
|
||||
- 远程怪
|
||||
- 近战怪
|
||||
- 远程怪
|
||||
|
||||
它们不会一瞬间全部出现,而是分批进入战斗。
|
||||
|
||||
这个阶段的重点是:
|
||||
|
||||
- 根据当前相位选择优先目标。
|
||||
- 不要被近战和远程同时压住。
|
||||
- 用技能快速减少敌人数。
|
||||
- 保持节奏,尽量不要让时间相位失控。
|
||||
|
||||
---
|
||||
|
||||
## 14. Boss 战
|
||||
|
||||
清完全部小怪后,右侧道路会打开。进入 Boss 区域后,Boss 战开始。
|
||||
|
||||
Boss 和小怪不同:
|
||||
|
||||
- Boss 不会像小怪那样有明显弱势相位。
|
||||
- Boss 的攻击一般都能打断你。
|
||||
- Boss 血量更高。
|
||||
- 普通攻击可以造成伤害,但不容易压制 Boss。
|
||||
- 消耗能量的技能更适合压制 Boss。
|
||||
|
||||
### 过去相位的 Boss
|
||||
|
||||
过去相位下,Boss 更偏向近战。
|
||||
|
||||
常见表现:
|
||||
|
||||
- 慢一些。
|
||||
- 攻击更重。
|
||||
- 会使用近身连击和突刺。
|
||||
- 部分强力招式前会给你时间锚点压力。
|
||||
|
||||
应对方式:
|
||||
|
||||
- 不要贪刀。
|
||||
- 观察它准备动作。
|
||||
- 用冲刺斩调整位置。
|
||||
- 用技能抓它收招后的空隙。
|
||||
|
||||
### 未来相位的 Boss
|
||||
|
||||
未来相位下,Boss 更偏向远程。
|
||||
|
||||
常见表现:
|
||||
|
||||
- 出手更频繁。
|
||||
- 会使用射击和弹幕类攻击。
|
||||
- 贴得太近时可能后撤拉开距离。
|
||||
|
||||
应对方式:
|
||||
|
||||
- 不要只站远处挨打。
|
||||
- 找节奏接近它。
|
||||
- 注意躲开弹道。
|
||||
- 用斩波或冲刺斩寻找反击机会。
|
||||
|
||||
### Boss 战核心建议
|
||||
|
||||
- 普通攻击用于回能和维持节奏。
|
||||
- 技能用于压制和打出爆发。
|
||||
- 时间锚点尽量不要丢。
|
||||
- 相位切换后,先观察 Boss 行为变化,不要立刻乱按。
|
||||
|
||||
---
|
||||
|
||||
## 15. 防御、受击和击退
|
||||
|
||||
### 防御
|
||||
|
||||
地面按 S 可以防御。
|
||||
|
||||
防御适合:
|
||||
|
||||
- 你来不及闪开时。
|
||||
- Boss 或强势敌人即将出手时。
|
||||
- 想稳住节奏而不是继续贪攻击时。
|
||||
|
||||
注意:
|
||||
|
||||
- 防御更适合挡正面攻击。
|
||||
- 背后被打时,防御效果会变差。
|
||||
- 防御不是无敌。
|
||||
|
||||
### 受击
|
||||
|
||||
受到强势攻击或 Boss 攻击时,你可能会:
|
||||
|
||||
- 被打断当前动作。
|
||||
- 进入短暂硬直。
|
||||
- 被击退。
|
||||
- 掉一部分攻击提升层数。
|
||||
|
||||
受到弱势小怪攻击时,通常只会掉血,不一定打断你。
|
||||
|
||||
### 击退
|
||||
|
||||
很多重攻击和技能都会击退敌人。
|
||||
|
||||
你也可能被敌人击退。被击退后,角色会短暂滑行一段距离,这段时间要等角色稳定后再重新找节奏出手。
|
||||
|
||||
---
|
||||
|
||||
## 16. 推荐试玩思路
|
||||
|
||||
第一次试玩可以按这个顺序熟悉:
|
||||
|
||||
1. 先只用 A / D 跟节奏打普通攻击。
|
||||
2. 观察 Perfect、Good、Bad、Miss 的区别。
|
||||
3. 尝试 A A A 或 D D D 的三段攻击。
|
||||
4. 尝试 A Space / D Space 的冲刺斩。
|
||||
5. 试着用 W 把战斗带入空中。
|
||||
6. 学会看过去/未来相位,优先打当前较弱的小怪。
|
||||
7. 清小怪后进入 Boss 区域。
|
||||
8. Boss 战中用普通攻击回能,用技能找机会压制。
|
||||
|
||||
---
|
||||
|
||||
## 17. 简单打法建议
|
||||
|
||||
### 面对近战怪
|
||||
|
||||
- 等它靠近后,用普通攻击或冲刺斩换边。
|
||||
- 它强势时少贪刀。
|
||||
- 它弱势时集中输出。
|
||||
|
||||
### 面对远程怪
|
||||
|
||||
- 不要让它一直在远处安全射击。
|
||||
- 用冲刺斩靠近。
|
||||
- 用斩波对射。
|
||||
- 它未来相位更危险,优先处理。
|
||||
|
||||
### 面对多只怪
|
||||
|
||||
- 先处理当前相位下较弱的怪。
|
||||
- 不要站在两只怪中间。
|
||||
- 用冲刺斩穿出去,避免被夹住。
|
||||
- 能量够时,用重击或蓄力技能快速减少敌人数。
|
||||
|
||||
### 面对 Boss
|
||||
|
||||
- 普通攻击主要用于回能。
|
||||
- 耗能技能更适合压制 Boss。
|
||||
- Boss 近战相位不要硬拼太久。
|
||||
- Boss 远程相位要主动寻找靠近机会。
|
||||
- 时间锚点出现时优先稳住节奏。
|
||||
|
||||
---
|
||||
|
||||
## 18. 常见问题
|
||||
|
||||
### 为什么按了方向键角色不走?
|
||||
|
||||
这是游戏设计。角色没有传统自由移动,移动来自动作。A、D、W、S、Space 都是节奏动作键。
|
||||
|
||||
### 为什么我按了键却没出招?
|
||||
|
||||
可能原因:
|
||||
|
||||
- 没踩中节奏,变成 Miss。
|
||||
- 能量不足,技能放不出来。
|
||||
- 正在上一招的收招阶段,还不能接下一招。
|
||||
- 刚刚乱按触发了短暂按键失效。
|
||||
|
||||
### 为什么敌人有时候很硬,有时候很脆?
|
||||
|
||||
因为过去/未来相位会改变小怪强弱。
|
||||
|
||||
- 近战怪:过去强,未来弱。
|
||||
- 远程怪:未来强,过去弱。
|
||||
|
||||
强势时敌人更难打,也更危险;弱势时更适合输出。
|
||||
|
||||
### 为什么相位突然切换?
|
||||
|
||||
你可能没有守住时间锚点。时间锚点出现后,如果没有及时输入,或者输入 Miss,就会切换过去/未来。
|
||||
|
||||
### 为什么我的攻击突然变强?
|
||||
|
||||
你可能连续守住了时间锚点,获得了攻击提升层数。层数越高,攻击越强。
|
||||
|
||||
### 为什么攻击提升又掉了?
|
||||
|
||||
相位切换会掉层,受到伤害进入受击状态也会掉层。
|
||||
|
||||
### 为什么冲刺斩可以穿过敌人?
|
||||
|
||||
这是冲刺斩的特点。它允许你穿过敌人,用来调整站位、躲避夹击或靠近远程敌人。
|
||||
|
||||
### 为什么普通攻击打 Boss 感觉压不住?
|
||||
|
||||
Boss 对普通攻击更难被压制。普通攻击适合回能和找节奏,真正压制 Boss 更依赖消耗能量的技能。
|
||||
|
||||
---
|
||||
|
||||
## 19. 一句话小抄
|
||||
|
||||
- 跟节奏,不要乱按。
|
||||
- A / D 普攻回能。
|
||||
- Space 派生技能。
|
||||
- A / D 长按松开是剑雨。
|
||||
- S 长按后按 Space 是斩波。
|
||||
- W 进入空中节奏。
|
||||
- 过去重点小心近战怪。
|
||||
- 未来重点小心远程怪。
|
||||
- 时间锚点尽量守住。
|
||||
- Boss 用普通攻击回能,用技能压制。
|
||||
@@ -38,7 +38,7 @@
|
||||
[ext_resource type="Resource" path="res://resources/effects/effect_on_landed_haste.tres" id="30_landed_haste"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_player"]
|
||||
size = Vector2(18, 48)
|
||||
size = Vector2(34, 52)
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_hitbox"]
|
||||
size = Vector2(48, 40)
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
[ext_resource type="Script" path="res://scenes/components/frame_collision_driver.gd" id="18_frame_collision"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_body"]
|
||||
size = Vector2(24, 58)
|
||||
size = Vector2(52, 64)
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_hurtbox"]
|
||||
size = Vector2(28, 62)
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
[ext_resource type="Script" path="res://scenes/components/overhead_health_bar.gd" id="18_overhead_health_bar"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_body"]
|
||||
size = Vector2(20, 46)
|
||||
size = Vector2(36, 50)
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_hurtbox"]
|
||||
size = Vector2(24, 50)
|
||||
|
||||
@@ -21,6 +21,10 @@ script = ExtResource("1_ground_background")
|
||||
[node name="GroundAnchor" type="Marker2D" parent="."]
|
||||
position = Vector2(2047, 560)
|
||||
|
||||
[node name="SkyParallaxLayer" type="Node2D" parent="."]
|
||||
position = Vector2(896.5, 0)
|
||||
z_index = -11
|
||||
|
||||
[node name="ArtLayer" type="Node2D" parent="."]
|
||||
position = Vector2(896.5, 0)
|
||||
z_index = -10
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
extends Node2D
|
||||
|
||||
## 2026-07-06 视差定案(策划:远景夕阳要时刻在画面内):背景是"远景天幕",
|
||||
## 仅以 0.2 倍速随世界滚动(即 80% 跟随相机)——此前 0.92/0.8 的"近景层"
|
||||
## 参数方向反了,背景几乎钉在世界上,所以一直"看不出视差"。
|
||||
## 锚点 1734.5 = 让夕阳(原画 x≈760,世界 x≈1656)在出生点镜头(可见中心
|
||||
## 1340.8)恰好居中;相机全程 [1340.8, 2753.2] 内夕阳屏幕位置从中心缓移到
|
||||
## 左 282px,始终在画面内(已验算含光晕不出屏)。两端背景覆盖也已验算:
|
||||
## 极左窗 [880,1801.6]⊂[581.5,2882.5]、极右窗 [2292.4,3214]⊂[1711.5,4012.5]。
|
||||
## 已知取舍:步道砖烙在原画里会明显随镜头走(角色脚下滑动感),单张烙死图
|
||||
## 无解;美术把"近景步道"拆成独立 1:1 层后即可消除。勿用 Parallax2D
|
||||
## (历史上因 CanvasModulate/wipe 兼容性移除过,见 decisions.md)。
|
||||
## 2026-07-07 视差修正:当前美术图里地面/步道和远景烙在同一张图上。
|
||||
## 为了让角色脚下参照物保持 1:1 世界坐标,ArtLayer 先作为 WorldGroundLayer
|
||||
## 固定不随相机做视差。未来美术拆出天空/夕阳后,把远景节点放入
|
||||
## SkyParallaxLayer,即可继续按 0.2 倍世界滚动(80% 跟随相机)。
|
||||
## 勿用 Parallax2D(历史上因 CanvasModulate/wipe 兼容性移除过,见 decisions.md)。
|
||||
const PARALLAX_SCROLL_SCALE := 0.2
|
||||
const PARALLAX_ANCHOR_X := 1734.5
|
||||
|
||||
@onready var past_background: CanvasItem = $ArtLayer/PastBackground
|
||||
@onready var future_background: CanvasItem = $ArtLayer/FutureBackground
|
||||
@onready var _art_layer: Node2D = $ArtLayer
|
||||
@onready var _world_ground_layer: Node2D = $ArtLayer
|
||||
@onready var _sky_parallax_layer: Node2D = get_node_or_null("SkyParallaxLayer") as Node2D
|
||||
|
||||
var _art_base_x := 0.0
|
||||
var _world_ground_base_x := 0.0
|
||||
var _sky_parallax_base_x := 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_art_base_x = _art_layer.position.x
|
||||
_world_ground_base_x = _world_ground_layer.position.x
|
||||
if _sky_parallax_layer != null:
|
||||
_sky_parallax_base_x = _sky_parallax_layer.position.x
|
||||
_set_time_phase(_current_time_phase())
|
||||
var bus := _event_bus_or_null()
|
||||
if bus != null and bus.has_signal("time_phase_changed") and not bus.is_connected("time_phase_changed", _on_time_phase_changed):
|
||||
@@ -29,14 +28,17 @@ func _ready() -> void:
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
_world_ground_layer.position.x = _world_ground_base_x
|
||||
if _sky_parallax_layer == null:
|
||||
return
|
||||
var camera := get_viewport().get_camera_2d()
|
||||
if camera == null:
|
||||
# headless 测试/无相机场景:背景保持原位。
|
||||
_art_layer.position.x = _art_base_x
|
||||
# headless 测试/无相机场景:远景层也保持原位。
|
||||
_sky_parallax_layer.position.x = _sky_parallax_base_x
|
||||
return
|
||||
# get_screen_center_position 含 limit 夹取,玩家贴墙时背景不会继续滑。
|
||||
# get_screen_center_position 含 limit 夹取,玩家贴墙时远景不会继续滑。
|
||||
var center_x := camera.get_screen_center_position().x
|
||||
_art_layer.position.x = _art_base_x + (center_x - PARALLAX_ANCHOR_X) * (1.0 - PARALLAX_SCROLL_SCALE)
|
||||
_sky_parallax_layer.position.x = _sky_parallax_base_x + (center_x - PARALLAX_ANCHOR_X) * (1.0 - PARALLAX_SCROLL_SCALE)
|
||||
|
||||
|
||||
func _on_time_phase_changed(_previous: StringName, current: StringName, _reason: StringName) -> void:
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
extends SceneTree
|
||||
|
||||
const PLAYER_BODY_MIN := Vector2(34, 52)
|
||||
const MINION_BODY_MIN := Vector2(36, 50)
|
||||
const BOSS_BODY_MIN := Vector2(52, 64)
|
||||
const ENEMY_BODY_MASK := 1 << 6
|
||||
|
||||
var failures: Array[String] = []
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
root.size = Vector2i(1152, 648)
|
||||
await process_frame
|
||||
await _check_background_art_layer_is_world_locked()
|
||||
await _check_body_shapes_match_visual_blocking_contract()
|
||||
await _check_scene_body_rects_overlap_before_visible_interpenetration()
|
||||
await _check_dash_through_ghost_query_uses_scene_body_shapes("Minion", "res://scenes/enemies/minion.tscn", 26.0, (PLAYER_BODY_MIN.x + MINION_BODY_MIN.x) * 0.5 + 2.0)
|
||||
await _check_dash_through_ghost_query_uses_scene_body_shapes("Boss", "res://scenes/enemies/boss.tscn", 40.0, (PLAYER_BODY_MIN.x + BOSS_BODY_MIN.x) * 0.5 + 2.0)
|
||||
_finish()
|
||||
|
||||
|
||||
func _check_background_art_layer_is_world_locked() -> void:
|
||||
var container := Node2D.new()
|
||||
root.add_child(container)
|
||||
var camera := Camera2D.new()
|
||||
root.add_child(camera)
|
||||
camera.global_position = Vector2(1734.5, 324.0)
|
||||
camera.make_current()
|
||||
|
||||
var ground := (load("res://scenes/ground/ground1.tscn") as PackedScene).instantiate() as Node2D
|
||||
container.add_child(ground)
|
||||
await process_frame
|
||||
await process_frame
|
||||
|
||||
var art_layer := ground.get_node("ArtLayer") as Node2D
|
||||
var base_x := art_layer.position.x
|
||||
camera.global_position.x += 640.0
|
||||
await process_frame
|
||||
await process_frame
|
||||
_expect_float(art_layer.position.x, base_x, "Ground ArtLayer should stay locked to world x while camera moves")
|
||||
|
||||
camera.free()
|
||||
container.free()
|
||||
|
||||
|
||||
func _check_body_shapes_match_visual_blocking_contract() -> void:
|
||||
var player := _instantiate_body("res://scenes/characters/player.tscn", "Player") as CharacterBody2D
|
||||
var minion := _instantiate_body("res://scenes/enemies/minion.tscn", "Minion") as CharacterBody2D
|
||||
var boss := _instantiate_body("res://scenes/enemies/boss.tscn", "Boss") as CharacterBody2D
|
||||
|
||||
_expect_vector_min(_body_shape_size(player), PLAYER_BODY_MIN, "Player body shape should cover the visual torso/feet")
|
||||
_expect_vector_min(_body_shape_size(minion), MINION_BODY_MIN, "Minion body shape should cover the visual torso/feet")
|
||||
_expect_vector_min(_body_shape_size(boss), BOSS_BODY_MIN, "Boss body shape should cover the visual torso/feet")
|
||||
|
||||
player.free()
|
||||
minion.free()
|
||||
boss.free()
|
||||
|
||||
|
||||
func _check_scene_body_rects_overlap_before_visible_interpenetration() -> void:
|
||||
var player := _instantiate_body("res://scenes/characters/player.tscn", "Player") as CharacterBody2D
|
||||
var minion := _instantiate_body("res://scenes/enemies/minion.tscn", "Minion") as CharacterBody2D
|
||||
var boss := _instantiate_body("res://scenes/enemies/boss.tscn", "Boss") as CharacterBody2D
|
||||
_disable_enemy_ai(minion)
|
||||
_disable_enemy_ai(boss)
|
||||
|
||||
player.global_position = Vector2(500.0, 300.0)
|
||||
minion.global_position = Vector2(526.0, 300.0)
|
||||
boss.global_position = Vector2(540.0, 300.0)
|
||||
await physics_frame
|
||||
|
||||
_expect(_body_rects_overlap(player, minion), "Player/Minion bodies should still overlap at a 26px center gap")
|
||||
_expect(_body_rects_overlap(player, boss), "Player/Boss bodies should still overlap at a 40px center gap")
|
||||
|
||||
minion.global_position = Vector2(500.0 + (PLAYER_BODY_MIN.x + MINION_BODY_MIN.x) * 0.5 + 2.0, 300.0)
|
||||
boss.global_position = Vector2(500.0 + (PLAYER_BODY_MIN.x + BOSS_BODY_MIN.x) * 0.5 + 2.0, 300.0)
|
||||
await physics_frame
|
||||
|
||||
_expect(not _body_rects_overlap(player, minion), "Player/Minion bodies should separate beyond the enlarged half-width sum")
|
||||
_expect(not _body_rects_overlap(player, boss), "Player/Boss bodies should separate beyond the enlarged half-width sum")
|
||||
|
||||
player.free()
|
||||
minion.free()
|
||||
boss.free()
|
||||
|
||||
|
||||
func _check_dash_through_ghost_query_uses_scene_body_shapes(label: String, scene_path: String, overlap_gap: float, clear_gap: float) -> void:
|
||||
var player := _instantiate_body("res://scenes/characters/player.tscn", "Player") as CharacterBody2D
|
||||
var enemy := _instantiate_body(scene_path, label) as CharacterBody2D
|
||||
_disable_enemy_ai(enemy)
|
||||
await process_frame
|
||||
|
||||
_expect(player.get_node_or_null("MotionExecutor") != null, "Player scene should include MotionExecutor for dash-through")
|
||||
_expect(player.get_node_or_null("FrameCollisionDriver") != null, "Player scene should include FrameCollisionDriver")
|
||||
_expect(enemy.get_node_or_null("FrameCollisionDriver") != null, "%s scene should include FrameCollisionDriver" % label)
|
||||
|
||||
player.global_position = Vector2(700.0, 300.0)
|
||||
enemy.global_position = Vector2(700.0 + overlap_gap, 300.0)
|
||||
await physics_frame
|
||||
|
||||
var motion := player.get_node("MotionExecutor")
|
||||
motion.call("execute", _dash_through_action(), Vector2.RIGHT, 0.5, 220.0)
|
||||
await physics_frame
|
||||
_expect((int(player.collision_mask) & ENEMY_BODY_MASK) == 0, "dash_through should ghost enemy bodies before restore")
|
||||
_expect(bool(motion.call("_overlaps_ghosted_bodies")), "dash_through ghost query should detect overlapped %s body shape" % label)
|
||||
|
||||
enemy.global_position = Vector2(700.0 + clear_gap, 300.0)
|
||||
await physics_frame
|
||||
_expect(not bool(motion.call("_overlaps_ghosted_bodies")), "dash_through ghost query should clear after %s body separation" % label)
|
||||
|
||||
motion.call("cancel")
|
||||
_expect(not bool(motion.call("is_ghosting")), "dash_through should restore collision after %s separation" % label)
|
||||
player.free()
|
||||
enemy.free()
|
||||
|
||||
|
||||
func _dash_through_action() -> Resource:
|
||||
var script: Script = load("res://resources/action_data.gd")
|
||||
var action: Resource = script.new()
|
||||
var action_tags: Array[StringName] = [&"dash_through"]
|
||||
action.set("id", &"test_scene_dash_through")
|
||||
action.set("action_tags", action_tags)
|
||||
action.set("move_mult_x", 1.0)
|
||||
action.set("startup_beats", 0.25)
|
||||
action.set("active_beats", 0.25)
|
||||
action.set("recovery_beats", 0.25)
|
||||
return action
|
||||
|
||||
|
||||
func _instantiate_body(scene_path: String, label: String) -> CharacterBody2D:
|
||||
var scene := load(scene_path) as PackedScene
|
||||
if scene == null:
|
||||
failures.append("%s scene should load from %s" % [label, scene_path])
|
||||
return null
|
||||
var actor := scene.instantiate() as CharacterBody2D
|
||||
if actor == null:
|
||||
failures.append("%s scene root should be CharacterBody2D" % label)
|
||||
return null
|
||||
root.add_child(actor)
|
||||
return actor
|
||||
|
||||
|
||||
func _disable_enemy_ai(actor: CharacterBody2D) -> void:
|
||||
if actor == null:
|
||||
return
|
||||
var behavior := actor.get_node_or_null("MinionBehavior")
|
||||
if behavior != null:
|
||||
behavior.set("enabled", false)
|
||||
var boss_behavior := actor.get_node_or_null("BossBehaviorTree")
|
||||
if boss_behavior != null:
|
||||
boss_behavior.set("enabled", false)
|
||||
actor.set_physics_process(false)
|
||||
|
||||
|
||||
func _body_shape_size(actor: CharacterBody2D) -> Vector2:
|
||||
if actor == null:
|
||||
return Vector2.ZERO
|
||||
var shape_node := actor.get_node_or_null("CollisionShape2D") as CollisionShape2D
|
||||
if shape_node == null or shape_node.shape == null:
|
||||
failures.append("%s should have a main CollisionShape2D" % actor.name)
|
||||
return Vector2.ZERO
|
||||
var rect := shape_node.shape as RectangleShape2D
|
||||
if rect == null:
|
||||
failures.append("%s main body shape should be RectangleShape2D" % actor.name)
|
||||
return Vector2.ZERO
|
||||
return rect.size
|
||||
|
||||
|
||||
func _body_rects_overlap(left_actor: CharacterBody2D, right_actor: CharacterBody2D) -> bool:
|
||||
var left_rect := _body_world_rect(left_actor)
|
||||
var right_rect := _body_world_rect(right_actor)
|
||||
return left_rect.intersects(right_rect, true)
|
||||
|
||||
|
||||
func _body_world_rect(actor: CharacterBody2D) -> Rect2:
|
||||
if actor == null:
|
||||
return Rect2()
|
||||
var shape_node := actor.get_node_or_null("CollisionShape2D") as CollisionShape2D
|
||||
if shape_node == null or shape_node.shape == null:
|
||||
failures.append("%s should have a main CollisionShape2D" % actor.name)
|
||||
return Rect2()
|
||||
var rect_shape := shape_node.shape as RectangleShape2D
|
||||
if rect_shape == null:
|
||||
failures.append("%s main body shape should be RectangleShape2D" % actor.name)
|
||||
return Rect2()
|
||||
var center := shape_node.global_position
|
||||
return Rect2(center - rect_shape.size * 0.5, rect_shape.size)
|
||||
|
||||
|
||||
func _expect(condition: bool, label: String) -> void:
|
||||
if not condition:
|
||||
failures.append(label)
|
||||
|
||||
|
||||
func _expect_float(actual: float, expected: float, label: String) -> void:
|
||||
if absf(actual - expected) > 0.5:
|
||||
failures.append("%s: expected %.2f, got %.2f" % [label, expected, actual])
|
||||
|
||||
|
||||
func _expect_vector_min(actual: Vector2, expected_min: Vector2, label: String) -> void:
|
||||
if actual.x + 0.5 < expected_min.x or actual.y + 0.5 < expected_min.y:
|
||||
failures.append("%s: expected at least %s, got %s" % [label, expected_min, actual])
|
||||
|
||||
|
||||
func _finish() -> void:
|
||||
if failures.is_empty():
|
||||
print("PASS parallax and body collision contract")
|
||||
quit(0)
|
||||
else:
|
||||
for failure: String in failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bdpqavi1hbe8t
|
||||
@@ -7,9 +7,9 @@ extends SceneTree
|
||||
## 2. 趔趄门禁:击退滑行期间 Boss/小怪不得起手新动作(EnemyActionDriver
|
||||
## 钩子),否则动作突进立刻接管 velocity 冲回玩家,把击退整个吃掉
|
||||
## (真实 stage 实测净位移为负);Boss 受击链强制撤退豁免(清残速标志)。
|
||||
## 3. 关卡背景单层视差:背景水平速度为相机 0.8 倍(0.92 肉眼难辨已加强),
|
||||
## 锚点 2047,无相机保持原位;脚下地砖轻微滑动是单张烙死原画的已知
|
||||
## 取舍,无伤视差待美术拆层。
|
||||
## 3. 关卡背景分层契约:当前整图里的地面/步道先锁在 1:1 世界层,避免脚下
|
||||
## 地砖相对角色滑动;美术拆出的天空/夕阳放入 SkyParallaxLayer 后继续按
|
||||
## 0.2 倍世界滚动(相机 0.8 跟随)。
|
||||
## (Boss 击退 ×9 的加码钉在 test_round3_fixes。)
|
||||
|
||||
var failures: Array[String] = []
|
||||
@@ -147,24 +147,28 @@ func _check_background_parallax() -> void:
|
||||
await process_frame
|
||||
await process_frame
|
||||
var art_layer := ground.get_node("ArtLayer") as Node2D
|
||||
var sky_layer := ground.get_node("SkyParallaxLayer") as Node2D
|
||||
|
||||
# 无相机:背景保持原位(headless/菜单兜底)。
|
||||
_expect_float(art_layer.position.x, 896.5, "无相机时背景保持基准位置")
|
||||
# 无相机:世界层与可选远景层都保持原位(headless/菜单兜底)。
|
||||
_expect_float(art_layer.position.x, 896.5, "无相机时地面世界层保持基准位置")
|
||||
_expect_float(sky_layer.position.x, 896.5, "无相机时远景视差层保持基准位置")
|
||||
|
||||
# 有相机:偏移 = (相机中心 - 1734.5) × 0.8——"远景天幕"语义,背景 80%
|
||||
# 跟随相机,仅 0.2 倍速随世界滚动,保证远景夕阳时刻在画面内(策划定案)。
|
||||
# 有相机:地面/步道保持 1:1 世界坐标;远景容器按
|
||||
# (相机中心 - 1734.5) × 0.8 视差移动。
|
||||
var camera := Camera2D.new()
|
||||
root.add_child(camera)
|
||||
camera.make_current()
|
||||
camera.global_position = Vector2(2547.0, 395.0)
|
||||
await process_frame
|
||||
await process_frame
|
||||
_expect_float(art_layer.position.x, 896.5 + (2547.0 - 1734.5) * 0.8, "背景 80% 跟随相机(天幕视差)")
|
||||
_expect_float(art_layer.position.x, 896.5, "相机右移时地面世界层不做视差")
|
||||
_expect_float(sky_layer.position.x, 896.5 + (2547.0 - 1734.5) * 0.8, "相机右移时远景层 80% 跟随")
|
||||
|
||||
camera.global_position = Vector2(1547.0, 395.0)
|
||||
await process_frame
|
||||
await process_frame
|
||||
_expect_float(art_layer.position.x, 896.5 + (1547.0 - 1734.5) * 0.8, "相机左移时天幕对称跟随")
|
||||
_expect_float(art_layer.position.x, 896.5, "相机左移时地面世界层仍不做视差")
|
||||
_expect_float(sky_layer.position.x, 896.5 + (1547.0 - 1734.5) * 0.8, "相机左移时远景层对称跟随")
|
||||
|
||||
camera.free()
|
||||
ground.free()
|
||||
|
||||
Reference in New Issue
Block a user