客户端问题修改

This commit is contained in:
shen
2026-09-07 14:53:36 +08:00
parent b19d5b5dd3
commit 61272b75e9
32 changed files with 1023 additions and 95 deletions
+151 -67
View File
@@ -15,6 +15,8 @@ extends Control
const UiAssets = preload("res://ui/ui_assets.gd")
const UiKit = preload("res://ui_kit.gd")
const PlayerView = preload("res://ui/player_view.gd")
const EquipModel = preload("res://ui/equip_model.gd")
const DESIGN_SIZE := Vector2(1472, 854)
signal select_requested(index: int)
signal delete_requested(index: int, private_code: String)
@@ -61,9 +63,11 @@ var _class_img: TextureRect
var _class_lbl: Label
var _status: Label
var _slot_lbl: Label
var _spin := 0.0
var _last_empire := -1
var _rename_pending := false
var _layout: Control
var _board: Control
var _proto: Node
func setup(client: Node, assets_root: String, chars: Array) -> void:
_client = client
@@ -72,9 +76,15 @@ func setup(client: Node, assets_root: String, chars: Array) -> void:
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_build_background()
_build_stage()
_layout = Control.new()
_layout.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_layout)
_build_class_name()
_build_panel()
_build_navigation()
_build_status()
resized.connect(_resize_layout)
_resize_layout()
_last_empire = _empire_id()
set_process(true)
_select(_first_occupied())
@@ -181,7 +191,8 @@ func _build_background() -> void:
tr.texture = tex
tr.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
# The original stretches the 1024x768 atlas region to the window.
tr.stretch_mode = TextureRect.STRETCH_SCALE
tr.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(tr)
@@ -206,12 +217,12 @@ func _build_stage() -> void:
var key := DirectionalLight3D.new()
key.rotation_degrees = Vector3(-32, 28, 0)
key.light_energy = 1.7
key.light_energy = 0.9
key.light_color = Color(1.0, 0.96, 0.90)
root.add_child(key)
var rim := DirectionalLight3D.new()
rim.rotation_degrees = Vector3(-6, 200, 0)
rim.light_energy = 0.7
rim.light_energy = 0.25
rim.light_color = Color(0.75, 0.83, 1.0)
root.add_child(rim)
@@ -220,17 +231,18 @@ func _build_stage() -> void:
e.background_mode = Environment.BG_COLOR
e.background_color = Color(0, 0, 0, 0)
e.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
e.ambient_light_color = Color(0.42, 0.45, 0.55)
e.ambient_light_energy = 0.9
e.tonemap_mode = Environment.TONE_MAPPER_FILMIC
e.ambient_light_color = Color(0.8, 0.8, 0.8)
e.ambient_light_energy = 0.65
e.tonemap_mode = Environment.TONE_MAPPER_LINEAR
we.environment = e
root.add_child(we)
_pivot = Node3D.new()
_pivot.rotation.y = -PI * 0.5
root.add_child(_pivot)
_cam = Camera3D.new()
_cam.fov = 30.0
_cam.fov = 10.0
_cam.position = Vector3(0.0, 1.0, 4.0)
root.add_child(_cam)
_cam.make_current()
@@ -251,29 +263,56 @@ func _frame_model() -> void:
first = false
if first:
return
var c := aabb.get_center()
var r: float = maxf(aabb.size.length() * 0.5, 0.9)
var dist: float = r / sin(deg_to_rad(_cam.fov * 0.5)) * 1.12 # 12% 余量
var off := Vector3(-r * 0.5, 0.0, 0.0)
_cam.position = c + off + Vector3(0.0, r * 0.04, dist)
_cam.look_at(c + off, Vector3.UP)
var height := maxf(aabb.size.y, 1.0)
# Reference: head at 14%, feet at 85%, center at 59% of the window.
var view_height := height / 0.71
var dist := view_height / (2.0 * tan(deg_to_rad(_cam.fov * 0.5)))
var aspect := size.x / maxf(size.y, 1.0)
var target := aabb.get_center()
target.x -= view_height * aspect * 0.09
target.y -= height * 0.007
_cam.position = target + Vector3(0, dist * sin(deg_to_rad(6.0)), dist * cos(deg_to_rad(6.0)))
_cam.look_at(target, Vector3.UP)
func _rebuild_model() -> void:
if _pivot == null:
return
if _pv != null and is_instance_valid(_pv):
_pv.hide()
_pv.queue_free()
_pv = null
var c := _cur()
if c.is_empty() or String(c.get("name", "")) == "":
return
var job := clampi(int(c.get("job", 0)), 0, 3)
var job := clampi(int(c.get("job", 0)), 0, 7)
var pump: Callable = _client.net_poll if _client and _client.has_method("net_poll") else Callable()
var pv := PlayerView.new()
_pivot.add_child(pv)
if pv.build(_assets, job, pump):
_pv = pv
pv.set_anim_state("wait")
if _proto == null and ClassDB.class_exists("Metin2Proto"):
_proto = ClassDB.instantiate("Metin2Proto")
add_child(_proto)
for lang in ["en", "common"]:
var ip := _assets.path_join("locale/locale/%s/item_proto" % lang)
if FileAccess.file_exists(ip):
_proto.call("load_item_proto", ip)
break
var equip := EquipModel.new()
pv.add_child(equip)
equip.setup_remote(null, func() -> Node: return pv, _assets, job,
[int(c.get("main_part", 0)), 0, 0, int(c.get("hair_part", 0))], _proto)
# Expansion armor textures live beside their GR2, outside the base PC pack.
var body_path := String(pv.model.get("gr2_path"))
pv.model.set("texture_dir", body_path.get_base_dir())
# Shape zero uses the head/hair already included in the armor mesh.
if int(c.get("hair_part", 0)) == 0:
pv.model.set("hair_gr2", "")
pv._ground_model()
var intro := pv.motion_dir.get_base_dir().path_join("intro/wait.msa")
if pv.anim and FileAccess.file_exists(intro):
pv.anim.set("anim_path", intro)
pv.anim.set("loop", true)
# 蒙皮 / LOD 定型要几帧,多 fit 两次取稳定包围盒
for i in 4:
await get_tree().process_frame
@@ -287,22 +326,22 @@ func _rebuild_model() -> void:
func _build_class_name() -> void:
_class_img = TextureRect.new()
_class_img.position = Vector2(48, 60)
_class_img.position = Vector2(92, 164)
_class_img.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT
_class_img.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_class_img)
_layout.add_child(_class_img)
_class_lbl = Label.new()
_class_lbl.position = Vector2(50, 54)
_class_lbl.position = Vector2(120, 180)
_class_lbl.add_theme_font_size_override("font_size", 84)
_class_lbl.add_theme_color_override("font_color", Color(0.86, 0.11, 0.09))
_class_lbl.add_theme_color_override("font_outline_color", Color(0.15, 0.05, 0.02))
_class_lbl.add_theme_constant_override("outline_size", 10)
_class_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_class_lbl)
_layout.add_child(_class_lbl)
func _refresh_class_name() -> void:
var job := clampi(int(_cur().get("job", 0)), 0, 3)
var job := int(_cur().get("job", 0)) & 3
var has_char := not _cur().is_empty() and String(_cur().get("name", "")) != ""
var tex: Texture2D = null
if has_char:
@@ -319,27 +358,29 @@ func _refresh_class_name() -> void:
# --- 左侧信息板 --------------------------------------------------------------
func _build_panel() -> void:
var board := UiKit.board(_assets, "board", 24, 96)
board.position = Vector2(56, 168)
board.size = Vector2(300, 396)
var board := UiKit.board(_assets, "thinboard", 16, 16)
_board = board
board.position = Vector2(119, 313)
board.size = Vector2(208, 323)
board.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(board)
_layout.add_child(board)
# 板底纹太透就垫一层
var tint := ColorRect.new()
tint.color = Color(0.03, 0.05, 0.06, 0.62)
tint.position = board.position + Vector2(10, 10)
tint.size = board.size - Vector2(20, 20)
tint.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(tint)
_layout.add_child(tint)
_panel_box = VBoxContainer.new()
_panel_box.position = board.position + Vector2(22, 22)
_panel_box.custom_minimum_size = Vector2(256, 0)
_panel_box.add_theme_constant_override("separation", 9)
add_child(_panel_box)
_panel_box.position = board.position + Vector2(14, 12)
_panel_box.custom_minimum_size = Vector2(180, 0)
_panel_box.add_theme_constant_override("separation", 6)
_layout.add_child(_panel_box)
func _refresh_panel() -> void:
for c in _panel_box.get_children():
_panel_box.remove_child(c)
c.queue_free()
var d := _cur()
var has_char := not d.is_empty() and String(d.get("name", "")) != ""
@@ -349,7 +390,8 @@ func _refresh_panel() -> void:
head.add_theme_constant_override("separation", 10)
_panel_box.add_child(head)
var crest := TextureRect.new()
crest.custom_minimum_size = Vector2(56, 56)
crest.custom_minimum_size = Vector2(70, 40)
crest.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
crest.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT
var gid := int(d.get("guild_id", 0))
if has_char and gid != 0 and _client and _client.has_method("get_guild_mark_image"):
@@ -365,9 +407,9 @@ func _refresh_panel() -> void:
head.add_child(crest)
var gbox := VBoxContainer.new()
head.add_child(gbox)
gbox.add_child(_plain(_empire_name() if has_char else "", 14, Color(0.95, 0.9, 0.7)))
gbox.add_child(_plain(_empire_name() if has_char else "", 10, Color(0.95, 0.9, 0.7)))
var gname := String(d.get("guild_name", "")).strip_edges()
gbox.add_child(_plain(gname if gname != "" else "没有所属帮会", 13, Color(0.8, 0.82, 0.86)))
gbox.add_child(_plain(gname if gname != "" else "没有所属帮会", 10, Color(0.8, 0.82, 0.86)))
_panel_box.add_child(_sep())
@@ -413,21 +455,6 @@ func _refresh_panel() -> void:
)
erow.add_child(eb)
# 槽位切换
if _chars.size() > 1:
var nav := HBoxContainer.new()
nav.alignment = BoxContainer.ALIGNMENT_CENTER
nav.add_theme_constant_override("separation", 12)
_panel_box.add_child(nav)
var prev := Button.new(); prev.text = ""; prev.custom_minimum_size = Vector2(38, 28)
prev.pressed.connect(func(): _select(wrapi(_sel - 1, 0, _chars.size())))
nav.add_child(prev)
_slot_lbl = _plain("%d / %d" % [_sel + 1, _chars.size()], 14, Color(1, 1, 1))
nav.add_child(_slot_lbl)
var nxt := Button.new(); nxt.text = ""; nxt.custom_minimum_size = Vector2(38, 28)
nxt.pressed.connect(func(): _select(wrapi(_sel + 1, 0, _chars.size())))
nav.add_child(nxt)
# 按钮
var brow := GridContainer.new()
brow.columns = 2
@@ -436,28 +463,78 @@ func _refresh_panel() -> void:
_panel_box.add_child(brow)
var start := Button.new()
start.text = "开始"
start.custom_minimum_size = Vector2(122, 34)
start.custom_minimum_size = Vector2(180, 22)
_style_button(start)
start.disabled = not has_char or empire_required
start.pressed.connect(_do_start)
brow.add_child(start)
_panel_box.add_child(start)
_panel_box.move_child(start, _panel_box.get_child_count() - 2)
start.visible = has_char
var create := Button.new()
create.text = "创建"
create.custom_minimum_size = Vector2(122, 34)
create.custom_minimum_size = Vector2(180, 22)
_style_button(create)
create.disabled = has_char or empire_required
create.pressed.connect(_open_create_dialog)
brow.add_child(create)
_panel_box.add_child(create)
_panel_box.move_child(create, _panel_box.get_child_count() - 2)
create.visible = not has_char
var del := Button.new()
del.text = "删除"
del.custom_minimum_size = Vector2(122, 34)
del.custom_minimum_size = Vector2(86, 20)
_style_button(del)
del.disabled = not has_char or empire_required
del.pressed.connect(_open_delete_dialog)
brow.add_child(del)
var quit := Button.new()
quit.text = "退出"
quit.custom_minimum_size = Vector2(122, 34)
quit.custom_minimum_size = Vector2(86, 20)
_style_button(quit)
quit.pressed.connect(func(): back_requested.emit())
brow.add_child(quit)
func _style_button(button: Button) -> void:
button.add_theme_font_size_override("font_size", 10)
for state in ["normal", "hover", "pressed", "disabled"]:
var suffix := "02" if state == "hover" else "03" if state == "pressed" else "01"
var prefix := "XLarge" if button.custom_minimum_size.x > 100 else "Large"
var texture := UiAssets.load_tex(_assets,
"d:/ymir work/ui/public/%s_Button_%s.sub" % [prefix, suffix])
if texture:
var textured := StyleBoxTexture.new()
textured.texture = texture
textured.set_texture_margin_all(3)
button.add_theme_stylebox_override(state, textured)
continue
var style := StyleBoxFlat.new()
style.bg_color = Color(0.22, 0.22, 0.20, 0.95)
if state == "hover":
style.bg_color = Color(0.32, 0.31, 0.27)
style.border_color = Color(0.48, 0.46, 0.39)
style.set_border_width_all(1)
style.set_corner_radius_all(3)
button.add_theme_stylebox_override(state, style)
func _build_navigation() -> void:
for direction in [-1, 1]:
var button := TextureButton.new()
var side := "left" if direction < 0 else "right"
var base := "d:/ymir work/ui/intro/select/dragon_%s_button_" % side
button.texture_normal = UiAssets.load_tex(_assets, base + "01.sub")
button.texture_hover = UiAssets.load_tex(_assets, base + "02.sub")
button.texture_pressed = UiAssets.load_tex(_assets, base + "03.sub")
button.position = Vector2(714 if direction < 0 else 1024, 712)
button.custom_minimum_size = Vector2(160, 44)
button.tooltip_text = "上一个角色" if direction < 0 else "下一个角色"
button.pressed.connect(func(): _select(wrapi(_sel + direction, 0, _chars.size())))
_layout.add_child(button)
func _resize_layout() -> void:
if _layout:
_layout.size = DESIGN_SIZE
_layout.scale = size / DESIGN_SIZE
_frame_model()
# --- 底部状态 --------------------------------------------------------------
func _build_status() -> void:
@@ -746,15 +823,11 @@ func _empire_flag_tex() -> Texture2D:
return null
return UiAssets.load_tex(_assets, "d:/ymir work/ui/intro/empire/empireflag_%s.sub" % f)
func _process(dt: float) -> void:
_spin += dt
func _process(_dt: float) -> void:
var empire := _empire_id()
if empire != _last_empire:
_last_empire = empire
_refresh_panel()
if _pivot:
# 正面朝相机 + 轻微来回摆
_pivot.rotation.y = PI + sin(_spin * 0.3) * 0.35
# --- 小工具 --------------------------------------------------------------
@@ -780,21 +853,32 @@ func _sep() -> Control:
func _kv(k: String, v: String) -> HBoxContainer:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 8)
var kl := _plain(k, 13, Color(0.72, 0.78, 0.85))
kl.custom_minimum_size = Vector2(64, 0)
row.custom_minimum_size.y = 20
var kl := _plain(k, 10, Color(0.82, 0.82, 0.80))
kl.custom_minimum_size = Vector2(42, 0)
row.add_child(kl)
row.add_child(_plain(v, 14, Color(1, 1, 1)))
var value := _plain(v, 10, Color(1, 1, 1))
value.size_flags_horizontal = Control.SIZE_EXPAND_FILL
value.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
var style := StyleBoxFlat.new()
style.bg_color = Color(0, 0, 0, 0.85)
style.border_color = Color(0.4, 0.4, 0.37)
style.set_border_width_all(1)
value.add_theme_stylebox_override("normal", style)
row.add_child(value)
return row
func _stat(label: String, value: int, col: Color) -> HBoxContainer:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 8)
var kl := _plain(label, 13, Color(0.72, 0.78, 0.85))
kl.custom_minimum_size = Vector2(44, 0)
row.custom_minimum_size.y = 20
var kl := _plain(label, 10, Color(0.82, 0.82, 0.80))
kl.custom_minimum_size = Vector2(26, 0)
row.add_child(kl)
var track := ColorRect.new()
track.color = Color(0, 0, 0, 0.5)
track.custom_minimum_size = Vector2(150, 14)
track.custom_minimum_size = Vector2(98, 8)
track.size_flags_vertical = Control.SIZE_SHRINK_CENTER
row.add_child(track)
var fill := ColorRect.new()
fill.color = col
@@ -804,7 +888,7 @@ func _stat(label: String, value: int, col: Color) -> HBoxContainer:
fill.offset_left = 1
fill.offset_top = 1
fill.offset_bottom = -1
fill.offset_right = 1 + int(clampf(value / 25.0, 0.0, 1.0) * 148.0)
fill.offset_right = 1 + int(clampf(value / 90.0, 0.0, 1.0) * 96.0)
track.add_child(fill)
row.add_child(_plain(str(value), 13, Color(1, 1, 1)))
row.add_child(_plain(str(value), 10, Color(1, 1, 1)))
return row
+6
View File
@@ -102,6 +102,8 @@ func build(assets_root: String, proto: Node, race: int, pump := Callable()) -> b
model.set("gr2_path", gr2) # 重:解 gr2 + 建网格
if pump.is_valid(): pump.call()
add_child(model)
model.ready.connect(_ground_model)
_ground_model()
_load_motlist()
if ClassDB.class_exists("Metin2AnimPlayer"):
anim = ClassDB.instantiate("Metin2AnimPlayer")
@@ -160,6 +162,10 @@ func set_display_name(n: String) -> void:
add_child(tag)
tag.text = n
func _ground_model() -> void:
if model and model.has_method("get_ground_offset"):
model.position.y = float(model.call("get_ground_offset"))
# --- 内部 -------------------------------------------------------------------
func _folder_candidates(proto: Node, race: int) -> Array:
+8
View File
@@ -54,6 +54,8 @@ func build(assets_root: String, race: int, pump := Callable()) -> bool:
model.set("hair_gr2", hair) # 重:解 hair gr2 + 折进网格
if pump.is_valid(): pump.call()
add_child(model)
model.ready.connect(_ground_model)
_ground_model()
if ClassDB.class_exists("Metin2AnimPlayer"):
anim = ClassDB.instantiate("Metin2AnimPlayer")
@@ -196,9 +198,15 @@ func _refresh_sound_script(motion_path: String) -> void:
func _set(prop: StringName, val: Variant) -> bool:
if String(prop) in _forward and model:
model.set(prop, val)
if String(prop) == "gr2_path":
call_deferred("_ground_model")
return true
return false
func _ground_model() -> void:
if model and model.has_method("get_ground_offset"):
model.position.y = float(model.call("get_ground_offset"))
# --- 路径解析 -------------------------------------------------------------
func _resolve_dir(assets_root: String, rel: String) -> String:
+103 -4
View File
@@ -15,6 +15,14 @@
extends Node
const PlayerSkill := preload("res://player_skill.gd")
const UiAssets := preload("res://ui/ui_assets.gd")
const JOB_DIR := {
"WARRIOR": "warrior",
"ASSASSIN": "assassin",
"SURA": "sura",
"SHAMAN": "shaman",
}
signal skill_activated(skill_id: int) # game_scene 接它播技能特效
# §3.8 修改 1:三层校验挡下(code = OnCannotUseSkill 字符串码),game_scene 弹文案
@@ -33,16 +41,19 @@ var _skill_gate: RefCounted # PlayerSkill(§3.8 修改 1
var _player_getter: Callable
var _root: Control
var item_mouse: Node
var _slots := [] # 当前页 UI[{btn, cd, lbl}]
var _assets_root := ""
var _slots := [] # 当前页 UI[{btn, cd, lbl, icon, grade}]
var _state := [] # 36 个服务端槽:[{kind, id, cd_end:float}]
var _page := 0
var _move_from := -1
var _mobile_mode := false
func setup(m2client: Node, skill_table: RefCounted, parent: Node, player_getter: Callable) -> void:
func setup(m2client: Node, skill_table: RefCounted, parent: Node, player_getter: Callable,
assets_root_override := "") -> void:
client = m2client
table = skill_table
_player_getter = player_getter
_assets_root = assets_root_override if assets_root_override != "" else AssetRoot.path()
_skill_gate = PlayerSkill.new()
_skill_gate.setup(client, table)
for _i in SLOT_COUNT:
@@ -52,6 +63,8 @@ func setup(m2client: Node, skill_table: RefCounted, parent: Node, player_getter:
client.skill_cooldown_end.connect(_on_cd_end)
if client.has_signal("quickslots_changed"):
client.quickslots_changed.connect(restore_from_server)
if client.has_signal("skills_changed"):
client.skills_changed.connect(_refresh_page)
restore_from_server()
# 从服务器 GC_QUICKSLOT_* 恢复全部 36 个快捷栏槽位。
@@ -314,17 +327,40 @@ func _build(parent: Node) -> void:
num.text = str(i + 1)
num.position = Vector2(3, 1)
num.add_theme_font_size_override("font_size", 10)
num.mouse_filter = Control.MOUSE_FILTER_IGNORE
slot.add_child(num)
var icon := TextureRect.new()
icon.name = "icon"
icon.position = Vector2(4, 4)
icon.size = Vector2(32, 32)
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
slot.add_child(icon)
var lbl := Label.new()
lbl.name = "lbl"
lbl.position = Vector2(3, 15)
lbl.add_theme_font_size_override("font_size", 9)
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
slot.add_child(lbl)
var grade := Label.new()
grade.name = "grade"
grade.position = Vector2(25, 25)
grade.size = Vector2(13, 13)
grade.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
grade.add_theme_font_size_override("font_size", 9)
grade.add_theme_color_override("font_color", Color(1.0, 0.9, 0.45))
grade.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.95))
grade.add_theme_constant_override("shadow_offset_x", 1)
grade.add_theme_constant_override("shadow_offset_y", 1)
grade.mouse_filter = Control.MOUSE_FILTER_IGNORE
slot.add_child(grade)
var cd := ColorRect.new()
cd.name = "cd"
cd.color = Color(0, 0, 0, 0.55)
cd.set_anchors_preset(Control.PRESET_FULL_RECT)
cd.visible = false
cd.mouse_filter = Control.MOUSE_FILTER_IGNORE
slot.add_child(cd)
var idx := i
slot.gui_input.connect(func(e: InputEvent):
@@ -334,7 +370,7 @@ func _build(parent: Node) -> void:
elif e.button_index == MOUSE_BUTTON_RIGHT:
_clear(idx))
row.add_child(slot)
_slots.append({"btn": slot, "cd": cd, "lbl": lbl})
_slots.append({"btn": slot, "cd": cd, "lbl": lbl, "icon": icon, "grade": grade})
if item_mouse and item_mouse.has_method("register_target"):
var local_idx: int = i
item_mouse.register_target(slot,
@@ -378,11 +414,74 @@ func _refresh_slot(slot: int) -> void:
return
var s: Dictionary = _state[_global_slot(slot)]
var txt := ""
var tex: Texture2D = null
var grade_text := ""
if s.kind == "skill" and table:
txt = table.name_of(s.id).substr(0, 5)
tex = _skill_icon(int(s.id))
grade_text = _skill_grade_text(int(s.id))
elif s.kind == "item":
txt = "#%d" % s.id
_slots[slot].lbl.text = txt
var icon: TextureRect = _slots[slot].icon
icon.texture = tex
icon.visible = tex != null
_slots[slot].lbl.text = txt if tex == null else ""
_slots[slot].grade.text = grade_text
_slots[slot].grade.visible = grade_text != "" and tex != null
func _skill_grade(skill_id: int) -> int:
if client and client.has_method("get_skills"):
for skill in client.get_skills():
if int(skill.get("id", 0)) == skill_id:
return clampi(int(skill.get("master", 0)), 0, 3)
return 0
func _skill_grade_text(skill_id: int) -> String:
match _skill_grade(skill_id):
1: return "M"
2: return "G"
3: return "P"
_: return ""
func _skill_icon(skill_id: int) -> Texture2D:
if _assets_root == "" or table == null or not table.has_method("entry"):
return null
var data: Dictionary = table.entry(skill_id)
var job := String(data.get("job", "")).to_upper()
var motion := String(data.get("motion", "")).strip_edges()
if motion == "":
return null
var candidates: Array[String] = []
if JOB_DIR.has(job):
var dir := String(JOB_DIR[job])
for suffix in _skill_icon_suffixes(skill_id):
candidates.append("ETC/ymir work/ui/skill/%s/%s%s.sub" % [dir, motion, suffix])
elif job == "SUPPORT":
for suffix in _skill_icon_suffixes(skill_id):
candidates.append("ETC/ymir work/ui/skill/common/support/%s%s.sub" % [motion, suffix])
candidates.append("ETC/ymir work/ui/skill/common/support/%s.sub" % motion)
elif job == "HORSE":
candidates.append("ETC/ymir work/ui/skill/common/horse/%s.sub" % motion)
elif job == "GUILD":
candidates.append("ETC/ymir work/ui/skill/common/guild/%s.sub" % motion)
for rel in candidates:
var tex: Texture2D = UiAssets.load_tex(_assets_root, rel)
if tex != null:
return tex
return null
func _skill_icon_suffixes(skill_id: int) -> Array[String]:
var grade := _skill_grade(skill_id)
var first := "_01"
if grade == 1:
first = "_02"
elif grade >= 2:
first = "_03"
var out: Array[String] = [first]
for suffix in ["_03", "_02", "_01"]:
if not out.has(suffix):
out.append(suffix)
return out
func _refresh_page() -> void:
if _root and _root.has_node("page"):
+17 -3
View File
@@ -100,9 +100,23 @@ static func _load_sub(path: String) -> Texture2D:
"bottom": b = int(parts[1])
if image_name == "":
return null
var img_path := path.get_base_dir().path_join(image_name)
if not FileAccess.file_exists(img_path):
img_path = path.get_base_dir().path_join(image_name.get_basename() + ".tga")
# Skill .sub files live below ui/skill/<job>, while their shared DDS
# lives in ui/. Search the containing directory and its parents.
var img_path := ""
var search_dir := path.get_base_dir()
for _i in 6:
for candidate in [image_name, image_name.get_basename() + ".tga",
image_name.get_basename() + ".png", image_name.get_basename() + ".dds"]:
var candidate_path := search_dir.path_join(candidate)
if FileAccess.file_exists(candidate_path):
img_path = candidate_path
break
if img_path != "":
break
var parent_dir := search_dir.get_base_dir()
if parent_dir == search_dir:
break
search_dir = parent_dir
if not FileAccess.file_exists(img_path):
return null
var base := _load_image_file(img_path)