test: 八项无头回归测试(道具/弹道/Boss/海面/地面单位/浮游炮/炸弹/冒烟模拟)
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
extends SceneTree
|
||||
## 炸弹系统回归测试(时间驱动):
|
||||
## 初始 2 枚 → 引爆清敌清弹-1 → 用尽 → 空仓无效 → Boss 击破 +1
|
||||
## → B 道具拾取 +1 → 上限 5
|
||||
## 输入映射层:fire 含 J、bomb 含 K
|
||||
## 运行:godot --headless -s tests/bomb_test.gd
|
||||
|
||||
const GLOBAL_TIMEOUT := 25.0
|
||||
|
||||
var _phase := 0
|
||||
var _phase_time := 0.0
|
||||
var _time := 0.0
|
||||
var _game: Game
|
||||
var _fails: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_game = (load("res://scenes/game.tscn") as PackedScene).instantiate()
|
||||
root.add_child(_game)
|
||||
print("[bomb_test] game instantiated")
|
||||
|
||||
|
||||
func _fail(msg: String) -> void:
|
||||
_fails.append(msg)
|
||||
print("[FAIL] " + msg)
|
||||
|
||||
|
||||
func _next_phase() -> void:
|
||||
_phase += 1
|
||||
_phase_time = 0.0
|
||||
|
||||
|
||||
func _finish() -> bool:
|
||||
if _fails.is_empty():
|
||||
print("[bomb_test] PASS")
|
||||
else:
|
||||
print("[bomb_test] FAIL: %d issue(s)" % _fails.size())
|
||||
quit(0 if _fails.is_empty() else 1)
|
||||
return true
|
||||
|
||||
|
||||
## 输入映射检查(真实按键输入无法无头模拟,检查映射配置本身)
|
||||
func _check_input_maps() -> void:
|
||||
var fire_ok := false
|
||||
if InputMap.has_action("fire"):
|
||||
for event in InputMap.action_get_events("fire"):
|
||||
if event is InputEventKey and (event as InputEventKey).physical_keycode == 74:
|
||||
fire_ok = true
|
||||
if not fire_ok:
|
||||
_fail("fire action missing J key")
|
||||
var bomb_ok := false
|
||||
if InputMap.has_action("bomb"):
|
||||
for event in InputMap.action_get_events("bomb"):
|
||||
if event is InputEventKey and (event as InputEventKey).physical_keycode == 75:
|
||||
bomb_ok = true
|
||||
if not bomb_ok:
|
||||
_fail("bomb action missing K key")
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
_time += delta
|
||||
if _time > GLOBAL_TIMEOUT:
|
||||
_fail("global timeout in phase %d" % _phase)
|
||||
return _finish()
|
||||
if _game._enemies == null:
|
||||
return false
|
||||
|
||||
_phase_time += delta
|
||||
match _phase:
|
||||
0:
|
||||
_check_input_maps()
|
||||
if _game.player.bombs != 2:
|
||||
_fail("initial bombs=%d expected 2" % _game.player.bombs)
|
||||
# 场上放 3 架敌机 + 2 发敌弹
|
||||
for i in 3:
|
||||
var enemy := EnemySmall.new()
|
||||
enemy.setup(150.0 + 120.0 * i, 300.0 + 60.0 * i, 100.0)
|
||||
_game._enemies.add_child(enemy)
|
||||
for i in 2:
|
||||
var bullet := EnemyBullet.new()
|
||||
bullet.position = Vector2(120.0 + 200.0 * i, 400.0)
|
||||
_game._enemy_bullets.add_child(bullet)
|
||||
_game.player.use_bomb()
|
||||
_next_phase()
|
||||
1:
|
||||
# 首波 spawner 敌机 0.8s 后才出现,检查点在 0.5s 安全
|
||||
if _phase_time >= 0.5:
|
||||
var enemies_left := _game._enemies.get_child_count()
|
||||
var bullets_left := _game._enemy_bullets.get_child_count()
|
||||
if enemies_left != 0:
|
||||
_fail("bomb left enemies: %d" % enemies_left)
|
||||
if bullets_left != 0:
|
||||
_fail("bomb left enemy bullets: %d" % bullets_left)
|
||||
if _game.player.bombs != 1:
|
||||
_fail("bombs=%d expected 1" % _game.player.bombs)
|
||||
else:
|
||||
print("[bomb_test] detonate ok: enemies/bullets cleared, bombs 2->1, score=%d" % _game.score)
|
||||
_next_phase()
|
||||
2:
|
||||
_game.player.use_bomb()
|
||||
_next_phase()
|
||||
3:
|
||||
if _phase_time >= 0.3:
|
||||
if _game.player.bombs != 0:
|
||||
_fail("second bomb not consumed: %d" % _game.player.bombs)
|
||||
_game.player.use_bomb() # 空仓
|
||||
_next_phase()
|
||||
4:
|
||||
if _phase_time >= 0.3:
|
||||
if _game.player.bombs != 0:
|
||||
_fail("empty stock still fired: %d" % _game.player.bombs)
|
||||
else:
|
||||
print("[bomb_test] stock drain + empty guard ok")
|
||||
var boss := _game.spawn_boss(1.0)
|
||||
boss.apply_damage(99999)
|
||||
_next_phase()
|
||||
5:
|
||||
if _phase_time >= 1.0:
|
||||
if _game.player.bombs != 1:
|
||||
_fail("boss kill did not grant bomb: %d" % _game.player.bombs)
|
||||
else:
|
||||
print("[bomb_test] boss kill +1 bomb ok")
|
||||
# B 道具拾取链路:1 → 2,再补到 5 验证上限
|
||||
_collect_bomb_pickup()
|
||||
_next_phase()
|
||||
6:
|
||||
if _phase_time >= 0.5:
|
||||
if _game.player.bombs != 2:
|
||||
_fail("B pickup did not add bomb: %d" % _game.player.bombs)
|
||||
else:
|
||||
print("[bomb_test] B pickup +1 ok")
|
||||
_collect_bomb_pickup()
|
||||
_collect_bomb_pickup()
|
||||
_collect_bomb_pickup()
|
||||
_collect_bomb_pickup()
|
||||
_next_phase()
|
||||
7:
|
||||
if _phase_time >= 0.5:
|
||||
if _game.player.bombs != 5:
|
||||
_fail("bomb cap broken: %d expected 5" % _game.player.bombs)
|
||||
else:
|
||||
print("[bomb_test] bomb cap at 5 ok")
|
||||
return _finish()
|
||||
return false
|
||||
|
||||
|
||||
func _collect_bomb_pickup() -> void:
|
||||
var powerups := _game.get_node_or_null("Powerups") as Node2D
|
||||
var pickup := Powerup.new()
|
||||
pickup.kind = Powerup.Kind.BOMB
|
||||
pickup.position = _game.player.global_position
|
||||
powerups.add_child(pickup)
|
||||
# 直接触发玩家拾取路径(与 area_entered 信号同一入口)
|
||||
_game.player._collect_powerup(pickup)
|
||||
@@ -0,0 +1 @@
|
||||
uid://5avhpka6nv87
|
||||
@@ -0,0 +1,82 @@
|
||||
extends SceneTree
|
||||
## Boss 回归测试(时间驱动,与无头模式实际帧率无关):
|
||||
## A) 独立 Boss:5s 内进场并开始弹幕;秒杀后 4s 内完成爆炸链释放
|
||||
## B) Game 集成:spawn_boss 血条显示→击杀后血条隐藏、得分 +5000
|
||||
## 运行:godot --headless -s tests/boss_test.gd
|
||||
|
||||
var _phase := 0
|
||||
var _time := 0.0
|
||||
var _boss: Boss
|
||||
var _game: Game
|
||||
var _bullets: Node2D
|
||||
var _bullet_time := 0.0
|
||||
var _phase_time := 0.0
|
||||
var _score_before := 0
|
||||
var _fails: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_bullets = Node2D.new()
|
||||
_bullets.add_to_group("enemy_bullets")
|
||||
root.add_child(_bullets)
|
||||
_boss = Boss.new()
|
||||
_boss.setup(1.0)
|
||||
root.add_child(_boss)
|
||||
print("[boss_test] phase A: standalone boss")
|
||||
|
||||
|
||||
func _fail(msg: String) -> void:
|
||||
_fails.append(msg)
|
||||
print("[FAIL] " + msg)
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
_time += delta
|
||||
_phase_time += delta
|
||||
match _phase:
|
||||
0:
|
||||
if _bullets.get_child_count() > 0:
|
||||
_bullet_time += delta
|
||||
if _phase_time >= 5.0:
|
||||
if not _boss._entered:
|
||||
_fail("boss did not enter (pos.y=%.1f)" % _boss.position.y)
|
||||
if _bullet_time < 0.4:
|
||||
_fail("boss fired too little (%.2fs bullet time)" % _bullet_time)
|
||||
_boss.apply_damage(99999)
|
||||
_next_phase()
|
||||
1:
|
||||
if not is_instance_valid(_boss):
|
||||
print("[boss_test] phase A done: %.2fs bullet time, death chain freed boss" % _bullet_time)
|
||||
_next_phase()
|
||||
elif _phase_time > 4.0:
|
||||
_fail("death chain did not free boss in 4s")
|
||||
_next_phase()
|
||||
2:
|
||||
_game = (load("res://scenes/game.tscn") as PackedScene).instantiate()
|
||||
root.add_child(_game)
|
||||
_score_before = _game.score
|
||||
_boss = _game.spawn_boss(1.0)
|
||||
if not _game._boss_bar_bg.visible:
|
||||
_fail("boss bar not shown after spawn")
|
||||
_boss.apply_damage(99999)
|
||||
_next_phase()
|
||||
3:
|
||||
if _phase_time >= 4.0:
|
||||
if _game._boss_bar_bg.visible:
|
||||
_fail("boss bar still visible after death")
|
||||
if _game.score < _score_before + 5000:
|
||||
_fail("score not credited: %d -> %d" % [_score_before, _game.score])
|
||||
else:
|
||||
print("[boss_test] phase B done: score %d -> %d" % [_score_before, _game.score])
|
||||
if _fails.is_empty():
|
||||
print("[boss_test] PASS")
|
||||
else:
|
||||
print("[boss_test] FAIL: %d issue(s)" % _fails.size())
|
||||
quit(0 if _fails.is_empty() else 1)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _next_phase() -> void:
|
||||
_phase += 1
|
||||
_phase_time = 0.0
|
||||
@@ -0,0 +1 @@
|
||||
uid://cd2jn0sw1u738
|
||||
@@ -0,0 +1,101 @@
|
||||
extends SceneTree
|
||||
## 玩家子弹弹道回归测试(时间驱动):
|
||||
## L4 齐射后快照所有子弹位置,0.25s 后比对实际位移方向,
|
||||
## 必须命中 UP / ±22° / ±30° 五个弹道之一,且各组数量正确。
|
||||
## 在 spawner 首波敌机(0.8s)出现前完成测量,保证确定性。
|
||||
## 运行:godot --headless -s tests/bullet_traj_test.gd
|
||||
|
||||
const SNAP_DELAY := 0.1
|
||||
const FLY_TIME := 0.25
|
||||
const DOT_EPS := 0.995
|
||||
const GLOBAL_TIMEOUT := 15.0
|
||||
|
||||
var _time := 0.0
|
||||
var _snapped := false
|
||||
var _game: Game
|
||||
var _pbullets: Node2D
|
||||
var _fails: Array[String] = []
|
||||
var _snapshot: Array = [] # [bullet: Node2D, pos: Vector2]
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_game = (load("res://scenes/game.tscn") as PackedScene).instantiate()
|
||||
root.add_child(_game)
|
||||
print("[traj_test] game instantiated")
|
||||
|
||||
|
||||
func _fail(msg: String) -> void:
|
||||
_fails.append(msg)
|
||||
print("[FAIL] " + msg)
|
||||
|
||||
|
||||
func _ensure_refs() -> bool:
|
||||
if _pbullets == null:
|
||||
_pbullets = _game.get_node_or_null("PlayerBullets") as Node2D
|
||||
return _pbullets != null
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
_time += delta
|
||||
if _time > GLOBAL_TIMEOUT:
|
||||
_fail("global timeout")
|
||||
return _finish()
|
||||
if not _ensure_refs():
|
||||
return false
|
||||
|
||||
if not _snapped and _time >= SNAP_DELAY:
|
||||
_snapped = true
|
||||
# 先射一梭子 L4(fire 从首帧按住,此处已有一发齐射在飞)
|
||||
_game.player.weapon_level = 4
|
||||
Input.action_release("fire")
|
||||
_game.player._fire()
|
||||
_snapshot = []
|
||||
for bullet in _pbullets.get_children():
|
||||
_snapshot.append([bullet, (bullet as Node2D).position])
|
||||
if _snapshot.size() != 12:
|
||||
_fail("L4 volley=%d expected 12" % _snapshot.size())
|
||||
return false
|
||||
|
||||
if _snapped and _time >= SNAP_DELAY + FLY_TIME:
|
||||
var allowed := [
|
||||
Vector2.UP,
|
||||
Vector2.UP.rotated(deg_to_rad(-22.0)),
|
||||
Vector2.UP.rotated(deg_to_rad(22.0)),
|
||||
Vector2.UP.rotated(deg_to_rad(-30.0)),
|
||||
Vector2.UP.rotated(deg_to_rad(30.0)),
|
||||
]
|
||||
var counts := [0, 0, 0, 0, 0]
|
||||
var classified := 0
|
||||
for entry in _snapshot:
|
||||
var bullet: Node2D = entry[0]
|
||||
var start: Vector2 = entry[1]
|
||||
if not is_instance_valid(bullet):
|
||||
_fail("bullet freed before measurement window ended")
|
||||
continue
|
||||
var disp: Vector2 = bullet.position - start
|
||||
if disp.length() < 50.0:
|
||||
_fail("bullet barely moved: %.1f px" % disp.length())
|
||||
continue
|
||||
var dir := disp.normalized()
|
||||
for i in allowed.size():
|
||||
if dir.dot(allowed[i]) >= DOT_EPS:
|
||||
counts[i] += 1
|
||||
classified += 1
|
||||
break
|
||||
if classified != _snapshot.size():
|
||||
_fail("only %d/%d bullets on valid trajectories" % [classified, _snapshot.size()])
|
||||
elif counts[0] != 6 or counts[1] != 2 or counts[2] != 2 or counts[3] != 1 or counts[4] != 1:
|
||||
_fail("trajectory group counts wrong: straight=%d L22=%d R22=%d L30=%d R30=%d" % counts)
|
||||
else:
|
||||
print("[traj_test] trajectories ok: straight=%d ±22=%d+%d ±30=%d+%d" % [counts[0], counts[1], counts[2], counts[3], counts[4]])
|
||||
return _finish()
|
||||
return false
|
||||
|
||||
|
||||
func _finish() -> bool:
|
||||
if _fails.is_empty():
|
||||
print("[traj_test] PASS")
|
||||
else:
|
||||
print("[traj_test] FAIL: %d issue(s)" % _fails.size())
|
||||
quit(0 if _fails.is_empty() else 1)
|
||||
return true
|
||||
@@ -0,0 +1 @@
|
||||
uid://dv8mr86bn8gmx
|
||||
@@ -0,0 +1,297 @@
|
||||
extends SceneTree
|
||||
## 浮游炮机型回归测试(时间驱动):
|
||||
## A) 默认战斗机无浮游炮;GameSession 传递变体 → 新局为浮游炮机
|
||||
## B) 等级 1/2/3/4 → 浮游炮 0/2/4/6 台
|
||||
## C) 常态散开:侧位浮游炮激光打得中侧旁堡垒,打不中中线堡垒
|
||||
## D) 按 I 聚焦:中线高血堡垒被合体粗激光快速击毁
|
||||
## E) focus 输入映射含 I 键
|
||||
## 运行:godot --headless -s tests/funnel_test.gd
|
||||
|
||||
const GLOBAL_TIMEOUT := 40.0
|
||||
|
||||
var _phase := 0
|
||||
var _step := 0
|
||||
var _phase_time := 0.0
|
||||
var _time := 0.0
|
||||
var _game: Game
|
||||
var _fort_center: EnemyFortress
|
||||
var _fort_left: EnemyFortress
|
||||
var _fort_right: EnemyFortress
|
||||
var _fort_focus: EnemyFortress
|
||||
var _fails: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
GameSession.plane_variant = 0
|
||||
_game = (load("res://scenes/game.tscn") as PackedScene).instantiate()
|
||||
root.add_child(_game)
|
||||
print("[funnel_test] game A instantiated (fighter)")
|
||||
|
||||
|
||||
func _fail(msg: String) -> void:
|
||||
_fails.append(msg)
|
||||
print("[FAIL] " + msg)
|
||||
|
||||
|
||||
func _next_phase() -> void:
|
||||
_phase += 1
|
||||
_step = 0
|
||||
_phase_time = 0.0
|
||||
|
||||
|
||||
func _restart_step() -> void:
|
||||
_step += 1
|
||||
_phase_time = 0.0
|
||||
|
||||
|
||||
func _finish() -> bool:
|
||||
GameSession.plane_variant = 0
|
||||
if _fails.is_empty():
|
||||
print("[funnel_test] PASS")
|
||||
else:
|
||||
print("[funnel_test] FAIL: %d issue(s)" % _fails.size())
|
||||
quit(0 if _fails.is_empty() else 1)
|
||||
return true
|
||||
|
||||
|
||||
func _clear_field() -> void:
|
||||
for enemy in _game._enemies.get_children():
|
||||
(enemy as Node).queue_free()
|
||||
_game.clear_enemy_bullets()
|
||||
# 防止上一阶段流弹干扰断言
|
||||
_game.player.invuln = 6.0
|
||||
|
||||
|
||||
func _spawn_fortress(x: float, y: float, hp: int) -> EnemyFortress:
|
||||
var fortress := EnemyFortress.new()
|
||||
fortress.setup(x, y, 1.0)
|
||||
_game._enemies.add_child(fortress)
|
||||
fortress.hp = hp
|
||||
return fortress
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
_time += delta
|
||||
if _time > GLOBAL_TIMEOUT:
|
||||
_fail("global timeout in phase %d" % _phase)
|
||||
return _finish()
|
||||
if _game._enemies == null:
|
||||
return false
|
||||
|
||||
_phase_time += delta
|
||||
match _phase:
|
||||
0:
|
||||
# A: 战斗机默认无浮游炮
|
||||
if _step == 0:
|
||||
if _game.player._funnels.size() != 0:
|
||||
_fail("fighter has funnels: %d" % _game.player._funnels.size())
|
||||
else:
|
||||
print("[funnel_test] fighter has no funnels ok")
|
||||
_restart_step()
|
||||
elif _phase_time >= 0.1:
|
||||
_next_phase()
|
||||
1:
|
||||
# A2: 切换变体 → 新局生效(旧 game 立即移除,避免跨实例物理污染)
|
||||
if _step == 0:
|
||||
GameSession.plane_variant = 1
|
||||
var game_b := (load("res://scenes/game.tscn") as PackedScene).instantiate()
|
||||
root.add_child(game_b)
|
||||
_game.queue_free()
|
||||
_game = game_b
|
||||
_restart_step()
|
||||
elif _game.player != null:
|
||||
if _game.player.variant != Player.Variant.FUNNEL:
|
||||
_fail("variant not applied: %d" % _game.player.variant)
|
||||
elif _game.player._funnels.size() != 0:
|
||||
_fail("L1 should have 0 funnels, got %d" % _game.player._funnels.size())
|
||||
else:
|
||||
print("[funnel_test] variant applied, L1=0 funnels ok")
|
||||
_next_phase()
|
||||
2:
|
||||
# B: 等级→浮游炮数 + 展开位置分散(两两间距 ≥30px)
|
||||
if _step == 0:
|
||||
for level in [2, 3, 4]:
|
||||
_game.player.weapon_level = level
|
||||
_game.player._sync_funnels()
|
||||
var expected: int = (level - 1) * 2
|
||||
if _game.player._funnels.size() != expected:
|
||||
_fail("L%d funnels=%d expected %d" % [level, _game.player._funnels.size(), expected])
|
||||
if _fails.is_empty():
|
||||
print("[funnel_test] L2/L3/L4 = 2/4/6 funnels ok")
|
||||
_restart_step()
|
||||
elif _phase_time >= 0.9:
|
||||
var positions: Array = []
|
||||
for funnel in _game.player._funnels:
|
||||
positions.append((funnel as Node2D).position)
|
||||
var min_dist := 9999.0
|
||||
for a in positions.size():
|
||||
for b in range(a + 1, positions.size()):
|
||||
min_dist = minf(min_dist, (positions[a] as Vector2).distance_to(positions[b]))
|
||||
if min_dist < 30.0:
|
||||
_fail("funnels not spread: min pairwise dist %.1f" % min_dist)
|
||||
else:
|
||||
print("[funnel_test] spread formation min pairwise dist %.1f ok" % min_dist)
|
||||
_next_phase()
|
||||
3:
|
||||
# E: focus 映射含 I(73)
|
||||
if _step == 0:
|
||||
var focus_ok := false
|
||||
for event in InputMap.action_get_events("focus"):
|
||||
if event is InputEventKey and (event as InputEventKey).physical_keycode == 73:
|
||||
focus_ok = true
|
||||
if not focus_ok:
|
||||
_fail("focus action missing I key")
|
||||
else:
|
||||
print("[funnel_test] focus=I mapping ok")
|
||||
_next_phase()
|
||||
4:
|
||||
# C: 散开覆盖(L2:±38 侧位)。目标放高处、玩家放低处,保证足够照射窗口
|
||||
match _step:
|
||||
0:
|
||||
_clear_field()
|
||||
_game.player.weapon_level = 2
|
||||
_game.player._sync_funnels()
|
||||
_game.player.position = Vector2(270.0, 700.0)
|
||||
_fort_center = _spawn_fortress(270.0, 150.0, 60) # 仅机体激光 18dps
|
||||
_fort_left = _spawn_fortress(234.0, 150.0, 10) # 左浮游炮位(-36)7dps
|
||||
_fort_right = _spawn_fortress(306.0, 150.0, 10) # 右浮游炮位(+36)
|
||||
Input.action_press("fire")
|
||||
_restart_step()
|
||||
1:
|
||||
if _phase_time >= 2.2:
|
||||
var center_stands := is_instance_valid(_fort_center) and not _fort_center.dead
|
||||
var left_dead := (not is_instance_valid(_fort_left)) or _fort_left.dead
|
||||
var right_dead := (not is_instance_valid(_fort_right)) or _fort_right.dead
|
||||
if center_stands and left_dead and right_dead:
|
||||
print("[funnel_test] spread: sides killed, center survives (hp=%d) ok" % [_fort_center.hp])
|
||||
else:
|
||||
_fail("spread outcome wrong: center_stands=%s L=%s R=%s" % [str(center_stands), str(left_dead), str(right_dead)])
|
||||
Input.action_release("fire")
|
||||
_next_phase()
|
||||
5:
|
||||
# D: 聚焦粗激光(L4 合体 = (18+6×7)×1.5 = 90dps)
|
||||
match _step:
|
||||
0:
|
||||
_clear_field()
|
||||
_game.player.weapon_level = 4
|
||||
_game.player._sync_funnels()
|
||||
_game.player.position = Vector2(270.0, 700.0)
|
||||
_fort_focus = _spawn_fortress(270.0, 150.0, 60) # 60hp / 90dps ≈ 0.67s
|
||||
Input.action_press("fire")
|
||||
Input.action_press("focus")
|
||||
_restart_step()
|
||||
1:
|
||||
var killed := (not is_instance_valid(_fort_focus)) or _fort_focus.dead
|
||||
if killed:
|
||||
var dps := _game.player._focus_beam.dps
|
||||
if dps < 89.0 or dps > 91.0:
|
||||
_fail("focus dps=%.1f expected 90.0" % dps)
|
||||
elif _phase_time < 1.6:
|
||||
print("[funnel_test] focused beam dps=%.0f, killed 60hp in %.2fs ok" % [dps, _phase_time])
|
||||
else:
|
||||
_fail("focused beam too slow: %.2fs" % _phase_time)
|
||||
Input.action_release("fire")
|
||||
Input.action_release("focus")
|
||||
_next_phase()
|
||||
elif _phase_time > 3.0:
|
||||
_fail("focused beam failed to kill (hp=%d)" % _fort_focus.hp)
|
||||
Input.action_release("fire")
|
||||
Input.action_release("focus")
|
||||
_next_phase()
|
||||
6:
|
||||
# E2: 聚焦期间移动速度减半(0.5s 期望位移 ~85px,全速为 170px)
|
||||
match _step:
|
||||
0:
|
||||
_game.player.position = Vector2(100.0, 700.0)
|
||||
Input.action_press("fire")
|
||||
Input.action_press("focus")
|
||||
Input.action_press("move_right")
|
||||
_restart_step()
|
||||
1:
|
||||
if _phase_time >= 0.5:
|
||||
Input.action_release("move_right")
|
||||
var dx := _game.player.position.x - 100.0
|
||||
if dx < 45.0 or dx > 125.0:
|
||||
_fail("focus speed wrong: dx=%.0f px in 0.5s (expect ~85)" % dx)
|
||||
else:
|
||||
print("[funnel_test] focus half-speed ok (dx=%.0f px)" % dx)
|
||||
Input.action_release("fire")
|
||||
Input.action_release("focus")
|
||||
_next_phase()
|
||||
7:
|
||||
# E3: 侧身帧状态切换;机头不歪不变量(无旋转、无缩放变换)
|
||||
match _step:
|
||||
0:
|
||||
Input.action_press("move_left")
|
||||
_restart_step()
|
||||
1:
|
||||
if _phase_time >= 0.5:
|
||||
var state := _game.player._bank_state
|
||||
if state != -1:
|
||||
_fail("bank state=%d expected -1 (left)" % state)
|
||||
elif absf(_game.player.sprite.rotation) > 0.001 or absf(_game.player.sprite.scale.x - 1.0) > 0.001:
|
||||
_fail("bank must be sprite-swap only: rot=%.3f scale.x=%.3f" % [_game.player.sprite.rotation, _game.player.sprite.scale.x])
|
||||
else:
|
||||
print("[funnel_test] bank-left state ok (nose straight, no transform)")
|
||||
Input.action_release("move_left")
|
||||
_restart_step()
|
||||
2:
|
||||
if _phase_time >= 0.6:
|
||||
if _game.player._bank_state != 0:
|
||||
_fail("bank not restored: state=%d" % _game.player._bank_state)
|
||||
else:
|
||||
print("[funnel_test] bank restored to neutral ok")
|
||||
_next_phase()
|
||||
8:
|
||||
# E4: 战斗机(P-38)侧身帧真实切换(素材已生成时应换贴图)
|
||||
match _step:
|
||||
0:
|
||||
var fighter := Player.new()
|
||||
fighter.variant = Player.Variant.FIGHTER
|
||||
fighter.position = Vector2(270.0, 700.0)
|
||||
_game.add_child(fighter)
|
||||
Input.action_press("move_left")
|
||||
_restart_step()
|
||||
1:
|
||||
if _phase_time >= 0.5:
|
||||
var p := _game.get_child(_game.get_child_count() - 1) as Player
|
||||
if p == null:
|
||||
_fail("fighter not found in tree")
|
||||
elif p._tex_bank_left == null:
|
||||
_fail("P-38-bank-left.png not loaded (missing art?)")
|
||||
elif p.sprite.texture != p._tex_bank_left:
|
||||
_fail("bank texture not swapped for fighter")
|
||||
elif p._bank_state != -1:
|
||||
_fail("fighter bank state=%d expected -1" % p._bank_state)
|
||||
else:
|
||||
print("[funnel_test] fighter P-38 bank texture swap ok")
|
||||
Input.action_release("move_left")
|
||||
_next_phase()
|
||||
9:
|
||||
# E5: 雷光(FUNNEL)侧身帧真实切换 + 平飞帧已是美术 PNG
|
||||
match _step:
|
||||
0:
|
||||
var funnel_p := Player.new()
|
||||
funnel_p.variant = Player.Variant.FUNNEL
|
||||
funnel_p.position = Vector2(270.0, 700.0)
|
||||
_game.add_child(funnel_p)
|
||||
Input.action_press("move_right")
|
||||
_restart_step()
|
||||
1:
|
||||
if _phase_time >= 0.5:
|
||||
var p := _game.get_child(_game.get_child_count() - 1) as Player
|
||||
if p == null:
|
||||
_fail("funnel player not found in tree")
|
||||
elif p._tex_neutral != null and p._tex_neutral == Player.TEX_FUNNEL:
|
||||
_fail("funnel neutral still SVG placeholder (plane_funnel.png not loaded?)")
|
||||
elif p._tex_bank_right == null:
|
||||
_fail("plane_funnel-bank-right.png not loaded (missing art?)")
|
||||
elif p.sprite.texture != p._tex_bank_right:
|
||||
_fail("bank texture not swapped for funnel variant")
|
||||
elif p._bank_state != 1:
|
||||
_fail("funnel bank state=%d expected 1" % p._bank_state)
|
||||
else:
|
||||
print("[funnel_test] funnel plane_funnel bank texture swap ok")
|
||||
Input.action_release("move_right")
|
||||
return _finish()
|
||||
return false
|
||||
@@ -0,0 +1 @@
|
||||
uid://b5qylgkck5nlg
|
||||
@@ -0,0 +1,166 @@
|
||||
extends SceneTree
|
||||
## 地面单位回归测试(时间驱动):
|
||||
## A) 独立堡垒:随地面滚动(~120px/s)、有开火产出敌弹
|
||||
## B) Game 集成:堡垒击杀 +300、船击杀 +800
|
||||
## C) 碰撞规则:玩家机体叠在堡垒上不掉命;玩家子弹真实命中堡垒
|
||||
## 运行:godot --headless -s tests/ground_test.gd
|
||||
|
||||
const GLOBAL_TIMEOUT := 25.0
|
||||
|
||||
var _phase := 0
|
||||
var _step := 0
|
||||
var _phase_time := 0.0
|
||||
var _time := 0.0
|
||||
var _fortress: EnemyFortress
|
||||
var _ship: EnemyShip
|
||||
var _game: Game
|
||||
var _bullets: Node2D
|
||||
var _start_y := 0.0
|
||||
var _bullet_time := 0.0
|
||||
var _score_before := 0
|
||||
var _lives_before := 3
|
||||
var _fails: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_bullets = Node2D.new()
|
||||
_bullets.add_to_group("enemy_bullets")
|
||||
root.add_child(_bullets)
|
||||
_fortress = EnemyFortress.new()
|
||||
_fortress.setup(270.0, 200.0, 1.0)
|
||||
root.add_child(_fortress)
|
||||
_start_y = 200.0
|
||||
print("[ground_test] phase A: standalone fortress")
|
||||
|
||||
|
||||
func _fail(msg: String) -> void:
|
||||
_fails.append(msg)
|
||||
print("[FAIL] " + msg)
|
||||
|
||||
|
||||
func _next_phase() -> void:
|
||||
_phase += 1
|
||||
_step = 0
|
||||
_phase_time = 0.0
|
||||
|
||||
|
||||
func _restart_step() -> void:
|
||||
_step += 1
|
||||
_phase_time = 0.0
|
||||
|
||||
|
||||
func _finish() -> bool:
|
||||
if _fails.is_empty():
|
||||
print("[ground_test] PASS")
|
||||
else:
|
||||
print("[ground_test] FAIL: %d issue(s)" % _fails.size())
|
||||
quit(0 if _fails.is_empty() else 1)
|
||||
return true
|
||||
|
||||
|
||||
## 清场(敌机+敌弹),保证后续断言确定性
|
||||
func _clear_field() -> void:
|
||||
for enemy in _game._enemies.get_children():
|
||||
(enemy as Node).queue_free()
|
||||
for bullet in _game._enemy_bullets.get_children():
|
||||
bullet.queue_free()
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
_time += delta
|
||||
if _time > GLOBAL_TIMEOUT:
|
||||
_fail("global timeout in phase %d" % _phase)
|
||||
return _finish()
|
||||
|
||||
_phase_time += delta
|
||||
match _phase:
|
||||
0:
|
||||
if _bullets.get_child_count() > 0:
|
||||
_bullet_time += delta
|
||||
if _phase_time >= 4.0:
|
||||
var moved := _fortress.position.y - _start_y
|
||||
if moved < 380.0 or moved > 620.0:
|
||||
_fail("fortress scroll wrong: moved %.0f px in 4s (expect ~480)" % moved)
|
||||
elif _bullet_time < 0.3:
|
||||
_fail("fortress fired too little: %.2fs" % _bullet_time)
|
||||
else:
|
||||
print("[ground_test] fortress scroll+fire ok (moved %.0f px, bullet %.1fs)" % [moved, _bullet_time])
|
||||
_fortress.apply_damage(999)
|
||||
_next_phase()
|
||||
1:
|
||||
if not is_instance_valid(_fortress):
|
||||
_next_phase()
|
||||
elif _phase_time > 2.0:
|
||||
_fail("fortress not freed after kill")
|
||||
_next_phase()
|
||||
2:
|
||||
if _game == null:
|
||||
_game = (load("res://scenes/game.tscn") as PackedScene).instantiate()
|
||||
root.add_child(_game)
|
||||
elif _game._enemies != null:
|
||||
_ship = EnemyShip.new()
|
||||
_ship.setup(270.0, 300.0, 1.0)
|
||||
_game._enemies.add_child(_ship)
|
||||
var f2 := EnemyFortress.new()
|
||||
f2.setup(150.0, 300.0, 1.0)
|
||||
_game._enemies.add_child(f2)
|
||||
_score_before = _game.score
|
||||
f2.apply_damage(999)
|
||||
_next_phase()
|
||||
3:
|
||||
if _phase_time >= 0.6:
|
||||
if _game.score != _score_before + 300:
|
||||
_fail("fortress score: %d -> %d (expected +300)" % [_score_before, _game.score])
|
||||
else:
|
||||
print("[ground_test] fortress kill +300 ok")
|
||||
_score_before = _game.score
|
||||
_ship.apply_damage(999)
|
||||
_next_phase()
|
||||
4:
|
||||
if _phase_time >= 0.6:
|
||||
if _game.score != _score_before + 800:
|
||||
_fail("ship score: %d -> %d (expected +800)" % [_score_before, _game.score])
|
||||
else:
|
||||
print("[ground_test] ship kill +800 ok")
|
||||
_next_phase()
|
||||
5:
|
||||
match _step:
|
||||
0:
|
||||
_clear_field()
|
||||
_lives_before = _game.lives
|
||||
var f3 := EnemyFortress.new()
|
||||
f3.setup(_game.player.global_position.x, _game.player.global_position.y, 1.0)
|
||||
_game._enemies.add_child(f3)
|
||||
_restart_step()
|
||||
1:
|
||||
if _phase_time >= 0.8:
|
||||
if _game.lives != _lives_before or not _game.player.active:
|
||||
_fail("ramming fortress hurt player: lives %d->%d active=%s" % [
|
||||
_lives_before, _game.lives, str(_game.player.active)])
|
||||
else:
|
||||
print("[ground_test] player overlaps fortress unharmed ok")
|
||||
_next_phase()
|
||||
6:
|
||||
match _step:
|
||||
0:
|
||||
_clear_field()
|
||||
var f4 := EnemyFortress.new()
|
||||
f4.setup(270.0, 300.0, 1.0)
|
||||
_game._enemies.add_child(f4)
|
||||
var bullet := PlayerBullet.new()
|
||||
bullet.direction = Vector2.UP
|
||||
(_game.get_node("PlayerBullets") as Node2D).add_child(bullet)
|
||||
bullet.position = Vector2(270.0, 430.0)
|
||||
_restart_step()
|
||||
1:
|
||||
if _phase_time >= 0.7:
|
||||
if _game._enemies.get_child_count() == 0:
|
||||
_fail("fortress missing before bullet-hit check")
|
||||
else:
|
||||
var target := _game._enemies.get_child(0) as EnemyFortress
|
||||
if target.hp != 3:
|
||||
_fail("player bullet did not hit fortress: hp=%d expected 3" % target.hp)
|
||||
else:
|
||||
print("[ground_test] player bullet hits fortress ok")
|
||||
return _finish()
|
||||
return false
|
||||
@@ -0,0 +1 @@
|
||||
uid://cfut4inswm23u
|
||||
@@ -0,0 +1,46 @@
|
||||
extends SceneTree
|
||||
## 海面覆盖回归测试:滚动两个完整周期(1200 帧),
|
||||
## 逐帧断言贴图联合覆盖包含整屏 Rect2(0,0,540,960)。
|
||||
## 运行:godot --headless -s tests/ocean_test.gd
|
||||
|
||||
const SCREEN := Rect2(0.0, 0.0, 540.0, 960.0)
|
||||
const TILE_HALF := Vector2(270.0, 270.0)
|
||||
const FRAMES := 1200
|
||||
|
||||
var _ocean: OceanScroll
|
||||
var _frames := 0
|
||||
var _fails := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_ocean = OceanScroll.new()
|
||||
root.add_child(_ocean)
|
||||
print("[ocean_test] ocean instantiated")
|
||||
|
||||
|
||||
func _process(_delta: float) -> bool:
|
||||
_frames += 1
|
||||
# 第 1 帧本回调先于海面节点的首个 _process,跳过(贴图尚未定位)
|
||||
if _frames >= 2:
|
||||
var covered := Rect2()
|
||||
var first := true
|
||||
for tile in _ocean._tiles:
|
||||
var rect := Rect2(tile.position - TILE_HALF, TILE_HALF * 2.0)
|
||||
if first:
|
||||
covered = rect
|
||||
first = false
|
||||
else:
|
||||
covered = covered.merge(rect)
|
||||
if not covered.encloses(SCREEN):
|
||||
_fails += 1
|
||||
if _fails <= 3:
|
||||
print("[FAIL] frame %d coverage=%s tiles=%s" % [_frames, covered, _ocean._tiles.map(func(t): return t.position)])
|
||||
|
||||
if _frames >= FRAMES:
|
||||
if _fails == 0:
|
||||
print("[ocean_test] PASS: %d 帧整屏全覆盖" % FRAMES)
|
||||
else:
|
||||
print("[ocean_test] FAIL: %d/%d 帧覆盖不足" % [_fails, FRAMES])
|
||||
quit(1 if _fails > 0 else 0)
|
||||
return true
|
||||
return false
|
||||
@@ -0,0 +1 @@
|
||||
uid://bp3enf7navowk
|
||||
@@ -0,0 +1,194 @@
|
||||
extends SceneTree
|
||||
## 道具系统回归测试(时间驱动):
|
||||
## L1 双发 → 拾取升级 L2 四发 → 直设 L3 六发(4 直射+2 斜射)
|
||||
## → 直设 L4 十二发(6 直射+4×22°+2×30°)
|
||||
## → 满级拾取 +500 分 → 死亡降一级 → 掉落标记敌机死亡产出道具
|
||||
## 注意:SceneTree._initialize 中 add_child 后节点 _ready 会在首帧才执行,
|
||||
## 因此容器引用在首个 _process 里惰性获取。
|
||||
## 运行:godot --headless -s tests/powerup_test.gd
|
||||
|
||||
const GLOBAL_TIMEOUT := 30.0
|
||||
|
||||
var _phase := 0
|
||||
var _step := 0
|
||||
var _phase_time := 0.0
|
||||
var _time := 0.0
|
||||
var _game: Game
|
||||
var _pbullets: Node2D
|
||||
var _powerups: Node2D
|
||||
var _score_before := 0
|
||||
var _fails: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_game = (load("res://scenes/game.tscn") as PackedScene).instantiate()
|
||||
root.add_child(_game)
|
||||
print("[powerup_test] game instantiated")
|
||||
|
||||
|
||||
func _fail(msg: String) -> void:
|
||||
_fails.append(msg)
|
||||
print("[FAIL] " + msg)
|
||||
|
||||
|
||||
func _next_phase() -> void:
|
||||
_phase += 1
|
||||
_step = 0
|
||||
_phase_time = 0.0
|
||||
|
||||
|
||||
func _restart_step_timer() -> void:
|
||||
_step += 1
|
||||
_phase_time = 0.0
|
||||
|
||||
|
||||
## 容器引用就绪前不进入任何阶段(_ready 在首帧执行)
|
||||
func _ensure_refs() -> bool:
|
||||
if _pbullets == null:
|
||||
_pbullets = _game.get_node_or_null("PlayerBullets") as Node2D
|
||||
_powerups = _game.get_node_or_null("Powerups") as Node2D
|
||||
return _pbullets != null and _powerups != null
|
||||
|
||||
|
||||
## 清空玩家弹容器并返回清空前数量
|
||||
func _count_and_clear_bullets() -> int:
|
||||
var children := _pbullets.get_children()
|
||||
var n := children.size()
|
||||
for bullet in children:
|
||||
_pbullets.remove_child(bullet)
|
||||
(bullet as Node).queue_free()
|
||||
return n
|
||||
|
||||
|
||||
func _spawn_pickup_at_player() -> void:
|
||||
var pickup := Powerup.new()
|
||||
pickup.position = _game.player.global_position
|
||||
_powerups.add_child(pickup)
|
||||
|
||||
|
||||
func _check_volley(expected: int, level_name: String) -> void:
|
||||
var n := _count_and_clear_bullets()
|
||||
Input.action_release("fire")
|
||||
if n != expected:
|
||||
_fail("%s volley=%d expected %d" % [level_name, n, expected])
|
||||
else:
|
||||
print("[powerup_test] %s volley ok: %d bullets" % [level_name, n])
|
||||
|
||||
|
||||
func _finish() -> bool:
|
||||
if _fails.is_empty():
|
||||
print("[powerup_test] PASS")
|
||||
else:
|
||||
print("[powerup_test] FAIL: %d issue(s)" % _fails.size())
|
||||
quit(0 if _fails.is_empty() else 1)
|
||||
return true
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
_time += delta
|
||||
if _time > GLOBAL_TIMEOUT:
|
||||
_fail("global timeout in phase %d" % _phase)
|
||||
return _finish()
|
||||
if not _ensure_refs():
|
||||
return false
|
||||
|
||||
_phase_time += delta
|
||||
match _phase:
|
||||
0:
|
||||
if _step == 0:
|
||||
Input.action_press("fire")
|
||||
_restart_step_timer()
|
||||
elif _phase_time >= 0.1:
|
||||
_check_volley(2, "L1")
|
||||
_next_phase()
|
||||
1:
|
||||
match _step:
|
||||
0:
|
||||
_spawn_pickup_at_player()
|
||||
_restart_step_timer()
|
||||
1:
|
||||
if _phase_time >= 0.6:
|
||||
if _game.player.weapon_level != 2:
|
||||
_fail("pickup did not level up: level=%d" % _game.player.weapon_level)
|
||||
elif _powerups.get_child_count() != 0:
|
||||
_fail("pickup not consumed: %d left" % _powerups.get_child_count())
|
||||
else:
|
||||
print("[powerup_test] pickup -> L2 ok")
|
||||
Input.action_press("fire")
|
||||
_restart_step_timer()
|
||||
2:
|
||||
if _phase_time >= 0.1:
|
||||
_check_volley(4, "L2")
|
||||
_next_phase()
|
||||
2:
|
||||
match _step:
|
||||
0:
|
||||
_game.player.weapon_level = 3
|
||||
Input.action_press("fire")
|
||||
_restart_step_timer()
|
||||
1:
|
||||
if _phase_time >= 0.1:
|
||||
_check_volley(6, "L3")
|
||||
_game.player.weapon_level = 4
|
||||
Input.action_press("fire")
|
||||
_restart_step_timer()
|
||||
2:
|
||||
if _phase_time >= 0.1:
|
||||
_check_volley(12, "L4")
|
||||
_next_phase()
|
||||
3:
|
||||
if _step == 0:
|
||||
_score_before = _game.score
|
||||
_spawn_pickup_at_player()
|
||||
_restart_step_timer()
|
||||
elif _phase_time >= 0.6:
|
||||
if _game.player.weapon_level != 4:
|
||||
_fail("capped pickup changed level: %d" % _game.player.weapon_level)
|
||||
elif _game.score != _score_before + 500:
|
||||
_fail("capped pickup score: %d -> %d (expected +500)" % [_score_before, _game.score])
|
||||
else:
|
||||
print("[powerup_test] capped pickup +500 ok")
|
||||
_next_phase()
|
||||
4:
|
||||
if _step == 0:
|
||||
_game.player.die()
|
||||
_restart_step_timer()
|
||||
elif _phase_time >= 0.1:
|
||||
if _game.player.weapon_level != 3:
|
||||
_fail("death downgrade: level=%d expected 3" % _game.player.weapon_level)
|
||||
else:
|
||||
print("[powerup_test] death downgrade 4->3 ok")
|
||||
_next_phase()
|
||||
5:
|
||||
if _step == 0:
|
||||
var enemy := EnemySmall.new()
|
||||
enemy.setup(270.0, 200.0, 100.0)
|
||||
enemy.drops_powerup = true
|
||||
_game._enemies.add_child(enemy)
|
||||
enemy.apply_damage(99)
|
||||
_restart_step_timer()
|
||||
elif _phase_time >= 0.5:
|
||||
if _powerups.get_child_count() != 1:
|
||||
_fail("dropper enemy produced %d powerups (expected 1)" % _powerups.get_child_count())
|
||||
else:
|
||||
print("[powerup_test] dropper spawn ok")
|
||||
_next_phase()
|
||||
6:
|
||||
if _step == 0:
|
||||
# 清掉前面阶段残留的道具再验证 Boss 必掉 P
|
||||
for pickup in _powerups.get_children():
|
||||
_powerups.remove_child(pickup)
|
||||
(pickup as Node).queue_free()
|
||||
var boss := _game.spawn_boss(1.0)
|
||||
boss.apply_damage(99999)
|
||||
_restart_step_timer()
|
||||
elif _phase_time >= 1.2:
|
||||
var kinds := []
|
||||
for pickup in _powerups.get_children():
|
||||
kinds.append((pickup as Powerup).kind)
|
||||
if kinds != [Powerup.Kind.POWER]:
|
||||
_fail("boss drop kinds=%s expected [POWER]" % str(kinds))
|
||||
else:
|
||||
print("[powerup_test] boss always drops POWER ok")
|
||||
return _finish()
|
||||
return false
|
||||
@@ -0,0 +1 @@
|
||||
uid://cep52sqhqlynf
|
||||
@@ -0,0 +1,65 @@
|
||||
extends SceneTree
|
||||
## 冒烟测试(时间驱动,与无头模式实际帧率无关):
|
||||
## 主动模式:模拟移动+按住开火跑满 110s,覆盖波次→Boss 战→下一轮全链路;
|
||||
## idle 模式:站桩 75s 被打死,覆盖掉命/游戏结束。
|
||||
## 运行:godot --headless -s tests/smoke_sim.gd [-- idle]
|
||||
|
||||
const ACTIVE_DURATION := 110.0
|
||||
const IDLE_DURATION := 75.0
|
||||
|
||||
var _time := 0.0
|
||||
var _duration := ACTIVE_DURATION
|
||||
var _next_print := 10.0
|
||||
var _next_bomb := 30.0
|
||||
var game: Game
|
||||
var idle := false
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
idle = OS.get_cmdline_user_args().has("idle")
|
||||
if idle:
|
||||
_duration = IDLE_DURATION
|
||||
var scene := load("res://scenes/game.tscn") as PackedScene
|
||||
game = scene.instantiate()
|
||||
root.add_child(game)
|
||||
if not idle:
|
||||
Input.action_press("fire")
|
||||
print("[smoke] game scene instantiated (idle=%s)" % str(idle))
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
# 游戏结束确认后会切回标题场景,模拟随之终止
|
||||
if not is_instance_valid(game):
|
||||
print("[smoke] left game scene (back to title) — stopping simulation")
|
||||
return true
|
||||
_time += delta
|
||||
if not idle:
|
||||
# 左右往返扫射(1.5s 周期),模拟真实操作
|
||||
if fmod(_time, 1.5) < 0.75:
|
||||
Input.action_release("move_left")
|
||||
Input.action_press("move_right")
|
||||
else:
|
||||
Input.action_release("move_right")
|
||||
Input.action_press("move_left")
|
||||
|
||||
# 每 10 秒打印一次局面快照
|
||||
if _time >= _next_print:
|
||||
_next_print += 10.0
|
||||
var boss_alive := is_instance_valid(game._boss)
|
||||
print("[smoke] t=%ds state=%d score=%d lives=%d bombs=%d boss=%s bar=%s enemies=%d pbullets=%d ebullets=%d" % [
|
||||
int(_time), game.state, game.score, game.lives, game.player.bombs,
|
||||
str(boss_alive), str(game._boss_bar_bg.visible and boss_alive),
|
||||
(game.get_node("Enemies") as Node2D).get_child_count(),
|
||||
(game.get_node("PlayerBullets") as Node2D).get_child_count(),
|
||||
(game.get_node("EnemyBullets") as Node2D).get_child_count(),
|
||||
])
|
||||
|
||||
# 主动模式每 30 秒投一颗炸弹(无头无法可靠模拟 just_pressed 边沿,直调公开接口)
|
||||
if not idle and _time >= _next_bomb:
|
||||
_next_bomb += 30.0
|
||||
game.player.use_bomb()
|
||||
|
||||
if _time >= _duration:
|
||||
print("[smoke] finished, no crash")
|
||||
return true
|
||||
return false
|
||||
@@ -0,0 +1 @@
|
||||
uid://bpb3766pd54l
|
||||
Reference in New Issue
Block a user