|
| 1 | +import { describe, expect, it } from "vite-plus/test"; |
| 2 | + |
| 3 | +import removal from "./fixtures/oracle-vulcanus-cliff-removal-probability.seed123456.json"; |
| 4 | + |
| 5 | +/** |
| 6 | + * **The MECHANISM of the ore -> cliff exclusion: `cliff_removal_probability`.** |
| 7 | + * |
| 8 | + * `cliffOreDirection.spec.ts` settled the DIRECTION - resources suppress |
| 9 | + * cliffs, not the reverse - by switching the resources off and regenerating. |
| 10 | + * That is as far as switching something off can go. Removing the ore removes |
| 11 | + * everything about the ore at once, so it can say which way the effect runs and |
| 12 | + * never how. |
| 13 | + * |
| 14 | + * The lever here is a PROTOTYPE field, which is a different kind of lever: |
| 15 | + * every one of the 945 resource entities stays exactly where the control has |
| 16 | + * them, and one property of them changes. |
| 17 | + * `ResourceEntityPrototype::cliff_removal_probability` defaults to **1.0**, and |
| 18 | + * no shipped prototype overrides it - grepped across `base/`, `core/`, |
| 19 | + * `space-age/`, `quality/` and `elevated-rails/`. It is therefore invisible |
| 20 | + * from the data alone and can only be seen by changing it. |
| 21 | + * |
| 22 | + * It cannot be changed the way `autoplace_controls` and `cliff_settings` are, |
| 23 | + * because those are surface settings and this is read at map-gen from the |
| 24 | + * loaded prototype. `OracleOptions.extraDataLua` exists for exactly this, and |
| 25 | + * writes `data-final-fixes.lua` rather than `data.lua`: the probe mod declares |
| 26 | + * no dependencies, so Factorio may load it before `space-age`, at which point |
| 27 | + * `data.raw.resource["tungsten-ore"]` does not exist yet and the override would |
| 28 | + * silently edit nothing. |
| 29 | + * |
| 30 | + * **What this does NOT change: the port.** At 1.0 the removal is |
| 31 | + * unconditional, so `vulcanusOreRejection.ts`'s box-overlap rejection is |
| 32 | + * correct exactly as written and no code moves. What changes is that its header |
| 33 | + * can stop saying the mechanism is unknown, and that the box-overlap SHAPE is |
| 34 | + * explained rather than fitted - a placed resource destroys the cliffs it |
| 35 | + * collides with. |
| 36 | + */ |
| 37 | + |
| 38 | +interface Ent { |
| 39 | + x: number; |
| 40 | + y: number; |
| 41 | + name: string; |
| 42 | +} |
| 43 | +interface Proto { |
| 44 | + type: string; |
| 45 | + layers: string[]; |
| 46 | + box?: { lx: number; ly: number; rx: number; ry: number }; |
| 47 | + cliff_removal_probability?: number; |
| 48 | + map_grid?: boolean; |
| 49 | +} |
| 50 | +interface Arm { |
| 51 | + label: string; |
| 52 | + zeroedCliffRemovalProbability: boolean; |
| 53 | + autoplaceControls: Record<string, { frequency: number; size: number; richness: number }> | null; |
| 54 | + effectiveAutoplace: Record<string, { frequency: number; size: number; richness: number }>; |
| 55 | + cliffs: Ent[]; |
| 56 | + resources: Ent[]; |
| 57 | + protos: Record<string, Proto>; |
| 58 | +} |
| 59 | + |
| 60 | +const arms = removal.cases as unknown as Arm[]; |
| 61 | +const CONTROL = 0; |
| 62 | +const ZEROED = 1; |
| 63 | +const ORE_OFF = 2; |
| 64 | + |
| 65 | +const vulcanusCliffs = (a: Arm): number => |
| 66 | + a.cliffs.filter((c) => c.name === "cliff-vulcanus").length; |
| 67 | + |
| 68 | +/** |
| 69 | + * The ten cells `#94` found the game leaves empty however `cliff_elevation` is |
| 70 | + * routed onto them, and `#99` showed fill completely once the ore is switched |
| 71 | + * off. Same list, verbatim, as `cliffOreDirection.spec.ts` - if it drifts, the |
| 72 | + * two specs are no longer talking about the same thing. |
| 73 | + */ |
| 74 | +const BLOB = [ |
| 75 | + "178,138.5", |
| 76 | + "178,142.5", |
| 77 | + "178,146.5", |
| 78 | + "178,150.5", |
| 79 | + "182,138.5", |
| 80 | + "182,142.5", |
| 81 | + "182,146.5", |
| 82 | + "182,150.5", |
| 83 | + "186,138.5", |
| 84 | + "186,142.5", |
| 85 | +]; |
| 86 | +const blobHits = (a: Arm): number => { |
| 87 | + const cells = new Set( |
| 88 | + a.cliffs.filter((c) => c.name === "cliff-vulcanus").map((c) => `${String(c.x)},${String(c.y)}`), |
| 89 | + ); |
| 90 | + return BLOB.filter((k) => cells.has(k)).length; |
| 91 | +}; |
| 92 | + |
| 93 | +describe("the ore -> cliff exclusion is cliff_removal_probability", () => { |
| 94 | + /** |
| 95 | + * The distinguishing arm. The ore is still there - all 945 of it, the same |
| 96 | + * count as the control - and every blob cell gets a cliff anyway. No account |
| 97 | + * in which the exclusion is about the ore's PRESENCE survives this. |
| 98 | + */ |
| 99 | + it("zeroing the field restores every blob cell with the ore still in place", () => { |
| 100 | + expect(blobHits(arms[CONTROL])).toBe(0); |
| 101 | + expect(blobHits(arms[ZEROED])).toBe(10); |
| 102 | + |
| 103 | + // Non-vacuity, both halves. The ore did NOT go away... |
| 104 | + expect(arms[ZEROED].resources.length).toBe(945); |
| 105 | + expect(arms[ZEROED].resources.length).toBe(arms[CONTROL].resources.length); |
| 106 | + // ...and the arm really did run with the field changed, read back off the |
| 107 | + // running game rather than echoed from what was written. |
| 108 | + expect(arms[CONTROL].protos["tungsten-ore"].cliff_removal_probability).toBe(1); |
| 109 | + expect(arms[ZEROED].protos["tungsten-ore"].cliff_removal_probability).toBe(0); |
| 110 | + }); |
| 111 | + |
| 112 | + /** |
| 113 | + * The field accounts for the effect ENTIRELY, not merely for some of it. |
| 114 | + * Zeroing it is indistinguishable from deleting every resource, and the |
| 115 | + * difference from the control is exactly the ten blob cells. |
| 116 | + */ |
| 117 | + it("zeroing the field is indistinguishable from having no resources at all", () => { |
| 118 | + expect(vulcanusCliffs(arms[CONTROL])).toBe(335); |
| 119 | + expect(vulcanusCliffs(arms[ZEROED])).toBe(345); |
| 120 | + expect(vulcanusCliffs(arms[ORE_OFF])).toBe(345); |
| 121 | + expect(vulcanusCliffs(arms[ZEROED])).toBe(vulcanusCliffs(arms[ORE_OFF])); |
| 122 | + expect(vulcanusCliffs(arms[ZEROED]) - vulcanusCliffs(arms[CONTROL])).toBe(BLOB.length); |
| 123 | + |
| 124 | + // The resources-OFF arm is the known control from cliffOreDirection: it |
| 125 | + // reaches the same cliff count by removing the ore rather than by changing |
| 126 | + // it, and its field is untouched. |
| 127 | + expect(arms[ORE_OFF].resources.length).toBe(0); |
| 128 | + expect(arms[ORE_OFF].protos["tungsten-ore"].cliff_removal_probability).toBe(1); |
| 129 | + expect(arms[ORE_OFF].effectiveAutoplace["tungsten_ore"]?.size).toBe(0); |
| 130 | + expect(arms[CONTROL].effectiveAutoplace["tungsten_ore"]?.size).toBe(1); |
| 131 | + }); |
| 132 | + |
| 133 | + /** |
| 134 | + * The default is what makes the ported rejection unconditional, and it is |
| 135 | + * asserted here so that a future Factorio version lowering it - or a mod |
| 136 | + * prototype arriving with a different value - fails loudly instead of |
| 137 | + * silently invalidating `vulcanusOreRejection.ts`'s "always" assumption. |
| 138 | + */ |
| 139 | + it("every resource takes the 1.0 default, which is why the port needs no probability", () => { |
| 140 | + for (const name of ["tungsten-ore", "calcite", "coal", "sulfuric-acid-geyser"]) { |
| 141 | + expect(arms[CONTROL].protos[name].cliff_removal_probability).toBe(1); |
| 142 | + } |
| 143 | + }); |
| 144 | + |
| 145 | + /** |
| 146 | + * Recorded here because the same read-back proved it and because it is the |
| 147 | + * reason a rendered map preview cannot count resource entities: `map_grid` |
| 148 | + * draws solid ores as a 2x2-block checkerboard. Only fluid and vent |
| 149 | + * resources opt out, and the geyser is the one in this fixture. |
| 150 | + */ |
| 151 | + it("map_grid is on for the solid ores and off for the geyser", () => { |
| 152 | + for (const name of ["tungsten-ore", "calcite", "coal"]) { |
| 153 | + expect(arms[CONTROL].protos[name].map_grid).toBe(true); |
| 154 | + } |
| 155 | + expect(arms[CONTROL].protos["sulfuric-acid-geyser"].map_grid).toBe(false); |
| 156 | + }); |
| 157 | +}); |
0 commit comments