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
This commit is contained in:
shen
2026-08-31 20:02:12 +09:00
co-authored by Claude Sonnet 5
parent f4917a2b3b
commit 47baf6c0c6
414 changed files with 69568 additions and 385 deletions
+74
View File
@@ -0,0 +1,74 @@
# SkillFx (P6 收尾) —— 技能 id / 内建特效 id → .mse 文件,接 P5 EffectRegistry。
#
# var sfx := preload("res://fx/skill_fx.gd").new()
# sfx.setup(effect_registry, skill_table)
# sfx.spawn_skill(1, 3, caster_node) # 技能 1(samyeon) grade→_4 挂 caster 播
# sfx.spawn_special(19, node) # GC_SPECIAL_EFFECT type 19 = SE_AUTO_HPUP -> drugup_red
#
# 技能特效表:skilldesc 的 motion_namecol13)就是特效基名;等级后缀 = 2 + master_type(夹 2..4)。
# effect/pc/<class>/effect/<motion>_<N>.mseEffectRegistry 的 basename 扫描能找到散包 / PC / season1
# 少数技能有 _blow / _head / _foot / _yong 子特效;这里只放主特效,缺就跳过。
extends RefCounted
# GC_SPECIAL_EFFECT 的 typeItemData.h / Packet.h 的 SE_* enum,值 = 下标)-> etc 特效基名。
# 不全 —— 只填常见的(真机 GM 号狂刷 19/20 自动喝药)。
const SPECIAL_FX := {
1: "drugup_red", # SE_HPUP_RED
2: "drugup_blue", # SE_SPUP_BLUE
3: "drugup_green", # SE_SPEEDUP_GREEN
4: "drugup_purple", # SE_DXUP_PURPLE
9: "firecracker_1", # SE_CHINA_FIREWORK
11: "firecracker_2", # SE_SUCCESS(凑合)
12: "fail", # SE_FAIL
19: "drugup_red", # SE_AUTO_HPUP
20: "drugup_blue", # SE_AUTO_SPUP
}
var fxr: RefCounted # EffectRegistry
var table: RefCounted # SkillTable
# 一个 motion 常见的子特效后缀(有就一起播)
const SUB_SUFFIX := ["", "_blow", "_hand", "_head", "_foot", "_yong"]
func setup(effect_registry: RefCounted, skill_table: RefCounted) -> void:
fxr = effect_registry
table = skill_table
# skill_id + master_type(0..3) -> 主特效基名(<motion>_<N>)。查不到 motion 返回 ""。
func skill_effect_name(skill_id: int, master: int = 0) -> String:
if table == null:
return ""
var mn := String(table.entry(skill_id).get("motion", ""))
if mn == "":
return ""
var n := clampi(2 + master, 2, 4)
return "%s_%d" % [mn, n]
# 在 host 下播技能特效(主 + 已存在的子特效各一份)。返回播了几个。
func spawn_skill(skill_id: int, master: int, host: Node3D) -> int:
if fxr == null or host == null:
return 0
var base := skill_effect_name(skill_id, master)
if base == "":
return 0
var parts := base.rsplit("_", false, 1) # "samyeon_4" -> ["samyeon", "4"]
var stem: String = parts[0]
var num: String = parts[1] if parts.size() > 1 else ""
var played := 0
for suf in SUB_SUFFIX:
var name: String = base
if suf != "":
name = "%s%s_%s" % [stem, suf, num] if num != "" else stem + suf
if fxr.resolve(name) != "":
fxr.spawn(name, host, true)
played += 1
return played
# GC_SPECIAL_EFFECT type -> 内建特效。返回是否播了。
func spawn_special(special_id: int, host: Node3D) -> bool:
if fxr == null or host == null or not SPECIAL_FX.has(special_id):
return false
var name: String = SPECIAL_FX[special_id]
if fxr.resolve(name) == "":
return false
fxr.spawn(name, host, true)
return true