Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
1135034
Repair device camera projection and rendering regressions
scrimshawlife-ctrl Oct 14, 2026
b52b7fe
Record baseline device profiling evidence
scrimshawlife-ctrl Oct 14, 2026
267c35c
Record failed device visual QA and asset geometry findings
scrimshawlife-ctrl Oct 14, 2026
de00e34
Expose solid footprints and add opt-in damage QA evidence
scrimshawlife-ctrl Oct 14, 2026
ce71f67
Retain scene lifetime and freeze sprite actions during pause
scrimshawlife-ctrl Oct 14, 2026
e20727d
Include presented camera center in damage diagnostics
scrimshawlife-ctrl Oct 14, 2026
0aa7c9d
Fix D-103 combat ordering and causal feedback under ruleset-008
scrimshawlife-ctrl Oct 14, 2026
e566710
Implement D-103 T1035 weapon and upgrade feedback
scrimshawlife-ctrl Oct 14, 2026
c2b808c
Correct CC-008 camera and boss field test preconditions
scrimshawlife-ctrl Oct 14, 2026
61ce8e9
Keep CC-008 reset assertions compatible with Swift Testing macros
scrimshawlife-ctrl Oct 14, 2026
cf526b3
Adopt D104 benchmark spec for T1042-T1046 reconciliation
scrimshawlife-ctrl Oct 14, 2026
981dbb4
fix: preserve camera integrity animation lifetimes (BP-004)
scrimshawlife-ctrl Oct 14, 2026
ffa1132
test: account for SpriteKit anchor precision and record BP-005 art audit
scrimshawlife-ctrl Oct 14, 2026
28e0fab
test: capture real renderer benchmark baseline and geometry overlay
scrimshawlife-ctrl Oct 14, 2026
3d46ddd
Add real-renderer revised benchmark composition fixture
scrimshawlife-ctrl Oct 14, 2026
e022e95
Adopt benchmark resolution and ground registration specification
scrimshawlife-ctrl Oct 14, 2026
d772842
Capture benchmark framing comparison before rasterization
scrimshawlife-ctrl Oct 14, 2026
34df070
BP-003 share exact animation durations across playback and reactions
scrimshawlife-ctrl Oct 14, 2026
f7c2d3d
Record rejected south locomotion study and phase-order evidence
scrimshawlife-ctrl Oct 14, 2026
ae830a1
Record gait-scale mismatch and incomplete run key-pose study
scrimshawlife-ctrl Oct 14, 2026
4a63e77
BP-008 animate exact sub-unit player travel without changing simulation
scrimshawlife-ctrl Oct 14, 2026
44e55ed
BP-004 capture registered municipal camera damage-state candidates
scrimshawlife-ctrl Oct 14, 2026
9501261
Add persistent camera damage-stage markers with renderer evidence
scrimshawlife-ctrl Oct 14, 2026
91724c8
Adopt ground-plan clarification and record wall separation study
scrimshawlife-ctrl Oct 14, 2026
89800ef
Measure ground projection and viewport tradeoffs for benchmark art
scrimshawlife-ctrl Oct 14, 2026
095be8b
Capture ground projection candidates in real benchmark renderer
scrimshawlife-ctrl Oct 14, 2026
8d6dabd
Register constrained building candidate against projected world footp…
scrimshawlife-ctrl Oct 14, 2026
c5b5037
Probe building contact and expose rear-player layering defect
scrimshawlife-ctrl Oct 14, 2026
897e952
Adopt occlusion contract and compare projected ground cues
scrimshawlife-ctrl Oct 14, 2026
14abe4a
test(BP-009): capture continuous building occlusion perimeter
scrimshawlife-ctrl Oct 14, 2026
385a06f
docs(BP-007): measure benchmark asset sampling at both framings
scrimshawlife-ctrl Oct 14, 2026
451a457
art(BP-007): export municipal camera states from higher-detail source
scrimshawlife-ctrl Oct 14, 2026
59b1ebd
fix(BP-003): unify strict clip parsing and authored facing selection
scrimshawlife-ctrl Oct 14, 2026
0d5fac0
chore(BP-003): adopt versioned timing migration target spec
scrimshawlife-ctrl Oct 14, 2026
d96fca0
feat(BP-003): decode exact v2 timing with paired facing selection
scrimshawlife-ctrl Oct 14, 2026
e78f38e
fix(BP-003): preserve event facing and elapsed reaction phase
scrimshawlife-ctrl Oct 14, 2026
a5e064f
test(BP-003): verify v2 holds through tracker and real renderer
scrimshawlife-ctrl Oct 14, 2026
7c0f062
BP-003 add reproducible exact-timing candidate exporter
scrimshawlife-ctrl Oct 14, 2026
53bc238
BP-003 bundle pinned clip target schema for contract parity
scrimshawlife-ctrl Oct 14, 2026
987c32a
BP-003 capture native reaction turning and record open visual gate
scrimshawlife-ctrl Oct 14, 2026
4cc9a62
BP-002 record higher-detail hooded hurt study and contact drift
scrimshawlife-ctrl Oct 14, 2026
94a41c5
BP-002 adopt controlled-source spec and record measured rig pilot
scrimshawlife-ctrl Oct 14, 2026
1c83e64
Record hooded body source refinement and failed visual gate
scrimshawlife-ctrl Oct 14, 2026
1a1a061
Refine editable costume geometry and record multi-angle failures
scrimshawlife-ctrl Oct 14, 2026
9e854e0
Compare painted four-view character source against benchmark
scrimshawlife-ctrl Oct 14, 2026
b33f637
Capture painted neutral facings in real benchmark renderer
scrimshawlife-ctrl Oct 14, 2026
40aecb1
Adopt BP-007 resolution-independent outline requirements
scrimshawlife-ctrl Oct 14, 2026
8781049
Generate bounded actor outlines independently of source resolution
scrimshawlife-ctrl Oct 15, 2026
b693cad
Compare finer outline sampling at fixed logical stroke (BP-007)
scrimshawlife-ctrl Oct 15, 2026
ee9331b
Adopt BP-010 and capture painted Player border alternatives
scrimshawlife-ctrl Oct 15, 2026
746ef65
Compare Player border on bundled dark ground surfaces (BP-010)
scrimshawlife-ctrl Oct 15, 2026
5eb18e2
Capture painted Player contrast in replay-verified encounter (BP-010)
scrimshawlife-ctrl Oct 15, 2026
6f0b3bd
Adopt BP-002 layered source prerequisites with verified parity
scrimshawlife-ctrl Oct 15, 2026
73cd208
Record BP-002 editable knee overlap source and review evidence
scrimshawlife-ctrl Oct 15, 2026
b5dbcbc
Refine BP-002 neutral source part ownership
scrimshawlife-ctrl Oct 15, 2026
bdf4a3e
Add BP-002 elbow and knee source overlap evidence
scrimshawlife-ctrl Oct 15, 2026
435b984
Save BP-002 linked shoulder and hip source rig scaffold
scrimshawlife-ctrl Oct 15, 2026
f724a50
Adopt BP-002 projected motion and contact clarification
scrimshawlife-ctrl Oct 15, 2026
5ff6707
Add BP-002 anchored coat-tail source deformation
scrimshawlife-ctrl Oct 15, 2026
a325cad
Repair BP-002 boot overlaps and south garment source order
scrimshawlife-ctrl Oct 15, 2026
37181b6
Record BP-002 boot contact study and failed pose review
scrimshawlife-ctrl Oct 15, 2026
81f5471
Add BP-002 lower-leg contact block and held-frame drift evidence
scrimshawlife-ctrl Oct 15, 2026
e6db65d
Add BP-002 painted contact key with retained original body
scrimshawlife-ctrl Oct 15, 2026
6082a24
Adopt BP-008 displayed-playback evidence requirements
scrimshawlife-ctrl Oct 15, 2026
3f2c158
Record actual SpriteKit player frames in live BP-008 capture
scrimshawlife-ctrl Oct 15, 2026
19f96d4
Capture full GameScene playback evidence for BP-008
scrimshawlife-ctrl Oct 15, 2026
e8193bd
Preserve shared municipal camera structure across BP-004 source states
scrimshawlife-ctrl Oct 15, 2026
1516302
Build six timed municipal camera source candidates for BP-004
scrimshawlife-ctrl Oct 15, 2026
7abdf2a
Exercise complete camera candidate clips through BP-004 runtime playback
scrimshawlife-ctrl Oct 15, 2026
ecac1ef
Capture BP-004 candidates in shared scene and restore camera contact cue
scrimshawlife-ctrl Oct 15, 2026
541f14c
Compare researched field contours on authoritative clipped paths
scrimshawlife-ctrl Oct 15, 2026
085ae1b
Compare field boundaries in a replay-verified encounter
scrimshawlife-ctrl Oct 15, 2026
0404685
Record authoritative camera registration in encounter capture
scrimshawlife-ctrl Oct 15, 2026
e4b668d
Restore registered projection in shared camera benchmark
scrimshawlife-ctrl Oct 15, 2026
0cc0793
Add specified street surfaces to registered benchmark comparison
scrimshawlife-ctrl Oct 15, 2026
5a5c338
Refine south contact source key against authored leg guide
scrimshawlife-ctrl Oct 15, 2026
ed7d416
Author recovery-leg volume guide after rejecting ambiguous painted co…
scrimshawlife-ctrl Oct 15, 2026
a2891ca
Record explicit boot tongue and heel ownership after lace placement r…
scrimshawlife-ctrl Oct 15, 2026
2a5350e
Review recovery boot ownership in full-character source placement
scrimshawlife-ctrl Oct 15, 2026
66cfb07
Reconcile recovery contact-key geometry with retained knee placement
scrimshawlife-ctrl Oct 15, 2026
be71e3c
Carry authored recovery-key correction through sixteen source poses
scrimshawlife-ctrl Oct 15, 2026
9ce79eb
Validate boot surface visibility across revised sixteen-pose source c…
scrimshawlife-ctrl Oct 15, 2026
b91f183
Transfer retained lace material only to owned forward boot surfaces
scrimshawlife-ctrl Oct 15, 2026
8ccb186
Refine shoe contours while preserving sole and attachment geometry
scrimshawlife-ctrl Oct 15, 2026
7db7a36
Join shoe and shaft into a continuous boot source across the motion c…
scrimshawlife-ctrl Oct 15, 2026
67673c8
Integrate corrected boot cycle with retained character layers and thi…
scrimshawlife-ctrl Oct 15, 2026
e06e4d8
Resolve lower-body source overlaps with shared depth and tapered calves
scrimshawlife-ctrl Oct 15, 2026
0e85768
Add anterior knee-cover source surfaces to close visible joint transi…
scrimshawlife-ctrl Oct 15, 2026
b0aa65c
Restore retained toe detail with explicit front-surface material owne…
scrimshawlife-ctrl Oct 15, 2026
81c58ec
Reuse retained north-facing heel artwork and reaffirm benchmark produ…
scrimshawlife-ctrl Oct 15, 2026
9227ec0
Compare rearward boot ankle placement against retained side-source pr…
scrimshawlife-ctrl Oct 15, 2026
718b392
Preserve narrower player stance and alternating arm source study
scrimshawlife-ctrl Oct 15, 2026
ae5fecc
Record player canvas and ground projection mismatch before live capture
scrimshawlife-ctrl Oct 15, 2026
88e79e8
Capture revised player source through live renderer with explicit reg…
scrimshawlife-ctrl Oct 15, 2026
241a1ef
Review refined player detail and seam at separate playback speeds
scrimshawlife-ctrl Oct 15, 2026
53a3a8c
docs: record current benchmark source and renderer evidence
scrimshawlife-ctrl Oct 15, 2026
56bf022
D-104: refine benchmark animation, preparation and projected scene va…
scrimshawlife-ctrl Oct 16, 2026
e0afb4e
Fix Release test builds by explicitly enabling renderer test hooks (#…
scrimshawlife-ctrl Oct 16, 2026
262cf76
Preserve DEBUG compilation condition with test-hook settings
scrimshawlife-ctrl Oct 16, 2026
ffbdb73
Preserve complete app CI failure diagnostics and result bundles
scrimshawlife-ctrl Oct 16, 2026
eb5c1fa
Fix SpriteKit preload callback actor isolation in playback tests (#139)
scrimshawlife-ctrl Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
21 changes: 18 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Verify pinned specification
run: grep -q "2377b99c6b6d1654e2cb976ee1b7e07b75c77e1a" SPEC_BASELINE.md
run: grep -q "d6027758fa8e46ed2073cbb3b6face8ee19847ef" SPEC_BASELINE.md
- name: Test deterministic core
run: swift test --skip CameraPlacementTests --skip CameraFairnessTests --parallel --num-workers 1
- name: Test camera placement
Expand All @@ -38,7 +38,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Verify pinned specification
run: grep -q "2377b99c6b6d1654e2cb976ee1b7e07b75c77e1a" SPEC_BASELINE.md
run: grep -q "d6027758fa8e46ed2073cbb3b6face8ee19847ef" SPEC_BASELINE.md
- name: Byte-compare bundled contracts and fixtures against the pinned SS-specs
run: bash scripts/spec_parity.sh

Expand Down Expand Up @@ -73,9 +73,24 @@ jobs:
set +e
xcodebuild -project SSRuntime.xcodeproj -scheme SSRuntime -sdk iphonesimulator \
-destination "platform=iOS Simulator,id=$sim" -configuration "${{ matrix.configuration }}" \
CODE_SIGNING_ALLOWED=NO ENABLE_TESTABILITY=YES test \
CODE_SIGNING_ALLOWED=NO ENABLE_TESTABILITY=YES \
SS_RENDERER_TEST_CONDITIONS=SS_RENDERER_TEST_SUPPORT \
-resultBundlePath /tmp/SSRuntime.xcresult test \
> /tmp/xcodebuild.log 2>&1
status=$?
set -e
grep -E "error:|warning:|✘|Test run with|BUILD (SUCCEEDED|FAILED)|TEST (SUCCEEDED|FAILED)" /tmp/xcodebuild.log || true
if [ "$status" -ne 0 ]; then
tail -n 250 /tmp/xcodebuild.log
xcrun xcresulttool get test-results summary --path /tmp/SSRuntime.xcresult || true
fi
exit "$status"
- name: Preserve app test evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: app-tests-${{ matrix.configuration }}
path: |
/tmp/xcodebuild.log
/tmp/SSRuntime.xcresult
retention-days: 7
157 changes: 157 additions & 0 deletions App/BenchmarkReviewArt.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import SpriteKit

/// Review-only texture injection. The test target owns all candidate files.
/// It cannot alter simulation geometry or asset admission records.
@MainActor
struct BenchmarkReviewArt {
struct GroundPatch {
let texture: SKTexture
let bounds: CGRect
let tileUnits: CGFloat
}
struct GroundLine {
let start: CGPoint
let end: CGPoint
let width: CGFloat
let color: SKColor
}
struct Placement {
let texture: SKTexture
let size: CGSize
let anchor: CGPoint
let position: CGPoint
}
enum PlayerPresentation: Equatable {
case staticComposition
case selectedClip
}
// Environment injection must not erase action/event ownership in live reviews.
var playerPresentation: PlayerPresentation = .staticComposition
let player: SKTexture
let camera: SKTexture
let building: Placement
let buildingID: String
let props: [Placement]
let paving: SKTexture
var cameraStates: [Int: SKTexture] = [:]
var groundPatches: [GroundPatch] = []
var groundLines: [GroundLine] = []
/// One proposed street arrangement shared by static and animated reviews.
/// World-space decoration only: never changes the arena or collision data.
mutating func configureStreetCorner(asphalt: SKTexture) {
groundPatches = [
.init(texture: asphalt, bounds: CGRect(x: 900, y: 0, width: 1404, height: 1536), tileUnits: 128),
.init(texture: asphalt, bounds: CGRect(x: 0, y: 0, width: 900, height: 480), tileUnits: 128)
]
func line(_ a: CGPoint, _ b: CGPoint, _ width: CGFloat, _ value: CGFloat) -> GroundLine {
.init(start: a, end: b, width: width, color: SKColor(white: value, alpha: 1))
}
groundLines = []
// Layered curb edge, with a gap aligned to the south crossing.
for (width, shade): (CGFloat, CGFloat) in [(8, 0.22), (5, 0.57)] {
groundLines.append(line(CGPoint(x: 896, y: 484), CGPoint(x: 896, y: 1536), width, shade))
groundLines.append(line(CGPoint(x: 0, y: 484), CGPoint(x: 492, y: 484), width, shade))
groundLines.append(line(CGPoint(x: 564, y: 484), CGPoint(x: 896, y: 484), width, shade))
}
// Rails sit inside the road; muted crossing paint does not rival cues.
for x: CGFloat in [960, 1000] {
groundLines.append(line(CGPoint(x: x, y: 0), CGPoint(x: x, y: 1536), 5, 0.12))
groundLines.append(line(CGPoint(x: x, y: 0), CGPoint(x: x, y: 1536), 1.5, 0.55))
}
for y in stride(from: 464, through: 320, by: -24) {
groundLines.append(line(CGPoint(x: 496, y: y), CGPoint(x: 560, y: y), 10, 0.62))
}
}

}

/// Geometry for the BP-007 review projection only. Not the gameplay/rules viewport.
enum BenchmarkProjectionGeometry {
static func project(_ point: CGPoint) -> CGPoint {
let r = CGFloat(0.5).squareRoot()
return CGPoint(x: r * (point.x - point.y), y: r * (point.x + point.y) / 2)
}

static func inverse(_ point: CGPoint) -> CGPoint {
let r = CGFloat(0.5).squareRoot()
return CGPoint(x: (point.x + 2 * point.y) / (2 * r),
y: (2 * point.y - point.x) / (2 * r))
}

/// Ground containment can conflict with player visibility; callers must measure both.
static func containedCenter(desired: CGPoint, bounds: CGRect,
viewport: CGSize, magnification: CGFloat) -> CGPoint? {
guard magnification.isFinite, magnification > 0,
viewport.width.isFinite, viewport.height.isFinite,
viewport.width > 0, viewport.height > 0,
desired.x.isFinite, desired.y.isFinite,
bounds.origin.x.isFinite, bounds.origin.y.isFinite,
bounds.width.isFinite, bounds.height.isFinite,
bounds.width > 0, bounds.height > 0 else { return nil }
let margin = (viewport.width / 2 + viewport.height) / (CGFloat(2).squareRoot() * magnification)
guard bounds.width >= 2 * margin, bounds.height >= 2 * margin else { return nil }
return CGPoint(x: min(max(desired.x, bounds.minX + margin), bounds.maxX - margin),
y: min(max(desired.y, bounds.minY + margin), bounds.maxY - margin))
}
/// Candidate priority: keep the registered player canvas visible, reporting
/// outside-arena coverage separately. No gameplay bound is changed.
static func playerVisibleCenter(desired: CGPoint, bounds: CGRect, viewport: CGSize,
magnification: CGFloat, player: CGPoint, canvas: CGRect,
safeRegion: CGRect? = nil) -> CGPoint? {
guard let contained = containedCenter(desired: desired, bounds: bounds,
viewport: viewport, magnification: magnification),
player.x.isFinite, player.y.isFinite,
canvas.origin.x.isFinite, canvas.origin.y.isFinite,
canvas.width.isFinite, canvas.height.isFinite,
canvas.width > 0, canvas.height > 0,
canvas.width * magnification <= viewport.width,
canvas.height * magnification <= viewport.height else { return nil }
let point = project(player), initial = project(contained)
let viewportRect = CGRect(x: -viewport.width / 2, y: -viewport.height / 2,
width: viewport.width, height: viewport.height)
let region = safeRegion ?? viewportRect
guard !region.isNull, !region.isInfinite,
viewportRect.contains(region),
region.width >= canvas.width * magnification,
region.height >= canvas.height * magnification else { return nil }
let minX = point.x + canvas.maxX - region.maxX / magnification
let maxX = point.x + canvas.minX - region.minX / magnification
let minY = point.y + canvas.maxY - region.maxY / magnification
let maxY = point.y + canvas.minY - region.minY / magnification
return inverse(CGPoint(x: min(max(initial.x, minX), maxX),
y: min(max(initial.y, minY), maxY)))
}

/// Static diagnostic composition, not a dynamic HUD-follow policy.
/// Enumerating obstacle edges finds the maximal center-containing rectangle.
static func unobstructedRegion(viewport: CGRect, exclusions: [CGRect],
minimumSize: CGSize) -> CGRect? {
guard !viewport.isNull, !viewport.isInfinite, viewport.width > 0, viewport.height > 0,
minimumSize.width.isFinite, minimumSize.height.isFinite,
minimumSize.width > 0, minimumSize.height > 0,
exclusions.allSatisfy({ !$0.isNull && !$0.isInfinite }) else { return nil }
let obstacles = exclusions.map { $0.intersection(viewport) }
.filter { !$0.isNull && $0.width > 0 && $0.height > 0 }
let xs = Set([viewport.minX, viewport.maxX] + obstacles.flatMap { [$0.minX, $0.maxX] }).sorted()
let ys = Set([viewport.minY, viewport.maxY] + obstacles.flatMap { [$0.minY, $0.maxY] }).sorted()
var best: CGRect?
for left in xs where left <= viewport.midX {
for right in xs where right >= viewport.midX && right-left >= minimumSize.width {
for bottom in ys where bottom <= viewport.midY {
for top in ys where top >= viewport.midY && top-bottom >= minimumSize.height {
let candidate = CGRect(x: left, y: bottom, width: right-left, height: top-bottom)
guard !obstacles.contains(where: {
let overlap = candidate.intersection($0)
return !overlap.isNull && overlap.width > 0 && overlap.height > 0
}) else { continue }
if best == nil || candidate.width * candidate.height > best!.width * best!.height {
best = candidate
}
}
}
}
}
return best
}

}
73 changes: 73 additions & 0 deletions App/CameraFieldGeometry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import CoreGraphics
import Foundation
import SurveillanceCore

/// Display-only visibility polygon; never used to decide detection.
/// All occluders and the fractional emitter origin come from the snapshot.
enum CameraFieldGeometry {
struct Key: Equatable {
let originX: Double
let originY: Double
let range: Int
let heading: Int
let angle: Int
let solids: [AABB]
init(camera: PresentationSnapshot.CameraSprite, solids: [AABB]) {
originX = camera.fieldOrigin.map { Double($0.x.raw) / Double(Q8.scale) } ?? Double(camera.x)
originY = camera.fieldOrigin.map { Double($0.y.raw) / Double(Q8.scale) } ?? Double(camera.y)
range = camera.range; heading = camera.headingMilli; angle = camera.fieldAngleMilli
self.solids = solids
}
}

static func path(_ key: Key) -> CGPath {
let path = CGMutablePath()
let origin = CGPoint(x: key.originX, y: key.originY)
guard key.range > 0, key.angle > 0 else { return path }
let base = -Double(key.heading) * .pi / 180_000
let half = Double(key.angle) * .pi / 360_000
// Sample the range arc and both sides of every obstacle corner. Uniform
// rays alone can bridge over a narrow occluder between adjacent rays.
let count = max(1, Int(ceil(Double(key.angle) / 1_000)))
var angles = (0...count).map { -half + 2 * half * Double($0) / Double(count) }
for box in key.solids {
for x in [box.minX, box.maxX] {
for y in [box.minY, box.maxY] {
let a = atan2(Double(y) - key.originY, Double(x) - key.originX) - base
let relative = atan2(sin(a), cos(a))
for offset in [-1e-7, 0, 1e-7] where abs(relative + offset) <= half {
angles.append(relative + offset)
}
}
}
}
path.move(to: origin)
for angle in angles.sorted() {
let dx = cos(base + angle), dy = sin(base + angle)
var reach = Double(key.range)
for box in key.solids {
if let entry = entry(origin: origin, dx: dx, dy: dy, box: box) {
reach = min(reach, entry)
}
}
path.addLine(to: CGPoint(x: key.originX + dx * reach, y: key.originY + dy * reach))
}
path.closeSubpath()
return path
}

private static func entry(origin: CGPoint, dx: Double, dy: Double, box: AABB) -> Double? {
var near = -Double.infinity, far = Double.infinity
for (o, d, lo, hi) in [(Double(origin.x), dx, Double(box.minX), Double(box.maxX)),
(Double(origin.y), dy, Double(box.minY), Double(box.maxY))] {
if abs(d) < 1e-12 {
if o < lo || o > hi { return nil }
} else {
let a = (lo - o) / d, b = (hi - o) / d
near = max(near, min(a, b)); far = min(far, max(a, b))
}
}
guard near <= far, far >= 0 else { return nil }
return max(0, near)
}
}
96 changes: 96 additions & 0 deletions App/CombatFeedbackRenderer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import SpriteKit
import SurveillanceCore

/// CC-008. Four persistent shapes; no per-frame node allocation, gameplay
/// mutation, random motion, or retained entity history.
@MainActor
final class CombatFeedbackRenderer {
let root = SKNode()
let aim = SKShapeNode()
let discharge = SKShapeNode(circleOfRadius: 6)
let jammer = SKShapeNode()
let concealment = SKShapeNode()
private var shotTick: UInt64?
private var lastTick: UInt64?

init() {
root.zPosition = 19
aim.name = "civic-pulse-aim"
discharge.name = "civic-pulse-discharge"
jammer.name = "signal-jammer-active"
concealment.name = "ghost-step-concealment"
let aimPath = CGMutablePath()
aimPath.addLines(between: [CGPoint(x: 23, y: -5), CGPoint(x: 31, y: 0), CGPoint(x: 23, y: 5)])
aim.path = aimPath
let warm = SKColor(red: 1, green: 0.94, blue: 0.78, alpha: 1)
aim.strokeColor = warm
discharge.strokeColor = warm
discharge.fillColor = warm.withAlphaComponent(0.65)
jammer.path = Self.segmentedContour(radius: 27, segments: 8)
jammer.strokeColor = SKColor(red: 0.3, green: 0.85, blue: 0.9, alpha: 1)
// Four right-angle corners distinguish concealment from interference
// even without colour. It is not the circular damage-immunity symbol.
let corners = CGMutablePath()
for (x, y) in [(1.0, 1.0), (-1.0, 1.0), (-1.0, -1.0), (1.0, -1.0)] {
corners.move(to: CGPoint(x: x * 19, y: y * 32))
corners.addLine(to: CGPoint(x: x * 32, y: y * 32))
corners.addLine(to: CGPoint(x: x * 32, y: y * 19))
}
concealment.path = corners
concealment.strokeColor = SKColor(white: 0.95, alpha: 1)
for shape in [aim, discharge, jammer, concealment] {
shape.lineWidth = 2
shape.isHidden = true
root.addChild(shape)
}
}

func reset() {
shotTick = nil
lastTick = nil
for node in root.children { node.isHidden = true; node.removeAllActions() }
}

func update(_ snap: PresentationSnapshot, events: [AuthoritativeEvent], settings: PresentationVFXSettings) {
if let lastTick, snap.tick < lastTick { reset() }
let fresh = snap.tick != lastTick
lastTick = snap.tick
guard !snap.outcome.isTerminal, !snap.upgradePending else {
for node in root.children { node.isHidden = true }
shotTick = nil
return
}
let player = CGPoint(x: snap.player.x, y: snap.player.y)
if fresh, let shot = snap.combatFeedback.shot,
events.contains(where: { $0.tick == snap.tick && $0.type == .weaponFired && $0.primaryEntityId == shot.projectile }),
shot.velocity != .zero {
shotTick = snap.tick
aim.position = player
aim.zRotation = atan2(CGFloat(shot.velocity.y.raw), CGFloat(shot.velocity.x.raw))
discharge.position = CGPoint(x: CGFloat(shot.origin.x.raw) / CGFloat(Q8.scale),
y: CGFloat(shot.origin.y.raw) / CGFloat(Q8.scale))
}
let shooting = shotTick.map { snap.tick >= $0 && snap.tick - $0 < 6 } ?? false
aim.isHidden = !shooting
aim.position = player
discharge.isHidden = !shooting
// Reduced motion/flash is a static outline for the same truthful cue.
discharge.fillColor = settings.reducedMotion || settings.reducedFlash ? .clear : aim.strokeColor.withAlphaComponent(0.65)
jammer.isHidden = !snap.combatFeedback.jammerReducedExposure
jammer.position = player
jammer.zRotation = settings.reducedMotion || settings.reducedFlash ? 0 : CGFloat(snap.tick % 120) * .pi / 60
concealment.isHidden = !snap.ghostConcealmentActive
concealment.position = player
}

private static func segmentedContour(radius: CGFloat, segments: Int) -> CGPath {
let path = CGMutablePath()
for index in 0..<segments {
let start = CGFloat(index) * 2 * .pi / CGFloat(segments)
path.move(to: CGPoint(x: radius * cos(start), y: radius * sin(start)))
path.addArc(center: .zero, radius: radius, startAngle: start,
endAngle: start + .pi / CGFloat(segments), clockwise: false)
}
return path
}
}
Loading
Loading