# 选人界面 —— 3 个旋转台角色 + 九宫格信息面板 + 选择/创建按钮。 # 数据来自 M2Client.char_list(联机)或 --mock 合成列表(离线开发)。 # 选中 -> M2Client.select_character(index) -> entered_game。 # 用法(离线预览):godot --path project --script charselect.gd --mock --quit-after 120 extends SceneTree const UiKit = preload("res://ui_kit.gd") const Audio = preload("res://audio.gd") const Locale = preload("res://locale.gd") const CLASS_DIR := ["warrior", "assassin", "sura", "shaman"] const CLASS_KEY := ["JOB_WARRIOR", "JOB_ASSASSIN", "JOB_SURA", "JOB_SHAMAN"] var _audio: Node var _loc: RefCounted var _assets: String var _client: Node # M2Client(联机时) var _slots: Array[Dictionary] = [] # {index,name,job,level,x,y} var _pedestals: Array[Node3D] = [] var _models: Array[Node3D] = [] var _sel := 0 var _t := 0.0 var _panel: Control var _status: Label var _mock := false func _init() -> void: _assets = AssetRoot.path() _mock = "--mock" in OS.get_cmdline_user_args() or "--mock" in OS.get_cmdline_args() _loc = Locale.new() _loc.setup(_assets, "en") _audio = Audio.new() get_root().add_child(_audio) _audio.setup(_assets) _audio.play_bgm("characterselect") _build_stage() _build_ui() if _mock: _slots = [ {"index": 0, "name": "Baalthazar", "job": 0, "level": 42, "x": 0, "y": 0}, {"index": 1, "name": "Nyx", "job": 1, "level": 7, "x": 0, "y": 0}, {"index": 2, "name": "", "job": 0, "level": 0, "x": 0, "y": 0}, # 空槽 ] _populate() else: if ClassDB.class_exists("M2Client"): _client = ClassDB.instantiate("M2Client") get_root().add_child(_client) _client.connect("char_list", _on_char_list) _client.connect("stage_changed", func(s): _set_status("stage: " + s)) _client.connect("login_failed", func(r): _set_status("login failed: " + r)) _client.connect("entered_game", func(): _set_status("entered game")) var host := "192.168.21.203" _client.call("connect_to_server", host, 11000, host, 13002, "admin", "123456789") _set_status("connecting to " + host + " ...") else: _set_status("M2Client 未注册 —— 用 --mock") # --- 3D 舞台 --------------------------------------------------------------- func _build_stage() -> void: var root := Node3D.new() root.name = "Stage" get_root().add_child(root) var sun := DirectionalLight3D.new() sun.rotation_degrees = Vector3(-35, 20, 0) sun.light_energy = 2.4 root.add_child(sun) var fill := DirectionalLight3D.new() # 正面补光 fill.rotation_degrees = Vector3(-10, 180, 0) fill.light_energy = 0.9 fill.light_color = Color(0.8, 0.85, 1.0) root.add_child(fill) var we := WorldEnvironment.new() var e := Environment.new() e.background_mode = Environment.BG_COLOR e.background_color = Color(0.10, 0.11, 0.14) e.ambient_light_color = Color(0.5, 0.52, 0.62) e.ambient_light_energy = 1.1 e.tonemap_mode = Environment.TONE_MAPPER_FILMIC we.environment = e root.add_child(we) # 三个台座,绕中心排成弧 for i in 3: var a := deg_to_rad(-28.0 + i * 28.0) var ped := Node3D.new() ped.name = "Pedestal%d" % i ped.position = Vector3(sin(a) * 2.3, 0.0, -cos(a) * 2.3 + 2.3) root.add_child(ped) var disc := MeshInstance3D.new() var cyl := CylinderMesh.new() cyl.top_radius = 0.62 cyl.bottom_radius = 0.7 cyl.height = 0.14 disc.mesh = cyl var m := StandardMaterial3D.new() m.albedo_color = Color(0.18, 0.19, 0.24) m.metallic = 0.5 m.roughness = 0.4 disc.material_override = m disc.position.y = 0.07 ped.add_child(disc) _pedestals.append(ped) _models.append(null) var cam := Camera3D.new() cam.name = "Cam" root.add_child(cam) cam.look_at_from_position(Vector3(0.0, 1.5, 5.4), Vector3(0.0, 1.15, 1.8), Vector3.UP) cam.fov = 44.0 cam.make_current() func _slot_model(job: int) -> Node3D: var cls: String = CLASS_DIR[clampi(job, 0, 3)] var m: Node3D = ClassDB.instantiate("Metin2Model") m.name = "Model" m.set("texture_dir", _assets.path_join("PC/ymir work/pc/" + cls)) m.set("gr2_path", _assets.path_join("PC/ymir work/pc/" + cls + "/" + cls + "_novice.gr2")) if ClassDB.class_exists("Metin2AnimPlayer"): var anim: Node = ClassDB.instantiate("Metin2AnimPlayer") anim.set("model_path", NodePath("../Model")) anim.set("loop", true) anim.set("anim_path", _assets.path_join("PC/ymir work/pc/" + cls + "/general/wait.msa")) m.add_child.call_deferred(anim) return m func _populate() -> void: for i in range(min(_slots.size(), _pedestals.size())): var s := _slots[i] if _models[i] != null: _models[i].queue_free() _models[i] = null if String(s.get("name", "")).is_empty(): continue var mdl := _slot_model(int(s.get("job", 0))) _pedestals[i].add_child(mdl) _models[i] = mdl _refresh_panel() # --- 2D 信息面板 -------------------------------------------------------- func _build_ui() -> void: var layer := CanvasLayer.new() get_root().add_child(layer) _panel = UiKit.board(_assets, "board", 32, 128) _panel.set_anchors_preset(Control.PRESET_CENTER_BOTTOM) _panel.position = Vector2(-280, -172) _panel.size = Vector2(560, 160) layer.add_child(_panel) _status = Label.new() _status.set_anchors_preset(Control.PRESET_TOP_WIDE) _status.position = Vector2(20, 16) _status.add_theme_font_size_override("font_size", 16) layer.add_child(_status) func _set_status(t: String) -> void: if _status: _status.text = t print("[charselect] ", t) func _refresh_panel() -> void: for c in _panel.get_children(): c.queue_free() var row := HBoxContainer.new() row.position = Vector2(24, 20) row.add_theme_constant_override("separation", 10) _panel.add_child(row) for i in range(_slots.size()): var s := _slots[i] var b := Button.new() var nm := String(s.get("name", "")) var job := clampi(int(s.get("job", 0)), 0, 3) b.text = ("[ %s ]" % _loc.t("SELECT_CREATE") if nm.is_empty() else "%s\n%s %s %d" % [nm, _loc.t(CLASS_KEY[job]), _loc.t("SELECT_LEVEL"), int(s.get("level", 0))]) b.custom_minimum_size = Vector2(150, 70) b.toggle_mode = true b.button_pressed = (i == _sel) var idx := i b.pressed.connect(func(): _select_slot(idx)) row.add_child(b) var enter := Button.new() enter.text = _loc.t("SELECT_SELECT") # "Start" enter.position = Vector2(24, 118) enter.custom_minimum_size = Vector2(150, 40) enter.pressed.connect(_enter) _panel.add_child(enter) func _select_slot(i: int) -> void: _sel = i _set_status("selected slot %d" % i) if _audio and i < _slots.size(): var job := clampi(int(_slots[i].get("job", 0)), 0, 3) _audio.play_ui(CLASS_DIR[job] + "_select") _refresh_panel() func _enter() -> void: var s := _slots[_sel] if _sel < _slots.size() else {} if String(s.get("name", "")).is_empty(): _set_status("空槽不能进入(创建角色未实现)") return if _client: _client.call("select_character", int(s.get("index", _sel))) _set_status("select_character(%d) sent" % int(s.get("index", _sel))) else: _set_status("[mock] 进入游戏:%s" % s.get("name")) func _on_char_list(list: Array) -> void: _slots.clear() for d in list: _slots.append(d) while _slots.size() < 3: _slots.append({"index": _slots.size(), "name": "", "job": 0, "level": 0}) _set_status("char_list: %d 槽" % list.size()) _populate() # --- 循环:旋转选中角色,截图 ------------------------------------------- func _process(dt: float) -> bool: _t += dt for i in range(_pedestals.size()): var spd := 0.6 if i == _sel else 0.15 _pedestals[i].rotate_y(spd * dt) if _mock and _t > 3.0: var img := get_root().get_texture().get_image() var out := ProjectSettings.globalize_path("res://../test/golden/charselect.png") DirAccess.make_dir_recursive_absolute(out.get_base_dir()) img.save_png(out) print("[charselect] saved ", out) quit(0) return false