Zum Inhalt

native/macos/KuroNativeSaver/Core/Actors.swift

// Combat actors — Swift mirror of src/engine/modes/actors.ts. DrawItem-based agents in
// the renderer's persistent `actors` list (survive scene swaps). Cinematic: they emit
// events + react, no damage model. Colour via DrawItem.colorOverride (no MaterialPool).

import Foundation
import Metal
import simd

protocol Actor: AnyObject {
    var items: [DrawItem] { get }
    var alive: Bool { get }
    /// advance; return false when finished (renderer culls it).
    func update(t: Double, dt: Double, cam: Camera, threat: Float) -> Bool
}

/// Camera world-space basis (forward = local -Z), from its euler rotation.
private func camBasis(_ cam: Camera) -> (fwd: SIMD3<Float>, right: SIMD3<Float>, up: SIMD3<Float>) {
    let r = Mathx.rotationX(cam.rotation.x) * Mathx.rotationY(cam.rotation.y) * Mathx.rotationZ(cam.rotation.z)
    func ax(_ x: Float, _ y: Float, _ z: Float) -> SIMD3<Float> {
        let o = r * SIMD4<Float>(x, y, z, 0); return SIMD3(o.x, o.y, o.z)
    }
    return (ax(0, 0, -1), ax(1, 0, 0), ax(0, 1, 0))
}

private func icoItem(_ device: MTLDevice, detail: Int) -> DrawItem {
    let g = Geo.icosahedron(detail: detail)
    let flat = g.pos.flatMap { [$0.x, $0.y, $0.z] }
    return DrawItem(positions: Geo.buffer(device, flat), indices: Geo.buffer(device, g.idx),
                    count: g.idx.count, vertexCount: g.pos.count, primitive: .line)
}

/// Lurking hunter: weaves at the edge of view, fires tracers toward the camera.
final class AntagonistDrone: Actor {
    private(set) var items: [DrawItem]
    var alive = true
    private var rng: LCG
    private var t0 = -1.0
    private var nextFire: Double
    private let side: Float
    private let enemyRGB: SIMD3<Float>
    private let device: MTLDevice
    private let onFire: () -> Void
    private let body: DrawItem
    private var tracer: DrawItem?
    private var tracerBorn = -1.0

    init(device: MTLDevice, enemyRGB: SIMD3<Float>, seed: Int32, onFire: @escaping () -> Void) {
        self.device = device; self.enemyRGB = enemyRGB; self.onFire = onFire
        rng = LCG(seed: seed ^ Int32(bitPattern: 0x0bad_5eed))
        side = rng.nextF() < 0.5 ? -1 : 1
        nextFire = 2 + Double(rng.nextF()) * 2
        body = icoItem(device, detail: 0)
        body.colorOverride = enemyRGB
        items = [body]
    }

    func update(t: Double, dt: Double, cam: Camera, threat: Float) -> Bool {
        if t0 < 0 { t0 = t }
        let lt = Float(t - t0)
        if threat < 0.05 { alive = false; return false }            // exhale at the crash
        let (fwd, right, up) = camBasis(cam)
        let dist = 26 + (1 - threat) * 18
        let lat = side * (10 + sin(lt * 0.5) * 3)
        let vert = 5 + sin(lt * 0.37) * 2.5
        let pos = cam.position + fwd * dist + right * lat + up * vert
        let spin = lt * 0.6
        body.model = Mathx.translation(pos) * Mathx.rotationY(spin) * Mathx.rotationX(spin * 0.5)
        if Double(lt) > nextFire {
            nextFire = Double(lt) + Double(4 - threat * 3) + Double(rng.nextF()) * 1.5
            fire(from: pos, cam: cam, t: t)
            onFire()
        }
        if let tr = tracer {
            let age = t - tracerBorn
            if age > 0.3 { items = [body]; tracer = nil }
            else { tr.opacity = 1 - Float(age / 0.3) }
        }
        return true
    }

    private func fire(from pos: SIMD3<Float>, cam: Camera, t: Double) {
        let verts: [Float] = [pos.x, pos.y, pos.z, cam.position.x, cam.position.y, cam.position.z]
        let idx: [UInt32] = [0, 1]                                  // world-space line, model = identity
        let tr = DrawItem(positions: Geo.buffer(device, verts), indices: Geo.buffer(device, idx),
                          count: 2, vertexCount: 2, primitive: .line)
        tr.colorOverride = enemyRGB
        tracer = tr; tracerBorn = t
        items = [body, tr]
    }
}

/// CORP support unit: arrive → hold → flee or crash.
final class SupportUnit: Actor {
    private(set) var items: [DrawItem]
    var alive = true
    private var t0 = -1.0
    private let side: Float
    private let crash: Bool
    private var crashFired = false
    private let onCrash: () -> Void
    private let body: DrawItem
    private var spin: Float = 0

    init(device: MTLDevice, accentRGB: SIMD3<Float>, seed: Int32, crash: Bool, onCrash: @escaping () -> Void) {
        self.crash = crash; self.onCrash = onCrash
        var rng = LCG(seed: seed ^ Int32(bitPattern: 0x0000_600d))
        side = rng.nextF() < 0.5 ? -1 : 1
        body = icoItem(device, detail: 0)            // accent colour (no override → uses scene accent)
        items = [body]
    }

    func update(t: Double, dt: Double, cam: Camera, threat: Float) -> Bool {
        if t0 < 0 { t0 = t }
        let lt = Float(t - t0)
        let (fwd, right, up) = camBasis(cam)
        let arrive = min(1, lt / 2.5)
        let ease = 1 - (1 - arrive) * (1 - arrive)
        var lateral = side * (28 * (1 - ease) + 12 * ease)
        var along: Float = 30
        var rise: Float = 6
        if lt > 6 {
            let d = lt - 6
            if crash {
                rise -= d * d * 1.4
                lateral += side * d * 2
                spin += Float(dt) * 7
                if d > 2.5 { if !crashFired { crashFired = true; onCrash() }; alive = false; return false }
            } else {
                along -= d * 16
                if d > 3 { alive = false; return false }
            }
        }
        let pos = cam.position + fwd * along + right * lateral + up * rise
        body.model = Mathx.translation(pos) * Mathx.rotationY(lt * 0.8) * Mathx.rotationZ(spin)
        return true
    }
}