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:
@@ -0,0 +1,242 @@
|
||||
# EffectPlayer (P5) —— 解析后的 `.mse` spec → Godot 节点树(GPUParticles3D 为主)。
|
||||
#
|
||||
# var fx := preload("res://fx/effect_player.gd").new()
|
||||
# fx.build(spec, assets_root) # spec 来自 fx/mse.gd
|
||||
# add_child(fx) # 挂到要出特效的位置(可 reparent 到骨骼)
|
||||
# fx.play() # 或 fx.play(true) 一次性
|
||||
#
|
||||
# 覆盖 Particle 组:发射形状 / 速率 / 寿命 / 方向 / 重力 / 缩放曲线 / 颜色渐变 /
|
||||
# billboard / 加法混合 / 旋转。纹理(.dds)暂用程序化径向渐变代替(GDScript 不解 DDS)。
|
||||
# Mesh 组:占位(MeshFileName 是 .mde,另需解码器)。SimpleLight:本资产集未用。
|
||||
extends Node3D
|
||||
|
||||
var spec := {}
|
||||
var assets_root := ""
|
||||
var one_shot := false
|
||||
var _emitters: Array[GPUParticles3D] = []
|
||||
var _mesh_nodes: Array[Node3D] = []
|
||||
var _bsphere_r := 0.0
|
||||
|
||||
static var _glow_tex: Texture2D
|
||||
|
||||
func build(mse_spec: Dictionary, assets := "") -> void:
|
||||
spec = mse_spec
|
||||
assets_root = assets
|
||||
_bsphere_r = float(spec.get("bsphere_r", 0.0))
|
||||
for p in spec.get("particles", []):
|
||||
var e := _build_particle(p)
|
||||
if e:
|
||||
add_child(e)
|
||||
_emitters.append(e)
|
||||
for m in spec.get("meshes", []):
|
||||
var mn := _build_mesh(m)
|
||||
if mn:
|
||||
add_child(mn)
|
||||
_mesh_nodes.append(mn)
|
||||
|
||||
func play(force_one_shot := false) -> void:
|
||||
for i in _emitters.size():
|
||||
var e := _emitters[i]
|
||||
var st := float(_emitters[i].get_meta("start_time", 0.0))
|
||||
if force_one_shot:
|
||||
e.one_shot = true
|
||||
var tree := get_tree()
|
||||
if st <= 0.0 or tree == null:
|
||||
e.restart()
|
||||
e.emitting = true
|
||||
else:
|
||||
tree.create_timer(st).timeout.connect(func():
|
||||
if is_instance_valid(e):
|
||||
e.restart()
|
||||
e.emitting = true)
|
||||
var tr := get_tree()
|
||||
if (force_one_shot or one_shot) and tr:
|
||||
var total := _longest_life() + 0.5
|
||||
tr.create_timer(maxf(total, 1.5)).timeout.connect(queue_free)
|
||||
|
||||
func stop() -> void:
|
||||
for e in _emitters:
|
||||
e.emitting = false
|
||||
|
||||
func _longest_life() -> float:
|
||||
var m := 1.0
|
||||
for e in _emitters:
|
||||
m = maxf(m, e.lifetime + float(e.get_meta("start_time", 0.0)))
|
||||
return m
|
||||
|
||||
# --- particle ---------------------------------------------------------
|
||||
|
||||
func _build_particle(p: Dictionary) -> GPUParticles3D:
|
||||
var emit: Dictionary = p.get("emitter", {})
|
||||
var prop: Dictionary = p.get("particle", {})
|
||||
var g := GPUParticles3D.new()
|
||||
g.set_meta("start_time", float(p.get("start_time", 0.0)))
|
||||
g.amount = maxi(1, int(_n(emit.get("MaxEmissionCount", 16))))
|
||||
g.explosiveness = 0.0
|
||||
|
||||
var life := _last_val(emit.get("TimeEventLifeTime", []), 1.0)
|
||||
g.lifetime = clampf(life, 0.05, 12.0)
|
||||
var cycle := _n(emit.get("CycleLength", 0.0))
|
||||
var loop := int(_n(emit.get("CycleLoopEnable", 0)))
|
||||
g.one_shot = (loop == 0)
|
||||
if cycle > 0.0 and loop != 0:
|
||||
g.lifetime = maxf(g.lifetime, cycle)
|
||||
|
||||
var pm := ParticleProcessMaterial.new()
|
||||
# 发射形状
|
||||
var shape := int(_n(emit.get("EmitterShape", 0)))
|
||||
if shape == 3:
|
||||
pm.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE
|
||||
pm.emission_sphere_radius = maxf(0.01, _n(emit.get("EmittingRadius", 10.0)) * 0.01)
|
||||
else:
|
||||
pm.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_POINT
|
||||
# 方向 / 速度
|
||||
var vel := _last_val(emit.get("TimeEventEmittingVelocity", []), 0.0) * 0.01
|
||||
pm.direction = Vector3(0, 1, 0)
|
||||
pm.spread = 25.0
|
||||
pm.initial_velocity_min = vel * 0.6
|
||||
pm.initial_velocity_max = maxf(vel, 0.05)
|
||||
# 重力
|
||||
var grav := _last_val(p.get("particle", {}).get("TimeEventGravity", []), 0.0)
|
||||
pm.gravity = Vector3(0, -grav * 0.01, 0)
|
||||
# 大小
|
||||
var sx := _last_val(emit.get("TimeEventSizeX", []), 32.0) * 0.01
|
||||
pm.scale_min = maxf(0.02, sx * 0.7)
|
||||
pm.scale_max = maxf(0.03, sx)
|
||||
pm.scale_curve = _curve_tex(prop.get("TimeEventScaleX", [[0, 1], [1, 0]]))
|
||||
# 颜色渐变(RGB 曲线各取末值 + Alpha 曲线)
|
||||
pm.color = _rgb(prop)
|
||||
var ramp := _alpha_ramp(prop.get("TimeEventAlpha", []), pm.color)
|
||||
if ramp:
|
||||
pm.color_ramp = ramp
|
||||
# 旋转
|
||||
var rspeed := _n(prop.get("RotationSpeed", 0.0))
|
||||
if rspeed != 0.0:
|
||||
pm.angular_velocity_min = deg_to_rad(rspeed) * 0.3
|
||||
pm.angular_velocity_max = deg_to_rad(rspeed)
|
||||
g.process_material = pm
|
||||
|
||||
# 绘制网格 + 材质(billboard + 混合)
|
||||
var qm := QuadMesh.new()
|
||||
qm.size = Vector2(1, 1)
|
||||
g.draw_pass_1 = qm
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.billboard_mode = _billboard(int(_n(prop.get("BillboardType", 1))))
|
||||
mat.billboard_keep_scale = true
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.blend_mode = _blend(int(_n(prop.get("SrcBlendType", 5))), int(_n(prop.get("DestBlendType", 2))))
|
||||
mat.albedo_texture = _glow()
|
||||
mat.vertex_color_use_as_albedo = true
|
||||
g.material_override = mat
|
||||
return g
|
||||
|
||||
func _build_mesh(m: Dictionary) -> Node3D:
|
||||
# .mde 需要专门解码器;先占位一个小 box,位置对
|
||||
var n := Node3D.new()
|
||||
n.name = "mesh_" + String(m.get("mesh_file", "?")).get_basename()
|
||||
var mi := MeshInstance3D.new()
|
||||
var bm := BoxMesh.new()
|
||||
bm.size = Vector3(0.1, 0.1, 0.4)
|
||||
mi.mesh = bm
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(1, 0.9, 0.5, 0.6)
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mi.material_override = mat
|
||||
n.add_child(mi)
|
||||
return n
|
||||
|
||||
# --- helpers ---------------------------------------------------------
|
||||
|
||||
func _n(v) -> float:
|
||||
if v is Array:
|
||||
return float(v[0]) if v.size() > 0 else 0.0
|
||||
if v is float or v is int:
|
||||
return float(v)
|
||||
return 0.0
|
||||
|
||||
# List 表末行的值(列 idx,默认 col 1 = 时间后第一个数)
|
||||
func _last_val(rows, def: float, col := 1) -> float:
|
||||
if rows is Array and rows.size() > 0:
|
||||
var r = rows[-1]
|
||||
if r is Array and r.size() > col and (r[col] is float or r[col] is int):
|
||||
return float(r[col])
|
||||
return def
|
||||
|
||||
func _rgb(prop: Dictionary) -> Color:
|
||||
return Color(
|
||||
_last_val(prop.get("TimeEventColorRed", []), 1.0),
|
||||
_last_val(prop.get("TimeEventColorGreen", []), 1.0),
|
||||
_last_val(prop.get("TimeEventColorBlue", []), 1.0),
|
||||
1.0)
|
||||
|
||||
# TimeEventScaleX rows [[t,v],...] -> CurveTexture
|
||||
func _curve_tex(rows) -> CurveTexture:
|
||||
var c := Curve.new()
|
||||
c.min_value = 0.0
|
||||
c.max_value = 2.0
|
||||
if rows is Array and rows.size() > 0:
|
||||
for r in rows:
|
||||
if r is Array and r.size() >= 2:
|
||||
c.add_point(Vector2(clampf(float(r[0]), 0, 1), float(r[1])))
|
||||
else:
|
||||
c.add_point(Vector2(0, 1))
|
||||
c.add_point(Vector2(1, 0))
|
||||
var t := CurveTexture.new()
|
||||
t.curve = c
|
||||
return t
|
||||
|
||||
func _alpha_ramp(rows, base: Color) -> GradientTexture1D:
|
||||
if not (rows is Array) or rows.size() < 1:
|
||||
return null
|
||||
var grad := Gradient.new()
|
||||
grad.offsets = PackedFloat32Array()
|
||||
grad.colors = PackedColorArray()
|
||||
var pts := []
|
||||
for r in rows:
|
||||
if r is Array and r.size() >= 2:
|
||||
pts.append([clampf(float(r[0]), 0, 1), float(r[1])])
|
||||
if pts.is_empty():
|
||||
return null
|
||||
pts.sort_custom(func(a, b): return a[0] < b[0])
|
||||
if pts[0][0] > 0.0:
|
||||
pts.push_front([0.0, pts[0][1]])
|
||||
if pts[-1][0] < 1.0:
|
||||
pts.append([1.0, pts[-1][1]])
|
||||
for pt in pts:
|
||||
grad.add_point(pt[0], Color(base.r, base.g, base.b, clampf(pt[1], 0, 1)))
|
||||
grad.remove_point(0)
|
||||
grad.remove_point(0)
|
||||
var gt := GradientTexture1D.new()
|
||||
gt.gradient = grad
|
||||
return gt
|
||||
|
||||
func _billboard(bt: int) -> int:
|
||||
# 0 = none/local, 1 = 面向相机, 4 = Y 轴
|
||||
if bt == 4:
|
||||
return BaseMaterial3D.BILLBOARD_FIXED_Y
|
||||
if bt == 0:
|
||||
return BaseMaterial3D.BILLBOARD_DISABLED
|
||||
return BaseMaterial3D.BILLBOARD_ENABLED
|
||||
|
||||
func _blend(src: int, dst: int) -> int:
|
||||
# D3D blend: 5=SRCALPHA 2=ONE 4=INVSRCALPHA 3=SRCCOLOR
|
||||
if dst == 2:
|
||||
return BaseMaterial3D.BLEND_MODE_ADD
|
||||
if dst == 4:
|
||||
return BaseMaterial3D.BLEND_MODE_MIX
|
||||
return BaseMaterial3D.BLEND_MODE_ADD
|
||||
|
||||
# 程序化径向渐变(代替 .dds 粒子纹理)
|
||||
static func _glow() -> Texture2D:
|
||||
if _glow_tex == null:
|
||||
var s := 48
|
||||
var img := Image.create(s, s, false, Image.FORMAT_RGBA8)
|
||||
for y in s:
|
||||
for x in s:
|
||||
var d := Vector2(x - s / 2.0, y - s / 2.0).length() / (s / 2.0)
|
||||
var a := clampf(1.0 - d, 0.0, 1.0)
|
||||
a = a * a
|
||||
img.set_pixel(x, y, Color(1, 1, 1, a))
|
||||
_glow_tex = ImageTexture.create_from_image(img)
|
||||
return _glow_tex
|
||||
Reference in New Issue
Block a user