Implement playable Mac client and rendering validation
This commit is contained in:
@@ -238,7 +238,9 @@ func _build_stage() -> void:
|
||||
root.add_child(we)
|
||||
|
||||
_pivot = Node3D.new()
|
||||
_pivot.rotation.y = PI * 0.5
|
||||
# Animation sampling no longer reapplies the exported InitialPlacement;
|
||||
# the intro pose already faces the camera without a quarter-turn correction.
|
||||
_pivot.rotation.y = 0.0
|
||||
root.add_child(_pivot)
|
||||
|
||||
_cam = Camera3D.new()
|
||||
|
||||
@@ -98,6 +98,9 @@ func build(assets_root: String, proto: Node, race: int, pump := Callable()) -> b
|
||||
if pump.is_valid(): pump.call()
|
||||
model = ClassDB.instantiate("Metin2Model")
|
||||
model.name = "Metin2Model"
|
||||
# NPC/monster GR2 uses the same source winding as PCs. Without conversion,
|
||||
# back-face culling exposes interiors and hides the outward-facing surfaces.
|
||||
model.set("flip_winding", true)
|
||||
model.set("texture_dir", _dir)
|
||||
model.set("gr2_path", gr2) # 重:解 gr2 + 建网格
|
||||
if pump.is_valid(): pump.call()
|
||||
@@ -129,6 +132,12 @@ func set_anim_state(s: String) -> void:
|
||||
anim.set("anim_path", msa)
|
||||
_refresh_sound_script(msa)
|
||||
|
||||
# Read-only motion resolver used by the forest acceptance harness. Returning
|
||||
# the selected .msa before playback lets the test distinguish a real action
|
||||
# from a silent "keep the previous animation" fallback.
|
||||
func get_motion_path(state: String) -> String:
|
||||
return _motion_for(state)
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _audio == null or anim == null or _sound_instances.is_empty() or not anim.has_method("get_time") \
|
||||
or not bool(anim.get("playing")):
|
||||
|
||||
@@ -32,10 +32,9 @@ func build(assets_root: String, race: int, pump := Callable()) -> bool:
|
||||
if race < 0 or race > 7 or not ClassDB.class_exists("Metin2Model"):
|
||||
return false
|
||||
var cls: String = CLASS_OF[race & 3]
|
||||
var pc_dir := "pc2/ymir work/pc2/%s" % cls if race in FEMALE_RACES else "PC/ymir work/pc/%s" % cls
|
||||
# pc contains races 0..3 (including female assassin/shaman); pc2 is 4..7.
|
||||
var pc_dir := "pc2/ymir work/pc2/%s" % cls if race >= 4 else "PC/ymir work/pc/%s" % cls
|
||||
var base := _resolve_dir(assets_root, pc_dir)
|
||||
if base == "":
|
||||
base = _resolve_dir(assets_root, "PC/ymir work/pc/%s" % cls) # 兜底用男模
|
||||
if base == "":
|
||||
return false
|
||||
motion_dir = base.path_join("general")
|
||||
|
||||
+12
-2
@@ -25,6 +25,7 @@ const JOB_DIR := {
|
||||
}
|
||||
|
||||
signal skill_activated(skill_id: int) # game_scene 接它播技能特效
|
||||
signal skill_cast_started(skill_id: int, target_vid: int) # resolved target, not later selection
|
||||
# §3.8 修改 1:三层校验挡下(code = OnCannotUseSkill 字符串码),game_scene 弹文案
|
||||
signal skill_rejected(skill_id: int, code: String)
|
||||
|
||||
@@ -44,6 +45,7 @@ var item_mouse: Node
|
||||
var _assets_root := ""
|
||||
var _slots := [] # 当前页 UI:[{btn, cd, lbl, icon, grade}]
|
||||
var _state := [] # 36 个服务端槽:[{kind, id, cd_end:float}]
|
||||
var _skill_cooldowns := {} # skill identity survives slot moves/restores within this UI session
|
||||
var _page := 0
|
||||
var _move_from := -1
|
||||
var _mobile_mode := false
|
||||
@@ -93,7 +95,9 @@ func restore_from_server() -> void:
|
||||
continue
|
||||
match int(qs.get("type", 0)):
|
||||
1: restored[pos] = {"kind": "item", "id": int(qs.get("ref", 0)), "cd_end": 0.0}
|
||||
2: restored[pos] = {"kind": "skill", "id": int(qs.get("ref", 0)), "cd_end": 0.0}
|
||||
2:
|
||||
var skill_id := int(qs.get("ref", 0))
|
||||
restored[pos] = {"kind": "skill", "id": skill_id, "cd_end": float(_skill_cooldowns.get(skill_id, 0.0))}
|
||||
3: restored[pos] = {"kind": "emote", "id": int(qs.get("ref", 0)), "cd_end": 0.0}
|
||||
_state = restored
|
||||
_refresh_page()
|
||||
@@ -108,7 +112,7 @@ func assign(slot: int, kind: String, id: int, persist := true) -> bool:
|
||||
var type := 2 if kind == "skill" else 1 if kind == "item" else 3 if kind == "emote" else 0
|
||||
if type == 0 or not client.quickslot_add(global, type, id):
|
||||
return false
|
||||
_state[global] = {"kind": kind, "id": id, "cd_end": 0.0}
|
||||
_state[global] = {"kind": kind, "id": id, "cd_end": float(_skill_cooldowns.get(id, 0.0)) if kind == "skill" else 0.0}
|
||||
_refresh_slot(slot)
|
||||
return true
|
||||
|
||||
@@ -207,8 +211,13 @@ func _activate_slot(slot: int, screen_direction: Vector2) -> void:
|
||||
if client.has_method("use_skill"):
|
||||
intent_sent = client.use_skill(int(s.id), target_vid)
|
||||
if intent_sent and client.cast_skill(mi, yaw, int(xy.x), int(xy.y)):
|
||||
skill_cast_started.emit(int(s.id), target_vid)
|
||||
s.cd_end = _now() + _skill_cooldown(int(s.id))
|
||||
_state[global] = s
|
||||
_skill_cooldowns[int(s.id)] = s.cd_end
|
||||
for peer in _state:
|
||||
if peer.kind == "skill" and peer.id == s.id:
|
||||
peer.cd_end = s.cd_end
|
||||
# §3.4:扇形 / 圆形技能的附加 fly-targeting(主目标已在 use_skill 里发过;
|
||||
# PythonPlayerSkill.cpp:684 的 `if (dwTargetMaxCount>0 …)` 补目标块)。
|
||||
if target_vid != 0 and net_play and table \
|
||||
@@ -550,6 +559,7 @@ func _refresh_page() -> void:
|
||||
_refresh_slot(slot)
|
||||
|
||||
func _on_cd_end(skill_id: int) -> void:
|
||||
_skill_cooldowns.erase(skill_id)
|
||||
for i in _state.size():
|
||||
var s: Dictionary = _state[i]
|
||||
if s.kind == "skill" and s.id == skill_id:
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
extends RefCounted
|
||||
|
||||
static var _cache := {}
|
||||
static var _dds_helper: Object = null
|
||||
|
||||
# DDS 运行时解码:走 Metin2World.load_dds(C++ 里的 dxt.cpp,DXT1/3/5 + BGRA8)。
|
||||
# Godot 无原生运行时 DDS 解码,UI 里的 select.dds / 职业名图都得走这条路。
|
||||
@@ -17,9 +16,12 @@ static func load_dds_image(path: String) -> Image:
|
||||
return null
|
||||
if not ClassDB.class_exists("Metin2World"):
|
||||
return null
|
||||
if _dds_helper == null or not is_instance_valid(_dds_helper):
|
||||
_dds_helper = ClassDB.instantiate("Metin2World")
|
||||
var img = _dds_helper.call("load_dds", path)
|
||||
# Metin2World is a Node, not RefCounted; a static reference never frees it.
|
||||
# The decoder returns an independently owned Image, so release the helper
|
||||
# after each decode (textures themselves remain cached by load_tex).
|
||||
var helper: Object = ClassDB.instantiate("Metin2World")
|
||||
var img = helper.call("load_dds", path)
|
||||
helper.free()
|
||||
return img if img is Image else null
|
||||
|
||||
static func load_tex(assets_root: String, vpath: String) -> Texture2D:
|
||||
|
||||
Reference in New Issue
Block a user