Files
mtgodot-poc/project/fx/effect_player.gd
T
shenandClaude Sonnet 5 47baf6c0c6 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
2026-08-31 20:02:12 +09:00

243 lines
7.8 KiB
GDScript

# 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