Files
mtgodot-poc/project/target_effect_test.gd
T
shenleiandClaude Opus 5 f39a55fdd5 feat(playable): 首个 Mac 联网内测版 STB-01 soak 工具与发布状态文档
- 新增 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
2026-09-11 22:06:48 +09:00

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)