40250 classic: parallel-dev checkpoint through increment 49 (W0 interface freeze)
Captures the uncommitted parallel-development work (increments 46-49) on the
40250 classic client port, folded into docs/CLIENT-GAP.md + CLIENT-GAP-FIX.md.
Increment 49 (W0 interface-freeze batch, Phase 1 prerequisite) lands the
cross-workflow shared-file scaffolding so the 4 Phase 1 workflows can run in
isolated worktrees without contention:
- entity_store.{h,cpp}: Entity gains empire/affect_flags/owner_vid/state_flags;
new mut_spawn_full() (single-shot two-packet merge, §2.3), mut_ownership()
(§2.5), mut_map_bgm() + take_bgm_dirty()/bgm_name()/bgm_volume() (§9.1),
drain_dirty() (§2.1/§2.5 bare-field-update queue); mut_char_info() now stores
empire; reset_for_map_change() clears m_dirty.
- m2_client.cpp: new bgm_changed(name, volume) signal; classic + m2dev pump
loops drain drain_dirty() -> entity_info and take_bgm_dirty() -> bgm_changed;
entity_dict() exposes the 4 new keys.
- classic/classic_parser.{h,cpp}: GC_MAIN_CHARACTER3_BGM /
GC_MAIN_CHARACTER4_BGM_VOL route bgm_name/bgm_volume to mut_map_bgm() (was
discarded); new m_pending_actor staging map (W1 fills §2.2 merge logic).
- project/bgm_director.gd (new) + game_scene.gd: BGM consumption split out of
net_world.gd so W2/W4 don't collide; §8.6/§4.8 keybind convergence (digits
1-4 -> quickslots 0-3, F1-F4 -> quickslots 4-7, Ctrl+1..9 -> _emote()).
Tests: cmake --build build clean; ctest 16/16; 15 GDScript regressions green
(netbridge, gamescene, netplay, p9, p10, p2b, p8, system_menu_ui, skill,
combat_fx, skill_fx, player_motion, char_status_ui, chat, inventory), no skips.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
70f200e885
commit
f32064c74a
+14
-15
@@ -1,19 +1,18 @@
|
||||
# AppFlow (P10) —— 登录 → 选人 → 进游戏 的串场控制器。
|
||||
#
|
||||
# 把它做场景根:一个 M2Client 贯穿全程,LoadingScreen / ReconnectUI 常驻。
|
||||
# 把它做场景根:一个 M2Client 贯穿全程,LoadingScreen 常驻。
|
||||
# var af := preload("res://app_flow.gd").new()
|
||||
# add_child(af)
|
||||
# af.start(assets_root) # assets_root 传给 game_scene
|
||||
#
|
||||
# 状态机:LOGIN → SELECT → GAME。M2Client 信号驱动切屏。
|
||||
# 断线:在 GAME 里保留场景交给 ReconnectUI 自动重连;在 LOGIN/SELECT 回登录。
|
||||
# 断线:任何阶段都回到登录阶段,和 ClientVS22 的 SetLoginPhase 一致。
|
||||
extends Node
|
||||
|
||||
const ServerInfoRes = preload("res://net/serverinfo.gd")
|
||||
const ChannelStatus = preload("res://net/channel_status.gd")
|
||||
const LoadingScreen = preload("res://ui/loading_screen.gd")
|
||||
const CharSelectScreen = preload("res://ui/char_select_screen.gd")
|
||||
const ReconnectUI = preload("res://ui/reconnect_ui.gd")
|
||||
const GameScene = preload("res://game_scene.gd")
|
||||
|
||||
enum { LOGIN, SELECT, GAME }
|
||||
@@ -24,7 +23,6 @@ var _state := LOGIN
|
||||
var _assets := ""
|
||||
var _ui: CanvasLayer # 登录 / 选人的临时界面层
|
||||
var _loading: CanvasLayer
|
||||
var _reconnect: CanvasLayer
|
||||
var _game: Node
|
||||
var build_game_scene := true # 测试里置 false,只跑状态机不建重场景
|
||||
var _sel_server := 0
|
||||
@@ -53,9 +51,6 @@ func start(assets_root: String = "", injected_client: Node = null) -> void:
|
||||
_loading = LoadingScreen.new()
|
||||
add_child(_loading)
|
||||
_loading.setup(client)
|
||||
_reconnect = ReconnectUI.new()
|
||||
add_child(_reconnect)
|
||||
_reconnect.setup(client)
|
||||
_goto_login()
|
||||
if auto_login:
|
||||
call_deferred("_do_connect")
|
||||
@@ -83,8 +78,12 @@ func _on_char_list(list: Array) -> void:
|
||||
sc.set_chars(list)
|
||||
return
|
||||
_goto_select()
|
||||
if auto_login and not _chars.is_empty() and client.has_method("select_character"):
|
||||
client.select_character(int(_chars[0].get("index", 0)))
|
||||
if auto_login and not _chars.is_empty():
|
||||
var idx := int(_chars[0].get("index", 0))
|
||||
if client.has_method("enter_game"):
|
||||
client.enter_game(idx)
|
||||
elif client.has_method("select_character"):
|
||||
client.select_character(idx)
|
||||
|
||||
func _on_char_name_changed(pid: int, name: String) -> void:
|
||||
var sc: Node = _ui.get_node_or_null("CharSelect") if _ui and _state == SELECT else null
|
||||
@@ -121,11 +120,9 @@ func _on_login_failed(reason: String) -> void:
|
||||
_goto_login()
|
||||
_set_status("登录失败:%s" % reason)
|
||||
|
||||
func _on_disconnected(_reason: String) -> void:
|
||||
# GAME 里:保留 game_scene,ReconnectUI 负责重连遮罩
|
||||
# LOGIN/SELECT 里:回登录
|
||||
if _state != GAME:
|
||||
_goto_login()
|
||||
func _on_disconnected(reason: String) -> void:
|
||||
_goto_login()
|
||||
_set_status("连接断开:%s" % reason)
|
||||
|
||||
# --- 切屏 ------------------------------------------------------------------
|
||||
|
||||
@@ -323,7 +320,9 @@ func _build_char_list() -> void:
|
||||
_ui.add_child(screen)
|
||||
screen.setup(client, _assets, _chars)
|
||||
screen.select_requested.connect(func(idx: int):
|
||||
if client.has_method("select_character"):
|
||||
if client.has_method("enter_game"):
|
||||
client.enter_game(idx)
|
||||
elif client.has_method("select_character"):
|
||||
client.select_character(idx))
|
||||
screen.back_requested.connect(func():
|
||||
if client.has_method("disconnect_from_server"):
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# BgmDirector —— 把 M2Client.bgm_changed 信号翻译成 Audio 的 BGM 播放。
|
||||
#
|
||||
# 存在的理由:§9.1 的地图 BGM 消费点,从 net_world.gd 里拆出来,避免 W2(可见性)
|
||||
# 和 W4(音频)同时改 net_world.gd。真正的「地图 → 曲目」解析和音量应用由 W4 在
|
||||
# 这里补全(见 docs/CLIENT-GAP-FIX.md §9.1)。
|
||||
#
|
||||
# 用法:
|
||||
# var bd := preload("res://bgm_director.gd").new()
|
||||
# add_child(bd)
|
||||
# bd.bind(client, audio)
|
||||
extends Node
|
||||
|
||||
var _client: Node
|
||||
var _audio: Node
|
||||
|
||||
# 服务器最近一次下发的 BGM(曲目 id 或路径)和音量(<0 表示未指定)。
|
||||
var current_track := ""
|
||||
var current_volume := -1.0
|
||||
|
||||
func bind(client: Node, audio: Node) -> void:
|
||||
_client = client
|
||||
_audio = audio
|
||||
if _client and _client.has_signal("bgm_changed"):
|
||||
_client.bgm_changed.connect(_on_bgm_changed)
|
||||
|
||||
func _on_bgm_changed(name: String, volume: float) -> void:
|
||||
current_track = name
|
||||
if volume >= 0.0:
|
||||
current_volume = volume
|
||||
if _audio and "master_bgm" in _audio:
|
||||
_audio.master_bgm = clampf(volume, 0.0, 1.0)
|
||||
if _audio == null:
|
||||
return
|
||||
if name == "":
|
||||
if _audio.has_method("stop_bgm"):
|
||||
_audio.stop_bgm()
|
||||
return
|
||||
if _audio.has_method("play_bgm"):
|
||||
_audio.play_bgm(name)
|
||||
@@ -1,6 +1,6 @@
|
||||
# 选人界面 —— 3 个旋转台角色 + 九宫格信息面板 + 选择/创建按钮。
|
||||
# 数据来自 M2Client.char_list(联机)或 --mock 合成列表(离线开发)。
|
||||
# 选中 -> M2Client.select_character(index) -> entered_game。
|
||||
# 选中 -> M2Client.enter_game(index) / DirectEnter -> entered_game。
|
||||
# 用法(离线预览):godot --path project --script charselect.gd --mock --quit-after 120
|
||||
extends SceneTree
|
||||
|
||||
@@ -209,8 +209,13 @@ func _enter() -> void:
|
||||
_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)))
|
||||
var idx := int(s.get("index", _sel))
|
||||
if _client.has_method("enter_game"):
|
||||
_client.call("enter_game", idx)
|
||||
_set_status("enter_game(%d) sent" % idx)
|
||||
else:
|
||||
_client.call("select_character", idx)
|
||||
_set_status("select_character(%d) sent" % idx)
|
||||
else:
|
||||
_set_status("[mock] 进入游戏:%s" % s.get("name"))
|
||||
|
||||
|
||||
+102
-20
@@ -19,6 +19,7 @@ const NetPlay = preload("res://net_play.gd")
|
||||
const Hud = preload("res://hud.gd")
|
||||
const AppLifecycle = preload("res://app_lifecycle.gd")
|
||||
const Audio = preload("res://audio.gd")
|
||||
const BgmDirector = preload("res://bgm_director.gd")
|
||||
const UiManager = preload("res://ui/ui_manager.gd")
|
||||
const InventoryUI = preload("res://ui/inventory_ui.gd")
|
||||
const ItemListDB = preload("res://ui/item_list.gd")
|
||||
@@ -107,8 +108,10 @@ var atlas_ui: Node # AtlasUI
|
||||
var world_time: Node # WorldTime
|
||||
var weather: Node3D # Weather
|
||||
var _assets := ""
|
||||
var map_path := ""
|
||||
var _model_built := false
|
||||
var _audio: Node
|
||||
var _bgm_director: Node
|
||||
var _mount: Node3D
|
||||
var _sun: DirectionalLight3D
|
||||
var _env: Environment
|
||||
@@ -118,14 +121,14 @@ var _guild_invite_dialog: ConfirmationDialog
|
||||
var _gift_dialog: AcceptDialog
|
||||
|
||||
func setup(m2client: Node, assets_root: String,
|
||||
map_path: String = "OutdoorA1/metin2_map_a1") -> void:
|
||||
initial_map_path: String = "OutdoorA1/metin2_map_a1") -> void:
|
||||
client = m2client
|
||||
_assets = assets_root
|
||||
# GC_MAIN_CHARACTER only carries the server-space coordinates, not the map
|
||||
# folder. The old fixed A1 fallback puts characters from A2/A3 outside the
|
||||
# rendered terrain, which looks like an empty map. Resolve the map from the
|
||||
# character position before constructing Metin2World.
|
||||
map_path = _resolve_map_path(map_path)
|
||||
map_path = _resolve_map_path(initial_map_path)
|
||||
_build_lighting()
|
||||
# 关键:setup 是协程。每个重活之间让出一帧,好让 M2Client._process 抽 socket
|
||||
# (否则整段 ~5s 同步阻塞会漏 PONG → 服务器 10s 后 "peer closed")。
|
||||
@@ -179,6 +182,11 @@ func setup(m2client: Node, assets_root: String,
|
||||
add_child(life)
|
||||
life.bind(client, _audio)
|
||||
|
||||
# §9.1 地图 BGM:M2Client.bgm_changed -> Audio。放在独立节点里,W4 补全曲目解析。
|
||||
_bgm_director = BgmDirector.new()
|
||||
add_child(_bgm_director)
|
||||
_bgm_director.bind(client, _audio)
|
||||
|
||||
# UI 层 + 物品 proto + 背包窗(I 键开关)
|
||||
ui = UiManager.new()
|
||||
add_child(ui)
|
||||
@@ -194,7 +202,7 @@ func setup(m2client: Node, assets_root: String,
|
||||
fx = EffectRegistry.new()
|
||||
fx.setup(_assets)
|
||||
|
||||
# 技能(P6)—— K 键技能窗,数字键 1-9 快捷栏,F1-F4 切页,+ 技能特效表
|
||||
# 技能(P6)—— K 键技能窗,数字键 1-4 → 快捷栏 0-3 / F1-F4 → 4-7 / Ctrl+数字 = 表情,+ 技能特效表
|
||||
if assets_root != "":
|
||||
skill_table = SkillTable.new()
|
||||
for lang in ["en", "common"]:
|
||||
@@ -378,6 +386,8 @@ func setup(m2client: Node, assets_root: String,
|
||||
system_menu_ui.setup(ui, client, assets_root, system_option_ui, game_option_ui)
|
||||
if client.has_signal("warp"):
|
||||
client.warp.connect(_on_warp)
|
||||
if client.has_signal("world_reset"):
|
||||
client.world_reset.connect(_on_world_reset)
|
||||
if client.has_signal("channel_changed") and hud and hud.has_method("set_channel"):
|
||||
client.channel_changed.connect(func(c): hud.set_channel(c))
|
||||
|
||||
@@ -495,12 +505,62 @@ func _skill_master(sid: int) -> int:
|
||||
return int(s.get("master", 0))
|
||||
return 0
|
||||
|
||||
# GC_WARP —— 同服传送:直接把玩家挪到新坐标(Y 交给地形贴地)。
|
||||
# 跨服(same_server=false)留给 P10 的重连串场。
|
||||
# GC_WARP —— 40250 的参考端无论是否“同服”都会重连到 lAddr:wPort;
|
||||
# world_reset 先清旧实体,随后这里重载地图并把玩家放到新坐标。
|
||||
func _on_warp(pos: Vector3, same_server: bool) -> void:
|
||||
if not same_server:
|
||||
push_warning("GC_WARP 到别的服务器 %s —— 跨服重连归 P10" % pos)
|
||||
_reload_map_for_warp(pos)
|
||||
return
|
||||
_place_player_at_net_pos(pos)
|
||||
|
||||
func _on_world_reset() -> void:
|
||||
if net_world and net_world.has_method("clear_for_map_change"):
|
||||
net_world.clear_for_map_change()
|
||||
if ground_items and ground_items.has_method("clear_for_map_change"):
|
||||
ground_items.clear_for_map_change()
|
||||
# The reference loading phase closes transient interaction state. Keep
|
||||
# persistent account data (inventory, quests, friends) intact.
|
||||
for widget in [shop_ui, exchange_ui, safebox_ui, mall_ui, cube_ui,
|
||||
private_shop_ui, select_item_ui, quest_dialog, quest_log, party_ui,
|
||||
friend_ui, refine_ui, dragon_soul_ui]:
|
||||
if widget == null or not is_instance_valid(widget):
|
||||
continue
|
||||
if widget.has_method("close"):
|
||||
widget.call("close")
|
||||
elif widget.has_method("_close"):
|
||||
widget.call("_close")
|
||||
if atlas_ui and atlas_ui.has_method("close"):
|
||||
atlas_ui.close()
|
||||
if ui and ui.has_method("close_top"):
|
||||
while ui.close_top():
|
||||
pass
|
||||
if _audio and _audio.has_method("stop_bgm"):
|
||||
_audio.stop_bgm(0.15)
|
||||
|
||||
func _reload_map_for_warp(pos: Vector3) -> void:
|
||||
var server_pos := Vector2(pos.x * 100.0, -pos.z * 100.0)
|
||||
var next_map := _resolve_map_path(map_path, server_pos)
|
||||
var old_world := world
|
||||
map_path = next_map
|
||||
_build_world(_assets, map_path)
|
||||
if old_world and is_instance_valid(old_world) and old_world != world:
|
||||
old_world.queue_free()
|
||||
if cam:
|
||||
cam.world = world
|
||||
if pc:
|
||||
pc.world = world
|
||||
if net_world:
|
||||
net_world.world = world
|
||||
if hud and hud.has_method("reload_world") and _map_loaded():
|
||||
hud.reload_world(world)
|
||||
if atlas_ui and atlas_ui.has_method("reload_map"):
|
||||
atlas_ui.reload_map(world, map_path.get_file())
|
||||
if world_time and world_time.has_method("set_lighting"):
|
||||
world_time.set_lighting(_sun if not _env_from_msenv else null,
|
||||
_env if not _env_from_msenv else null)
|
||||
_place_player_at_net_pos(pos)
|
||||
|
||||
func _place_player_at_net_pos(pos: Vector3) -> void:
|
||||
if player:
|
||||
var wp := MapCoord.to_world(pos)
|
||||
if world and world.has_method("set_focus_position"):
|
||||
@@ -556,13 +616,26 @@ func _unhandled_input(e: InputEvent) -> void:
|
||||
elif e.keycode == KEY_Z and ground_items:
|
||||
ground_items.try_pickup()
|
||||
elif e.keycode >= KEY_F1 and e.keycode <= KEY_F4 and quickbar:
|
||||
quickbar.set_page(e.keycode - KEY_F1)
|
||||
elif e.keycode >= KEY_1 and e.keycode <= KEY_9 and quickbar:
|
||||
quickbar.activate(e.keycode - KEY_1)
|
||||
# 参考端 game.py:318 —— F1..F4 固定 __PressQuickSlot(4..7)。
|
||||
quickbar.activate(4 + (e.keycode - KEY_F1))
|
||||
elif e.keycode >= KEY_1 and e.keycode <= KEY_9:
|
||||
# 参考端 __PressNumKey(game.py:431):
|
||||
# Ctrl+1..9 → 表情;否则 1..4 → 快捷栏 0..3,5..9 无操作。
|
||||
var n: int = e.keycode - KEY_1
|
||||
if e.ctrl_pressed or e.meta_pressed:
|
||||
_emote(n)
|
||||
elif n < 4 and quickbar:
|
||||
quickbar.activate(n)
|
||||
elif e.keycode == KEY_ESCAPE and system_menu_ui:
|
||||
# ESC:ui_manager 先关最顶层窗口并吃掉事件;到这里说明没有窗口打开。
|
||||
system_menu_ui.toggle()
|
||||
|
||||
# 表情快捷键(§8.6 / §4.8)—— 参考端 Ctrl+1..9 = chrmgr.SetEmoticon(-1, n) + net.SendEmoticon(n)。
|
||||
# W3 在 §8.6 里补全:本地立即播表情动作 + 发 classic 表情包。这里先留统一入口。
|
||||
func _emote(index: int) -> void:
|
||||
if client and client.has_method("send_emoticon"):
|
||||
client.send_emoticon(index)
|
||||
|
||||
func _job_name() -> String:
|
||||
var r := int(client.get_entity(client.get_main_vid()).get("race", 0)) & 3
|
||||
return ["WARRIOR", "ASSASSIN", "SURA", "SHAMAN"][r]
|
||||
@@ -642,6 +715,7 @@ func _build_world(assets_root: String, map_path: String) -> void:
|
||||
if not ClassDB.class_exists("Metin2World"):
|
||||
push_warning("GameScene: Metin2World 未注册(扩展没编?)")
|
||||
return
|
||||
_env_from_msenv = false
|
||||
world = ClassDB.instantiate("Metin2World")
|
||||
world.set("auto_load", false) # 别让 _ready() 自动加载一遍,下面显式调一次拿返回值
|
||||
world.set("assets_root", assets_root)
|
||||
@@ -667,6 +741,10 @@ func _build_world(assets_root: String, map_path: String) -> void:
|
||||
_sun = msenv_sun
|
||||
_env = msenv_we.environment
|
||||
_env_from_msenv = true
|
||||
elif _sun == null or not is_instance_valid(_sun) or _sun.get_parent() != self:
|
||||
# A map without .msenv still needs the fallback lighting. This branch is
|
||||
# mainly for a cross-map reload from an outdoor .msenv map.
|
||||
_build_lighting()
|
||||
# .msenv 换过 Environment 后,把持久化的雾等设置重新贴上去
|
||||
if system_option_ui and system_option_ui.has_method("_apply_all"):
|
||||
system_option_ui._apply_all()
|
||||
@@ -677,16 +755,20 @@ func _map_loaded() -> bool:
|
||||
var rep: Dictionary = world.call("get_load_report")
|
||||
return int(rep.get("map_size_x", 0)) > 0
|
||||
|
||||
func _resolve_map_path(requested: String) -> String:
|
||||
if client == null or _assets == "" or not client.has_method("get_main_vid"):
|
||||
return requested
|
||||
var vid := int(client.get_main_vid())
|
||||
if vid == 0 or not client.has_method("get_entity"):
|
||||
return requested
|
||||
var entity: Dictionary = client.get_entity(vid)
|
||||
var pos: Variant = entity.get("pos_cm", null)
|
||||
func _resolve_map_path(requested: String, forced_pos: Variant = null) -> String:
|
||||
var pos: Variant = forced_pos
|
||||
if pos == null:
|
||||
if client == null or _assets == "" or not client.has_method("get_main_vid"):
|
||||
return requested
|
||||
var vid := int(client.get_main_vid())
|
||||
if vid == 0 or not client.has_method("get_entity"):
|
||||
return requested
|
||||
var entity: Dictionary = client.get_entity(vid)
|
||||
pos = entity.get("pos_cm", null)
|
||||
if not (pos is Vector3):
|
||||
return requested
|
||||
if not (pos is Vector2):
|
||||
return requested
|
||||
var map_pos := Vector2(pos.x, pos.y)
|
||||
|
||||
var settings: Array[String] = []
|
||||
_collect_map_settings(_assets, settings)
|
||||
@@ -700,7 +782,7 @@ func _resolve_map_path(requested: String) -> String:
|
||||
var size: Vector2i = bounds["size"]
|
||||
var max_x := base.x + float(size.x) * 25600.0
|
||||
var max_y := base.y + float(size.y) * 25600.0
|
||||
if pos.x < base.x or pos.x >= max_x or pos.y < base.y or pos.y >= max_y:
|
||||
if map_pos.x < base.x or map_pos.x >= max_x or map_pos.y < base.y or map_pos.y >= max_y:
|
||||
continue
|
||||
var root := _assets.trim_suffix("/")
|
||||
var rel_setting := setting_path.substr(root.length() + 1)
|
||||
@@ -720,7 +802,7 @@ func _resolve_map_path(requested: String) -> String:
|
||||
best = candidate
|
||||
|
||||
if best != requested:
|
||||
print("[GameScene] map resolved by server position ", pos, " -> ", best)
|
||||
print("[GameScene] map resolved by server position ", map_pos, " -> ", best)
|
||||
return best
|
||||
|
||||
func _collect_map_settings(dir_path: String, out: Array[String]) -> void:
|
||||
|
||||
@@ -7,6 +7,7 @@ extends CanvasLayer
|
||||
var _world: Node
|
||||
var _player: Node3D
|
||||
var _minimap_rect: TextureRect
|
||||
var _minimap_frame: Control
|
||||
var _dot: ColorRect
|
||||
var _map_w_m := 1024.0
|
||||
var _map_h_m := 1280.0
|
||||
@@ -43,6 +44,21 @@ func setup(world: Node, player: Node3D) -> void:
|
||||
_build_hotbar()
|
||||
_build_inventory()
|
||||
|
||||
func reload_world(new_world: Node) -> void:
|
||||
_world = new_world
|
||||
if _world == null:
|
||||
return
|
||||
var rep: Dictionary = _world.call("get_load_report")
|
||||
_map_w_m = float(rep.get("map_size_x", 1)) * 256.0
|
||||
_map_h_m = float(rep.get("map_size_y", 1)) * 256.0
|
||||
if _minimap_frame and is_instance_valid(_minimap_frame):
|
||||
_minimap_frame.queue_free()
|
||||
_minimap_frame = null
|
||||
_minimap_rect = null
|
||||
_dot = null
|
||||
if int(rep.get("map_size_x", 0)) > 0 and int(rep.get("map_size_y", 0)) > 0:
|
||||
_build_minimap(int(rep["map_size_x"]), int(rep["map_size_y"]))
|
||||
|
||||
# --- real UI texture loading (§6.1) -------------------------------------------
|
||||
|
||||
func _ui_tex(name: String) -> Texture2D:
|
||||
@@ -149,6 +165,7 @@ func _build_minimap(nx: int, ny: int) -> void:
|
||||
big.blit_rect(img, Rect2i(0, 0, cell, cell), Vector2i(tx * cell, ty * cell))
|
||||
var mm_h := 192 * ny / nx if nx > 0 else 192
|
||||
var frame := _nine("board", 32, 128)
|
||||
_minimap_frame = frame
|
||||
frame.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
||||
frame.position = Vector2(-192 - 24 - 12, 12)
|
||||
frame.size = Vector2(192 + 24, mm_h + 24)
|
||||
|
||||
+4
-1
@@ -134,7 +134,10 @@ func _on_chars(list: Array) -> void:
|
||||
if list.size() > 0 and (_auto_select or "--auto" in OS.get_cmdline_args()):
|
||||
var idx: int = int(list[0].get("index", 0))
|
||||
_msg("自动选择槽位 %d ..." % idx)
|
||||
_client.call("select_character", idx)
|
||||
if _client.has_method("enter_game"):
|
||||
_client.call("enter_game", idx)
|
||||
else:
|
||||
_client.call("select_character", idx)
|
||||
|
||||
func _on_entered_game() -> void:
|
||||
_msg("已进入游戏 —— 加载场景")
|
||||
|
||||
@@ -117,6 +117,21 @@ func set_local_vid(vid: int) -> void:
|
||||
func set_local_node(node: Node3D) -> void:
|
||||
_local_node = node
|
||||
|
||||
# ClientVS22 destroys the network actor manager when Loading starts. This is
|
||||
# called from M2Client.world_reset before the replacement connection sends its
|
||||
# first GC_MAIN_CHARACTER/GC_CHARACTER_ADD burst.
|
||||
func clear_for_map_change() -> void:
|
||||
for vid in _by_vid.keys():
|
||||
var node: Node3D = _by_vid[vid]
|
||||
if is_instance_valid(node):
|
||||
node.queue_free()
|
||||
entity_removed.emit(int(vid))
|
||||
_by_vid.clear()
|
||||
_main_vid = 0
|
||||
_pvp_relations.clear()
|
||||
_duel_opponents.clear()
|
||||
_duel_cannot_attack = false
|
||||
|
||||
func node_for(vid: int) -> Node3D:
|
||||
return _by_vid.get(vid, null)
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ func _test_m2client_surface() -> void:
|
||||
var c: Object = ClassDB.instantiate("M2Client")
|
||||
get_root().add_child(c)
|
||||
for m in ["move", "character_position", "sync_positions", "request_warp", "fishing", "request_dungeon", "attack", "set_target", "say", "shoot", "add_fly_targeting", "change_name", "select_empire", "download_guild_symbol", "get_guild_symbol", "script_select_item", "quest_cancel", "party_use_skill", "get_skill_group", "get_entity", "get_entities",
|
||||
"get_main_vid", "get_main_pid", "get_points", "get_target", "select_character", "reconnect",
|
||||
"get_main_vid", "get_main_pid", "get_points", "get_target", "select_character", "enter_game", "reconnect",
|
||||
"suspend", "resume", "move_item", "use_item", "drop_item", "drop_item_count", "use_item_to_item", "give_item", "pickup_item",
|
||||
"get_inventory", "get_equipment", "get_belt_inventory", "get_view_equipment", "get_item", "get_ground_items", "get_pvp_relations", "get_duel", "get_lover", "get_land_areas", "get_observers", "is_observer_mode", "get_observer_count", "has_mobile_flag", "combo_skill_enabled", "get_stamina_state", "whisper",
|
||||
"party_invite", "party_answer", "party_leave", "party_set_distribute", "party_set_state", "get_party", "get_party_distribute_mode",
|
||||
|
||||
+4
-1
@@ -119,7 +119,10 @@ func _run() -> void:
|
||||
_ck(picked and fc2.selects == [0], "flow: pick char -> select_character(0)")
|
||||
fc2.entered_game.emit()
|
||||
_ck(af.state() == AppFlow.GAME, "flow: entered_game -> GAME")
|
||||
# 在 SELECT 时断线 -> 回 LOGIN
|
||||
# 在 GAME 时断线 -> 回 LOGIN(ClientVS22 SetLoginPhase 语义)
|
||||
fc2.disconnected.emit("game dropped")
|
||||
_ck(af.state() == AppFlow.LOGIN, "flow: disconnect in GAME -> LOGIN")
|
||||
# 再次收到角色列表时允许回到 SELECT
|
||||
fc2.char_list.emit([{"index": 0, "name": "Hero", "level": 42}])
|
||||
_ck(af.state() == AppFlow.SELECT, "flow: back to SELECT")
|
||||
fc2.disconnected.emit("dropped")
|
||||
|
||||
@@ -64,6 +64,14 @@ func close() -> void:
|
||||
ui.close(_win)
|
||||
_win = null
|
||||
|
||||
func reload_map(metin_world: Node, name := "") -> void:
|
||||
world = metin_world
|
||||
if name != "":
|
||||
map_name = name
|
||||
if is_open():
|
||||
close()
|
||||
open()
|
||||
|
||||
func _build_window() -> void:
|
||||
_win = Control.new()
|
||||
_win.name = "AtlasWindow"
|
||||
|
||||
@@ -45,6 +45,13 @@ func setup(m2client: Node, parent: Node, proto_node: Node = null) -> void:
|
||||
func is_open() -> bool:
|
||||
return _root != null and _root.visible
|
||||
|
||||
func close() -> void:
|
||||
if _root:
|
||||
_root.visible = false
|
||||
_sel = -1
|
||||
_cube_slots.clear()
|
||||
_craft_in_flight = false
|
||||
|
||||
func _on_open(npc_vnum: int) -> void:
|
||||
if npc_vnum <= 0:
|
||||
return
|
||||
|
||||
@@ -32,6 +32,14 @@ func setup(m2client: Node, parent: Node, proto_node: Node = null) -> void:
|
||||
func is_open() -> bool:
|
||||
return _root != null and _root.visible
|
||||
|
||||
func close() -> void:
|
||||
if _root:
|
||||
_root.visible = false
|
||||
_next_display = 0
|
||||
_offered_cells.clear()
|
||||
if _gold_input:
|
||||
_gold_input.text = ""
|
||||
|
||||
func offer(inv_window: int, inv_cell: int) -> void:
|
||||
if not is_open() or inv_window < 0 or inv_cell < 0:
|
||||
return
|
||||
|
||||
@@ -29,6 +29,10 @@ func setup(m2client: Node, parent: Node) -> void:
|
||||
func is_open() -> bool:
|
||||
return _root != null and _root.visible
|
||||
|
||||
func close() -> void:
|
||||
if _root:
|
||||
_root.visible = false
|
||||
|
||||
func toggle() -> void:
|
||||
_root.visible = not _root.visible
|
||||
if _root.visible:
|
||||
|
||||
@@ -77,6 +77,13 @@ func _on_removed(vid: int) -> void:
|
||||
_by_vid.erase(vid)
|
||||
n.queue_free()
|
||||
|
||||
func clear_for_map_change() -> void:
|
||||
for vid in _by_vid.keys():
|
||||
var node: Node3D = _by_vid[vid]
|
||||
if is_instance_valid(node):
|
||||
node.queue_free()
|
||||
_by_vid.clear()
|
||||
|
||||
func _process(dt: float) -> void:
|
||||
var p: Node3D = _player_getter.call() if _player_getter.is_valid() else null
|
||||
if p == null:
|
||||
|
||||
@@ -29,6 +29,13 @@ func setup(m2client: Node, parent: Node, proto_node: Node = null) -> void:
|
||||
func is_open() -> bool:
|
||||
return _root != null and _root.visible
|
||||
|
||||
func close() -> void:
|
||||
if _root:
|
||||
_root.visible = false
|
||||
if is_instance_valid(_password_dialog):
|
||||
_password_dialog.queue_free()
|
||||
_password_dialog = null
|
||||
|
||||
func _name_of(vnum: int) -> String:
|
||||
if proto and proto.has_method("item"):
|
||||
var d: Dictionary = proto.item(vnum)
|
||||
|
||||
@@ -62,6 +62,10 @@ func setup(m2client: Node, parent: Node, dlg: Node = null) -> void:
|
||||
func is_open() -> bool:
|
||||
return _root != null and _root.visible
|
||||
|
||||
func close() -> void:
|
||||
if _root:
|
||||
_root.visible = false
|
||||
|
||||
# --- 로컬 플레이어가 파티장인가 ---
|
||||
func _local_is_leader() -> bool:
|
||||
if client == null or not client.has_method("get_main_vid"):
|
||||
|
||||
@@ -22,6 +22,10 @@ func setup(m2client: Node, parent: Node) -> void:
|
||||
func is_open() -> bool:
|
||||
return _root != null and _root.visible
|
||||
|
||||
func close() -> void:
|
||||
if _root:
|
||||
_root.visible = false
|
||||
|
||||
func toggle() -> void:
|
||||
_root.visible = not _root.visible
|
||||
if _root.visible:
|
||||
|
||||
@@ -26,6 +26,11 @@ func setup(m2client: Node, parent: Node, proto_node: Node = null) -> void:
|
||||
func is_open() -> bool:
|
||||
return _root != null and _root.visible
|
||||
|
||||
func close() -> void:
|
||||
if _root:
|
||||
_root.visible = false
|
||||
_cur = {}
|
||||
|
||||
func _name_of(vnum: int) -> String:
|
||||
if vnum == 0:
|
||||
return "-"
|
||||
|
||||
@@ -34,6 +34,13 @@ func setup(m2client: Node, parent: Node, proto_node: Node = null) -> void:
|
||||
func is_open() -> bool:
|
||||
return _root != null and _root.visible
|
||||
|
||||
func close() -> void:
|
||||
if _root:
|
||||
_root.visible = false
|
||||
if is_instance_valid(_password_dialog):
|
||||
_password_dialog.queue_free()
|
||||
_password_dialog = null
|
||||
|
||||
func deposit(inv_window: int, inv_cell: int) -> void:
|
||||
if not is_open() or inv_window < 0 or inv_cell < 0:
|
||||
return
|
||||
|
||||
@@ -36,6 +36,9 @@ func setup(m2client: Node, parent: Node, proto_node: Node = null, ilist: RefCoun
|
||||
func is_open() -> bool:
|
||||
return _root != null and _root.visible
|
||||
|
||||
func close() -> void:
|
||||
_close()
|
||||
|
||||
func open() -> void:
|
||||
_root.visible = true
|
||||
_err.text = ""
|
||||
|
||||
@@ -29,6 +29,10 @@ func setup(m2client: Node, sun_light: DirectionalLight3D, environment: Environme
|
||||
if t > 0:
|
||||
_on_time(t)
|
||||
|
||||
func set_lighting(sun_light: DirectionalLight3D, environment: Environment = null) -> void:
|
||||
sun = sun_light
|
||||
env = environment
|
||||
|
||||
func _on_time(server_epoch: int) -> void:
|
||||
_epoch = float(server_epoch)
|
||||
_since = 0.0
|
||||
|
||||
Reference in New Issue
Block a user