Zum Inhalt

native/macos/KuroNativeSaver/Core/BootSequence.swift

// BootSequence — a BIOS-style boot overlay shown at startup: a centred column of
// system lines, each with a right-aligned [ OK ], revealed one at a time, then a
// scene-specific header. Web mirror (hud/boot.ts + DICT.BOOT_HEADERS). Drawn flat
// over the final composite via the TextRenderer; when it finishes, the renderer
// fires the CRT power-on so the scene expands into view ("machine just booted").

import simd
import Foundation

struct BootSequence {
    private let rows: [(text: String, ok: Bool, header: Bool)]
    private let lineDur: Double      // s between line reveals
    private let endHold: Double      // s held after the last line
    private var elapsed: Double = 0
    private(set) var finished = false

    /// Facade over the SSOT (`story-content.json` → `boot.headers`), keyed by scene id.
    /// Used to be a second hand-kept copy of the same table; the web reads it as
    /// `DICT.BOOT_HEADERS`. The JSON also carries `metro`, which native cannot select yet —
    /// harmless, and there the day METRO is ported (see AGENTS.md § The Swift twin).
    private static var headers: [String: String] { StoryContent.shared.boot.headers }

    /// The seven pool lines this boot shows, in order. Split out of `init` so a test can
    /// read the selection without a TextRenderer — and shuffled rather than `prefix(7)`,
    /// which showed 8 of the 15 authored lines never (web parity, hud/boot.ts).
    static func lines(seed: Int32) -> [String] {
        var rng = LCG(seed: seed)
        return shuffledPrefix(Script.boot, 7, &rng)
    }

    init(scene: String, speed: Settings.BootSpeed, seed: Int32) {
        switch speed {
        case .fast:      lineDur = 0.06; endHold = 0.30
        case .normal:    lineDur = 0.16; endHold = 0.60
        case .cinematic: lineDur = 0.32; endHold = 1.00
        }
        var r: [(String, Bool, Bool)] = BootSequence.lines(seed: seed).map { ($0, true, false) }
        r.append(("", false, false))                                     // spacer
        r.append((BootSequence.headers[scene] ?? ">> SYSTEM ONLINE", false, true))
        rows = r
    }

    /// Advance the reveal clock; returns true exactly once, when the boot finishes.
    mutating func advance(_ dt: Double) -> Bool {
        guard !finished else { return false }
        elapsed += dt
        if elapsed >= Double(rows.count) * lineDur + endHold { finished = true; return true }
        return false
    }

    func render(_ tr: TextRenderer, W: Float, H: Float, accent: SIMD3<Float>) {
        tr.fillRect(xPx: 0, yPx: 0, wPx: W, hPx: H, color: SIMD3(0, 0, 0), opacity: 1)   // opaque backdrop
        let cell = H / 64 * 1.1
        let lh = cell * 1.7
        let colW = min(W * 0.62, cell * 34)
        let x0 = (W - colW) / 2
        let y0 = (H - Float(rows.count) * lh) / 2
        let dim = accent * 0.85
        let revealed = min(rows.count, Int(elapsed / lineDur) + 1)
        for i in 0..<revealed {
            let row = rows[i]
            if row.text.isEmpty { continue }
            let y = y0 + Float(i) * lh
            tr.add(row.text, xPx: x0, yPx: y, pxHeight: cell,
                   color: row.header ? accent : dim, opacity: row.header ? 0.95 : 0.85)
            if row.ok {
                let ok = "[ OK ]"
                tr.add(ok, xPx: x0 + colW - tr.width(ok, pxHeight: cell), yPx: y,
                       pxHeight: cell, color: accent, opacity: 0.8)
            }
        }
    }
}