Das Submodul zeigte auf GitHub, der eingetragene Stand 028c652 lag aber nur lokal vor: GitHub-Master ac39682 plus Showcase mit Figuren und launch.json. Frische Klone konnten den Controller deshalb nicht laden. Jetzt liegt dieser Stand als normaler Ordner im Repo, ohne Verweis auf GitHub. dist/ und node_modules/ im Controller bleiben ignoriert und werden wie bisher lokal gebaut. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
591 lines
23 KiB
JavaScript
591 lines
23 KiB
JavaScript
import { AnimationMixer, LoopOnce, Object3D, Quaternion, Vector3 } from "three";
|
||
|
||
export const MODE = Object.freeze({ NORMAL: "normal", PRIMARY: "primary" });
|
||
|
||
const _muzzleWorldPos = new Vector3(); // 枪口世界坐标
|
||
|
||
export class WeaponController {
|
||
constructor({ scene, camera, localPlayer, decalSystem, effects, hud, zombieManager }) {
|
||
// ==================== 场景引用 ====================
|
||
this._scene = scene;
|
||
this._camera = camera;
|
||
this._player = localPlayer;
|
||
this._decalSystem = decalSystem;
|
||
this._effects = effects;
|
||
this._hud = hud;
|
||
this._zombieManager = zombieManager;
|
||
|
||
// ==================== 武器配置 ====================
|
||
this._gunScale = 0.1; // 枪模型缩放
|
||
this._gunPos = [1, 26.5, 2]; // 枪挂载到右手骨骼的位置偏移
|
||
this._gunBarrelDir = new Vector3(0, 0, -1); // 枪管沿模型局部 -Z
|
||
this._gunTargetDir = new Vector3(0, 1, 0); // 手骨 +Y = 手指方向
|
||
this._gunRoll = Math.PI / 2;
|
||
this._gunMuzzleOffset = [0, 80, -480]; // 枪口偏移 [左, 上, 前]
|
||
|
||
// ==================== 武器模型 ====================
|
||
this._weaponModel = null; // 枪模型根节点
|
||
this._muzzlePoint = null; // 枪口标记点(供特效定位)
|
||
this._weaponMixer = null; // 武器动画混合器
|
||
this._weaponReloadAction = null; // 换弹动画
|
||
this._weaponShootAction = null; // 射击动画
|
||
this._shotSound = null; // 射击音效
|
||
this._shakeIntensity = 0; // 当前镜头抖动强度
|
||
this._shakeDecay = 10; // 抖动衰减系数
|
||
|
||
// ==================== 模式 & 武器槽 ====================
|
||
this._currentMode = MODE.NORMAL;
|
||
this._weaponSlots = [
|
||
{ key: "1", mode: MODE.PRIMARY, label: "Rifle" },
|
||
{ key: "4", mode: MODE.NORMAL, label: "Fists" },
|
||
];
|
||
|
||
// ==================== 相机距离限制 ====================
|
||
this._normalMaxCam = 220; // 徒手模式最远距离
|
||
this._armedMaxCam = 100; // 瞄准模式最远距离
|
||
|
||
// ==================== 移速配置 ====================
|
||
this._baseSpeed = 300;
|
||
this._armedSpeed = 240; // 持枪移速 = baseSpeed * 0.8
|
||
|
||
// ==================== 开火状态机 ====================
|
||
this._isAiming = false; // 是否瞄准中(RMB 触发,含视角缩放)
|
||
this._isSoftAiming = false; // 是否软瞄准中(单击开火触发,不缩放视角)
|
||
this._isFiring = false; // 是否连射中
|
||
this._isTriggerDown = false; // 鼠标左键是否按住
|
||
this._isReloading = false; // 是否换弹中
|
||
this._magSize = 30; // 弹夹容量
|
||
this._currentAmmo = 30; // 当前子弹数
|
||
this._totalAmmo = 300; // 总备用弹药量
|
||
this._firstShotTimer = null; // 180ms 后必出一枪的计时器
|
||
this._holdAimTimer = null; // 停火后 2s 才放枪的计时器
|
||
this._lastFireTime = 0;
|
||
this._elapsed = 0; // 由 update() 每帧更新,供 setTimeout 回调读取
|
||
|
||
this._reloadTimer1 = null; // 换弹半程计时器
|
||
this._reloadTimer2 = null; // 换弹完成计时器
|
||
this._RELOAD_DURATION_MS = 2000; // 换弹动画时长,需根据实际模型动画调整
|
||
this._FIRE_RATE_S = 0.1; // 连射间隔(秒)
|
||
this._FIRE_ANIM_FADE_MS = 180; // 与 playAnimation 默认 fade(0.18s) 对齐
|
||
this._HOLD_AIM_DURATION = 2000; // 停火后保持瞄准状态时长(ms)
|
||
|
||
// ==================== 射线命中缓存 ====================
|
||
this._frameHit = null; // 每帧射线命中结果
|
||
|
||
// ==================== 多人扩展 ====================
|
||
this.onHitPlayer = null; // (playerId, damage) => void,由外部注入
|
||
}
|
||
|
||
// ==================== 初始化 ====================
|
||
|
||
// 加载武器模型并挂载到右手骨骼
|
||
async load(gltfLoader, baseUrl) {
|
||
const gltf = await gltfLoader.loadAsync(baseUrl + "./glb/ak47.glb");
|
||
this._weaponModel = gltf.scene;
|
||
|
||
const person = this._player.getPlayerModel();
|
||
const rightHand = person?.getObjectByName("mixamorigRightHand");
|
||
if (!rightHand) {
|
||
console.warn("[WeaponController] 未找到右手骨骼 mixamorigRightHand");
|
||
return;
|
||
}
|
||
|
||
this._weaponModel.scale.setScalar(this._gunScale);
|
||
this._weaponModel.position.set(...this._gunPos);
|
||
|
||
this._magazineBone = null;
|
||
this._weaponModel.traverse((child) => {
|
||
if (child.isMesh) {
|
||
child.castShadow = true;
|
||
child.receiveShadow = true;
|
||
child.frustumCulled = false;
|
||
}
|
||
});
|
||
|
||
// 找到备用弹夹骨骼,初始缩为 0 隐藏
|
||
this._magazineBone = this._weaponModel.getObjectByName("Bone002_01");
|
||
if (this._magazineBone) this._magazineBone.scale.setScalar(0.0001);
|
||
|
||
// 对齐枪管方向并附加滚转
|
||
const alignQ = new Quaternion().setFromUnitVectors(this._gunBarrelDir, this._gunTargetDir);
|
||
const rollQ = new Quaternion().setFromAxisAngle(this._gunTargetDir, this._gunRoll);
|
||
this._weaponModel.quaternion.copy(rollQ.multiply(alignQ));
|
||
|
||
this._weaponModel.visible = false;
|
||
rightHand.add(this._weaponModel);
|
||
|
||
// 枪口标记点(供特效定位)
|
||
this._muzzlePoint = new Object3D();
|
||
this._muzzlePoint.position.set(...this._gunMuzzleOffset);
|
||
this._weaponModel.add(this._muzzlePoint);
|
||
|
||
// 初始化武器动画
|
||
this._weaponMixer = new AnimationMixer(this._weaponModel);
|
||
const reloadClip = gltf.animations.find(a => a.name === "Armature.003|reload");
|
||
if (reloadClip) {
|
||
this._weaponReloadAction = this._weaponMixer.clipAction(reloadClip);
|
||
this._weaponReloadAction.setLoop(LoopOnce);
|
||
// 关键修改:取消锁定在最后一帧。
|
||
// 这样动画结束后,骨骼会自动通过 Mixer 的权重回归到 Bind Pose(初始插在枪上的位置)。
|
||
this._weaponReloadAction.clampWhenFinished = false;
|
||
this._weaponReloadAction.timeScale = 1.3;
|
||
}
|
||
|
||
}
|
||
|
||
// 注册所有射击相关动画集
|
||
setupAnimations() {
|
||
this._player.registerLocomotionSet("primary", {
|
||
idle: "rifle_idle",
|
||
walking: "rifle_walk",
|
||
running: "rifle_run",
|
||
jumping: "rifle_jump",
|
||
});
|
||
this._player.registerLocomotionSet("primary_aim", {
|
||
idle: "rifle_idle_aim3",
|
||
walking: "rifle_walk",
|
||
running: "rifle_run",
|
||
jumping: "rifle_jump",
|
||
});
|
||
|
||
// 上半身专用动画:仅覆写脊椎以上骨骼,下半身继续播放移动动画
|
||
this._player.registerUpperAnimation("upper_shoot", "rifle_shoot3", { loop: true, timeScale: 0.5 });
|
||
this._player.registerUpperAnimation("upper_reload", "reload", { loop: false, timeScale: 1.5 });
|
||
this._player.registerUpperAnimation("upper_aim", "rifle_idle_aim3", { loop: true, timeScale: 0.5 });
|
||
|
||
this._player.initIdleHipsQ("rifle_idle_aim3");
|
||
}
|
||
|
||
// 绑定键鼠输入
|
||
bindInput() {
|
||
document.addEventListener("mousedown", (e) => {
|
||
if (!this._canRunCombatLogic()) return;
|
||
if (e.button === 2 && !this._player.getIsFirstPerson()) {
|
||
this.enterAim();
|
||
}
|
||
if (e.button === 0) {
|
||
if (!this._firstShotTimer && !this._isFiring) this._triggerShootAnim();
|
||
this._startFiring();
|
||
}
|
||
});
|
||
|
||
document.addEventListener("mouseup", (e) => {
|
||
if (e.button === 2 && !this._player.getIsFirstPerson()) this.exitAim();
|
||
if (e.button === 0) this._stopFiring();
|
||
});
|
||
|
||
document.addEventListener("keydown", (e) => {
|
||
const k = e.key.toLowerCase();
|
||
if (k === "1") this.switchMode(MODE.PRIMARY);
|
||
if (k === "4") this.switchMode(MODE.NORMAL);
|
||
if (k === "q") this.switchMode(this._currentMode === MODE.PRIMARY ? MODE.NORMAL : MODE.PRIMARY);
|
||
if (k === "r") this.reload();
|
||
});
|
||
|
||
document.addEventListener("wheel", (e) => {
|
||
if (this._isReloading) return;
|
||
const idx = this._weaponSlots.findIndex((s) => s.mode === this._currentMode);
|
||
const next = (idx + (e.deltaY > 0 ? 1 : -1) + this._weaponSlots.length) % this._weaponSlots.length;
|
||
this.switchMode(this._weaponSlots[next].mode);
|
||
}, { passive: true });
|
||
}
|
||
|
||
// ==================== 主循环 ====================
|
||
|
||
// 每帧由 shooting.js 主循环调用
|
||
update(elapsed, dt) {
|
||
this._elapsed = elapsed;
|
||
const canRunCombatLogic = this._canRunCombatLogic();
|
||
|
||
// 飞行时强制取消换弹
|
||
if (this._currentMode === MODE.PRIMARY && this._isReloading && this._player.getIsFlying()) {
|
||
this._cancelReload();
|
||
}
|
||
// 飞行/跳跃等不可战斗状态时强制中断武器逻辑(换弹中不打断,由换弹自己管)
|
||
if (this._currentMode === MODE.PRIMARY && !canRunCombatLogic && !this._isReloading) {
|
||
this._forceStopCombatLogic();
|
||
}
|
||
|
||
// 第一人称:静止时自动进入瞄准,移动时退出
|
||
if (this._currentMode === MODE.PRIMARY && this._player.getIsFirstPerson()) {
|
||
if (canRunCombatLogic && !this._isAiming) this.enterAim();
|
||
else if (!canRunCombatLogic && this._isAiming) this.exitAim();
|
||
}
|
||
|
||
// 更新粒子特效
|
||
this._effects?.update(dt);
|
||
|
||
// 更新武器动画混合器
|
||
if (this._weaponMixer) this._weaponMixer.update(dt);
|
||
|
||
// 非换弹时每帧强制隐藏备用弹夹骨骼(mixer 更新后覆写)
|
||
if (this._magazineBone && !this._isReloading) this._magazineBone.scale.setScalar(0.0001);
|
||
|
||
// 镜头抖动(指数衰减)
|
||
if (this._shakeIntensity > 0.0001) {
|
||
this._camera.rotation.x += (Math.random() - 0.5) * this._shakeIntensity;
|
||
this._camera.rotation.y += (Math.random() - 0.5) * this._shakeIntensity * 0.4;
|
||
this._shakeIntensity *= Math.exp(-this._shakeDecay * dt);
|
||
} else {
|
||
this._shakeIntensity = 0;
|
||
}
|
||
|
||
// 更新射线命中缓存
|
||
this._frameHit = this._player.getCenterScreenRaycastHit();
|
||
|
||
// 连射节拍
|
||
if (canRunCombatLogic && this._isFiring && elapsed - this._lastFireTime >= this._FIRE_RATE_S) {
|
||
if (this._currentAmmo <= 0) {
|
||
this._stopFiring();
|
||
// 只有总弹药大于0时才触发自动换弹
|
||
if (this._totalAmmo > 0) this.reload();
|
||
} else {
|
||
this._lastFireTime = elapsed;
|
||
this._fireOnce();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ==================== 状态查询 ====================
|
||
|
||
// 是否处于任意武器激活状态(等待首发 / 连射 / hold-aim 冷却 / 瞄准)
|
||
// SpineIK 和主循环通过此方法判断是否需要驱动 IK
|
||
isGunEngaged() {
|
||
return (
|
||
this._isAiming ||
|
||
this._isSoftAiming ||
|
||
this._isFiring ||
|
||
this._isReloading ||
|
||
this._firstShotTimer !== null ||
|
||
this._holdAimTimer !== null
|
||
);
|
||
}
|
||
|
||
getMode() { return this._currentMode; }
|
||
|
||
resetAmmo() {
|
||
this._currentAmmo = this._magSize;
|
||
this._totalAmmo = 300;
|
||
this._hud.updateAmmo?.(this._currentAmmo, this._totalAmmo);
|
||
}
|
||
|
||
_canRunCombatLogic() {
|
||
if (this._currentMode !== MODE.PRIMARY) return false;
|
||
if (this._isReloading) return false;
|
||
if (this._player.getIsFlying()) return false;
|
||
if (typeof this._player.isCombatLocomotionAllowed === "function") {
|
||
return this._player.isCombatLocomotionAllowed();
|
||
}
|
||
return true;
|
||
}
|
||
|
||
_forceStopCombatLogic() {
|
||
this._isTriggerDown = false;
|
||
if (this._firstShotTimer) {
|
||
clearTimeout(this._firstShotTimer);
|
||
this._firstShotTimer = null;
|
||
}
|
||
this._isFiring = false;
|
||
if (this._weaponShootAction) this._weaponShootAction.stop();
|
||
this._player.stopUpperBody(0.18);
|
||
this._cancelHoldAimTimer();
|
||
if (this._isAiming) this.exitAim();
|
||
else {
|
||
this._isSoftAiming = false;
|
||
this._hud.hideCrosshair();
|
||
}
|
||
}
|
||
|
||
// ==================== 模式切换 & 瞄准 ====================
|
||
|
||
switchMode(newMode) {
|
||
if (this._currentMode === newMode || this._isReloading) return;
|
||
|
||
// 退出旧模式
|
||
if (this._currentMode === MODE.PRIMARY) {
|
||
this.exitAim();
|
||
this._isSoftAiming = false;
|
||
this._isTriggerDown = false;
|
||
this._isFiring = false;
|
||
if (this._weaponShootAction) this._weaponShootAction.stop();
|
||
this._player.stopUpperBody(0.18);
|
||
if (this._firstShotTimer) { clearTimeout(this._firstShotTimer); this._firstShotTimer = null; }
|
||
this._cancelHoldAimTimer();
|
||
this._hud.hideCrosshair();
|
||
this._hud.hideAmmo?.();
|
||
this._player.switchLocomotionSet("default");
|
||
this._player.setMaxCamDistance(this._normalMaxCam);
|
||
this._player.setPlayerSpeed(this._baseSpeed);
|
||
}
|
||
|
||
this._currentMode = newMode;
|
||
|
||
// 进入新模式
|
||
if (this._currentMode === MODE.PRIMARY) {
|
||
this._player.switchLocomotionSet("primary");
|
||
this._player.setPlayerSpeed(this._armedSpeed);
|
||
this._hud.updateAmmo?.(this._currentAmmo, this._totalAmmo);
|
||
this._hud.showAmmo?.();
|
||
}
|
||
|
||
if (this._weaponModel) this._weaponModel.visible = (this._currentMode === MODE.PRIMARY);
|
||
}
|
||
|
||
// 进入瞄准模式;soft=true 时为软瞄准,跳过视角缩放
|
||
enterAim(soft = false) {
|
||
if (soft) {
|
||
if (this._isSoftAiming) return;
|
||
this._isSoftAiming = true;
|
||
} else {
|
||
if (!this._canRunCombatLogic() || this._isAiming) return;
|
||
this._isAiming = true;
|
||
this._isSoftAiming = false;
|
||
this._player.setMaxCamDistance(this._armedMaxCam);
|
||
}
|
||
this._player.switchLocomotionSet("primary_aim");
|
||
this._hud.showCrosshair();
|
||
this._player.playUpperBody("upper_aim", { fade: 0.18 });
|
||
if (this._player.getIsFirstPerson()) {
|
||
this._camera.rotation.x = this._player.getFirstPersonPitchOffset();
|
||
}
|
||
}
|
||
|
||
// 退出瞄准模式;soft=true 时为软瞄准,跳过视角还原
|
||
exitAim(soft = false) {
|
||
if (soft) {
|
||
if (!this._isSoftAiming) return;
|
||
this._isSoftAiming = false;
|
||
} else {
|
||
if (!this._isAiming) return;
|
||
this._isAiming = false;
|
||
this._player.setMaxCamDistance(this._normalMaxCam);
|
||
this._cancelHoldAimTimer();
|
||
}
|
||
this._player.switchLocomotionSet(
|
||
this._currentMode === MODE.PRIMARY ? "primary" : "default"
|
||
);
|
||
this._player.stopUpperBody(0.18);
|
||
this._hud.hideCrosshair();
|
||
if (this._player.getIsFirstPerson()) {
|
||
this._camera.rotation.x = this._player.pitchTarget1P;
|
||
}
|
||
}
|
||
|
||
// 换弹
|
||
reload() {
|
||
if (this._currentMode !== MODE.PRIMARY || this._isReloading || this._totalAmmo <= 0 || this._currentAmmo === this._magSize) return;
|
||
if (this._player.getIsFlying()) return;
|
||
|
||
this._isReloading = true;
|
||
|
||
// 1. 中断当前所有攻击状态
|
||
this._isTriggerDown = false;
|
||
this._isFiring = false;
|
||
if (this._weaponShootAction) this._weaponShootAction.stop();
|
||
if (this._firstShotTimer) { clearTimeout(this._firstShotTimer); this._firstShotTimer = null; }
|
||
this._cancelHoldAimTimer();
|
||
this.exitAim(); // 换弹时强制退出瞄准
|
||
|
||
// 2. 播放换弹动画(仅上半身,下半身继续播放移动动画)
|
||
this._player.playUpperBody("upper_reload", { force: true, fade: 0.18 });
|
||
|
||
// 换弹音效
|
||
this._effects?.triggerReloadSound();
|
||
|
||
// 显示备用弹夹骨骼
|
||
if (this._magazineBone) this._magazineBone.scale.setScalar(1);
|
||
|
||
// 播放武器自身的换弹动画
|
||
if (this._weaponReloadAction) {
|
||
this._weaponReloadAction.reset();
|
||
this._weaponReloadAction.play();
|
||
}
|
||
|
||
// 动画播放到一半时停止,同时隐藏备用弹夹骨骼(避免 snap-back 到悬空位置时可见)
|
||
this._reloadTimer1 = setTimeout(() => {
|
||
this._reloadTimer1 = null;
|
||
if (this._isReloading && this._weaponReloadAction) {
|
||
this._weaponReloadAction.stop();
|
||
}
|
||
if (this._magazineBone) this._magazineBone.scale.setScalar(0.0001);
|
||
}, this._RELOAD_DURATION_MS / 2);
|
||
|
||
// 3. 锁定状态直到动画结束
|
||
this._reloadTimer2 = setTimeout(() => {
|
||
this._reloadTimer2 = null;
|
||
// 此时 _isReloading 变回 false,允许开火
|
||
this._isReloading = false;
|
||
|
||
// 停止上半身换弹动画,让全身 locomotion 动画重新完整接管上半身
|
||
this._player.stopUpperBody(0.18);
|
||
|
||
// 确保动画彻底停止(以防上面的一半时间停止没触发成功)
|
||
if (this._weaponReloadAction) this._weaponReloadAction.stop();
|
||
|
||
// 计算需要补充的子弹数量
|
||
const need = this._magSize - this._currentAmmo;
|
||
const transfer = Math.min(need, this._totalAmmo);
|
||
this._currentAmmo += transfer;
|
||
this._totalAmmo -= transfer;
|
||
this._hud.updateAmmo?.(this._currentAmmo, this._totalAmmo);
|
||
|
||
// 换弹结束后,衔接回持枪状态
|
||
if (this._currentMode === MODE.PRIMARY) {
|
||
this._player.switchLocomotionSet("primary_aim");
|
||
if (this._isAiming || this._isSoftAiming || !this._player.isMoving) {
|
||
this._player.playUpperBody("upper_aim", { fade: 0.18 });
|
||
this._scheduleHoldAim();
|
||
} else {
|
||
this._player.stopUpperBody(0.18);
|
||
}
|
||
}
|
||
}, this._RELOAD_DURATION_MS);
|
||
}
|
||
|
||
// ==================== 开火状态机 ====================
|
||
|
||
_startFiring() {
|
||
if (!this._canRunCombatLogic()) return;
|
||
if (this._firstShotTimer || this._isFiring) return;
|
||
|
||
// 没子弹时按左键,直接触发换弹
|
||
if (this._currentAmmo <= 0) {
|
||
if (this._totalAmmo > 0) this.reload();
|
||
return;
|
||
}
|
||
|
||
// 切换到软瞄准状态
|
||
if (!this._isAiming && !this._isSoftAiming) {
|
||
this.enterAim(true);
|
||
}
|
||
|
||
this._cancelHoldAimTimer();
|
||
this._isTriggerDown = true;
|
||
|
||
// 无论鼠标是否松开,180ms 后必定打出第一枪
|
||
this._firstShotTimer = setTimeout(() => {
|
||
this._firstShotTimer = null;
|
||
if (!this._canRunCombatLogic()) {
|
||
this._forceStopCombatLogic();
|
||
return;
|
||
}
|
||
this._fireOnce();
|
||
this._lastFireTime = this._elapsed;
|
||
|
||
if (this._isTriggerDown) {
|
||
this._isFiring = true; // 持续按住 → 连射
|
||
} else {
|
||
// 单击松开:打出第一枪后处理后续状态
|
||
if (this._weaponShootAction) this._weaponShootAction.stop();
|
||
if (this._isAiming || this._isSoftAiming || !this._player.isMoving) {
|
||
this._scheduleHoldAim();
|
||
} else {
|
||
this._player.stopUpperBody(0.18);
|
||
}
|
||
}
|
||
}, this._FIRE_ANIM_FADE_MS);
|
||
}
|
||
|
||
_stopFiring() {
|
||
this._isTriggerDown = false;
|
||
if (this._weaponShootAction) this._weaponShootAction.stop();
|
||
|
||
if (!this._isFiring) {
|
||
// 换弹中或 hold-aim 缓冲期内松开鼠标,不打断上半身动画
|
||
if (this._isReloading || this._holdAimTimer) return;
|
||
|
||
// 单发(鼠标在 180ms 定时器触发前就松开):恢复或停止上半身
|
||
if (this._isAiming || this._isSoftAiming) {
|
||
this._player.playUpperBody("upper_aim", { fade: 0.18 });
|
||
} else {
|
||
this._player.stopUpperBody(0.18);
|
||
}
|
||
return;
|
||
}
|
||
|
||
this._isFiring = false;
|
||
if (this._isAiming || this._isSoftAiming || !this._player.isMoving) {
|
||
this._player.playUpperBody("upper_aim", { fade: 0.18 });
|
||
this._scheduleHoldAim();
|
||
} else {
|
||
this._player.stopUpperBody(0.18);
|
||
}
|
||
}
|
||
|
||
_fireOnce() {
|
||
if (!this._canRunCombatLogic()) return;
|
||
|
||
// 播放音效
|
||
if (this._shotSound && this._shotSound.buffer) {
|
||
if (this._shotSound.isPlaying) this._shotSound.stop();
|
||
this._shotSound.play();
|
||
}
|
||
|
||
// 第三人称触发镜头抖动
|
||
if (!this._player.getIsFirstPerson()) {
|
||
this._shakeIntensity = 0.01; // 弧度单位,0.02 约为 1.1 度
|
||
}
|
||
|
||
this._currentAmmo--;
|
||
this._hud.updateAmmo?.(this._currentAmmo, this._totalAmmo);
|
||
|
||
// 枪口火焰 + 音效:无论命中什么都必须先触发
|
||
if (this._effects && this._muzzlePoint) {
|
||
this._muzzlePoint.updateWorldMatrix(true, false);
|
||
this._muzzlePoint.getWorldPosition(_muzzleWorldPos);
|
||
this._effects.triggerMuzzleFlash(_muzzleWorldPos, this._camera);
|
||
}
|
||
|
||
// 多人模式:检测是否命中远程玩家(dmgMult 由命中部位决定:头×2,躯干×1,四肢×0.75)
|
||
const hitPlayerId = this._frameHit?.object?.userData?.playerId;
|
||
if (hitPlayerId && this.onHitPlayer) {
|
||
const dmgMult = this._frameHit.object.userData.dmgMult ?? 1.0;
|
||
this.onHitPlayer(hitPlayerId, Math.round(30 * dmgMult));
|
||
this._hud.flashHit();
|
||
return;
|
||
}
|
||
|
||
const zombieId = this._frameHit?.object?.userData?.zombieId;
|
||
if (zombieId && this._zombieManager) {
|
||
this._zombieManager.onHit(zombieId, 30);
|
||
this._hud.flashHit();
|
||
} else {
|
||
this._decalSystem.spawn(this._frameHit, this._effects);
|
||
}
|
||
}
|
||
|
||
_triggerShootAnim() {
|
||
if (!this._canRunCombatLogic()) return;
|
||
// 上半身播放开火动画,下半身继续播放当前移动动画
|
||
this._player.playUpperBody("upper_shoot", { force: true, fade: 0.18 });
|
||
if (this._weaponShootAction) this._weaponShootAction.play();
|
||
}
|
||
|
||
// 停火后延迟 2s 自动退出瞄准姿态
|
||
_scheduleHoldAim() {
|
||
this._cancelHoldAimTimer();
|
||
this._holdAimTimer = setTimeout(() => {
|
||
this._holdAimTimer = null;
|
||
if (!this._isAiming) {
|
||
this.exitAim(true);
|
||
}
|
||
}, this._HOLD_AIM_DURATION);
|
||
}
|
||
|
||
_cancelReload() {
|
||
if (!this._isReloading) return;
|
||
this._isReloading = false;
|
||
if (this._reloadTimer1) { clearTimeout(this._reloadTimer1); this._reloadTimer1 = null; }
|
||
if (this._reloadTimer2) { clearTimeout(this._reloadTimer2); this._reloadTimer2 = null; }
|
||
if (this._weaponReloadAction) this._weaponReloadAction.stop();
|
||
if (this._magazineBone) this._magazineBone.scale.setScalar(0.0001);
|
||
this._effects?.stopReloadSound();
|
||
this._player.stopUpperBody(0.18);
|
||
this._hud.hideCrosshair();
|
||
}
|
||
|
||
_cancelHoldAimTimer() {
|
||
if (this._holdAimTimer) { clearTimeout(this._holdAimTimer); this._holdAimTimer = null; }
|
||
}
|
||
}
|