Weapons and pickups
Five weapons, one button to put them away, and a table of pickups that is read off the art rather than off the box. Taking a gun makes you a different player, which is why the guns are here and not with the classes that carry them.
The pickups are one table and one test
A hundred and forty stat* records across the sixteen levels, and they are the one system the whole game shares: one creator, 0x45b160, and one collector, 0x45b270, called from the player's own think every frame.
Each chapter's init reads its own list of names and hands the creator a NEGATIVE code — 0x45b19a dispatches on code + 9, so the nine are −9 to −1 — and the cel and script it picks are all in PLAYER.SBK rather than in the level's book, which is what lets one table cover every level. Two of the nine are in no book at all (18000 and 18062), and those two are exactly the two no level places: statpunch and statshield.
statscoreup is one name and three pickups. 0x451420 switches on the record's own param and hands −6, −5 or −4 — worth 2000, 5000 and 10000 — which is why the eleven levels that place one place it with a param.
Collecting is two tests and no button: 0x434140 for a rect overlap and then 0x40e680, which compares the two sprites pixel by pixel. No facing, no range band, no action key — you walk into it. And the reach is the record's own rect, filed at user+4 by the creator; the art is only what is drawn.
0x42827a's table is what each one does, and every sound comes out of the CHARACTER's bank (skulz.snd) rather than the level's:
-1 stathealth 0x402b20(0x190) four hundred health, clamped
-2 statlife 0x40d400(lives + 1) one life, and 0x40d400 caps five
-4 statscoreup 0x40d450(0x2710) ten thousand
-5 statscoreup 0x40d450(0x1388) five thousand
-6 statscoreup 0x40d450(0x7d0) two thousand
-8 stattimer 0x40d350(-850) eight hundred and fifty back on the clock
-9 (unnamed) walks the level's `initplayer` records for the one whose
rect holds it and stores that index — a CHECKPOINTSTREETS' first four are on the roofs. They sit at y914…994 where the street is 1223 and the top of a jump is 1099, so twenty jumps from the pavement reach none of them. The level's ladder is not a shortcut, it is the way to the pickups.
The other creator is the guns, and it is below.
The guns are the other creator, and taking one makes you a different player
0x45af60 takes the POSITIVE codes, and nothing about it is the pickups' shape.
It has a button. 0x4298a1 is the gate — the idle state calls the reach handler 0x42edd0 only while S is held — and 0x42f081 ducks instead when the probe comes back empty. One key, two jobs, and which one you get depends entirely on what is in front of you.
It has a facing, and a band rather than a rect. 0x42f017 shifts the player's own point 35 pixels the way they are looking, and 0x45ae90 asks three things of it: that it falls inside x ± 55 — a band the creator writes at user+6 and user+0xa, unrelated to the record's rect — that the two are within 150 pixels vertically, and that obj+0x30 is set, which a weapon still bouncing after a swap is not.
Code 2 is invisible. The class's draw (0x45ae23) skips any pickup whose code is 2, which is the one a Coke machine's can files when it comes to rest. The can lying on top is the picture, and taking it pays 150 health, not a weapon.
And taking one changes the player. 0x45eed0 sets 0x479438 and the pickup case installs the weapon's own script; that script's kind becomes player+0x18, and 0x4284ed's table sends the entire state machine somewhere else. Five weapons, five kinds, five handlers of about 2,400 bytes each, and every one of them re-implements the idle, the walk, the run, the jump, the fall, the landing and the duck in its own cels:
6 blaster 0x471458 kind 22 0x42dbd0 4000s icon 10304 max 160
9 flaregun 0x470a78 kind 20 0x42cb80 2700s icon 10306 max 16
10 flamer 0x470f98 kind 18 0x42b860 1200s icon 10307 max 160
12 soaker 0x471260 kind 19 0x42c1e0 3200s icon 10311 max 160
16 scepter 0x470c40 kind 21 0x42d2b0 3300s icon 10308 max 160The whole table lives at 0x4a7f10 + id * 12 as { icon, max, rounds, fire }, and it is written by the chapter's own entry function rather than by anything in the pickup code. 0x40d681 draws the icon and 0x40d6a2 makes the panel's bar out of rounds * 64 / max, so the bar is that table read twice.
The CHAPTER is the unit, not the level. The four entry functions — 0x4511f0 flamer, 0x43bb00 flare gun, 0x421aa0 soaker, 0x416110 blaster — each zero all 21 rounds counts and name their own weapon, leaving the hands empty. That is why SEWER places two statflare and no gun to fire them with: you are expected to still be holding SERVICE's. It is also why every statflamer in the rip is in WOODS or CITY and every statflare is in MALL, SERVICE, SEWER or ARCADE — the placement scan and the four init functions agree exactly.
Every callback is three instructions and all seven are the same three: the weapon, the rounds, the panel. What separates a gun from a refill is one line — statblaster and statblasterpack both give 40 rounds, and only the base weapon's pickup case goes on to call 0x45eed0. Which is why picking up a tank with nothing in your hands leaves you with nothing in your hands.
Swapping throws the old one away: 0x42f0dc calls 0x45b060 the moment you press S at a gun that is not the one you are holding, spawning your weapon as a falling object with the player's own gravity, before the reach has even played. You can only ever carry one. No single level places two kinds, so this only happens across a chapter.
The flare is a shot, and it does a number. Its fire function is 0x436d40:
436d43 cmp [0x4a7f82], 0 ; rounds left, or nothing happens at all
436d5e 0x40ef30(mall.snd, 0x49) ; "#0700 flare gun"
436d6d 0x45ef00(1) ; one round
436df0 x = player.x -+ 0x3c ; sixty pixels ahead, by facing
436db0 0x430d40(0x474cb8, …) ; and there it is, at 0x1a = 100The flight is a corkscrew. The spawner files 0x434540(17) + 13 — 14…30 — at user+2 and 0x43ac3b reads it down two at a time, each frame adding that value to the flare's vertical velocity and flipping its sign — written outright from 7 up and added below it. So a flare leaves the barrel thrashing and straightens out over about seven frames. It is not aimed and it is not flat. Its strength of 100 is a percentage: 0x42f910 makes the blow out of the flight cel's own pair (7207's dx 8) plus the flare's velocity, forty a frame across and whatever the corkscrew is doing, so a flare hits for about fifty. Whatever it hits or lands on, it burns out (tag 4) and slides to a stop before it goes.
The other four fire functions differ. The flamer's 0x44dae0 is a held stream rather than a shot — its modes −1 and −2 reach into every live flame to stop it — and the flame's blow strength is 0xfff7, −9. That is a code and not a number: it is the same −9 the kragg tests for, and what it means is each class handler's own business. The soaker's 0x41f820 is the same shape, but its droplet does carry a real number — 0x4217ba writes the same hundred the flare has — so what stops that one is the stream rather than the damage. The scepter fires one of the same objects but does not hold it: 0x424620 frees it the frame its six cels end, and its fire function spends forty a press. All three are STREAMS in src/guns.ts, one contract with three sets of numbers; the blaster's bolt is BOLT, and fireGun is where the five meet.
The blaster and the flamer spend no ammunition at all. 0x45ef00 appears once in the flare gun's fire function and twice each in the soaker's and the scepter's, and not once in either of the other two.
The panel's special-weapon window at 290,305–380,450 paints its plate and its four gauge rows (0x40d691); the icon appears while 0x479438 is set, and the gauge is the weapon record's own rounds * 64 / max.
Five weapons, and three of them pour
Each weapon owns a state machine and a fire function, and the table at 0x4a7f10 + id * 12 is how they meet: {dword icon, word max, word rounds, dword fire}, with the fire pointer at +8.
6 blaster state 0x42dbd0 fire 0x412a70 script 0x471458 cels 4000s
9 flaregun state 0x42cb80 fire 0x436d40 script 0x470a78 cels 2700s
10 flamer state 0x42b860 fire 0x44dae0 script 0x470f98 cels 1200s
12 soaker state 0x42c1e0 fire 0x41f820 script 0x471260 cels 3200s
16 scepter state 0x42d2b0 fire 0x41f6b0 script 0x470c40 cels 3300sA state machine dispatches on the TAG the player is on (movsx eax, [eax+0x44]) and calls its weapon's fire function with a small number — the VARIANT — which every fire function reads its own way. Tag 2 is the standing shot and tag 6 the ducking one across all five, and the flamer and soaker send -2 from the tags either side of those, which is how a stream knows to stop.
The three that pour
The flamer, the soaker and the scepter do not launch anything. Each adds an object to a list the player owns, and 0x421700 plants it fresh every frame:
42170a if (player mirrored) x = player.x - user[+4]
42171e else x = player.x + user[+4]
42172f x += player.vx ; ...and it leads your own motion
42173b y = user[+2] + player.y + player.vySo a stream is not fired and forgotten, it is redrawn where you are — which is why walking while you hold the button sweeps it across a room. (The flamer's own copy of that arithmetic is 0x453c70, the think its class files at 0x453ba8; it reads the same two user fields the same way.)
The offset belongs to a pose, and the pose is in a second script. Each fire function files a muzzle offset chosen by the variant the state machine passes — the flamer's four are at 0x44db90 and the three after it, (135, -35), (63, 15), (49, -22), (66, 5) — and the flame cel is anchored at its NOZZLE (9508 is 255x56 with posX 1), so the offset has to land on the barrel. It only does for one cel. Measuring the blue muzzle spark every armed cel carries, against that cel's own anchor:
1240 (73,-14) 1241 (86,-34) 1242 (97,-54) 1243 (132,-37)
1222..1227 (crouching) (~61,+14) -> variant 1 to the pixel0x470f98 tag 2 is a single frame of cel 1240, and it is not the firing pose: drawn with it, the player holds the gun low with the flame hanging 135 pixels away and nothing in between. The sustained pose is a different script: 0x46faf8, installed by 0x423726 (tag 0) and 0x42370d (tag 1, for the second character), whose tag 0 is 1240 1241 1242 1243 1244 1243 1244 — the gun RISES and the arm extends, settling on 1243/1244, whose spark is variant 0's offset to four pixels. The offset is right for that pose.
Two negative variants end it: -2 walks the list installing the shutting-off tag, and -1 writes the expire kind straight into every member. The player's own hit handler sends that -1 before it has even looked at the blow (0x448c19, 0x448c40), so being hit puts your flamethrower out.
They cost a round an engine frame (0x45ef00(1)), which makes a full hundred and sixty about eleven seconds of flame — against the scepter's forty a shot, four shots and no more. RAVECAVE's statscepter files no rounds at all, so what you get is the single round 0x45eed0 gives for arming you, one beam, and an empty gauge.
They do not all hit with the same thing
0x4217ba soaker mov word ptr [edi+0x1a], 0x64 ; a hundred, every frame
0x424630 scepter mov word ptr [ecx+0x1a], 0x64 ; a hundred
0x453b9b flamer mov word ptr [esi+0x1a], 0xfff7 ; MINUS NINETwo of the three are ordinary damage; the flame is a code. And -9 is the one code that is not in the player's own table — it falls below 0x448c84's range test — so what reads it is a −9 arm in the victim's own hit handler (Fighting). Like the blaster's bolt — which only chapter four's own handlers read, Boggs', the TCop's and the test tube's — the flamethrower is a key rather than a weapon. What a full gauge of it kills is three kinds of thing: MOLITOV (initwerec), whose −9 arm at 0x45296e ends in its death; the dog, whose arm at 0x4550d3 installs its death script 0x478208 and pays its 200 (0x455115); and CITY's crows, which burn for eight frames and tumble (The nameless handler is the crow's). This page carries all three.
The damage, where there is any, is small and comes from the art: 0x42f910 scales the striking cel's own (dy, dx) by the object's strength, and the soaker's 9806 carries dx 8. Eight a frame, so a two-hundred-health zombie takes about twenty-five frames of water.
INV puts the gun away and gets it out again
There is no inventory screen anywhere in SC.EXE, and only one weapon slot, 0x479434: taking a different gun throws yours down (0x42f0dc). What INV does is switch between that one gun and your fists.
Two things say "armed", and they are not the same. 0x479438 is carried: the panel's icon (0x40d663) and the swap at a different gun (0x42f0a8) read it. player+0x18 being one of the five armed kinds 0x12..0x16 is out (0x448bf0): the fire button, the armed moveset, the knockdown's disarm (0x44911b) and the surge's recharge (0x402ee0) ask that.
Every player state answers the button with the same instruction, mov word ptr [eax+0x18], 0xf — the unarmed idle unconditionally (0x4298c0), the unarmed walk and run only while you carry a gun (0x429af4), and all five guns' handlers. State 15 is 0x428975:
428975 cmp word ptr [0x4ac386], 0
42897d jne 0x428c46 ; down: install the unarmed idle 0x471648
428983 ...0x479434 through 0x429624 ; up: dispatch on the weapon
4289c1 cmp dword ptr [eax+0x3e], 0x471458
4289c8 je 0x4289ea ; the gun's script is already in: put it away
4289ca cmp word ptr [0x479438], 0
4289d2 je 0x4289ea ; nothing carried: nothing to get out
4289d6 push 0x471458 / call 0x45d090 ; get it out
4289ea ...0x4717c8 with S held, else 0x471648So it is a toggle, decided on the release. The release has to land on a state 15 frame, and a hold alternates: 0x45d090 writes the new script's kind into player+0x18 (0x45d0a7), so the unarmed idle the held branch installs is kind 0, and kind 0's handler sees the button still down and goes back to 15. A tap — in on one engine frame, out on the next — flips cleanly. A hold ends on whichever of the two its last frame was, and a release on kind 0's frame leaves the idle standing with the gun away.
Nothing is spent and 0x479438 is never touched. With the gun away the player is on the unarmed state machine, so P punches and K kicks again.
The gun also goes away, still carried, whenever an unarmed script goes in: every level opens on the idle 0x475c88 (0x448bc7) and a save load only sets 0x479438 (0x45e041), a respawn installs 0x471648 (0x42950f), the ladder and the bar end on the unarmed fall 0x471b28 (0x42af84, 0x42b597), the flail 0x472350 ends on the idle, a knockdown is 0x476890, and a reach that took anything but one of the five guns ends on 0x471648 (0x42884d). A knockdown throws the gun down only if it was out. INV brings it back from any of these.
A pickup is taken on the ART, not the box
0x45b270 intersects the two rects with 0x434140 and only then calls 0x40e680, which is the second test and the real one. That walks the intersection looking for a row where BOTH cels have an opaque span — and it is spans rather than pixels because that is how the SHP stores a row, which is why 0x4320c0 compares span lists and never touches a pixel value.
The player's cel is a tall rectangle with a great deal of nothing in it: STREETS' statlife runs 3629..3729, and standing at 3600 or 3760 overlaps that rect by a pixel or two while touching none of the art. The rect alone hands you the life; the art does not, and the suite asserts exactly that pair of positions.
The point 0x40e680 hands back is the centre of the RECT intersection (0x40e782 reads back what 0x434140 wrote), not the centre of the pixels it found. So the pixel walk only ever answers yes or no, and can stop at the first row that touches.