Spaces:
Sleeping
Sleeping
| /** | |
| * @license | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { Vector3, Quaternion, Euler, MathUtils } from "three"; | |
| import { Pilot, FlightCommand, AircraftSpecs } from "../types"; | |
| import { physical, locomotive, destructible, control } from "../types/components"; | |
| import { AerodynamicsEngine } from "./aeroSurfaceModel"; | |
| import { getTerrainHeight } from "./terrainModel"; | |
| import { MAP_REGISTRY } from "./content/maps/registry"; | |
| import { KnownMaps } from "./content/maps/mapTypes"; | |
| import { MODIFICATIONS } from "./content/modifications/modificationData"; | |
| import { | |
| LOCAL_FORWARD, | |
| LOCAL_UP, | |
| LOCAL_RIGHT, | |
| safeNormalize, | |
| airDensityAtAltitude | |
| } from "./math"; | |
| const G = 9.81; | |
| // AXIS CONTRACT | |
| function getAircraftQuaternion(qx: number, qy: number, qz: number, qw: number) { | |
| return new Quaternion(qx, qy, qz, qw); | |
| } | |
| function getAircraftBasis(qx: number, qy: number, qz: number, qw: number) { | |
| const q = getAircraftQuaternion(qx, qy, qz, qw); | |
| return { | |
| q, | |
| forward: LOCAL_FORWARD.clone().applyQuaternion(q).normalize(), | |
| up: LOCAL_UP.clone().applyQuaternion(q).normalize(), | |
| right: LOCAL_RIGHT.clone().applyQuaternion(q).normalize() | |
| }; | |
| } | |
| function approach(current: number, target: number, rate: number, dt: number): number { | |
| if (current < target) return Math.min(current + rate * dt, target); | |
| if (current > target) return Math.max(current - rate * dt, target); | |
| return current; | |
| } | |
| function getAoA(localVelocity: Vector3) { | |
| // Aircraft local +Z is nose-forward. | |
| // Positive AoA means nose above flight path. | |
| return Math.atan2(-localVelocity.y, localVelocity.z); | |
| } | |
| function requireSpecInRange( | |
| specs: AircraftSpecs, | |
| keyName: "wingArea" | "aspectRatio" | "oswaldEfficiency", | |
| min: number, | |
| max: number | |
| ): number { | |
| const value = specs[keyName]; | |
| if (typeof value !== "number" || !Number.isFinite(value)) { | |
| throw new Error( | |
| `Aircraft spec "${specs.id}" is missing required aerodynamic field "${keyName}". ` + | |
| `Add a finite number for ${keyName} in aircraftData.ts.` | |
| ); | |
| } | |
| if (value <= 0 || value < min || value > max) { | |
| throw new Error( | |
| `Aircraft spec "${specs.id}" has invalid aerodynamic field "${keyName}" = ${value}. ` + | |
| `Expected ${keyName} between ${min} and ${max}.` | |
| ); | |
| } | |
| return value; | |
| } | |
| export function applyModifications( | |
| specs: AircraftSpecs, | |
| equippedIds: string[] | |
| ): AircraftSpecs { | |
| const modified = { ...specs }; | |
| equippedIds.forEach(id => { | |
| const mod = MODIFICATIONS.find(m => m.id === id); | |
| if (!mod) return; | |
| const eff = mod.effects; | |
| if (eff.maxThrust !== undefined) { | |
| modified.maxThrust *= (1 + eff.maxThrust); | |
| } | |
| if (eff.mass !== undefined) { | |
| modified.mass *= (1 + eff.mass); | |
| } | |
| if (eff.cd0 !== undefined) { | |
| modified.cd0 = Math.max(0.001, modified.cd0 + eff.cd0); | |
| } | |
| if (eff.durability !== undefined) { | |
| modified.durability *= (1 + eff.durability); | |
| } | |
| if (eff.rollRate !== undefined) { | |
| modified.rollRateDegPerSec = (modified.rollRateDegPerSec ?? 90) * (1 + eff.rollRate); | |
| } | |
| }); | |
| return modified; | |
| } | |
| /** | |
| * Updates aircraft flight dynamics using a custom aerodynamic force model. | |
| * | |
| * Aircraft local axes: | |
| * +Z = nose / forward | |
| * +Y = up | |
| * +X = right wing | |
| */ | |
| export function updateFlightPhysics( | |
| pilot: Pilot, | |
| command: FlightCommand, | |
| dt: number, | |
| mapId: string = KnownMaps.IslandChain | |
| ) { | |
| if (dt <= 0) return; | |
| dt = Math.min(dt, 0.05); | |
| // Extract components once to avoid repeated Map lookups on each property access | |
| const phys = physical(pilot.entity); | |
| const loco = locomotive(pilot.entity); | |
| const destr = destructible(pilot.entity); | |
| const ctrl = control(pilot.entity); | |
| const dm = destr.damageModel!; | |
| const specs = applyModifications(pilot.specs, pilot.modifications); | |
| requireSpecInRange(specs, "wingArea", 8, 70); | |
| requireSpecInRange(specs, "aspectRatio", 3, 12); | |
| requireSpecInRange(specs, "oswaldEfficiency", 0.55, 0.95); | |
| const pos = new Vector3(phys.x, phys.y, phys.z); | |
| const vel = new Vector3(phys.vx, phys.vy, phys.vz); | |
| let speed = vel.length(); | |
| let speedKmph = speed * 3.6; | |
| let { q, forward } = getAircraftBasis(phys.qx, phys.qy, phys.qz, phys.qw); | |
| let pitchInput = command.pitch; | |
| let rollInput = command.roll; | |
| let yawInput = command.yaw; | |
| const throttleInput = command.throttleDelta; | |
| const boost = command.boost; | |
| const airbrake = command.airbrake; | |
| loco.throttle = MathUtils.clamp( | |
| loco.throttle + throttleInput * dt * 0.65, | |
| 0.0, | |
| boost ? 1.1 : 1.0 | |
| ); | |
| if (boost) { | |
| loco.throttle = Math.min(1.1, loco.throttle + dt * 0.35); | |
| } | |
| const targetTemp = 50 + loco.throttle * 70; | |
| loco.engineTemperature = (loco.engineTemperature ?? 75) + | |
| (targetTemp - (loco.engineTemperature ?? 75)) * dt * 0.05; | |
| const engineHealth = dm.engine; | |
| const leftWingHealth = dm.leftWing; | |
| const rightWingHealth = dm.rightWing; | |
| const tailHealth = dm.tail; | |
| const cockpitHealth = dm.cockpit; | |
| const wingHealth = (leftWingHealth + rightWingHealth) / 2; | |
| const controlFactor = 0.3 + 0.7 * cockpitHealth; | |
| // Append roll asymmetry from wing damage | |
| rollInput -= (rightWingHealth - leftWingHealth) * 0.3; | |
| loco.airbrakeDeployed = command.airbrake; | |
| loco.flaps = command.flaps; | |
| loco.gearDeployed = command.gearDeployed; | |
| // Physical separation: Raw input -> Pilot Intent -> Actuator Deflection rate limit | |
| ctrl.pitchIntent = approach(ctrl.pitchIntent ?? 0, pitchInput, 3.2, dt); | |
| ctrl.rollIntent = approach(ctrl.rollIntent ?? 0, rollInput, 4.2, dt); | |
| ctrl.yawIntent = approach(ctrl.yawIntent ?? 0, yawInput, 2.2, dt); | |
| // Surfaces actuator physical lag | |
| ctrl.elevatorDeflection = approach(ctrl.elevatorDeflection ?? 0, ctrl.pitchIntent, 4.5, dt); | |
| ctrl.aileronDeflection = approach(ctrl.aileronDeflection ?? 0, ctrl.rollIntent, 5.5, dt); | |
| ctrl.rudderDeflection = approach(ctrl.rudderDeflection ?? 0, ctrl.yawIntent, 3.8, dt); | |
| // Apply continuous smoothed surface deflections downstream | |
| pitchInput = ctrl.elevatorDeflection; | |
| rollInput = ctrl.aileronDeflection; | |
| yawInput = ctrl.rudderDeflection; | |
| const currentPitchRate = | |
| (specs.pitchRateDegPerSec ?? 45) * (0.3 + 0.7 * tailHealth) * controlFactor; | |
| const currentRollRate = | |
| (specs.rollRateDegPerSec ?? 90) * (0.4 + 0.6 * wingHealth) * controlFactor; | |
| const currentYawRate = | |
| (specs.yawRateDegPerSec ?? 30) * (0.3 + 0.7 * tailHealth) * controlFactor; | |
| // Active Angular Velocity matching pilot model (X = Pitch, Y = Yaw, Z = Roll) | |
| const localAngularVelocity = new Vector3( | |
| phys.avx ?? 0, // pitch | |
| phys.avy ?? 0, // yaw | |
| phys.avz ?? 0 // roll | |
| ); | |
| const terrainInfo = getTerrainHeight(pos.x, pos.z, mapId); | |
| const altitudeAGL = Math.max(0, pos.y - terrainInfo.height); | |
| // Invoke high-fidelity aero surface calculations | |
| const aero = AerodynamicsEngine.computeForces({ | |
| pilot, | |
| specs, | |
| controls: { | |
| pitchInput, | |
| rollInput, | |
| yawInput, | |
| airbrake: !!airbrake | |
| }, | |
| localAngularVelocity, | |
| altitudeAGL | |
| }); | |
| // Calculate pre-rotation airspeed and angles to determine stall conditions accurately | |
| const wind = new Vector3(0, 0, 0); | |
| const airVelocityWorld = vel.clone().sub(wind); | |
| const airspeed = airVelocityWorld.length(); | |
| const airspeedKmph = airspeed * 3.6; | |
| const initialInvQ = q.clone().invert(); | |
| const initialLocalVelocity = airVelocityWorld.clone().applyQuaternion(initialInvQ); | |
| const initialAlpha = getAoA(initialLocalVelocity); | |
| const initialAlphaDeg = Math.abs(MathUtils.radToDeg(initialAlpha)); | |
| const isStallingByAoA = initialAlphaDeg > 17.5; | |
| const isSupportedByGround = altitudeAGL <= 1.5; | |
| const isCurrentlyStalled = !isSupportedByGround && (isStallingByAoA || (airspeedKmph < specs.stallSpeedKmph)); | |
| // 1. Aerodynamic and structural restoring moments accelerate local angular rates | |
| const inertiaVal = AerodynamicsEngine.estimateInertia(specs); | |
| localAngularVelocity.x += (aero.torque.x / Math.max(1, inertiaVal.x)) * dt; // Pitch rate (around X) | |
| localAngularVelocity.y += (aero.torque.y / Math.max(1, inertiaVal.y)) * dt; // Yaw rate (around Y) | |
| localAngularVelocity.z += (aero.torque.z / Math.max(1, inertiaVal.z)) * dt; // Roll rate (around Z) | |
| const maxRateLimit = 6.0; | |
| localAngularVelocity.x = MathUtils.clamp(localAngularVelocity.x, -maxRateLimit, maxRateLimit); | |
| localAngularVelocity.y = MathUtils.clamp(localAngularVelocity.y, -maxRateLimit, maxRateLimit); | |
| localAngularVelocity.z = MathUtils.clamp(localAngularVelocity.z, -maxRateLimit, maxRateLimit); | |
| // 2. Control authority rolls off realistically at low speeds to simulate lack of over-wing flow | |
| let controlAuthority = MathUtils.clamp((airspeedKmph / specs.stallSpeedKmph) * 1.12, 0.04, 1.25); | |
| if (isCurrentlyStalled) { | |
| // Reduce control authority significantly when stalled to prevent pilot/bot from overriding the nose-drop | |
| controlAuthority *= 0.22; | |
| } | |
| const directPitchRate = -pitchInput * (currentPitchRate * Math.PI / 180) * controlAuthority; | |
| const directYawRate = -yawInput * (currentYawRate * Math.PI / 180) * controlAuthority; | |
| const directRollRate = rollInput * (currentRollRate * Math.PI / 180) * controlAuthority; | |
| // Blending direct arcade assist (35%) and full rigid-body torque integration (65%) | |
| const finalPitchRate = MathUtils.lerp(localAngularVelocity.x, directPitchRate, 0.35); // Pitch (local X) | |
| const finalYawRate = MathUtils.lerp(localAngularVelocity.y, directYawRate, 0.35); // Yaw (local Y) | |
| const finalRollRate = MathUtils.lerp(localAngularVelocity.z, directRollRate, 0.35); // Roll (local Z) | |
| // 3. Inject Stall Wing Drop instability and nose-heavy recovery moment | |
| if (isCurrentlyStalled) { | |
| loco.isStalling = true; | |
| loco.stallSeverity = MathUtils.clamp( | |
| isStallingByAoA | |
| ? (initialAlphaDeg - 17.5) / 10 | |
| : (specs.stallSpeedKmph * 1.05 - airspeedKmph) / (specs.stallSpeedKmph * 0.35), | |
| 0.1, | |
| 1.0 | |
| ); | |
| // Wing drop spins: when stalled, steering actions or minor slips flip the wing into an uncontrolled roll and deep dive | |
| if (airspeedKmph > 18) { | |
| const dropFreq = Date.now() * 0.0015; | |
| const wingDropFactor = loco.stallSeverity * (specs.rollRateDegPerSec ?? 90) * (Math.PI / 180) * 1.6; | |
| localAngularVelocity.z += Math.sin(dropFreq) * wingDropFactor * dt; // Z is roll | |
| localAngularVelocity.y += Math.cos(dropFreq + 1.1) * wingDropFactor * 0.35 * dt; // Y is yaw | |
| } | |
| // Nose-heavy center-of-gravity moment forces a rapid pitching drop to recover airspeed | |
| // This must apply at all speeds to ensure the aircraft naturally falls nose-down when stalled | |
| localAngularVelocity.x -= 0.92 * loco.stallSeverity * dt; // X is pitch | |
| } else { | |
| loco.isStalling = false; | |
| loco.stallSeverity = 0; | |
| } | |
| const totalPitchRate = finalPitchRate; | |
| const totalYawRate = finalYawRate; | |
| const totalRollRate = finalRollRate; | |
| // 4. One-Shot Quaternion Integration to perfectly evolve attitude without Euler locks | |
| const qCurrent = getAircraftQuaternion(phys.qx, phys.qy, phys.qz, phys.qw); | |
| const omega = new Vector3(totalPitchRate, totalYawRate, totalRollRate); | |
| const omegaMag = omega.length(); | |
| if (omegaMag > 1e-8) { | |
| const axis = omega.clone().normalize(); | |
| const dq = new Quaternion().setFromAxisAngle(axis, omegaMag * dt); | |
| qCurrent.multiply(dq); | |
| qCurrent.normalize(); | |
| } | |
| phys.qx = qCurrent.x; | |
| phys.qy = qCurrent.y; | |
| phys.qz = qCurrent.z; | |
| phys.qw = qCurrent.w; | |
| // Takeoff & rollout attitude stabilization: prevent wings dipping or nose diving below takeoff speed on ground | |
| const terrainCheckForAttitude = getTerrainHeight(pos.x, pos.z, mapId); | |
| if (pos.y <= terrainCheckForAttitude.height + 1.2 && speed * 3.6 < 130) { | |
| const euler = new Euler().setFromQuaternion(qCurrent, "YXZ"); | |
| euler.x = MathUtils.clamp(euler.x, -0.01, 0.05); | |
| euler.z = 0; | |
| qCurrent.setFromEuler(euler); | |
| phys.qx = qCurrent.x; | |
| phys.qy = qCurrent.y; | |
| phys.qz = qCurrent.z; | |
| phys.qw = qCurrent.w; | |
| } | |
| // Store angular rates back into the pilot's kinematic registers | |
| phys.avx = totalPitchRate; // X is pitch | |
| phys.avy = totalYawRate; // Y is yaw | |
| phys.avz = totalRollRate; // Z is roll | |
| // Recalculate 3D basis vectors | |
| ({ q, forward } = getAircraftBasis(phys.qx, phys.qy, phys.qz, phys.qw)); | |
| // Compute aerodynamic forces using updated orientation frame | |
| speed = vel.length(); | |
| speedKmph = speed * 3.6; | |
| const rho = airDensityAtAltitude(pos.y); | |
| // 1. Compute engine thrust and apply altitude power dropoff | |
| const altitudePower = MathUtils.clamp( | |
| rho / 1.225, | |
| 0.35, | |
| 1.0 | |
| ); | |
| const throttle01 = MathUtils.clamp(loco.throttle, 0, 1.0); | |
| const thrustBoost = boost ? 1.08 : 1.0; | |
| // Propeller thrust falls as forward speed approaches the aircraft's design | |
| // envelope. Without this lapse, constant static thrust remains available at | |
| // every speed and the high-power aircraft accelerate beyond 1,000 km/h. | |
| const envelopeRatio = speedKmph / Math.max(1, specs.structuralLimitSpeedKmph); | |
| const thrustSpeedLapse = MathUtils.clamp( | |
| 1 - 0.72 * envelopeRatio * envelopeRatio, | |
| 0.18, | |
| 1 | |
| ); | |
| const actualThrust = | |
| specs.maxThrust * | |
| throttle01 * | |
| engineHealth * | |
| altitudePower * | |
| thrustBoost * | |
| thrustSpeedLapse; | |
| const thrustForce = forward.clone().multiplyScalar(actualThrust); | |
| const gravityForce = new Vector3(0, -specs.mass * G, 0); | |
| // 2. Sum physical forces (Thrust + Gravity + component aero surface forces) | |
| // aero.force integrates Lift, Drag, Sideslip, induced drag, and global gear/airbrake forces computed per-surface! | |
| const totalForce = new Vector3() | |
| .add(thrustForce) | |
| .add(gravityForce) | |
| .add(aero.force); | |
| // In a vertical climb the AoA is near 0° so the per-surface stall drag never | |
| // fires. We impose an additional downward force when stalled so altitude loss | |
| // is guaranteed regardless of aircraft orientation or available thrust. | |
| if (isCurrentlyStalled) { | |
| totalForce.y -= specs.mass * G * loco.stallSeverity * 2.4; | |
| } | |
| const accel = totalForce.divideScalar(specs.mass); | |
| vel.addScaledVector(accel, dt); | |
| const stalled = loco.isStalling ?? false; | |
| let alignmentAlpha = MathUtils.clamp( | |
| (airspeedKmph / Math.max(1, specs.stallSpeedKmph)) * 0.08, | |
| 0.01, | |
| 0.12 | |
| ); | |
| if (stalled) { | |
| alignmentAlpha *= 0.35; | |
| } | |
| const newSpeed = vel.length(); | |
| const travelDir = safeNormalize(vel.clone(), forward); | |
| const blendedDir = travelDir | |
| .clone() | |
| .lerp(forward, alignmentAlpha) | |
| .normalize(); | |
| vel.copy(blendedDir).multiplyScalar(newSpeed); | |
| if (speedKmph > specs.structuralLimitSpeedKmph) { | |
| const excess = speedKmph - specs.structuralLimitSpeedKmph; | |
| if (excess > 10) { | |
| const severity = Math.min(1, excess / 180); | |
| dm.leftWing = Math.max(0, dm.leftWing - dt * 0.05 * severity); | |
| dm.rightWing = Math.max(0, dm.rightWing - dt * 0.05 * severity); | |
| dm.fuselage = Math.max(0.1, dm.fuselage - dt * 0.02 * severity); | |
| } | |
| } | |
| pos.addScaledVector(vel, dt); | |
| const terrain = getTerrainHeight(pos.x, pos.z, mapId); | |
| const groundElevation = terrain.height; | |
| if (pos.y < groundElevation) { | |
| const sinkRate = -vel.y; | |
| pos.y = groundElevation; | |
| vel.y = 0; // Touchdown: vertical velocity is zeroed. | |
| const isAirfield = terrain.isAirfield || groundElevation <= 12; | |
| if (!isAirfield) { | |
| // Off-airfield rough landing | |
| if (loco.gearDeployed && sinkRate < 4.0 && speedKmph < 185) { | |
| dm.fuselage = Math.max(0.15, dm.fuselage - dt * 0.12); | |
| const rolloutSpeed = Math.max(0, vel.length() - dt * 45.0); | |
| vel.copy(forward).multiplyScalar(rolloutSpeed); | |
| if (rolloutSpeed * 3.6 < 5 && loco.throttle < 0.1) vel.set(0, 0, 0); | |
| } else { | |
| dm.fuselage = 0; | |
| dm.engine = 0; | |
| } | |
| } else { | |
| // Landing on actual airfield runway | |
| if (loco.gearDeployed) { | |
| if (sinkRate >= 8.5) { | |
| dm.fuselage = 0; | |
| dm.engine = 0; | |
| } else if (sinkRate >= 4.5) { | |
| dm.fuselage = Math.max(0.1, dm.fuselage - 0.45); | |
| loco.gearDeployed = false; | |
| dm.engine = Math.max(0.0, dm.engine - 0.25); | |
| const rolloutSpeed = Math.max(0, vel.length() - dt * 65.0); | |
| vel.copy(forward).multiplyScalar(rolloutSpeed); | |
| if (rolloutSpeed * 3.6 < 5 && loco.throttle < 0.1) vel.set(0, 0, 0); | |
| } else { | |
| const rolloutSpeed = Math.max(0, vel.length() - dt * 14.5); | |
| vel.copy(forward).multiplyScalar(rolloutSpeed); | |
| if (rolloutSpeed * 3.6 < 5 && loco.throttle < 0.1) vel.set(0, 0, 0); | |
| } | |
| } else { | |
| // Belly slide (Gear-Up Landing on runway) | |
| if (sinkRate >= 5.5) { | |
| dm.fuselage = 0; | |
| dm.engine = 0; | |
| } else if (sinkRate >= 2.8) { | |
| dm.fuselage = Math.max(0.1, dm.fuselage - 0.55); | |
| dm.engine = 0.0; | |
| const rolloutSpeed = Math.max(0, vel.length() - dt * 55.0); | |
| vel.copy(forward).multiplyScalar(rolloutSpeed); | |
| if (rolloutSpeed * 3.6 < 5 && loco.throttle < 0.1) vel.set(0, 0, 0); | |
| } else { | |
| dm.fuselage = Math.max(0.15, dm.fuselage - 0.22); | |
| dm.engine = Math.max(0.0, dm.engine - 0.4); | |
| const rolloutSpeed = Math.max(0, vel.length() - dt * 42.0); | |
| vel.copy(forward).multiplyScalar(rolloutSpeed); | |
| if (rolloutSpeed * 3.6 < 5 && loco.throttle < 0.1) vel.set(0, 0, 0); | |
| } | |
| } | |
| } | |
| } | |
| const maxAltitude = MAP_REGISTRY[mapId]?.world.maxAltitude ?? 7500; | |
| if (pos.y > maxAltitude) { | |
| pos.y = maxAltitude; | |
| vel.y = Math.min(0, vel.y); | |
| } | |
| if (dm.hasFire) { | |
| dm.fuelTank = Math.max(0, dm.fuelTank - dt * 0.04); | |
| dm.fuselage = Math.max(0, dm.fuselage - dt * 0.03); | |
| dm.engine = Math.max(0, dm.engine - dt * 0.02); | |
| if (speedKmph > 450 && Math.random() < 0.1 * dt) { | |
| dm.hasFire = false; | |
| } | |
| } | |
| // Keep DestructibleComponent dead-flag consistent with fuselage health | |
| destr.isDead = dm.fuselage <= 0.05; | |
| phys.x = pos.x; | |
| phys.y = pos.y; | |
| phys.z = pos.z; | |
| phys.vx = vel.x; | |
| phys.vy = vel.y; | |
| phys.vz = vel.z; | |
| } | |
| /** | |
| * Perform a component-level hit damage raycast/bounding check. | |
| * | |
| * hitSpot uses aircraft local axes: | |
| * +Z = nose / engine | |
| * -Z = tail | |
| * +X = right wing | |
| * -X = left wing | |
| * +Y = canopy / top | |
| */ | |
| export function applyComponentDamage( | |
| pilot: Pilot, | |
| damage: number, | |
| bulletType: string, | |
| hitSpot: Vector3 | string | |
| ) { | |
| const zone = typeof hitSpot === "string" ? hitSpot : determineHitZone(hitSpot); | |
| const dm = destructible(pilot.entity).damageModel!; | |
| const baseScale = 100 / pilot.specs.durability; | |
| const scaledDamageMultiplier = baseScale * 0.12; | |
| const damageValue = damage * scaledDamageMultiplier; | |
| switch (zone) { | |
| case "engine": | |
| dm.engine = Math.max(0, dm.engine - damageValue); | |
| if (dm.engine < 0.4 && Math.random() < 0.2) dm.hasFire = true; | |
| break; | |
| case "leftWing": | |
| dm.leftWing = Math.max(0, dm.leftWing - damageValue); | |
| break; | |
| case "rightWing": | |
| dm.rightWing = Math.max(0, dm.rightWing - damageValue); | |
| break; | |
| case "tail": | |
| dm.tail = Math.max(0, dm.tail - damageValue * 0.9); | |
| break; | |
| case "cockpit": | |
| dm.cockpit = Math.max(0.1, dm.cockpit - damageValue * 0.85); | |
| if (Math.random() < 0.05) dm.hasOilLeak = true; | |
| break; | |
| case "fuelTank": | |
| dm.fuelTank = Math.max(0, dm.fuelTank - damageValue); | |
| if (dm.fuelTank < 0.5 && Math.random() < 0.18) dm.hasFire = true; | |
| break; | |
| default: | |
| dm.fuselage = Math.max(0, dm.fuselage - damageValue * 0.7); | |
| break; | |
| } | |
| } | |
| function determineHitZone( | |
| spot: Vector3 | |
| ): "engine" | "leftWing" | "rightWing" | "tail" | "cockpit" | "fuelTank" | "fuselage" { | |
| if (spot.z > 0.4) { | |
| return "engine"; | |
| } | |
| if (spot.z < -0.5) { | |
| return "tail"; | |
| } | |
| if (Math.abs(spot.x) > 0.28) { | |
| return spot.x < 0 ? "leftWing" : "rightWing"; | |
| } | |
| if (spot.y > 0.22 && spot.z > 0.0 && spot.z < 0.3) { | |
| return "cockpit"; | |
| } | |
| if (spot.y < -0.1 && spot.z < 0.1 && spot.z > -0.3) { | |
| return "fuelTank"; | |
| } | |
| return "fuselage"; | |
| } | |
| export class FlightPhysicsEngine { | |
| public static update = updateFlightPhysics; | |
| public static applyDamage = applyComponentDamage; | |
| } | |