diff --git a/README.md b/README.md index 9d74e35..d334a07 100644 --- a/README.md +++ b/README.md @@ -1456,11 +1456,14 @@ if (target) target.order("move", locations.Exit); A unit has `hp`, `shields` and `energy` in whole points, `maxHp` / `maxShields` of its type, `owner`, `type`, `x`, `y`, `kills`, `orderId`, `cooldown`, `resources` (a mineral -field's), `buildTime` (frames left of its making), `rank`, `matrix` (frames left of a +field's), `buildTime` (frames left of its making; a building finished early keeps the hit +points it had reached, and a Terran one under a third burns, so heal it after), `rank`, `matrix` (frames left of a defensive matrix), `speed` (how fast it is moving now, in 256ths of a pixel a frame), the timers `stim` `ensnare` `plague` `lockdown` `maelstrom` `irradiate` `stasis` in the game's own ticks, and the booleans `invincible`, `hallucinated`, `speedUpgrade`, `cloaked`, -`burrowed`, `underAttack`, `hasTarget` (its order is aimed at a unit) and `inTransport`. +`burrowed`, `underAttack` (the game raised its under-attack alert for it within the last +second or so, which it does for a hit out of the player's view, not one on screen), +`hasTarget` (its order is aimed at a unit) and `inTransport`. Hit points, shields, energy, kills, the cooldown (the ground weapon's, the air weapon's and the spells' at once), the resources, the build time, the rank, the matrix (a write gives 250 points of matrix for that many frames; 0 takes it away), the timers, `invincible`, diff --git a/compiler/backend/units.ts b/compiler/backend/units.ts index d1f3186..bb3765e 100644 --- a/compiler/backend/units.ts +++ b/compiler/backend/units.ts @@ -67,7 +67,15 @@ const FIELDS: Partial> = { stim: word(0x115, 1), ensnare: word(0x116, 1), lockdown: word(0x117, 1), irradiate: word(0x118, 1), stasis: word(0x119, 1), plague: word(0x11a, 1), maelstrom: word(0x124, 1), buildTime: word(0xac, 2), rank: word(0x8e, 1), matrix: word(0x114, 1), speed: word(0x38, 4), }; -const HP = 0x08 >> 2, SHIELDS = 0x60 >> 2, ENERGY = word(0xa2, 2), STATUS = 0xdc >> 2, ATTACKED = word(0x87, 1), USER_ACTION = word(0x93, 1); +const HP = 0x08 >> 2, SHIELDS = 0x60 >> 2, ENERGY = word(0xa2, 2), STATUS = 0xdc >> 2, USER_ACTION = word(0x93, 1); +/** + * "Under attack": the attack-notify timer (+0x87), which the game starts when it raises its "under attack" alert for the + * unit — a hit out of the player's view — and runs for about a second. A unit hit on screen in a game played alone does + * not show it (Remastered, Magenta probe 6, 2026-10-25). The last-event bytes BWAPI reads instead (+0x8A timer, +0x8B + * 174 = was hit) are in a dword Remastered's EUD emulator ends the game on (data/allowlist.json, +0x88), so this is the + * one "recently hit" signal a map may read. + */ +const ATTACKED = word(0x87, 1); /** The status bits a flag is (cloaked: both of two). */ const FLAG_BITS: Partial> = { invincible: 0x04000000, hallucinated: 0x40000000, burrowed: 0x10, cloaked: 0x300, speedUpgrade: 0x10000000, inTransport: 0x40 }; /** The unit an order is aimed at (a pointer, 0 for none), and the defensive matrix's hit points (a word, 256 a point) beside its timer. */ diff --git a/compiler/declarations.ts b/compiler/declarations.ts index 623778d..e9cf7e4 100644 --- a/compiler/declarations.ts +++ b/compiler/declarations.ts @@ -147,7 +147,11 @@ ${kw}interface Unit { readonly orderId: Order; /** Frames until the unit can attack or cast again, 0 … 255; writing it holds the unit's fire that long (the ground weapon's, the air weapon's and the spells' alike). */ cooldown: number; - /** Frames left of a unit's or building's making, 0 … 65535; 0 once complete. Writing it lengthens or shortens the making. */ + /** + * Frames left of a unit's or building's making, 0 … 65535; 0 once complete. Writing it lengthens or shortens the making. + * A building finished early keeps the hit points it had reached, and a Terran one under a third of its full hit points + * burns down (Remastered, played): heal it after. + */ buildTime: number; /** The unit's rank, added to the type's (the one shown on its panel: Private, Corporal …), 0 … 255. */ rank: number; @@ -173,7 +177,10 @@ ${kw}interface Unit { speedUpgrade: boolean; readonly cloaked: boolean; readonly burrowed: boolean; - /** True for about a second after something hit the unit. */ + /** + * True for about a second after the game raised its "under attack" alert for the unit: a hit out of the player's view. + * A unit hit on the player's screen does not count — the game keeps no other "recently hit" mark a map may read. + */ readonly underAttack: boolean; /** True while the unit's order is aimed at another unit (attacking, following, repairing …). */ readonly hasTarget: boolean; diff --git a/compiler/gameSim.ts b/compiler/gameSim.ts index 848da83..18820ef 100644 --- a/compiler/gameSim.ts +++ b/compiler/gameSim.ts @@ -206,7 +206,9 @@ class GameUnit implements SimUnit { get hasTarget() { return this.read("hasTarget", () => this.m.getUint32(this.at + 0x5c, true) !== 0); } get inTransport() { return this.read("inTransport", () => this.flag(0x00000040)); } // Not read from the game yet: the spell timers and whether the unit is under attack. - stim = 0; ensnare = 0; plague = 0; lockdown = 0; maelstrom = 0; irradiate = 0; stasis = 0; underAttack = false; + stim = 0; ensnare = 0; plague = 0; lockdown = 0; maelstrom = 0; irradiate = 0; stasis = 0; + /** The attack-notify timer (+0x87) running: the game raised its under-attack alert for the unit within the last second or so. */ + get underAttack() { return this.read("underAttack", () => this.game.u8[this.at + 0x87] !== 0); } } export class GameSim implements TestWorldLike { diff --git a/tests/emitter.test.ts b/tests/emitter.test.ts index abee5f9..f2e6204 100644 --- a/tests/emitter.test.ts +++ b/tests/emitter.test.ts @@ -1481,6 +1481,7 @@ describe("a map of our own, units", () => { put(at + 0x38, u.speed ?? 0); put(at + 0x5c, u.hasTarget ? 0x700000 + 4 : 0); put(at + 0x8e, u.rank ?? 0, 1); + if (u.underAttack) put(at + 0x87, 12, 1); put(at + 0xac, u.buildTime ?? 0, 2); put(at + 0x114, u.matrix ?? 0, 1); put(at + 0xdc, (u.invincible ? 0x04000000 : 0) | (u.hallucinated ? 0x40000000 : 0) | (u.speedUpgrade ? 0x10000000 : 0) | (u.inTransport ? 0x40 : 0) | (u.burrowed ? 0x10 : 0)); @@ -1586,7 +1587,7 @@ describe("a map of our own, units", () => { const units: SimUnitInit[] = [ { type: 106, owner: 0, x: 400, y: 300, buildTime: 900, rank: 2, speed: 0 }, { type: 0, owner: 0, x: 420, y: 340, speed: 1024, hasTarget: true, speedUpgrade: true, matrix: 100 }, - { type: 0, owner: 0, x: 430, y: 330, inTransport: true, hallucinated: true }, + { type: 0, owner: 0, x: 430, y: 330, inTransport: true, hallucinated: true, underAttack: true }, ]; same([ " const cc = first({ type: units.TerranCommandCenter })!;", @@ -1595,7 +1596,7 @@ describe("a map of our own, units", () => { " let second: Unit | null = null;", " for (const u of unitsOf(P1, { type: units.TerranMarine })) second = u;", " print(`${cc.buildTime} ${cc.rank} ${cc.speed} ${m.speed} ${m.hasTarget} ${m.speedUpgrade} ${m.matrix} ${m.inTransport} ${m.hallucinated}`);", - " if (second) print(`${second.inTransport} ${second.hallucinated} ${second.hasTarget} ${second.speedUpgrade}`);", + " if (second) print(`${second.inTransport} ${second.hallucinated} ${second.hasTarget} ${second.speedUpgrade} ${second.underAttack} ${m.underAttack}`);", " cc.buildTime = 60; cc.rank = 7; m.matrix = 40; m.speedUpgrade = false; m.hallucinated = true; cc.rank += 1;", " print(`${cc.buildTime} ${cc.rank} ${m.matrix} ${m.speedUpgrade} ${m.hallucinated}`);", " m.matrix = 0; cc.buildTime = 70000; m.rank = 300;", @@ -1617,6 +1618,24 @@ describe("a map of our own, units", () => { ].join("\n"), UNITS, HOME); }); + it("reads a location's size into consts and writes its edges from them, as Magenta's put-back row does", () => { + same([ + " const b = bounds(1 as Location);", + " const w = b.right - b.left;", + " const h = b.bottom - b.top;", + " b.left = 576; b.top = 256;", + " b.right = 576 + w; b.bottom = 256 + h;", + " print(`${b.left} ${b.top} ${b.right} ${b.bottom}`);", + " const c = bounds(1 as Location);", + " c.left += 64; c.right += 64;", + " print(`${c.left} ${c.right}`);", + " const d = bounds(1 as Location);", + " const w2 = d.right - d.left;", + " d.left = 100; d.right = 100 + w2;", + " print(`${d.left} ${d.right}`);", + ].join("\n"), UNITS, HOME); + }); + it("walks a table every place of which has been handed out once, place 0 among them, one place a turn and two", () => { const full: SimUnitInit[] = Array.from({ length: 1700 }, (_, i) => ({ type: i % 3 ? 0 : 37, owner: i % 3 ? 0 : 1, x: 100 + (i % 50), y: 100 + Math.floor(i / 50) })); same([