Complete remaining client gap features

This commit is contained in:
shenlei
2026-09-04 13:25:05 +09:00
parent 13ccb8d02d
commit 9a4a044da5
47 changed files with 6667 additions and 140 deletions
+187 -16
View File
@@ -6,17 +6,31 @@
# c.world = metin2_world; add_child(c); c.make_current()
extends Camera3D
enum CameraMode {
NORMAL,
STAND,
BLEND,
}
# ClientVS22 stores camera settings in centimetres/degrees. The Godot scene
# is metres/radians, so keep the conversion at this boundary instead of
# leaking mixed units into the orbit controls.
const DEFAULT_DISTANCE_CM := 1550.0
const DEFAULT_PITCH_DEG := 27.0
const DEFAULT_ROTATION_DEG := 0.0
const DEFAULT_HEIGHT_CM := 100.0
var target: Node3D
var world: Node # Metin2World,用 sample_height 防穿(可空)
var head_offset := Vector3(0, 1.15, 0)
var yaw := 0.7
var pitch := 0.55
var dist := 8.0
var yaw := deg_to_rad(DEFAULT_ROTATION_DEG)
var pitch := deg_to_rad(DEFAULT_PITCH_DEG)
var dist := DEFAULT_DISTANCE_CM * 0.01
var min_pitch := deg_to_rad(12.0)
var max_pitch := deg_to_rad(78.0)
var min_dist := 3.5
var max_dist := 20.0
var min_dist := 2.0
var max_dist := 25.0
var sensitivity := 0.006
var zoom_step := 1.4
var follow_lerp := 12.0
@@ -41,6 +55,20 @@ var _key_orbit := 0
var _key_zoom := 0
var _key_pitch := 0
# PythonApplicationCamera.cpp's NORMAL / STAND / BLEND modes. The normal
# camera follows the player; event cameras use a fixed centre and temporarily
# lock the orbit input. `setting` dictionaries intentionally retain the
# reference field names so quest scripts can be passed through unchanged.
var camera_mode: CameraMode = CameraMode.NORMAL
var _event_setting := {}
var _default_setting := {}
var _blend_from := {}
var _blend_to := {}
var _blend_elapsed := 0.0
var _blend_duration := 0.0
var _event_offset := Vector3.ZERO
var _event_center := Vector3.ZERO
const OCCLUDER_MASK := 1 << 1 # 静态遮挡物层(Metin2World 给建筑的盒碰撞)
const FADE_ALPHA := 0.72 # 挡住玩家时的透明度
var _faded: Array[Node] = [] # 上一帧被淡出的 GeometryInstance3D
@@ -48,6 +76,7 @@ var _faded: Array[Node] = [] # 上一帧被淡出的 GeometryInstance3D
func _ready() -> void:
fov = 55.0
far = 4000.0
_apply_setting(_normal_setting())
if target:
global_position = _desired_pos(true)
_pos_ready = true
@@ -55,12 +84,14 @@ func _ready() -> void:
func _unhandled_input(e: InputEvent) -> void:
if e is InputEventMouseButton:
if e.button_index == MOUSE_BUTTON_RIGHT:
_dragging = e.pressed
_dragging = e.pressed and not is_event_locked()
elif e.button_index == MOUSE_BUTTON_WHEEL_UP and e.pressed:
dist = maxf(min_dist, dist - zoom_step)
if not is_event_locked():
dist = maxf(min_dist, dist - zoom_step)
elif e.button_index == MOUSE_BUTTON_WHEEL_DOWN and e.pressed:
dist = minf(max_dist, dist + zoom_step)
elif e is InputEventMouseMotion and _dragging:
if not is_event_locked():
dist = minf(max_dist, dist + zoom_step)
elif e is InputEventMouseMotion and _dragging and not is_event_locked():
yaw -= e.relative.x * sensitivity
pitch = clampf(pitch + e.relative.y * sensitivity, min_pitch, max_pitch)
elif e is InputEventScreenTouch:
@@ -115,6 +146,9 @@ func heading() -> float:
# 相机看向的水平方向(供角色移动「相对相机」用)
return yaw
func is_event_locked() -> bool:
return camera_mode != CameraMode.NORMAL
func set_key_orbit(direction: int) -> void:
_key_orbit = clampi(direction, -1, 1)
@@ -125,12 +159,16 @@ func set_key_pitch(direction: int) -> void:
_key_pitch = clampi(direction, -1, 1)
func _desired_pos(snap := false) -> Vector3:
var head := target.global_position + head_offset
var head := _look_target()
var off := Vector3(
sin(yaw) * cos(pitch),
sin(pitch),
cos(yaw) * cos(pitch)) * dist
var want := head + off
var want := head + off + _event_offset
if is_event_locked():
# Event cameras are deliberately fixed shots. The reference event
# camera does not chase the actor while a quest is being presented.
return want
# 地形防穿:沿 head->want 采样,低于地表就把相机拉回
if world and world.has_method("sample_height"):
var d := want - head
@@ -151,6 +189,129 @@ func _desired_pos(snap := false) -> Vector3:
want = hit.position - n * 0.3
return want
func _look_target() -> Vector3:
if is_event_locked():
return _event_center
if target:
return target.global_position + head_offset
return global_position
func _normal_setting() -> Dictionary:
var centre := target.global_position if target else Vector3.ZERO
var server := MapCoord.to_server_cm(centre)
return {
"x": server.x, "y": server.y, "z": centre.y * 100.0,
"up": 0.0, "view": 0.0, "cross": 0.0,
"distance": dist * 100.0,
"rot": rad_to_deg(yaw),
"pitch": rad_to_deg(pitch),
}
static func _setting_value(setting: Dictionary, key: String, fallback: float) -> float:
if not setting.has(key):
return fallback
return float(setting[key])
func _setting_world_center(setting: Dictionary) -> Vector3:
# SCameraSetting uses the same x/y ground plane and z-up convention as
# server positions. MapCoord is the single source of truth for the map
# origin and the Y/Z handedness conversion.
var sx := _setting_value(setting, "x", 0.0)
var sy := _setting_value(setting, "y", 0.0)
var sz := _setting_value(setting, "z", DEFAULT_HEIGHT_CM)
return MapCoord.to_world(Vector3(sx * 0.01, sz * 0.01, -sy * 0.01))
func _runtime_setting(setting: Dictionary) -> Dictionary:
var s := setting.duplicate(true)
s["_world_center"] = _setting_world_center(s)
s["distance"] = clampf(_setting_value(s, "distance", DEFAULT_DISTANCE_CM) * 0.01,
min_dist, max_dist)
s["pitch"] = clampf(deg_to_rad(_setting_value(s, "pitch", DEFAULT_PITCH_DEG)),
min_pitch, max_pitch)
s["rot"] = deg_to_rad(_setting_value(s, "rot", DEFAULT_ROTATION_DEG))
s["up"] = _setting_value(s, "up", 0.0) * 0.01
s["view"] = _setting_value(s, "view", 0.0) * 0.01
s["cross"] = _setting_value(s, "cross", 0.0) * 0.01
return s
func _apply_setting(setting: Dictionary) -> void:
var s := _runtime_setting(setting)
yaw = s["rot"]
pitch = s["pitch"]
dist = s["distance"]
_event_offset = Vector3(s["cross"], s["up"], s["view"])
func _setting_for_blend() -> Dictionary:
if camera_mode == CameraMode.NORMAL:
return _normal_setting()
return _event_setting.duplicate(true)
func _lerp_setting(a: Dictionary, b: Dictionary, t: float) -> Dictionary:
var out := {}
for key in ["x", "y", "z", "up", "view", "cross", "distance", "rot", "pitch"]:
out[key] = lerpf(_setting_value(a, key, 0.0), _setting_value(b, key, 0.0), t)
return out
# CPythonApplication::SetEventCamera. The first event preserves the player's
# current normal settings so RESTORE_CAMERA returns to the user's view.
func set_event_camera(setting: Dictionary) -> void:
if camera_mode == CameraMode.NORMAL:
_default_setting = _normal_setting()
_event_setting = setting.duplicate(true)
_event_center = _setting_world_center(_event_setting)
_apply_setting(_event_setting)
camera_mode = CameraMode.STAND
_pos_ready = false
# CPythonApplication::BlendEventCamera. `blendtime` follows the reference
# camera API and is expressed in seconds (the event parser passes it through).
func blend_event_camera(setting: Dictionary, blendtime: float) -> void:
_blend_from = _setting_for_blend()
_blend_to = setting.duplicate(true)
_blend_elapsed = 0.0
_blend_duration = maxf(0.0, blendtime)
_event_setting = _blend_from.duplicate(true)
_event_center = _setting_world_center(_blend_from)
camera_mode = CameraMode.BLEND
_event_offset = Vector3.ZERO
_event_center = Vector3.ZERO
if _blend_duration <= 0.0:
_event_setting = _blend_to.duplicate(true)
_event_center = _setting_world_center(_event_setting)
_apply_setting(_event_setting)
camera_mode = CameraMode.STAND
_pos_ready = false
func set_default_camera() -> void:
camera_mode = CameraMode.NORMAL
_blend_elapsed = 0.0
_blend_duration = 0.0
_event_offset = Vector3.ZERO
if not _default_setting.is_empty():
_apply_setting(_default_setting)
_default_setting = {}
_event_setting = {}
_pos_ready = false
func get_camera_setting() -> Dictionary:
var s := _setting_for_blend()
# During BLEND, expose the interpolated frame, matching GetCameraSetting's
# use by a subsequent camera event.
if camera_mode == CameraMode.BLEND:
var t := 1.0 if _blend_duration <= 0.0 else clampf(_blend_elapsed / _blend_duration, 0.0, 1.0)
s = _lerp_setting(_blend_from, _blend_to, t)
return s
func snap_to_target() -> void:
if target == null:
return
_pos_ready = false
if camera_mode == CameraMode.NORMAL:
global_position = _desired_pos(true)
else:
global_position = _desired_pos(true)
look_at(_look_target(), Vector3.UP)
var _shake := 0.0 # 当前抖动强度(米),指数衰减
var _shake_decay := 8.0
@@ -162,11 +323,20 @@ func shake(strength: float, decay := 8.0) -> void:
func _process(dt: float) -> void:
if target == null:
return
if _key_orbit != 0:
if camera_mode == CameraMode.BLEND:
_blend_elapsed += maxf(0.0, dt)
var t := 1.0 if _blend_duration <= 0.0 else clampf(_blend_elapsed / _blend_duration, 0.0, 1.0)
_event_setting = _lerp_setting(_blend_from, _blend_to, clampf(t, 0.0, 1.0))
_event_center = _setting_world_center(_event_setting)
_apply_setting(_event_setting)
if t >= 1.0:
camera_mode = CameraMode.STAND
_event_setting = _blend_to.duplicate(true)
if camera_mode == CameraMode.NORMAL and _key_orbit != 0:
yaw += float(_key_orbit) * dt * 1.8
if _key_zoom != 0:
if camera_mode == CameraMode.NORMAL and _key_zoom != 0:
dist = clampf(dist + float(_key_zoom) * dt * 5.0, min_dist, max_dist)
if _key_pitch != 0:
if camera_mode == CameraMode.NORMAL and _key_pitch != 0:
pitch = clampf(pitch + float(_key_pitch) * dt * 1.2, min_pitch, max_pitch)
var want := _desired_pos()
if _pos_ready:
@@ -179,8 +349,9 @@ func _process(dt: float) -> void:
_shake = lerpf(_shake, 0.0, clampf(_shake_decay * dt, 0.0, 1.0))
else:
_shake = 0.0
look_at(target.global_position + head_offset, Vector3.UP)
_fade_occluders()
look_at(_look_target(), Vector3.UP)
if camera_mode == CameraMode.NORMAL:
_fade_occluders()
# §8.2 相机与玩家之间的建筑半透明淡出。沿 cam->head 逐段射线,逐个 hit 淡出,
# 移过撞点继续;本帧没被挡到的恢复不透明。