From 96e21757c370c920bae175395e0bdbe63dc3c412 Mon Sep 17 00:00:00 2001 From: yvmou_ Date: Fri, 9 Oct 2026 15:51:24 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=BA=E7=89=A9=E6=8E=A7=E5=88=B6=E5=99=A81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .workbuddy/memory/2026-10-09.md | 45 +++++++++++++++++++++++++++++ character/player/player.gd | 5 +++- character/player/player.tscn | 12 +++++--- character/player/player_animator.gd | 19 ++++++++---- character/player/player_look.gd | 43 +++++++++++++++++++++++++-- character/player/player_motor.gd | 41 ++++++++++++++++++-------- 6 files changed, 140 insertions(+), 25 deletions(-) diff --git a/.workbuddy/memory/2026-10-09.md b/.workbuddy/memory/2026-10-09.md index 6a697bb..71967f7 100644 --- a/.workbuddy/memory/2026-10-09.md +++ b/.workbuddy/memory/2026-10-09.md @@ -54,3 +54,48 @@ - **修复 2(碰撞例外)**:`_grab()` 里 `_current.add_collision_exception_with(_body)`,`_drop()` / `_throw()` 经 `_forget()` 用 `remove_collision_exception_with` 解除。只解除「物体 ↔ 玩家」这一对,物体与场景其他碰撞体照常交互。 - 另加 `@export var max_drag_speed: float = 30.0` 速度上限,物体离得远时不会被一股脑拽飞。 - **接口变更**:`PlayerPickup.setup(ray, hold_pos, body)` 多了第三个参数(玩家本体),`player.gd` 里传 `self`。`_grab()` 加了 `is_instance_valid(_current)` 早退,避免重复抓取时漏掉碰撞例外。 + +### 玩家缩放层级(查明,供后续改尺寸参考) +- `character/player/player.tscn` 的缩放链:`Player ×0.2` → 子节点 Collision(胶囊 r0.545/h1.406,本地 y0.703) / CameraRig(眼高 y1.6) / Body/mouse(×0.1824) 累计 ×0.2。 +- `Camera ×5` **是补偿值**,用来把相机子树抵消回世界缩放 1.0(0.2×5=1.0),因此 InteractionRay 世界长度 0.5 m、InteractionPos 世界距离 0.444 m。 +- 推论:**改 `Player.scale` 必须同步 `Camera.scale = 1 / Player.scale`**,否则交互距离/持握距离会跟着缩。另外 PlayerMotor 的 walk/sprint/jump/accl 是世界单位,不会自动缩放,改尺寸时需手动同倍调整。 +- 世界尺寸现值:碰撞胶囊半径 0.109 / 高 0.281,眼高 0.32 m。 + +### 整体人物缩到 0.6 倍(已实施) +- `player.tscn`:`Player` 缩放 0.2 → **0.12**;`Camera` 缩放 5 → **8.333333**(= 1/0.12,保持相机子树净缩放 1.0,**交互距离/持握距离不变**)。Camera 的位置偏移与 CameraRig 的 y=1.6 都自动随父级缩放,未改。 +- `player_motor.gd`:walk 5.0→**3.0**、sprint 8.5→**5.1**、acceleration 45→**27**、air_acceleration 12→**7.2**,均为 ×0.6;jump_velocity 5.0→**3.87**(×√0.6,因跳跃高度 = v²/2g,直接乘 0.6 会跳得偏矮)。 +- `player_animator.gd`:**基准速度也必须 ×0.6** —— walk 5.0→3.0、run 8.5→5.1、crouch 2.5→1.5、sneak 1.6→0.96。原因:模型缩到 0.6 倍后,同一片段一个循环跨出的世界距离也缩到 0.6 倍,不改基准速度脚步会明显打滑。同时修正了 `_resolve_state()` 里那句「jump 片段 0.5s ≈ 5/9.8 上升时间」的过时注释。 +- 未改动:`pick_power` 10(新滞后 3.0/10 = 0.3 m < 0.444 m 持握距离,且已有速度前馈,安全)、`throw_speed` 12(被扔的物体本身没缩,投掷距离不该变)、`mouse_sensitivity`(纯角度)。 +- 缩放后世界尺寸:胶囊半径 0.0654 / 高 0.169,眼高 0.192 m;跳跃绝对高度 1.276 → 0.766 m(相对身高观感不变)。 + +### 再缩到「视觉高度 0.15 m」(已实施) +- **口径确认(重要)**:用户选的是**视觉模型高度**,不是碰撞胶囊。两者不等: + - 模型:glb 顶点原始高 `8.557729`(min y -4.7746 / max y 3.7832)× mouse 节点缩放 `0.18240134` = 本地 1.560941;世界 = ×0.12 = **0.187313 m**(当时) + - 胶囊:本地 1.4063476 × 0.12 = **0.1687617 m** + - 模型底部在本地 y ≈ -0.0007(脚踩原点,与胶囊底部对齐),故测量可信。模型比胶囊高 11%(多出耳朵/尾巴)。 +- 缩放系数 `k = 0.15 / 0.187313 = 0.800799`。 +- `player.tscn`:`Player.scale` 0.12 → **0.096096**;`Camera.scale` 8.333333 → **10.406274**(= 1/0.096096)。 +- `player_motor.gd`:walk 3.0→**2.4024**、sprint 5.1→**4.0841**、accel 27→**21.6216**、air_accel 7.2→**5.7658**、jump 3.87→**3.4632**(×√k)。 +- `player_animator.gd`:walk_ref 3.0→**2.4024**、run_ref 5.1→**4.0841**、crouch_ref 1.5→**1.2012**、sneak_ref 0.96→**0.7688**。 +- 注释已改成「按同一系数同步缩放」的通用表述,不再写死 0.6,避免下次再失效。 +- 结果:胶囊世界 半径 0.05241 / 高 0.13514,眼高 0.08729 m,跳跃绝对高度 ≈ 0.612 m;拖拽滞后 = 2.4024/10 = 0.24 m < 0.444 m 持握距离,安全。 +- **可复用公式**:`Player.scale = 目标世界高 / (模型本地总高)`,`Camera.scale = 1 / Player.scale`,速度与动画基准速度均 ×k(jump 用 ×√k)。 + +### 按 0.15 m 身高重新设计移动参数(已实施) +- 用户明确:「不用管之前,按 0.15 的身高重新设计」→ 采取**以人类为参照的等比设计**,不再依赖旧基准。 +- 设计基准:人 1.75 m,游戏惯例 walk 5.0 / sprint 8.5 / accel 45 / air_accel 12 / jump 5.0(跳跃高度 1.28 m ≈ 0.73 身高)。系数 `k = 0.15 / 1.75 ≈ 0.0857143`。 +- `player_motor.gd`:walk 2.4024→**0.4286**、sprint 4.0841→**0.7286**、accel 21.6216→**3.8571**、air_accel 5.7658→**1.0286**、jump 3.4632→**1.4639**(×√k)。 +- `player_animator.gd`:walk_ref 2.4024→**0.4286**、run_ref 4.0841→**0.7286**、crouch_ref 1.2012→**0.2143**、sneak_ref 0.7688→**0.1371**。 +- 校验:两边都是「走路 = 2.86 身长/秒」「跳跃 = 0.73 身高」,比例一致。加速度给出约 0.11 秒加到全速;跳跃高度 0.109 m。 +- 拖拽滞后 = 0.4286/10 = 0.043 m ≪ 0.444 m 持握距离,安全。 +- **未改动**(有意保持):交互距离/持握距离(射线 0.5 m、持握 0.444 m —— 若按 k 缩到 0.038 m 会够不到任何东西)、`pick_power` 10、`throw_speed` 12(被扔物体是世界尺寸,未缩)、`mouse_sensitivity`(纯角度)。 +- **世界尺度查不到可靠线索**:`world/main2.tscn` 有 295 个 `Transform3D`,但混入了嵌套实例的局部变换,位置跨度(X33/Z57)不可信;198 个实例缩放为 1.0,资产疑似米制。若实际关卡跨度大、用户觉得慢,把 walk/sprint 等线性量乘同一倍数即可。 + +### 蹲伏真正生效(已实施) +- 背景:**蹲下改速度、改摄像机高度这两件事从来没实现过**(原 `player.gd` 也只把 `crouching` 传给动画器切 CROUCH/SNEAK 片段),不是回归。 +- `player_motor.gd`:新增 `@export var crouch_speed: float = 0.2143`(= walk 的一半 = 2.5×k)。`step()` 里改为**蹲伏优先于疾跑**:`sprinting = wants_sprint and not crouching`,速度按 `crouch_speed > sprint_speed` 优先级取。 +- `player_look.gd`:新增蹲伏组 `crouch_height_ratio`(=0.6) / `crouch_transition_speed`(=10);`setup()` 从场景读 `_stand_height = rig.position.y`(不写死 1.6);新增 `set_crouching(bool)` 设目标高度;`_process()` 用 `1 - exp(-k·delta)` 指数收敛平滑升降(**渲染帧**,物理帧会一顿一顿);`_ready()` 里 `set_process(false)`,`setup()` 才开 —— 远端玩家不调 setup 就不会跑。 +- `player.gd`:`_physics_process` 里 `motor.step(delta)` 之后加 `look.set_crouching(motor.crouching)` —— **蹲伏状态由 Motor 单点判定**,不让 Look 自己读蹲键,避免两处各判一次不同步。 +- `player_animator.gd`:`sneak_ref_speed` 0.1371 → **0.2143**,与 `crouch_speed` 对齐 —— 蹲着走播的就是 SNEAK 片段,两边不等会打滑(原来会以 1.56× 播放,逼近 1.6 的 clamp 上限)。注释里写明了这条配对关系。 +- 世界高度:站立 `1.6 × 0.096096 = 0.1538 m` → 蹲下 `0.0923 m`。 +- **未做(可以后续补)**:蹲下时没有缩小碰撞胶囊 —— 所以现在还钻不过低矮缝隙。要做需要额外处理「头顶有障碍时不许起身」的检测。 diff --git a/character/player/player.gd b/character/player/player.gd index 2750310..12b5325 100644 --- a/character/player/player.gd +++ b/character/player/player.gd @@ -79,10 +79,13 @@ func _on_pickup_interact_state_changed(hovering: bool, holding: bool) -> void: func _physics_process(delta: float) -> void: - # 顺序固定:算速度 → 推进 → 播动画 → 搬物体。 + # 顺序固定:算速度 → 同步蹲伏高度 → 推进 → 播动画 → 搬物体。 # 由根节点统一编排,各组件自己不许写 _physics_process —— # 否则顺序取决于节点树顺序,改个节点位置就会出现「先滑再算速度」这类隐形 bug。 motor.step(delta) + # 蹲伏状态由 Motor 单点判定,再转交给 Look 去升降摄像机 —— + # 不让 Look 自己去读蹲键,免得两个组件各判一次、将来不同步。 + look.set_crouching(motor.crouching) move_and_slide() animation_player.set_motion(velocity, is_on_floor(), motor.sprinting, motor.crouching) pickup.physics_step() diff --git a/character/player/player.tscn b/character/player/player.tscn index a249790..87bbe11 100644 --- a/character/player/player.tscn +++ b/character/player/player.tscn @@ -4,9 +4,11 @@ [ext_resource type="Script" uid="uid://baid7qspg2mla" path="res://ui/crosshair.gd" id="2_vuhga"] [ext_resource type="PackedScene" uid="uid://css1vk3u8gt7o" path="res://character/player/assets/mouse.glb" id="2_wu8e1"] [ext_resource type="Script" uid="uid://roaqmma0vjon" path="res://character/player/player_animator.gd" id="3_g20gq"] +[ext_resource type="Texture2D" uid="uid://bkq6ya350cae8" path="res://character/assets/grab_hover.png" id="3_i6sc0"] [ext_resource type="SceneReplicationConfig" uid="uid://c4ubbxjfrrs47" path="res://character/player/player_sync.tres" id="3_wu8e1"] [ext_resource type="AnimationLibrary" uid="uid://stsads1ivf5g" path="res://character/player/assets/animations/crouch.glb" id="4_g20gq"] [ext_resource type="AnimationLibrary" uid="uid://ddnmtmswkjx6g" path="res://character/player/assets/animations/fall.glb" id="4_hr31g"] +[ext_resource type="Texture2D" uid="uid://c67qe66m2aft4" path="res://character/assets/grab.png" id="4_ywhvj"] [ext_resource type="AnimationLibrary" uid="uid://ddkl5cqdd5usx" path="res://character/player/assets/animations/idle.glb" id="5_lqp71"] [ext_resource type="AnimationLibrary" uid="uid://esw60hwb2cty" path="res://character/player/assets/animations/jump.glb" id="6_vuhga"] [ext_resource type="AnimationLibrary" uid="uid://rqhclfyiingo" path="res://character/player/assets/animations/run.glb" id="7_muf2e"] @@ -22,7 +24,7 @@ radius = 0.54541016 height = 1.4063476 [node name="Player" type="CharacterBody3D" unique_id=869687860] -transform = Transform3D(0.2, 0, 0, 0, 0.2, 0, 0, 0, 0.2, 0, 0, 0) +transform = Transform3D(0.096096, 0, 0, 0, 0.096096, 0, 0, 0, 0.096096, 0, 0, 0) script = ExtResource("1_gtsfv") [node name="Collision" type="CollisionShape3D" parent="." unique_id=215132582] @@ -33,14 +35,14 @@ shape = SubResource("CapsuleShape3D_rkbax") transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.6, 0) [node name="Camera" type="Camera3D" parent="CameraRig" unique_id=52294557] -transform = Transform3D(5, 0, 0, 0, 5, 0, 0, 0, 5, 0, -0.6916404, -0.11179829) +transform = Transform3D(10.406274, 0, 0, 0, 10.406274, 0, 0, 0, 10.406274, 0, -0.6916404, -0.11179829) [node name="InteractionRay" type="RayCast3D" parent="CameraRig/Camera" unique_id=388601766] transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0, 0, 0) -target_position = Vector3(0, 0.5, 0) +target_position = Vector3(0, 0.2, 0) [node name="InteractionPos" type="Marker3D" parent="CameraRig/Camera" unique_id=816323614] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -0.44393378) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -0.2425185) [node name="HUD" type="CanvasLayer" parent="CameraRig" unique_id=2008353247] @@ -55,6 +57,8 @@ size_flags_horizontal = 4 size_flags_vertical = 4 mouse_filter = 2 script = ExtResource("2_vuhga") +hover_texture = ExtResource("3_i6sc0") +hold_texture = ExtResource("4_ywhvj") [node name="Body" type="Node3D" parent="." unique_id=394283421] diff --git a/character/player/player_animator.gd b/character/player/player_animator.gd index f62b197..f94ee6e 100644 --- a/character/player/player_animator.gd +++ b/character/player/player_animator.gd @@ -46,10 +46,17 @@ const ANIM_LOOPED: Array[int] = [ @export_group("片段基准速度") ## 各片段在多少米/秒下按 1.0 倍播放。这是片段自身的属性,不是控制器的手感参数: ## 控制器把跑速调到 6 m/s 时,这段动画自然播成 1.2 倍,脚步自动跟上,两边不用对齐。 -@export var walk_ref_speed: float = 5.0 -@export var run_ref_speed: float = 8.5 -@export var crouch_ref_speed: float = 2.5 -@export var sneak_ref_speed: float = 1.6 +## +## 注意:这几个数和「模型缩放」严格绑定 —— 角色尺寸一变,同一个片段一个循环跨出的 +## 世界距离也按同比例变,所以基准速度必须跟着乘同一系数,否则脚步会明显打滑。 +## 当前值对应 0.15 m 身高(基准系数 k ≈ 0.0857,推导见 player_motor.gd 顶部的说明)。 +## +## sneak_ref_speed 要和 player_motor.gd 的 crouch_speed 保持一致 —— +## 蹲着走时播的就是 SNEAK 片段,两边不等就会打滑。(当前两者都是 0.2143) +@export var walk_ref_speed: float = 0.4286 +@export var run_ref_speed: float = 0.7286 +@export var crouch_ref_speed: float = 0.2143 +@export var sneak_ref_speed: float = 0.2143 ## 状态 → AnimationPlayer 里真正的片段键(形如 "walk/walk")。_ready() 里解析一次。 ## 不直接拿 ANIM_CLIPS 的值去播,是因为多库时短名查不到 —— 见 _ready() 里的说明。 @@ -138,8 +145,8 @@ func _resolve_state( velocity: Vector3, on_floor: bool, sprinting: bool, crouching: bool ) -> int: if not on_floor: - # 上升用 jump、越过顶点用 fall。jump 片段长 0.5s, - # 恰好约等于 jump_velocity 5 / 重力 9.8 的上升时间。 + # 上升用 jump、越过顶点用 fall。jump 片段长 0.5s,比「jump_velocity / 重力」 + # 的上升时间略长,所以上升段不会播完 —— 越过顶点立刻切 fall,观感上是连贯的。 return AnimState.JUMP if velocity.y > 0.0 else AnimState.FALL var moving := Vector2(velocity.x, velocity.z).length() >= idle_threshold if crouching: diff --git a/character/player/player_look.gd b/character/player/player_look.gd index 148496e..657fa2c 100644 --- a/character/player/player_look.gd +++ b/character/player/player_look.gd @@ -1,7 +1,7 @@ class_name PlayerLook extends Node -## 视角组件:把鼠标位移转成 CameraRig 的 yaw 与 Camera 的 pitch。 -## 只管转摄像机 —— 不碰速度、不碰动画。 +## 视角组件:把鼠标位移转成 CameraRig 的 yaw 与 Camera 的 pitch, +## 并按蹲伏状态平滑升降摄像机。只管摄像机 —— 不碰速度、不碰动画。 ## ## 接法:Player 下挂一个普通 Node,脚本挂上去;由 Player._ready() 调 setup() 注入依赖。 @@ -13,6 +13,12 @@ extends Node ## 最高仰角(向上看) @export var pitch_max_deg: float = 89.0 +@export_group("蹲伏") +## 蹲下时摄像机高度 = 站立高度 × 这个比例 +@export_range(0.3, 1.0, 0.05) var crouch_height_ratio: float = 0.6 +## 蹲起过渡速度。这是「每秒收敛掉的比例」,不是米/秒 —— 越大越干脆。 +@export var crouch_transition_speed: float = 10.0 + ## 眼睛分两层转:CameraRig 绕竖直轴转(yaw),Camera 原地抬头低头(pitch)。 ## 分开是为了让 pitch 不掺进移动方向 —— 移动只用 rig 的 basis,低头看地也不会走歪。 var _yaw: float = 0.0 @@ -22,6 +28,15 @@ var _rig: Node3D var _camera: Camera3D var _body: Node3D +## 站立时 CameraRig 的本地高度(从场景读,不写死),以及当前要收敛到的高度。 +var _stand_height: float = 0.0 +var _target_height: float = 0.0 + + +func _ready() -> void: + # 依赖是 setup() 注入的 —— 没接线(远端玩家不会调 setup)就不该跑 _process。 + set_process(false) + ## 由 Player 在 _ready() 注入依赖。组件不自己 get_node,才能脱离具体场景复用。 func setup(rig: Node3D, camera: Camera3D, body: Node3D) -> void: @@ -30,7 +45,11 @@ func setup(rig: Node3D, camera: Camera3D, body: Node3D) -> void: _body = body _yaw = rig.rotation.y _pitch = clampf(camera.rotation.x, deg_to_rad(pitch_min_deg), deg_to_rad(pitch_max_deg)) + # 站立高度直接读场景里的 CameraRig —— 以后在编辑器里把它挪高挪低,这里自动跟上。 + _stand_height = rig.position.y + _target_height = _stand_height _apply() + set_process(true) func _unhandled_input(event: InputEvent) -> void: @@ -44,6 +63,26 @@ func _unhandled_input(event: InputEvent) -> void: _apply() +## 由 Player 每物理帧调用,把「是否蹲着」这个事实转成目标高度。 +## 只在目标真的变了才写入,避免每帧无谓赋值。 +func set_crouching(crouching: bool) -> void: + if _rig == null: + return + var target: float = _stand_height * crouch_height_ratio if crouching else _stand_height + if is_equal_approx(target, _target_height): + return + _target_height = target + + +## 蹲起过渡放在渲染帧而不是物理帧 —— 物理帧只有 60Hz,看起来会一顿一顿。 +## 用指数收敛(1 - e^(-k·delta))而不是定量 lerp:过渡时间不受帧率影响。 +func _process(delta: float) -> void: + if _rig == null: + return + var t := 1.0 - exp(-crouch_transition_speed * delta) + _rig.position.y = lerpf(_rig.position.y, _target_height, t) + + ## 把 _yaw / _pitch 落到节点上。鼠标一动就调一次(渲染帧),所以视角跟手、不等物理帧。 ## 身体跟着一起转 —— 第一人称里「你看哪,身体就朝哪」,别人看你也是这个朝向。 func _apply() -> void: diff --git a/character/player/player_motor.gd b/character/player/player_motor.gd index c292c65..0641ae2 100644 --- a/character/player/player_motor.gd +++ b/character/player/player_motor.gd @@ -9,18 +9,29 @@ extends Node const JUMP_BUFFER_TIME: float = 0.1 @export_group("移动") -## 常规移动速度(米/秒) -@export var walk_speed: float = 5.0 -## 疾跑速度(米/秒) -@export var sprint_speed: float = 8.5 -## 地面加速度(米/秒²) -@export var acceleration: float = 45.0 +## 【设计基准】以人类为参照做等比缩放:人 1.75 m,游戏惯例 walk 5.0 / sprint 8.5 m/s。 +## 本角色身高 0.15 m,系数 k = 0.15 / 1.75 ≈ 0.0857,下面所有线性量都是原值 × k。 +## 这样「每秒走过几个自身体长」和人类一致,动作比例看起来才对。 +## +## 换尺寸时的公式:线性量(速度/加速度)× k',跳跃初速度 × √k'(因为跳跃高度 = v²/2g)。 +## +## 常规移动速度(米/秒)—— 约 2.9 身长/秒,和人类走路同一比例 +@export var walk_speed: float = 0.4286 +## 疾跑速度(米/秒)—— 约 4.9 身长/秒 +@export var sprint_speed: float = 0.7286 +## 蹲行速度(米/秒)—— 默认是走速的一半。蹲下时按住 Shift 也不会疾跑。 +## 注意:改这个值要同步改 player_animator.gd 的 sneak_ref_speed,否则蹲走时脚步会打滑。 +@export var crouch_speed: float = 0.2143 +## 地面加速度(米/秒²)—— 约 0.11 秒加到全速,起步干脆 +@export var acceleration: float = 3.8571 ## 空中加速度(米/秒²) -@export var air_acceleration: float = 12.0 -## 起跳初速度(米/秒) -@export var jump_velocity: float = 5.0 +@export var air_acceleration: float = 1.0286 +## 起跳初速度(米/秒)—— 跳跃高度 ≈ 0.109 m,约 0.73 身高 +## 注意是 ×√k 而不是 ×k:跳跃高度 = v² / 2g,想让高度也等比缩,速度就得开方。 +@export var jump_velocity: float = 1.4639 -## 本帧是否在疾跑 / 是否按住蹲键。Player 读这两个值转交给 PlayerAnimator。 +## 本帧是否在疾跑 / 是否按住蹲键。 +## Player 读这两个值转交给 PlayerAnimator 切片段,crouching 还用来压低摄像机。 var sprinting: bool = false var crouching: bool = false @@ -72,9 +83,15 @@ func step(delta: float) -> void: wish = wish.normalized() # 只有真正按了方向键才允许疾跑(避免原地"疾跑"触发跑动动画) - sprinting = Input.is_action_pressed("sprint") and input_dir.length_squared() > 0.000001 + var wants_sprint := Input.is_action_pressed("sprint") and input_dir.length_squared() > 0.000001 crouching = Input.is_action_pressed("crouch") - var target_speed := sprint_speed if sprinting else walk_speed + # 蹲伏优先于疾跑:蹲着时按住 Shift 不算疾跑,速度取蹲行速度。 + sprinting = wants_sprint and not crouching + var target_speed := walk_speed + if crouching: + target_speed = crouch_speed + elif sprinting: + target_speed = sprint_speed var accel := acceleration if _body.is_on_floor() else air_acceleration var horizontal := Vector2(_body.velocity.x, _body.velocity.z)