diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86ba4824..fd44d836 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Verify pinned specification - run: grep -q "393b06f7c195e6040be74d4d170e4f930d2a8845" SPEC_BASELINE.md + run: grep -q "b1ed874e1e8eb0e61b2174b9093201e9a61e1a4b" SPEC_BASELINE.md - name: Test deterministic core run: swift test --skip CameraPlacementTests --skip CameraFairnessTests --parallel --num-workers 1 - name: Test camera placement @@ -38,7 +38,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Verify pinned specification - run: grep -q "393b06f7c195e6040be74d4d170e4f930d2a8845" SPEC_BASELINE.md + run: grep -q "b1ed874e1e8eb0e61b2174b9093201e9a61e1a4b" SPEC_BASELINE.md - name: Byte-compare bundled contracts and fixtures against the pinned SS-specs run: bash scripts/spec_parity.sh diff --git a/App/WorldRenderer.swift b/App/WorldRenderer.swift index 4ac8f315..db0b6d8d 100644 --- a/App/WorldRenderer.swift +++ b/App/WorldRenderer.swift @@ -1121,9 +1121,10 @@ final class WorldRenderer { } mineNode.position = CGPoint(x: mine.x, y: mine.y) if let shape = mineNode as? SKShapeNode { - shape.fillColor = mine.armed ? Palette.mineArmed : Palette.mineArming + shape.fillColor = .clear shape.strokeColor = Palette.mineStroke - shape.lineWidth = mine.armed ? 3 : 1 + // Weight carries arming versus armed. The path stays the radius. + shape.lineWidth = mine.armed ? 4 : 2 } } endLayer(.mines) @@ -1576,9 +1577,8 @@ enum Palette { static let playerProjectileTrail = SKColor(red: 1, green: 0.94, blue: 0.78, alpha: 0.35) static let hostileProjectile = SKColor(red: 0.95, green: 0.45, blue: 0.30, alpha: 1) static let hostileProjectileTrail = SKColor(red: 0.95, green: 0.45, blue: 0.30, alpha: 0.30) - static let mineArmed = SKColor(red: 0.85, green: 0.35, blue: 0.25, alpha: 0.35) - static let mineArming = SKColor(white: 0.6, alpha: 0.15) - static let mineStroke = SKColor(red: 0.9, green: 0.5, blue: 0.35, alpha: 0.8) + /// D-121. The ring is this stroke. White 0.92 at alpha 0.95. The fill is clear. + static let mineStroke = SKColor(white: 0.92, alpha: 0.95) static let telegraphStroke = SKColor(red: 0.95, green: 0.55, blue: 0.35, alpha: 0.9) /// D-094 gate barrier: the rail the warning lights sit on, the lights, /// and the drawn fallback used only if the barricade art is missing. diff --git a/AppTests/MineRingPresentationTests.swift b/AppTests/MineRingPresentationTests.swift new file mode 100644 index 00000000..8af6bdeb --- /dev/null +++ b/AppTests/MineRingPresentationTests.swift @@ -0,0 +1,74 @@ +import CoreGraphics +import SpriteKit +import Testing +@testable import SSRuntime +@testable import SurveillanceCore + +/// T1060 / D-121. The Vendor mine is a stroke-only ring at the snapshot +/// radius. Arming is width 2. Armed is width 4. The fill stays clear. +@Suite(.serialized) +@MainActor +struct MineRingPresentationTests { + /// SpriteKit stores an assigned gray `SKColor` as float components in + /// extended sRGB. Compare at the thousandth, which is the precision of + /// white 0.92 and alpha 0.95. + private static func rgba(_ color: SKColor) -> (CGFloat, CGFloat, CGFloat, CGFloat) { + let space = CGColorSpace(name: CGColorSpace.extendedSRGB)! + let converted = color.cgColor.converted(to: space, intent: .defaultIntent, options: nil) ?? color.cgColor + let parts = converted.components ?? [] + let thousandth = { (index: Int) -> CGFloat in + let value = index < parts.count ? parts[index] : -1 + return (value * 1000).rounded() / 1000 + } + return (thousandth(0), thousandth(1), thousandth(2), thousandth(3)) + } + + @Test func ringIsClearStrokeAtTheAuthoredRadius() throws { + let state = try Simulation.make(seed: 1).state + let mine = try #require(state.content.standardEnemies[.victorianVendor]?.mine) + var snap = PresentationSnapshot(state) + let arming = PresentationSnapshot.MineSprite( + id: EntityID(7), + x: 40, + y: 50, + radius: mine.radius, + armed: false, + armRemaining: mine.arm, + lifeRemaining: mine.lifetime + ) + let armed = PresentationSnapshot.MineSprite( + id: EntityID(8), + x: 80, + y: 90, + radius: mine.radius, + armed: true, + armRemaining: 0, + lifeRemaining: mine.lifetime + ) + snap.mines = [arming, armed] + + let renderer = WorldRenderer() + renderer.render(snap) + + let armingNode = try #require(renderer.root.childNode(withName: "//mine-7") as? SKShapeNode) + let armedNode = try #require(renderer.root.childNode(withName: "//mine-8") as? SKShapeNode) + let authored = SKShapeNode(circleOfRadius: CGFloat(mine.radius)) + let authoredBounds = try #require(authored.path).boundingBox + let clear = (CGFloat(0), CGFloat(0), CGFloat(0), CGFloat(0)) + let neutral = (CGFloat(0.92), CGFloat(0.92), CGFloat(0.92), CGFloat(0.95)) + #expect(Self.rgba(Palette.mineStroke) == neutral) + for (sprite, node) in [(arming, armingNode), (armed, armedNode)] { + #expect(Self.rgba(node.fillColor) == clear) + #expect(Self.rgba(node.strokeColor) == neutral) + #expect(node.path?.boundingBox == authoredBounds) + #expect(node.position == CGPoint(x: sprite.x, y: sprite.y)) + } + #expect(armingNode.lineWidth == 2) + #expect(armedNode.lineWidth == 4) + + snap.mines = [] + renderer.render(snap) + #expect(renderer.root.childNode(withName: "//mine-7") == nil) + #expect(renderer.root.childNode(withName: "//mine-8") == nil) + } +} diff --git a/SPEC_BASELINE.md b/SPEC_BASELINE.md index d18cb1ce..01a2b1c5 100644 --- a/SPEC_BASELINE.md +++ b/SPEC_BASELINE.md @@ -5,7 +5,7 @@ Status: PINNED | Identity | Value | |---|---| | Specification repository | `scrimshawlife-ctrl/SS-specs` | -| Specification commit | `393b06f7c195e6040be74d4d170e4f930d2a8845` | +| Specification commit | `b1ed874e1e8eb0e61b2174b9093201e9a61e1a4b` | | Ruleset | `ss-rules-014` | | Content | `civic-seam-content-009` | | Arena | `civic-seam-arena-004` | @@ -86,3 +86,5 @@ D-118 adoption (SS-specs #87, ruleset `ss-rules-014`, content `civic-seam-conten D-119 adoption (SS-specs #89): lethal warning, option 2 (T1058). A telegraph or a 60-tick contact horizon that would remove the current Integrity shows `LETHAL THREAT` ahead of Lockdown and Extraction. The card is presentation only: no lethal-warning event, no golden-stream change, and no ruleset, content, arena, or replay identity change. No admitted file matches the cue, so the carriers are the card and the `warning` haptic. The asset catalog's `specificationCommit` moves with this pin; its entries do not. D-120 adoption (SS-specs #90): own the existing shell labels `START`, `SETTINGS`, and `RESTART` (T1059). No rename and no new control. Run-shell ยง 8.1 stays open. No contract or fixture change; pin move only. Ruleset, content, arena, and replay identities are unchanged. + +D-121 adoption (SS-specs #91): the Victorian Vendor mine is a clear-filled ring (T1060). One light neutral stroke, white 0.92 at alpha 0.95, weight 2 while arming and 4 once armed. Arm, life, radius, damage, and the cap of two are unchanged. No caption and no asset id. No ruleset or content identity change. The asset catalog's `specificationCommit` moves with this pin; its entries do not. diff --git a/Sources/SurveillanceCore/ContractVersions.swift b/Sources/SurveillanceCore/ContractVersions.swift index 5470d592..ad886824 100644 --- a/Sources/SurveillanceCore/ContractVersions.swift +++ b/Sources/SurveillanceCore/ContractVersions.swift @@ -1,5 +1,5 @@ public enum ContractVersions { - public static let specificationCommit = "393b06f7c195e6040be74d4d170e4f930d2a8845" + public static let specificationCommit = "b1ed874e1e8eb0e61b2174b9093201e9a61e1a4b" public static let ruleset = "ss-rules-014" public static let content = "civic-seam-content-009" public static let arena = "civic-seam-arena-004" diff --git a/Sources/SurveillanceCore/Resources/SPEC_PIN.txt b/Sources/SurveillanceCore/Resources/SPEC_PIN.txt index 3f104354..06bbb55c 100644 --- a/Sources/SurveillanceCore/Resources/SPEC_PIN.txt +++ b/Sources/SurveillanceCore/Resources/SPEC_PIN.txt @@ -1,4 +1,4 @@ -SS-specs commit 393b06f7c195e6040be74d4d170e4f930d2a8845 +SS-specs commit b1ed874e1e8eb0e61b2174b9093201e9a61e1a4b ruleset ss-rules-014 content civic-seam-content-009 arena civic-seam-arena-004 diff --git a/Sources/SurveillanceCore/Resources/contracts/asset-catalog-001.json b/Sources/SurveillanceCore/Resources/contracts/asset-catalog-001.json index c8dda6c9..4123598b 100644 --- a/Sources/SurveillanceCore/Resources/contracts/asset-catalog-001.json +++ b/Sources/SurveillanceCore/Resources/contracts/asset-catalog-001.json @@ -2,7 +2,7 @@ "schemaVersion": "asset-catalog-001", "legacyRepository": "scrimshawlife-ctrl/Surveillance-Survivor", "legacyCommit": "3b20d88d6a6e1fe8f07f45f581359d371fa65d98", - "specificationCommit": "393b06f7c195e6040be74d4d170e4f930d2a8845", + "specificationCommit": "b1ed874e1e8eb0e61b2174b9093201e9a61e1a4b", "entries": [ { "admissionDecision": "excluded",