- 新增 script/playable_soak.sh:先做 soak 配置/资源校验,无 --allow-gameplay 只校验不启动客户端;N(>=10) 次独立正常退出运行 + 墙钟 soak,失败即停止后续 运行并记 BLOCKED;写本批次 release-manifest.json 后聚合 - run_client_gate.sh:确认的故障代理对所有 suite 启动(共享场景的退出运行也经代理); soak 超时下限只约束 soak 客户端(validate_soak 增加 soak_client 参数) - 新增本地 127.0.0.1 故障代理、RSS 采样/内存判定、窗口/指标/流程模块及其测试 - forest_mob_render_test 输出 PASS/FAIL 标记,供 rendering_batch_test.sh 识别 - 新增 docs/FIRST-MAC-PLAYABLE-STATUS.md:如实记录 PASS/BLOCKED、已知问题与环境需求 - .gitignore 排除本地场景配置、凭据文件与运行输出 离线回归:rendering_batch_test.sh failures=0,playable_gate_test.sh PASS, node 夹具测试 PASS。未联网运行,未重建候选包。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ft3kXpNdLbKEfg5uDLfqVF
83 lines
3.5 KiB
GDScript
83 lines
3.5 KiB
GDScript
extends SceneTree
|
|
const Anchor = preload("res://fx/target_effect_anchor.gd")
|
|
const GameScene = preload("res://game_scene.gd")
|
|
class Client extends Node:
|
|
func get_main_vid() -> int: return 1
|
|
class World extends Node:
|
|
var target: Node3D
|
|
func node_for(_vid: int) -> Node3D: return target
|
|
class Registry extends RefCounted:
|
|
var seen: Array = []
|
|
func spawn_target(event: Dictionary, target: Node3D, _world: Node3D): seen.append([event, target])
|
|
var failures := 0
|
|
func check(ok: bool, label: String) -> void:
|
|
if not ok:
|
|
failures += 1
|
|
printerr("FAIL: " + label)
|
|
func _init() -> void:
|
|
call_deferred("run")
|
|
func run() -> void:
|
|
var target := Node3D.new()
|
|
root.add_child(target)
|
|
target.position = Vector3(8, 2, -5)
|
|
target.rotation.y = PI * 0.5
|
|
var effect := Node3D.new()
|
|
root.add_child(effect)
|
|
var anchor := Anchor.new()
|
|
effect.add_child(anchor)
|
|
check(anchor.configure(target, Vector3(100, 0, 100), true), "following target configured")
|
|
check(effect.global_position.distance_to(target.to_global(Vector3(1, 1, 0))) < 0.00001, "source Z-up/cm and target rotation")
|
|
target.position.x += 3
|
|
anchor._process(0)
|
|
check(effect.global_position.distance_to(target.to_global(Vector3(1, 1, 0))) < 0.00001, "follows target movement")
|
|
anchor.configure(target, Vector3(100, 0, 100), false)
|
|
check(effect.global_position.distance_to(target.global_position + Vector3(1, 1, 0)) < 0.00001, "snapshot offset is world-aligned")
|
|
var position := effect.global_position
|
|
target.position.x += 2
|
|
await process_frame
|
|
check(effect.global_position == position, "snapshot does not follow")
|
|
var game := GameScene.new()
|
|
root.add_child(game)
|
|
game.client = Client.new()
|
|
game.add_child(game.client)
|
|
var world := World.new()
|
|
game.add_child(world)
|
|
world.target = target
|
|
game.net_world = world
|
|
var registry := Registry.new()
|
|
game.fx = registry
|
|
game._on_skill_cast_started(1, 2)
|
|
world.target = null # selected target later changes, original cast remains.
|
|
game._on_local_motion_event_detailed({"type": 10, "effect": "full/path.mse", "sound": "", "pos": Vector3.ZERO})
|
|
check(registry.seen.size() == 1 and registry.seen[0][1] == target, "event keeps cast-time target and full path")
|
|
check(registry.seen[0][0].effect == "full/path.mse", "path not reduced to basename")
|
|
target.free()
|
|
game._on_local_motion_event_detailed({"type": 10, "effect": "full/path.mse", "sound": "", "pos": Vector3.ZERO})
|
|
check(registry.seen.size() == 1, "despawned target not replaced by caster")
|
|
game._on_skill_cast_started(1, 0)
|
|
game._on_local_motion_event_detailed({"type": 10, "effect": "full/path.mse", "sound": "", "pos": Vector3.ZERO})
|
|
check(registry.seen.size() == 1, "cast without a target spawns no target effect")
|
|
# A following effect whose target dies/despawns mid-life is freed, never left at a stale position.
|
|
var victim := Node3D.new()
|
|
root.add_child(victim)
|
|
var outside := Node3D.new()
|
|
var outside_anchor := Anchor.new()
|
|
check(not outside_anchor.configure(outside, Vector3.ZERO, true), "target outside the tree is rejected")
|
|
outside_anchor.free()
|
|
outside.free()
|
|
var follower := Node3D.new()
|
|
root.add_child(follower)
|
|
var follow_anchor := Anchor.new()
|
|
follower.add_child(follow_anchor)
|
|
check(follow_anchor.configure(victim, Vector3.ZERO, true), "follower configured")
|
|
var follower_ref: WeakRef = weakref(follower)
|
|
victim.free()
|
|
await process_frame
|
|
await process_frame
|
|
check(follower_ref.get_ref() == null, "follower freed after its target left the tree")
|
|
game.free()
|
|
effect.free()
|
|
await process_frame
|
|
print("target_effect_test: failures=%d" % failures)
|
|
quit(1 if failures else 0)
|