Files
mtgodot-poc/project/world_demo.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

170 lines
6.1 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.
# W6 —— 玩家 / NPC / 第三人称相机。SHINSOO §9-W6。
# 交互:godot --path project --script world_demo.gd -- --play
# 左键点地走过去 / 左键点 NPC 选中 · WASD 直接走 · 右键拖拽转视角 · 滚轮缩放 · Shift 跑
# 截图:默认(无 --play)跑脚本化「前进」并存 test/golden/world-a1-r2.png
extends SceneTree
const GameCamera = preload("res://game_camera.gd")
const PlayerCtl = preload("res://player_controller.gd")
var _world: Node
var _player: Node3D
var _model: Node3D
var _anim: Node
var _npc_anim: Node
var _npc: Node3D
var _cam: Camera3D
var _pc: Node # PlayerController
var _target: Node3D # 当前选中实体
var _anim_state := ""
var _audio: Node
var _assets := ""
var _t := 0.0
var _shot_mode := false
var _shot_done := false
func _init() -> void:
_assets = AssetRoot.path()
_shot_mode = "--play" not in OS.get_cmdline_user_args() # 默认脚本化截图;`-- --play` 进交互
_world = ClassDB.instantiate("Metin2World")
_world.set("assets_root", _assets)
_world.set("map_path", "OutdoorA1/metin2_map_a1")
_world.set("auto_load", false)
_world.set("load_radius_tiles", 1) # W8: 3×3 流式
_world.set("stream_budget", 3)
get_root().add_child(_world)
var ok: bool = _world.call("load_map")
print("load ok=", ok, " ", _world.call("get_load_report"))
# 出生点:村庄一带(全局 cm
var spawn_cm := Vector3(45000.0, -38000.0, 17800.0)
var gx := spawn_cm.x * 0.01
var gz := -spawn_cm.y * 0.01
var gy: float = _world.call("sample_height", gx, gz)
_world.call("set_focus_position", gx, gz) # 流式对准出生点
_player = Node3D.new()
_player.name = "Player"
_player.position = Vector3(gx, gy, gz)
get_root().add_child(_player)
_model = ClassDB.instantiate("Metin2Model")
_model.name = "Metin2Model"
_model.set("texture_dir", _assets.path_join("PC/ymir work/pc/warrior"))
_model.set("gr2_path", _assets.path_join("PC/ymir work/pc/warrior/warrior_cheongrin.gr2"))
_model.set("hair_gr2", _assets.path_join("PC/ymir work/pc/warrior/hair/hair_1_1.gr2"))
# SourceSkin(hair_1_1.dds) -> TargetSkin 换色(PARITY §2.4
_model.set("hair_skin", _assets.path_join("PC/ymir work/pc/warrior/warrior_hair_01_red.dds"))
# 一手剑挂 equip_right_handPARITY §2.1;无物品系统,demo 写死)
_model.set("weapon_gr2", _assets.path_join("item/ymir work/item/weapon/00040.gr2"))
_player.add_child(_model)
_audio = preload("res://audio.gd").new()
get_root().add_child(_audio)
_audio.setup(_assets)
if ClassDB.class_exists("Metin2AnimPlayer"):
_anim = ClassDB.instantiate("Metin2AnimPlayer")
_anim.set("model_path", NodePath("../Metin2Model"))
_anim.set("loop", true)
_player.add_child(_anim)
# .msa 动作事件里的声音(脚步 / 挥击等)-> 3D 定位播放
_anim.connect("motion_event", func(_type, _effect, sound, _pos):
if _audio and sound != "":
_audio.play_at(sound, _model.global_position, _player))
_set_anim("wait")
_spawn_npc(Vector3(gx + 6.0, 0.0, gz + 4.0))
var hud := preload("res://hud.gd").new()
get_root().add_child(hud)
hud.setup(_world, _player)
_cam = GameCamera.new()
_cam.target = _player
_cam.world = _world
get_root().add_child(_cam)
_cam.make_current()
_pc = PlayerCtl.new()
_pc.player = _player
_pc.camera = _cam
_pc.world = _world
_pc.force_run = _shot_mode
get_root().add_child(_pc)
_pc.anim_state.connect(_set_anim)
_pc.moved.connect(func(p): _world.call("set_focus_position", p.x, p.z))
# F5:后台暂停游戏树 + 停 BGM + 降帧;前台还原(截图模式下不挂,免打断脚本)
if not _shot_mode:
var life := preload("res://app_lifecycle.gd").new()
get_root().add_child(life)
life.bind(null, _audio)
_pc.target_selected.connect(func(n):
_target = n
print("[target] ", n.name, " @ ", n.global_position))
if _npc:
_pc.pickables.append(_npc)
func _spawn_npc(pos: Vector3) -> void:
var npc := Node3D.new()
npc.name = "NPC"
npc.position = Vector3(pos.x, _world.call("sample_height", pos.x, pos.z), pos.z)
npc.rotation.y = PI
get_root().add_child(npc)
_npc = npc
var m: Node3D = ClassDB.instantiate("Metin2Model")
m.name = "Metin2Model"
m.set("texture_dir", _assets.path_join("PC/ymir work/pc/shaman"))
m.set("gr2_path", _assets.path_join("PC/ymir work/pc/shaman/shaman_lord.gr2"))
npc.add_child(m)
var tag := Label3D.new()
tag.text = "村民"
tag.position = Vector3(0, 2.2, 0)
tag.billboard = BaseMaterial3D.BILLBOARD_ENABLED
tag.no_depth_test = false
tag.pixel_size = 0.012
npc.add_child(tag)
if ClassDB.class_exists("Metin2AnimPlayer"):
_npc_anim = ClassDB.instantiate("Metin2AnimPlayer")
_npc_anim.set("model_path", NodePath("../Metin2Model"))
_npc_anim.set("loop", true)
_npc_anim.set("anim_path", _assets.path_join("PC/ymir work/pc/shaman/general/wait.msa"))
npc.add_child(_npc_anim)
func _set_anim(state: String) -> void:
if state == _anim_state or _anim == null:
return
_anim_state = state
var map := {"wait": "general/wait.msa", "walk": "general/walk.msa", "run": "general/run.msa"}
var f: String = map[state]
_anim.set("anim_path", _assets.path_join("PC/ymir work/pc/warrior/" + f))
func _process(dt: float) -> bool:
_t += dt
if int(_t * 2.0) != int((_t - dt) * 2.0):
print("t=%.1f player=%s anim=%s perf=%s" % [_t, _player.position, _anim_state, _world.call("get_perf")])
# 脚本化截图:走向一个前方点(展示点地移动 + 相机跟随),再选中 NPC
if _shot_mode:
if _t > 2.4 and _t < 2.5:
var ahead := _player.position - _player.global_transform.basis.z * 60.0
_pc.call("walk_to", Vector3(ahead.x, 0, ahead.z))
if _t > 6.0 and _t < 6.1 and _npc:
_pc.target_selected.emit(_npc)
if _shot_mode and _t > 7.0 and not _shot_done:
_shot_done = true
var img := get_root().get_texture().get_image()
var p := ProjectSettings.globalize_path("res://../test/golden/world-a1-r2.png")
DirAccess.make_dir_recursive_absolute(p.get_base_dir())
img.save_png(p)
print("saved world-a1-r2.png ", img.get_size(), " player=", _player.position,
" anim=", _anim_state, " target=", (_target.name if _target else "none"))
quit(0)
return false
func _finalize() -> void:
pass