From bdbac16078d5934f1bcc1c7870f97d5485522f58 Mon Sep 17 00:00:00 2001 From: Daniel Meyer Date: Sun, 1 Nov 2026 21:54:47 -0800 Subject: [PATCH 1/2] D-121: draw the Vendor mine as a ring --- .github/workflows/ci.yml | 4 ++-- App/WorldRenderer.swift | 10 +++++----- SPEC_BASELINE.md | 4 +++- Sources/SurveillanceCore/ContractVersions.swift | 2 +- Sources/SurveillanceCore/Resources/SPEC_PIN.txt | 2 +- .../Resources/contracts/asset-catalog-001.json | 2 +- 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3fc84bc..38c4ead5 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/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", From 4a525f1791257760a00c6054cec2c36f5832f221 Mon Sep 17 00:00:00 2001 From: Daniel Meyer Date: Fri, 6 Nov 2026 17:31:31 -0800 Subject: [PATCH 2/2] T1060: test the Vendor mine ring The renderer test draws an arming mine and an armed mine. The fill is clear, the stroke is white 0.92 at alpha 0.95, the widths are 2 and 4, and the path is the authored radius. --- AppTests/MineRingPresentationTests.swift | 74 ++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 AppTests/MineRingPresentationTests.swift 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) + } +}