Files
mtgodot-poc/project/eterngrn_polish_test.gd
T
shenandClaude Sonnet 5 47baf6c0c6 Metin2 game client (P0–P11) + mobile asset pipeline
Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
  phases, EntityStore world model, ~all GC/CG headers. char create/delete,
  private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
  quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
  char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
  token), system-option + game-option + ESC system menu, private-shop 39-grid,
  party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
  dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.

Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.

Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).

ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
2026-08-31 20:02:12 +09:00

142 lines
5.0 KiB
GDScript
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.
# eterngrn_polish_test —— EterGrnLib 对齐打磨(项 3/5/6)的 headless 自检。
# godot --headless --path project --script eterngrn_polish_test.gd
# 项 1/2/4(材质 flag / LOD 淡入 / ease 曲线)是 C++ 内部 + 渲染,离线看不出,走目视。
extends SceneTree
const CharShadow = preload("res://ui/char_shadow.gd")
const PlayerCtl = preload("res://player_controller.gd")
var _fail := 0
func _ck(c: bool, m: String) -> void:
if not c:
_fail += 1
printerr("FAIL: " + m)
func _init() -> void:
_run()
if _fail == 0:
print("PASS: eterngrn_polish_test")
quit(0)
else:
printerr("%d check(s) failed" % _fail)
quit(1)
func _run() -> void:
# ---- 5. CharShadowDecal + 强制 cast_shadow ----
var root := Node3D.new()
get_root().add_child(root)
var mi := MeshInstance3D.new()
mi.mesh = BoxMesh.new()
mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF # 模拟被 world OFF 波及
root.add_child(mi)
CharShadow.attach(root, 1.4)
_ck(mi.cast_shadow == GeometryInstance3D.SHADOW_CASTING_SETTING_ON,
"CharShadow 把 mesh cast_shadow 拉回 ON")
var blob := root.get_node_or_null("BlobShadow")
_ck(blob != null and blob is Decal, "CharShadow 加了 BlobShadow Decal")
_ck(blob != null and blob.texture_albedo != null, "Decal 有径向渐变贴图")
_ck(blob != null and absf(blob.size.x - 1.4) < 0.01, "Decal 足印尺寸 = footprint")
CharShadow.attach(root, 1.4) # 幂等
var n_blob := 0
for c in root.get_children():
if c.name == "BlobShadow": n_blob += 1
_ck(n_blob == 1, "CharShadow.attach 幂等,不重复加 Decal")
# ---- 6. player_controller._ray_pick_t:射线 vs 网格世界 AABB ----
var pc = PlayerCtl.new()
get_root().add_child(pc)
var ent := Node3D.new()
ent.position = Vector3(0, 0, -10)
get_root().add_child(ent)
var emi := MeshInstance3D.new()
var bm := BoxMesh.new()
bm.size = Vector3(2, 2, 2)
emi.mesh = bm
emi.position = Vector3(0, 1, 0) # 盒中心 (0,1,-10)
ent.add_child(emi)
await process_frame
# 从原点朝 -Z 打,正中盒子
var t_hit: float = pc._ray_pick_t(Vector3.ZERO, Vector3(0, 0, -1), ent)
_ck(t_hit > 8.0 and t_hit < 10.0, "_ray_pick_t 命中:进入 t≈9 (got %.2f)" % t_hit)
# 朝 +X 打,不该命中
var t_miss: float = pc._ray_pick_t(Vector3.ZERO, Vector3(1, 0, 0), ent)
_ck(t_miss < 0.0, "_ray_pick_t 未命中返回 -1 (got %.2f)" % t_miss)
# 擦边未命中(偏 5m)
var t_edge: float = pc._ray_pick_t(Vector3(5, 1, 0), Vector3(0, 0, -1), ent)
_ck(t_edge < 0.0, "_ray_pick_t 偏 5m 不命中")
# 无网格 → 回退胶囊近似(PICK_RADIUS 内命中)
var ent2 := Node3D.new()
ent2.position = Vector3(0, 0, -6)
get_root().add_child(ent2)
var t_cap: float = pc._ray_pick_t(Vector3.ZERO, Vector3(0, 0, -1), ent2)
_ck(t_cap > 0.0, "_ray_pick_t 无网格 → 胶囊近似仍能命中 (got %.2f)" % t_cap)
# ---- 3. game_scene._on_local_motion_event 分派 ----
var GS = load("res://game_scene.gd")
var gs = GS.new()
get_root().add_child(gs)
# 桩:cam.shake / fx.spawn_at / _audio.play_at / ui
var calls := {"shake": 0, "spawn": [], "sound": []}
var fake_cam := Camera3D.new()
fake_cam.set_script(_inline_stub("""
extends Camera3D
var log
func shake(s, d=8.0): log["shake"] += 1
"""))
fake_cam.set("log", calls)
var fake_fx = RefCounted.new()
fake_fx.set_script(_inline_stub("""
extends RefCounted
var log
func spawn_at(n, p, gp, o=true): log["spawn"].append(n); return null
"""))
fake_fx.set("log", calls)
var fake_audio := Node.new()
fake_audio.set_script(_inline_stub("""
extends Node
var log
func play_at(rel, pos, parent=null): log["sound"].append(rel)
"""))
fake_audio.set("log", calls)
gs.cam = fake_cam
gs.fx = fake_fx
gs._audio = fake_audio
gs.player = Node3D.new()
gs._mount = Node3D.new()
var fake_client := Node.new()
fake_client.set_script(_inline_stub("""
extends Node
var shots := []
func shoot(skill): shots.append(skill); return true
"""))
get_root().add_child(fake_client)
gs.client = fake_client
get_root().add_child(gs._mount)
gs.ui = CanvasLayer.new()
get_root().add_child(gs.ui)
gs.hud = Node.new()
gs._on_local_motion_event(5, "", "sfx/hit.wav", Vector3.ZERO)
_ck(calls["sound"] == ["sfx/hit.wav"], "motion_event: sound → play_at")
gs._on_local_motion_event(1, "effect/sword_trail.mse", "", Vector3(0, 100, 0))
_ck(calls["spawn"] == ["sword_trail"], "motion_event: effect → fx.spawn_at(basename)")
gs._on_local_motion_event(2, "", "", Vector3.ZERO) # SCREEN_WAVING
_ck(calls["shake"] == 1, "motion_event: type 2 → cam.shake")
gs._on_local_motion_event(3, "", "", Vector3.ZERO) # SCREEN_FLASHING
await process_frame
_ck(gs.ui.get_node_or_null("ColorRect") != null or gs._flash_rect != null,
"motion_event: type 3 → 屏闪 ColorRect")
gs._pending_shoot_skill = 77
gs._on_local_motion_event(6, "", "", Vector3.ZERO)
_ck(fake_client.shots == [77], "motion_event: FLY → sends pending CG_SHOOT once")
gs._on_local_motion_event(6, "", "", Vector3.ZERO)
_ck(fake_client.shots == [77], "motion_event: FLY clears pending shoot")
gs.queue_free()
func _inline_stub(src: String) -> GDScript:
var s := GDScript.new()
s.source_code = src
s.reload()
return s