Compare commits
51
Commits
64ff8310f3
...
f4ca1cae57
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4ca1cae57 | ||
|
|
29eee51e35 | ||
|
|
a671dcb59a | ||
|
|
569ec6e5b6 | ||
|
|
ef4ba6d24a | ||
|
|
6bc4fd5928 | ||
|
|
9fc1b92080 | ||
|
|
54e7756ead | ||
|
|
ccca66c0c1 | ||
|
|
99d4889f1e | ||
|
|
72efe85a1b | ||
|
|
c3f9387b24 | ||
|
|
a0f254fdeb | ||
|
|
dd8068dfe6 | ||
|
|
02e338a46f | ||
|
|
45538cc18e | ||
|
|
fd35bf334f | ||
|
|
198bea54da | ||
|
|
30c811ea80 | ||
|
|
65338b2b27 | ||
|
|
787721ad35 | ||
|
|
ff9fd7b1b8 | ||
|
|
828c71487c | ||
|
|
9ff9013c68 | ||
|
|
552b9b6480 | ||
|
|
155708b41d | ||
|
|
18528a7f73 | ||
|
|
664e086969 | ||
|
|
8ce71f478a | ||
|
|
3cb0182ac7 | ||
|
|
be93ad1251 | ||
|
|
2e5ad0f1a1 | ||
|
|
8cdc47fd33 | ||
|
|
3ee85429fd | ||
|
|
578b55316b | ||
|
|
1f8a89db6a | ||
|
|
5e7069d637 | ||
|
|
b74a9173eb | ||
|
|
9bc5a6c3af | ||
|
|
0cf615eb3d | ||
|
|
e19cb784d2 | ||
|
|
24eb9559f8 | ||
|
|
814b51df46 | ||
|
|
f19facbe5c | ||
|
|
7d1be16094 | ||
|
|
f744d786f1 | ||
|
|
8087c28cb5 | ||
|
|
381019b7aa | ||
|
|
1a300bf744 | ||
|
|
20d0627d75 | ||
|
|
5ce44f842e |
@@ -5,7 +5,7 @@ A falling-sand factory game in the browser, in the style of Sandustry.
|
|||||||
Every grain of sand, ore, water and fire is simulated. You fly a small bot over a side-view
|
Every grain of sand, ore, water and fire is simulated. You fly a small bot over a side-view
|
||||||
world, dig with a resonance drill and vacuum up what breaks loose. You build furnaces from
|
world, dig with a resonance drill and vacuum up what breaks loose. You build furnaces from
|
||||||
blocks, melt ore with coal and air, and cast the metal in molds. Then you sell bars, alloys and
|
blocks, melt ore with coal and air, and cast the metal in molds. Then you sell bars, alloys and
|
||||||
other goods at a trade dock, automating with belts, screw tubes, filters, fans, pipes and
|
other goods at a trade dock, automating with belts, screw conveyors, filters, fans, pipes and
|
||||||
machines, until the land claim is paid off and beyond.
|
machines, until the land claim is paid off and beyond.
|
||||||
|
|
||||||
## Play
|
## Play
|
||||||
|
|||||||
+174
-9
@@ -49,7 +49,7 @@ decides something new, add it under "Decisions" with the date.
|
|||||||
4. Bars drop out of the bottom of the mold; collect them and sell them at the trade dock, or fill buyers' orders, which pay more.
|
4. Bars drop out of the bottom of the mold; collect them and sell them at the trade dock, or fill buyers' orders, which pay more.
|
||||||
5. Better bits (copper, then iron, then steel) open harder rock: granite, then basalt. Deeper
|
5. Better bits (copper, then iron, then steel) open harder rock: granite, then basalt. Deeper
|
||||||
ores give tin, zinc, silver and gold, and alloys (bronze, brass) pay more.
|
ores give tin, zinc, silver and gold, and alloys (bronze, brass) pay more.
|
||||||
6. Automate with belts, screw tubes, filters, fans, pipes and machines. Pay off the claim.
|
6. Automate with belts, screw conveyors, filters, fans, pipes and machines. Pay off the claim.
|
||||||
|
|
||||||
## Systems in brief
|
## Systems in brief
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ Details and numbers live in the code and in `docs/systems/`.
|
|||||||
- **Drill:** see `docs/systems/dig-tools.md`. Pulse widths, tuning to one material, hardness by
|
- **Drill:** see `docs/systems/dig-tools.md`. Pulse widths, tuning to one material, hardness by
|
||||||
bit tier.
|
bit tier.
|
||||||
- **Building:** a 4×4-grain block grid.
|
- **Building:** a 4×4-grain block grid.
|
||||||
- Blocks: walls of any of several materials, belts, screw tubes, filters, grates, gates,
|
- Blocks: walls of any of several materials, belts, screw conveyors, filters, grates, gates,
|
||||||
pipes, pumps, outlets.
|
pipes, pumps, outlets.
|
||||||
- Machines: fan, Refiner, Stone cutter, Silo.
|
- Machines: fan, Refiner, Stone cutter, Silo.
|
||||||
- Molds: ingot, bit, tank. Ingot bars drop out of the bottom onto whatever is under the mold;
|
- Molds: ingot, bit, tank. Ingot bars drop out of the bottom onto whatever is under the mold;
|
||||||
@@ -140,7 +140,7 @@ above, including:
|
|||||||
- **Ingot molds drop bars below automatically.** The owner reported bars being thrown into the
|
- **Ingot molds drop bars below automatically.** The owner reported bars being thrown into the
|
||||||
furnace above and wants belts underneath to carry them off. Blocked bars wait in the trough;
|
furnace above and wants belts underneath to carry them off. Blocked bars wait in the trough;
|
||||||
short stubs stay for the next firing. The mold cleans itself and rakes the grate after dropping bars or clearing junk.
|
short stubs stay for the next firing. The mold cleans itself and rakes the grate after dropping bars or clearing junk.
|
||||||
- **Screw tubes carry no more than belts:** they move every other tick. For more throughput,
|
- **Screw conveyors carry no more than belts:** they move every other tick. For more throughput,
|
||||||
build more lines.
|
build more lines.
|
||||||
- **A heap on a belt moves as one**, up to 96 grains deep, not just its bottom layer. If a grain is blocked,
|
- **A heap on a belt moves as one**, up to 96 grains deep, not just its bottom layer. If a grain is blocked,
|
||||||
the load above it stands still and the belt slips underneath; the top no longer slides over
|
the load above it stands still and the belt slips underneath; the top no longer slides over
|
||||||
@@ -177,7 +177,7 @@ above, including:
|
|||||||
popup. The owner asked for a general description rather than the selected building.
|
popup. The owner asked for a general description rather than the selected building.
|
||||||
- **Ingot molds need no F.** They push junk and mixed metal out of the bottom as loose grains with the bars, throw slag away and rake the grate. Removing any mold returns its set metal: whole bars as bars, the rest as scrap. The owner asked what F was for once bars dropped on their own, and chose this over keeping F for cleaning.
|
- **Ingot molds need no F.** They push junk and mixed metal out of the bottom as loose grains with the bars, throw slag away and rake the grate. Removing any mold returns its set metal: whole bars as bars, the rest as scrap. The owner asked what F was for once bars dropped on their own, and chose this over keeping F for cleaning.
|
||||||
|
|
||||||
- **Fewer, smaller messages.** Molds show only a small percentage, red when blocked or spoiled and gold when a casting waits for F; details are in the hover panel. Bars dropping, blocked molds and set castings no longer pop up cards or toasts. Fan heat is a red marker on the fan; melted tubes and machines get a short red label where they were. Status lines are one short sentence. The owner found the messages too large and too many.
|
- **Fewer, smaller messages.** Molds show only a small percentage, red when blocked or spoiled and gold when a casting waits for F; details are in the hover panel. Bars dropping, blocked molds and set castings no longer pop up cards or toasts. Fan heat is a red marker on the fan; melted screw conveyors and machines get a short red label where they were. Status lines are one short sentence. The owner found the messages too large and too many.
|
||||||
|
|
||||||
- **World sounds are local.** Sounds from the world (water sizzling on hot metal or lava, and
|
- **World sounds are local.** Sounds from the world (water sizzling on hot metal or lava, and
|
||||||
any other event with a place) are heard around the bot, whatever the zoom: full volume within
|
any other event with a place) are heard around the bot, whatever the zoom: full volume within
|
||||||
@@ -190,14 +190,14 @@ above, including:
|
|||||||
- **The silo has pixel art in the pixel look**, drawn grain by grain like the rest of the world;
|
- **The silo has pixel art in the pixel look**, drawn grain by grain like the rest of the world;
|
||||||
the smooth look keeps the illustrated silo. The owner noticed it looked the same in both.
|
the smooth look keeps the illustrated silo. The owner noticed it looked the same in both.
|
||||||
|
|
||||||
- **Screw tubes use all their lanes.** Grains in an up or down tube shift sideways toward the
|
- **Screw conveyors use all their lanes.** Grains in an up or down screw conveyor shift sideways toward the
|
||||||
emptier lanes of the block, and grains taken in from the side go to the far lanes first. A
|
emptier lanes of the block, and grains taken in from the side go to the far lanes first. A
|
||||||
block-wide lift fed by a belt from the side now carries about 90% of what the belt brings
|
block-wide lift fed by a belt from the side now carries about 90% of what the belt brings
|
||||||
(before, only the edge lane filled: the owner saw one line on either side). Belts keep their
|
(before, only the edge lane filled: the owner saw one line on either side). Belts keep their
|
||||||
speed.
|
speed.
|
||||||
- **Screw tubes carry bars**, one whole bar at a time, in line with the grains: a belt pushes a
|
- **Screw conveyors carry bars**, one whole bar at a time, in line with the grains: a belt pushes a
|
||||||
bar into a tube's side, it rides through corners and leaves the mouth. The owner asked how bars
|
bar into a screw conveyor's side, it rides through corners and leaves the mouth. The owner asked how bars
|
||||||
should be moved and chose tubes over a separate bar lift.
|
should be moved and chose screw conveyors over a separate bar lift.
|
||||||
- **Several worlds are kept.** Each world has its own save and autosave; the title screen's Worlds
|
- **Several worlds are kept.** Each world has its own save and autosave; the title screen's Worlds
|
||||||
list loads, renames and deletes them, and a new or imported world never overwrites another. An
|
list loads, renames and deletes them, and a new or imported world never overwrites another. An
|
||||||
old single save becomes 'World 1'. The owner asked for multiple saves.
|
old single save becomes 'World 1'. The owner asked for multiple saves.
|
||||||
@@ -214,7 +214,7 @@ above, including:
|
|||||||
and the surface start seen. The owner chose line of sight over a plain circle around the bot.
|
and the surface start seen. The owner chose line of sight over a plain circle around the bot.
|
||||||
- **Bars pile neatly, and a Bar rack stores them (t1).** Bars that come to rest snap to a tidy
|
- **Bars pile neatly, and a Bar rack stores them (t1).** Bars that come to rest snap to a tidy
|
||||||
brick pattern so piles have no air gaps; a Bar rack machine (the Silo for bars) takes them from
|
brick pattern so piles have no air gaps; a Bar rack machine (the Silo for bars) takes them from
|
||||||
a belt or tube, counts them and lets them out with F. The owner wanted bar storage that doesn't
|
a belt or screw conveyor, counts them and lets them out with F. The owner wanted bar storage that doesn't
|
||||||
waste space, like stacked ingots in real life.
|
waste space, like stacked ingots in real life.
|
||||||
As built: a resting bar snaps to a 2-grain grid, and one with fewer than 2 of its 4 bottom cells
|
As built: a resting bar snaps to a 2-grain grid, and one with fewer than 2 of its 4 bottom cells
|
||||||
supported tips into the gap beside it. Storage became Rack blocks rather than a machine, like
|
supported tips into the gap beside it. Storage became Rack blocks rather than a machine, like
|
||||||
@@ -364,6 +364,171 @@ above, including:
|
|||||||
tick, nearest first (a Huge world is fully started after 17 s), and flying toward areas still
|
tick, nearest first (a Huge world is fully started after 17 s), and flying toward areas still
|
||||||
waiting starts them. Nothing is skipped: the same settling happens, spread out.
|
waiting starts them. Nothing is skipped: the same settling happens, spread out.
|
||||||
|
|
||||||
|
- **Sloped walls and diagonal wall lines (t36, 2026-10-25).** The owner wanted funnel walls that
|
||||||
|
material slides down instead of catching on block steps. A Sloped wall is a half-block triangle
|
||||||
|
with a flat 45° face, in four orientations, built of any wall material at half a wall block's
|
||||||
|
cost. Dragging walls or slopes at about 45° lays a diagonal line; for slopes the drag direction
|
||||||
|
picks the orientation (solid side below) and T flips it, or turns a single piece. Each slope
|
||||||
|
also fills one cell past its corner so a diagonal run is sealed (grains move diagonally and
|
||||||
|
would otherwise slip through where two pieces meet). Square walls placed diagonally still
|
||||||
|
touch only at corners and leak there, like any square blocks.
|
||||||
|
Later the same day the owner asked for the Wall piece itself to work diagonally, the way screw conveyors
|
||||||
|
do, so the separate Sloped wall piece left the build menu: a wall drag at about 45° lays slopes
|
||||||
|
(solid side below, T flips it during the drag), straight drags and clicks lay squares, and for a
|
||||||
|
single block T cycles square and the four corners. Slope ids are unchanged, so saves still load.
|
||||||
|
|
||||||
|
- **Diagonal screw conveyors (t35, 2026-10-25).** Screw conveyors run in eight directions. A drag at about 45°
|
||||||
|
lays a diagonal run pointing along the drag, and R cycles all eight for a single screw conveyor. Diagonal
|
||||||
|
runs carry grains at the same rate as straight ones, join straight screw conveyors and belts, and carry
|
||||||
|
bars level, a block every eight ticks. They use new part ids (11-14) because part ids are saved.
|
||||||
|
|
||||||
|
- **Sloped belts and filters (t40, 2026-10-25).** The owner asked for belts, then filters, to work
|
||||||
|
diagonally like screw conveyors and walls. A Belt or Filter drag at about 45° lays slope-shaped blocks
|
||||||
|
whose face is a running belt, carrying grains and bars up or down in the drag's direction at
|
||||||
|
flat-belt speed; the face grips its load, so nothing slides back. A sloped filter drops what it
|
||||||
|
passes out underneath and carries the rest. New block codes (19-22), so saves still load.
|
||||||
|
- **Square corners hold grains (t39, 2026-10-26).** The owner wants square wall staircases to
|
||||||
|
hold material instead of leaking through each touching corner. Powder, liquid and gas cannot
|
||||||
|
step diagonally between two solid neighbours; powder neighbours still allow free piles to
|
||||||
|
slide as before. Free flyers and diagonal screw conveyor intakes obey the same solid-corner boundary.
|
||||||
|
Screw lane transport is unchanged. World generation disables cell corner sealing while
|
||||||
|
settling loose ground, preserving existing seed layouts. This supersedes the square-wall
|
||||||
|
leakage described in the t36 decision below.
|
||||||
|
- **Silo contents are visible without opening the gate (t38, 2026-10-26).** The owner wants
|
||||||
|
contents beside the cursor anywhere over the outline and a material-coloured fill window at
|
||||||
|
every zoom in both looks, while keeping the real steel silo shape.
|
||||||
|
- **Distant drill aim is normal (t41, 2026-10-26).** The owner normally points beyond the
|
||||||
|
drill's reach and expects players to do the same. Keep the pulse colour and light crosshair,
|
||||||
|
mark the pulse's far edge, and omit distance warnings for the drill. Other tools still warn.
|
||||||
|
- **Screw conveyors (t44, 2026-10-26).** The owner chose the real machine name for screw
|
||||||
|
conveyors; code uses `screw` because `conveyor` also covers belts and filters. Transport,
|
||||||
|
numeric ids and save versions stay unchanged.
|
||||||
|
- **No backward compatibility for now (2026-10-26).** The owner will start a new world after
|
||||||
|
changes: no one is playing the game for real yet. Changes may break old saves and blueprints;
|
||||||
|
don't add migration or alias code for them (first used by the t44 rename to screw conveyors).
|
||||||
|
- **No uphill sloped filters (t43, 2026-10-26).** The owner wants sloped belts and filters to be
|
||||||
|
realistic in how they look and behave. Real sorting screens run flat or tilted downhill and
|
||||||
|
shake; none climbs a 45° slope. So filters are flat or downhill
|
||||||
|
only; to sort while lifting, run a sloped belt up, then a filter. Not built yet.
|
||||||
|
|
||||||
|
- **Conveyor drawing and sleep follow-up (t42/t43, 2026-10-26).** The owner wants visible
|
||||||
|
rubber ribs on the actual cleat raster, filled corrugated far walls behind the load, and
|
||||||
|
smooth continuous mesh with rails for inclined screens. Walls stop at the slope ends;
|
||||||
|
only loaded screens shake. Empty moving cleats must let chunks sleep. Keep pocket transport,
|
||||||
|
uphill throughput near 71 grains/s and exact sorting unchanged.
|
||||||
|
|
||||||
|
- **Sidewall belts with moving cleats (t42, 2026-10-26; physics replaced by the entry below).** To replace the old full-column
|
||||||
|
grip, use moving barrier lines rather than material cells: the barrier mask blocks normal
|
||||||
|
powder movement without overwriting grains, terrain or parts. Cleats stand perpendicular
|
||||||
|
to the face, spaced 8 horizontal face cells apart and 6 grains tall (integer normal reach
|
||||||
|
4); named constants keep these tunable. A finite periodic pocket envelope carries only
|
||||||
|
what fits, stepping every other tick. End pockets continue while the cleat rounds the band.
|
||||||
|
Between steps no grains are stamped: bare face slides and excess rolls back. Water drains;
|
||||||
|
bars stay level. Dark corrugated far walls sit behind visible grains; drawing uses the
|
||||||
|
actual barrier mask, including after save/load. The envelope is a discrete transport model,
|
||||||
|
not a calculation of contact forces. A generous flat feed delivers about 71 grains/s uphill
|
||||||
|
versus 90 flat; accepting that reduction remains the owner's playtest decision.
|
||||||
|
- **Inclined vibrating screens built (t43, 2026-10-26).** Diagonal filters always travel
|
||||||
|
downhill, even when dragged uphill. R leaves a picked slope unchanged; flipping a blueprint
|
||||||
|
mirrors its slope and keeps downhill travel. Placement and filter settings enforce the
|
||||||
|
same direction. Sloped filters have neither column carry nor grip: gravity moves grains,
|
||||||
|
with one mesh-contact nudge every 4 ticks (15 downhill steps/s plus gravity). Unselected
|
||||||
|
bars step on that same cadence; selected bars fall through. Flat filters retain their
|
||||||
|
linear transport. Sorting both slope shapes in either mode conserves the complete mix.
|
||||||
|
Both looks use a sturdy mesh frame with a small tick-based shake only while loaded;
|
||||||
|
idle mesh has no running belt animation. No old uphill-save migration was added.
|
||||||
|
|
||||||
|
- **Screw conveyors stay, in every direction (2026-10-26).** The owner asked whether screw
|
||||||
|
conveyors are still needed now that belts climb 45°, whether they're realistic, and what a
|
||||||
|
downward screw is for. Kept as they are: real plants use both (screws for steep, vertical and
|
||||||
|
enclosed runs; belts for long, cheap, high-volume ones), and a downward screw is the same
|
||||||
|
machine turning the other way, useful for letting material down at a steady rate.
|
||||||
|
- **Stalls are the player's to fix, but visible; belts carry a realistic depth (t45,
|
||||||
|
2026-10-26).** An overloaded belt line jammed against a screw conveyor above it. The owner
|
||||||
|
agreed that's realistic and stays the player's problem, but wants a warning on the stalled
|
||||||
|
belt and the animation stopped on everything affected, and belts limited to a realistic
|
||||||
|
load depth instead of carrying 96-grain stacks. Not built yet.
|
||||||
|
|
||||||
|
- **Cleats really push their pockets (t42 feedback, 2026-10-26).** The owner found the first
|
||||||
|
cleats passed through heavy loads while the material climbed as before, and wants cleats that
|
||||||
|
hold the material and drag it up. Now nothing carries the load on a slope but the ribs and a
|
||||||
|
friction drag on the layer touching the belt (which gravity beats on 45°, so loose grains slide
|
||||||
|
back until a rib holds them). Each belt step every rib moves one face cell and pushes the
|
||||||
|
grains in its way ahead of it; whatever is heaped above the rib is left to roll back. A rib
|
||||||
|
never moves into a grain, never crosses a machine, and stops short of anything built. If any
|
||||||
|
rib of a run can't push its way, the whole run stalls that step and every move is undone (the
|
||||||
|
stall t45 will show). At the foot of an uphill run, each rib coming up around the end roller
|
||||||
|
scoops what lies on the feeding belt behind it into its pocket, like a real boot section.
|
||||||
|
Measured: about 80-86 grains/s uphill under a heavy feed (flat 90); downhill about 38, since
|
||||||
|
the top only feeds a layer two grains deep onto the face.
|
||||||
|
- **No cleats on downhill belts (t42 feedback, 2026-10-26).** The owner asked why a downhill
|
||||||
|
belt has cleats: on 45° the load slides anyway. A downhill sloped belt is now a plain belt, with
|
||||||
|
no ribs and no sidewall, that carries at belt speed like a flat one (90 grains/s); only uphill
|
||||||
|
runs are sidewall belts. A whole run also shares one cleat phase, so pieces added later line up
|
||||||
|
with it.
|
||||||
|
- **Build range about the size of the screen (t64, 2026-10-27).** The owner asked whether
|
||||||
|
building needs a range limit at all, since the bot needs no arms to build. Agreed: keep a limit,
|
||||||
|
but make it about what's on screen, so anything the player can see and aim at builds directly
|
||||||
|
and anything beyond stays a blueprint built when the bot comes near. No limit at all would let
|
||||||
|
players build in places they've never been and link far claims instantly. Built: the range is
|
||||||
|
the visible view box, at least 64 grains from its centre and at most 200 when zoomed far out.
|
||||||
|
- **Sticky materials slide freely on built surfaces (t61, 2026-10-27).** Stickiness (STICK) is the
|
||||||
|
chance per tick a grain takes a diagonal step, which made clay lumps, snow, wood chunks, ash and
|
||||||
|
dirt creep down sloped walls (a single clay grain never got down a 20-piece slope). The owner
|
||||||
|
chose to make stickiness grain against grain only: on anything built, every material slides
|
||||||
|
freely, so slopes are useful for every material, while heaps keep their shape. Real chutes clog
|
||||||
|
with sticky material and need cleaning, but that is micromanagement the game doesn't need.
|
||||||
|
- **Belts and other transport pieces never fill a rectangle (2026-10-27).** The owner said Shift
|
||||||
|
doesn't need to fill a rectangle for belts, conveyors and the like. Belts, filters, screw
|
||||||
|
conveyors, shafts, pipes, pumps and outlets (`lineOnly()`) always draw a line, Shift or not, and
|
||||||
|
a diagonal belt drag stays sloped. Walls, grates and gates still Shift-fill.
|
||||||
|
- **The dev console ships in every build (t66, 2026-10-27).** The owner is happy for players to
|
||||||
|
experiment and play their own way, so the console isn't hidden behind a dev build. Turning it
|
||||||
|
on warns first that the save will be marked, and the mark stays with the world; any future
|
||||||
|
achievements won't unlock in a marked world. Besides money, items and upgrades it may have
|
||||||
|
game speed and other things just for fun.
|
||||||
|
- **One join rule for every transport piece: the next piece goes right after the end
|
||||||
|
(2026-10-27, replaces the t62 same-row rule).** The owner wants all pieces to place and join
|
||||||
|
the same way, so players know what to expect when building. Joins follow the direction the
|
||||||
|
material travels: a piece's outlet points at the block right after its end, which is the
|
||||||
|
next block along a straight piece, or the block diagonally past a diagonal's end. The next
|
||||||
|
piece joins when it starts in that block and carries the same general way. So a horizontal
|
||||||
|
then a diagonal (up or down) starts the diagonal in the next block on the same row; a
|
||||||
|
diagonal then a horizontal or vertical starts it diagonally past the diagonal's end; a
|
||||||
|
vertical going up then a diagonal starts the diagonal directly above. Any other placement is
|
||||||
|
two separate ends that pass nothing. Belts carry grains on top, so they bend the rule where
|
||||||
|
grains can't follow it: an uphill belt's top feeds a flat belt on the same row, and a flat
|
||||||
|
belt one block below an uphill belt's low end also feeds it. The owner first approved a rule
|
||||||
|
based only on shape, then found in testing that horizontal → uphill and vertical → diagonal
|
||||||
|
didn't join the way the other pieces did.
|
||||||
|
- **Warehouses replace the trade dock, eventually (t68, 2026-10-27).** The dock only exists to
|
||||||
|
get selling working for testing; the owner finds it unengaging. The plan: a warehouse at each
|
||||||
|
edge of the map, on indestructible ground, served by a road and trucks. Warehouses accept
|
||||||
|
loose grain as well as packaged goods (pallets, barrels or sacks from packaging machines).
|
||||||
|
Trucks matter to play, not just looks: what's waiting is collected when a truck comes, on its
|
||||||
|
schedule. Finding them: the player spawns a short walk from the left warehouse, which takes
|
||||||
|
loose and bulk goods at lower prices with frequent trucks; the right one pays more for
|
||||||
|
packaged goods and is the long-haul goal. Both always show on the minimap and full map with an
|
||||||
|
edge-of-screen arrow, the first-start card names both and suggests starting left, signposts
|
||||||
|
stand on indestructible plinths at spawn, and each warehouse is a landmark (crane or silo,
|
||||||
|
beacon at night) with its road running a little way inland.
|
||||||
|
- **Screws melting under hot loads stays, but must read clearly (t69, t70, 2026-10-27).** Hot
|
||||||
|
material still damages screw conveyors (above 1000°), but the owner wants the damage obvious
|
||||||
|
(a scorched or broken look, hover text, a first-time message), cheap to repair (build over it),
|
||||||
|
and warned about before it happens (a glow near the limit). A heat-resistant screw comes later
|
||||||
|
as an upgrade. Prompted by a long vertical screw that seemed to leak for no reason while it
|
||||||
|
carried hot rubble.
|
||||||
|
- **Melts that cool loose are ordinary rock (2026-10-27).** Magma poured from the bag became an
|
||||||
|
invisible, undrillable basalt the game treated as built. Now any melt except metal and glass
|
||||||
|
cools into natural rock wherever it came from, and any solid no block, mold or machine owns can
|
||||||
|
be drilled, so nothing can get permanently stuck in the world.
|
||||||
|
- **Resonance pylon: metal sets hardness, power sets reach (t47, 2026-10-27).** A pylon is built
|
||||||
|
in copper, iron or steel, which caps the hardest rock it can break (4, 5, 6), the same way
|
||||||
|
bits and tanks step up through metals. Power from the shaft network sets its radius and pulse
|
||||||
|
rate, up to a cap per metal; power never sets hardness, so it can't skip the steel bit. It is
|
||||||
|
tuned like the drill and never harms anything built. It waits until power is switched back on,
|
||||||
|
since today every machine runs without power.
|
||||||
|
|
||||||
## Ideas not built yet
|
## Ideas not built yet
|
||||||
|
|
||||||
- **Concrete**, as a stronger wall material from rubble.
|
- **Concrete**, as a stronger wall material from rubble.
|
||||||
|
|||||||
+141
-63
@@ -13,13 +13,13 @@ tasks/, .kandown/ the owner's to-do list: a kandown board (docs/kandown.md;
|
|||||||
build.py joins src/ into dist/index.html (one self-contained page)
|
build.py joins src/ into dist/index.html (one self-contained page)
|
||||||
src/manifest.json the build order: simulation files, then browser files
|
src/manifest.json the build order: simulation files, then browser files
|
||||||
src/shell.html page markup (HUD, menus, dialogs); CSS and JS are put in at the markers
|
src/shell.html page markup (HUD, menus, dialogs); CSS and JS are put in at the markers
|
||||||
src/styles/style.css all styles
|
src/styles/ base styles and the dev console panel
|
||||||
src/sim/ the simulation. No DOM. The headless tests load only these files.
|
src/sim/ the simulation. No DOM. The headless tests load only these files.
|
||||||
src/render/ drawing: the pixel look (CPU) and the smooth look (WebGL2), overlays
|
src/render/ drawing: the pixel look (CPU) and the smooth look (WebGL2), overlays
|
||||||
src/ui/ the browser shell: input, HUD, build menu, notebook, settings, saves,
|
src/ui/ the browser shell: input, HUD, build menu, notebook, settings, saves,
|
||||||
audio, the frame loop
|
audio, the frame loop
|
||||||
test/ headless suites (t_*.js) and their harness; browser tests (play.py,
|
test/ headless suites (t_*.js) and their harness; browser tests (play.py,
|
||||||
t_smooth.py); run_all.py
|
t_smooth.py, t_dev_console.py); run_all.py
|
||||||
tools/ serve.py (build, serve, rebuild on change), shot.py (screenshots),
|
tools/ serve.py (build, serve, rebuild on change), shot.py (screenshots),
|
||||||
where.py, map.py, perf.py, playthrough.py, use_venv.py (switches browser
|
where.py, map.py, perf.py, playthrough.py, use_venv.py (switches browser
|
||||||
scripts to .venv), scenes/ (screenshot setups), experiments/
|
scripts to .venv), scenes/ (screenshot setups), experiments/
|
||||||
@@ -29,6 +29,11 @@ docs/ this map, DESIGN.md, TESTING.md, systems/ (notes per syste
|
|||||||
dist/, shots/ build output and screenshots (not committed)
|
dist/, shots/ build output and screenshots (not committed)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The dev console pairs `src/sim/devconsole.js` (headless command dispatch, mark, log and
|
||||||
|
runtime controls) with `src/ui/devconsole.js` (confirmation and bottom panel). Save metadata
|
||||||
|
keeps `dev` and the last 200 `devLog` entries; free building, speed and dev pause reset on load.
|
||||||
|
The frame loop applies speed and queued steps while keeping the per-frame tick limit.
|
||||||
|
|
||||||
## How it runs
|
## How it runs
|
||||||
|
|
||||||
- **One script.** All files share one global scope; there is no import/export.
|
- **One script.** All files share one global scope; there is no import/export.
|
||||||
@@ -44,7 +49,7 @@ dist/, shots/ build output and screenshots (not committed)
|
|||||||
4. update the HUD.
|
4. update the HUD.
|
||||||
- **One tick** (`simTick` in `src/sim/tick.js`), in this order:
|
- **One tick** (`simTick` in `src/sim/tick.js`), in this order:
|
||||||
- update the sky roof line (every 20 ticks);
|
- update the sky roof line (every 20 ticks);
|
||||||
- weather, machines (including belts and tubes), pipes, drill pulses;
|
- weather, machines (including belts and screw conveyors), pipes, drill pulses;
|
||||||
- bars (`entTick`) and grains in flight (`flyTick`);
|
- bars (`entTick`) and grains in flight (`flyTick`);
|
||||||
- falling sand (`simMove`), then heat (`heatPass`);
|
- falling sand (`simMove`), then heat (`heatPass`);
|
||||||
- every so often: molds, milestones, plants, cold, tree support;
|
- every so often: molds, milestones, plants, cold, tree support;
|
||||||
@@ -62,7 +67,7 @@ dist/, shots/ build output and screenshots (not committed)
|
|||||||
- `temp`: °C;
|
- `temp`: °C;
|
||||||
- `flags`: `F_NAT` natural rock, `F_CAST`, `F_BURN` burning;
|
- `flags`: `F_NAT` natural rock, `F_CAST`, `F_BURN` burning;
|
||||||
- `stamp`: moved this tick;
|
- `stamp`: moved this tick;
|
||||||
- `part`: which built part a cell belongs to (`P_*`: tube directions, grate, open gate, filter,
|
- `part`: which built part a cell belongs to (`P_*`: screw conveyor directions, grate, open gate, filter,
|
||||||
pipe...);
|
pipe...);
|
||||||
- `mach`: machine id;
|
- `mach`: machine id;
|
||||||
- `digp`: drill damage;
|
- `digp`: drill damage;
|
||||||
@@ -81,7 +86,7 @@ dist/, shots/ build output and screenshots (not committed)
|
|||||||
(open inside).
|
(open inside).
|
||||||
- `molds`;
|
- `molds`;
|
||||||
- `ents` (bars, 4×2 grains, with their own physics);
|
- `ents` (bars, 4×2 grains, with their own physics);
|
||||||
- `flyers` (grains in the air: blown, off belt ends, out of tube mouths, rain);
|
- `flyers` (grains in the air: blown, off belt ends, out of screw conveyor mouths, rain);
|
||||||
- `pipeNets`;
|
- `pipeNets`;
|
||||||
- `ghostBlocks`, `ghostMachines`, `ghostMolds`.
|
- `ghostBlocks`, `ghostMachines`, `ghostMolds`.
|
||||||
- **Player:** `bot` (position), `bag` (count per material), `player` (bit and tank tier, lens,
|
- **Player:** `bot` (position), `bag` (count per material), `player` (bit and tank tier, lens,
|
||||||
@@ -118,6 +123,17 @@ dist/, shots/ build output and screenshots (not committed)
|
|||||||
- kind `mold`.
|
- kind `mold`.
|
||||||
`hidden` pieces are not in the menu. The menu's tabs and order are `BUILD_CATS` in
|
`hidden` pieces are not in the menu. The menu's tabs and order are `BUILD_CATS` in
|
||||||
`src/ui/build-menu.js`.
|
`src/ui/build-menu.js`.
|
||||||
|
- **Transport geometry:** `joins.js` owns travel-directed ends and mutual joins for belts,
|
||||||
|
filters and screws. `pieceEnds(code, v)` returns precomputed geometric left/right ends
|
||||||
|
(vertical screws: up/down), with travel and inlet/outlet roles. `endJoin(bx, by, endIndex,
|
||||||
|
planned = null)` returns `neighbourBlock * 2 + neighbourEnd`, or -1; `pieceJoins` returns
|
||||||
|
bits 0/1. Plans override world blocks, including the queried block. Outlets target the next
|
||||||
|
block along travel, with the belt surface exceptions. Inlet ties prefer same-shape travel,
|
||||||
|
then top-to-bottom, left-to-right; a join must be reciprocal. Both looks, ghosts, previews,
|
||||||
|
diagonal screw grain/bar transfers and straight screw entry into diagonals share this rule.
|
||||||
|
Flat-belt bar entry uses `entBeltScrewEnter` and the swept-path collision check. Straight
|
||||||
|
screw side-feeding into lifts remains independent. Edge elbows bend inside the
|
||||||
|
diagonal; diagonal outlets bend inside the receiving straight block.
|
||||||
- **Placing and removing:**
|
- **Placing and removing:**
|
||||||
- `placeBlock` / `removeBlock` (`blocks.js`);
|
- `placeBlock` / `removeBlock` (`blocks.js`);
|
||||||
- `placeMachineAt` / `addMachine` / `removeMachine` (`actions.js`, `machines.js`);
|
- `placeMachineAt` / `addMachine` / `removeMachine` (`actions.js`, `machines.js`);
|
||||||
@@ -134,13 +150,14 @@ dist/, shots/ build output and screenshots (not committed)
|
|||||||
|
|
||||||
- **The pixel look:**
|
- **The pixel look:**
|
||||||
- `drawWorld` (`src/render/world.js`) fills one pixel per grain;
|
- `drawWorld` (`src/render/world.js`) fills one pixel per grain;
|
||||||
|
- `diagonal-screws.js` derives diagonal and straight end bends from `endJoin`, sharing casing geometry between pixels, smooth shading tags, ghosts and previews;
|
||||||
- `computeLight` (`light.js`) adds coloured light;
|
- `computeLight` (`light.js`) adds coloured light;
|
||||||
- overlays (machines, molds, the bot, rain, arrows) are drawn on the 2D canvas afterwards
|
- overlays (machines, molds, the bot, rain, arrows) are drawn on the 2D canvas afterwards
|
||||||
(`machines.js`, `overlays.js`).
|
(`machines.js`, `overlays.js`).
|
||||||
- **The smooth look** (`src/render/smooth.js`) takes the buffers `drawWorld` fills (colour and
|
- **The smooth look** (`src/render/smooth.js`) takes the buffers `drawWorld` fills (colour and
|
||||||
a class per cell, the colour behind, trees, fields, light, glow) and shades them with WebGL2.
|
a class per cell, the colour behind, trees, fields, light, glow) and shades them with WebGL2.
|
||||||
- The cell classes are set in `world.js` and read in the shader: 4 is a crisp built piece,
|
- The cell classes are set in `world.js` and read in the shader: 4 is a crisp built piece,
|
||||||
6 a wall tile, 7/8 a belt, 9–12 a screw tube, 13/14 a filter.
|
6 a wall tile, 7/8 a belt, 9–12 a screw conveyor, 13/14 a filter.
|
||||||
- On software GL it falls back to pixels, unless `window.__allowSoftwareGL` is set (the test
|
- On software GL it falls back to pixels, unless `window.__allowSoftwareGL` is set (the test
|
||||||
tools set it).
|
tools set it).
|
||||||
|
|
||||||
@@ -171,7 +188,7 @@ dist/, shots/ build output and screenshots (not committed)
|
|||||||
| Starter furnace designs | `src/sim/progress.js` (`STARTER_DESIGNS`, `smelterBlueprint`) |
|
| Starter furnace designs | `src/sim/progress.js` (`STARTER_DESIGNS`, `smelterBlueprint`) |
|
||||||
| Falling sand, liquids, gases | `src/sim/cells.js` |
|
| Falling sand, liquids, gases | `src/sim/cells.js` |
|
||||||
| Heat, melting, fire, reactions, alloys, steel | `src/sim/heat.js` |
|
| Heat, melting, fire, reactions, alloys, steel | `src/sim/heat.js` |
|
||||||
| Belts, filters, screw tubes | `src/sim/conveyors.js`, `src/sim/machine-tick.js` |
|
| Belts, filters, screw conveyors | `src/sim/conveyors.js`, `src/sim/slope-conveyors.js` (uphill cleat barriers and screen nudges), `src/sim/machine-tick.js` |
|
||||||
| Fans | `src/sim/machine-tick.js` (`fanBlow`) |
|
| Fans | `src/sim/machine-tick.js` (`fanBlow`) |
|
||||||
| Refiner, Stone cutter, Silo | `src/sim/processors.js` |
|
| Refiner, Stone cutter, Silo | `src/sim/processors.js` |
|
||||||
| Pipes, pumps, outlets | `src/sim/pipes.js` |
|
| Pipes, pumps, outlets | `src/sim/pipes.js` |
|
||||||
@@ -201,14 +218,14 @@ Material definitions: ids, properties and metal families. ids, and drill/tank ti
|
|||||||
- functions: def, metalFamily
|
- functions: def, metalFamily
|
||||||
- variables: EMPTY, GRASS, DIRT, LDIRT, SAND, GRAVEL, CLAY, STONE, GRANITE, BASALT, NMAT, MD, FIRETO
|
- variables: EMPTY, GRASS, DIRT, LDIRT, SAND, GRAVEL, CLAY, STONE, GRANITE, BASALT, NMAT, MD, FIRETO
|
||||||
|
|
||||||
### src/sim/material-tables.js (49 lines)
|
### src/sim/material-tables.js (54 lines)
|
||||||
Material tables: typed properties, casting groups, flags, parts and tool tiers. Typed property tables
|
Material tables: typed properties, casting groups, flags, parts and tool tiers. Typed property tables
|
||||||
- variables: PH, DENS, KC, ICAP, CASTMETAL, METAL_TIER, SOLIDMETAL, F_NAT, F_CAST, F_BURN, P_NONE, P_TU, P_TD, P_TL, P_TR, P_GRATE, P_GOPEN, P_WHEEL, P_INTAKE, P_FILTER, P_PIPE, TDX, TDY, BIT_TIERS, TANK_TIERS, REACH
|
- variables: PH, DENS, KC, ICAP, CASTMETAL, METAL_TIER, SOLIDMETAL, F_NAT, F_CAST, F_BURN, P_NONE, P_SCREW_U, P_SCREW_D, P_SCREW_L, P_SCREW_R, P_GRATE, P_GOPEN, P_WHEEL, P_INTAKE, P_FILTER, P_PIPE, P_SCREW_UR, P_SCREW_DR, P_SCREW_DL, P_SCREW_UL, SCREW_PARTS, SCREW_DIRS, IS_SCREW, SCREW_DX, SCREW_DY, BIT_TIERS, TANK_TIERS, REACH
|
||||||
|
|
||||||
### src/sim/pieces.js (143 lines)
|
### src/sim/pieces.js (179 lines)
|
||||||
Everything the player can build (PIECES): blocks, machines and molds, with costs and descriptions; filter settings; wall materials and their cost.
|
Everything the player can build (PIECES): blocks, machines and molds, with costs and descriptions; filter settings; wall materials and their cost.
|
||||||
- functions: isPipeCode, filterable, cleanFilterCfg, filterRec, filterPassAt, filterInfo, sameFilter, wallCost
|
- functions: isSlopeBelt, isFilter, isBelt, beltGrade, slopeShape, beltFaceY, filterVariant, beltDirection, isSlope, isWall, slopeCell, slopeCode, isPipeCode, lineOnly, filterable, cleanFilterCfg, filterRec, filterPassAt, filterInfo, sameFilter, wallCost
|
||||||
- variables: BS, BP_WALL, BP_BELT, BP_TUBE, BP_SHAFT, BP_GRATE, BP_GATE, BP_FILTER, BP_PIPE, BP_PUMP, BP_OUTLET, BP_GRIND, BP_SAW, BP_PRESS, BP_RACK, filterCfg, WALL_OF, WALL_LIST, BAR_OF, GRAINS_PER_BAR, POWER_FREE, CASH, PIECES, PIECE_BY_ID, PIECE_BY_CODE, BELT_POWER, TUBE_POWER, FAN_POWER, WHEEL_K, ENGINE_K, MOLDS
|
- variables: BS, BP_WALL, BP_BELT, BP_SCREW, BP_SHAFT, BP_GRATE, BP_GATE, BP_FILTER, BP_PIPE, BP_PUMP, BP_OUTLET, BP_GRIND, BP_SAW, BP_PRESS, BP_RACK, BP_SLOPE_BL, BP_SLOPE_BR, BP_SLOPE_TL, BP_SLOPE_TR, BP_BELT_UP, BP_BELT_DOWN, BP_FILTER_UP, BP_FILTER_DOWN, filterCfg, WALL_OF, WALL_LIST, BAR_OF, GRAINS_PER_BAR, POWER_FREE, CASH, PIECES, PIECE_BY_ID, PIECE_BY_CODE, BELT_POWER, SCREW_POWER, FAN_POWER, WHEEL_K, ENGINE_K, MOLDS
|
||||||
|
|
||||||
### src/sim/economy.js (152 lines)
|
### src/sim/economy.js (152 lines)
|
||||||
Money, prices, market saturation, the trade dock, buyer and builder orders.
|
Money, prices, market saturation, the trade dock, buyer and builder orders.
|
||||||
@@ -220,27 +237,27 @@ Starter furnace designs and the milestone list shown in the notebook.
|
|||||||
- functions: smelterBlueprint
|
- functions: smelterBlueprint
|
||||||
- variables: STARTER_DESIGNS, MILESTONES
|
- variables: STARTER_DESIGNS, MILESTONES
|
||||||
|
|
||||||
### src/sim/world.js (104 lines)
|
### src/sim/world.js (107 lines)
|
||||||
World state: the per-cell typed arrays, alloc(), ambient temperature by depth, waking.
|
World state: the per-cell typed arrays, alloc(), ambient temperature by depth, waking.
|
||||||
- functions: emit, bagCap, discover, alloc, depthU, depthY, meanSurface, computeAmbient, fillNoise, wakeXY, wakeHeatXY, wakeIdx, wakeHeatIdx
|
- functions: emit, bagCap, discover, alloc, depthU, depthY, meanSurface, computeAmbient, fillNoise, wakeXY, wakeHeatXY, wakeIdx, wakeHeatIdx
|
||||||
- variables: W, H, N, CW, CH, NCH, mat, temp, life, flags, stamp, part, mach, digp, noiseB, airS, netId, isPow, occ, surfY, dirtY, graniteY, basaltY, magmaY, roofY, AMB, worldSeed, surfBase, underD, shallowD, lapse, BW, BH, NB, blockP, blockV, ents, bgm, flyers, ghostBlocks, ghostMolds, awake, hot, hotLvl, powderArea, roofDirty, loadWakeOrder, loadWakePending, loadWakeIndex, tick, tickStamp, machines, molds, nets, pcells, powerDirty, beltL, beltR, tubeLists, brokenMachines, ghostMachines, ghostBlocked, EV, bot, bag, bagTotal, player, seen, ledger, stats
|
- variables: W, H, N, CW, CH, NCH, mat, temp, life, flags, stamp, part, mach, digp, noiseB, airS, netId, isPow, occ, surfY, dirtY, graniteY, basaltY, magmaY, roofY, AMB, worldSeed, surfBase, underD, shallowD, lapse, BW, BH, NB, blockP, blockV, ents, bgm, flyers, ghostBlocks, ghostMolds, awake, hot, hotLvl, powderArea, roofDirty, loadWakeOrder, loadWakePending, loadWakeIndex, tick, tickStamp, machines, molds, nets, pcells, powerDirty, beltL, beltR, screwLists, brokenMachines, ghostMachines, ghostBlocked, EV, bot, bag, bagTotal, player, seen, ledger, stats
|
||||||
|
|
||||||
### src/sim/fog.js (52 lines)
|
### src/sim/fog.js (52 lines)
|
||||||
Exploration only: never changes grains or the random stream.
|
Exploration only: never changes grains or the random stream.
|
||||||
- functions: fogAlloc, fogMark, fogSeen, fogInit, fogLook, fogRevealAround, fogRevealAll
|
- functions: fogAlloc, fogMark, fogSeen, fogInit, fogLook, fogRevealAround, fogRevealAll
|
||||||
- variables: FOG, FOG_SENSE, FOG_SENSE_FADE, fog, FW, FH
|
- variables: FOG, FOG_SENSE, FOG_SENSE_FADE, fog, FW, FH
|
||||||
|
|
||||||
### src/sim/cells.js (207 lines)
|
### src/sim/cells.js (225 lines)
|
||||||
Cell helpers (setCell, move, enter) and the falling-sand movement of powders, liquids and gases (simMove).
|
Cell helpers (setCell, move, enter) and the falling-sand movement of powders, liquids and gases (simMove).
|
||||||
- functions: setCell, swapCells, move, enter, powder, passable, liquid, gas, wakeBotPowder, simMove
|
- functions: setCell, swapCells, move, enter, enterFree, powder, passable, liquid, gas, wakeBotPowder, simMove
|
||||||
- variables: SORTP, SORTP_FUEL, LSTEP, LFILM
|
- variables: SORTP, SORTP_FUEL, LSTEP, LFILM, CORNER_SEAL
|
||||||
|
|
||||||
### src/sim/heat.js (265 lines)
|
### src/sim/heat.js (266 lines)
|
||||||
Heat: conduction, melting and freezing, reactions, fire and air, steel.
|
Heat: conduction, melting and freezing, reactions, fire and air, steel.
|
||||||
- functions: curAir, airflowAt, alloyMix, airy, hasO2, condPair, heatPass, heatChunk, neighborDir, react, char, quench, burningNear, burn, steelCheck
|
- functions: curAir, airflowAt, alloyMix, airy, hasO2, condPair, heatPass, heatChunk, neighborDir, react, char, quench, burningNear, burn, steelCheck
|
||||||
- variables: POROUS, SOIL, HOTLIQ, REACT_MIN, ALLOY
|
- variables: POROUS, SOIL, HOTLIQ, REACT_MIN, ALLOY
|
||||||
|
|
||||||
### src/sim/machines.js (148 lines)
|
### src/sim/machines.js (157 lines)
|
||||||
Machines: the power network, footprints, placing and removing machines.
|
Machines: the power network, footprints, placing and removing machines.
|
||||||
- functions: markPow, partLost, rebuildPower, netSat, evictCells, cellBuildable, machineFootprint, canPlaceMachine, addMachine, removeMachine
|
- functions: markPow, partLost, rebuildPower, netSat, evictCells, cellBuildable, machineFootprint, canPlaceMachine, addMachine, removeMachine
|
||||||
- variables: nextPowerBuild
|
- variables: nextPowerBuild
|
||||||
@@ -260,44 +277,60 @@ Pipes, pumps and outlets: each joined line holds one liquid as a count, not as g
|
|||||||
- functions: sideOf, pipeClosedSide, pipeJoins, rebuildPipes, pipeNetOf, pipeTakeShare, faceCell, pumpDraw, outletPour, pipesTick
|
- functions: sideOf, pipeClosedSide, pipeJoins, rebuildPipes, pipeNetOf, pipeTakeShare, faceCell, pumpDraw, outletPour, pipesTick
|
||||||
- variables: PIPE_CAP, PUMP_RATE, OUTLET_RATE, PUMP_MAX_T, SIDE_V, PUMP_V, OUTLET_V, pipeNets, pipeNetAt, pipesDirty
|
- variables: PIPE_CAP, PUMP_RATE, OUTLET_RATE, PUMP_MAX_T, SIDE_V, PUMP_V, OUTLET_V, pipeNets, pipeNetAt, pipesDirty
|
||||||
|
|
||||||
### src/sim/machine-tick.js (164 lines)
|
### src/sim/machine-tick.js (157 lines)
|
||||||
Per-tick machine work: steam engines, fans, belts, filters, screw tubes (machinesTick).
|
Per-tick machine work: steam engines, fans, belts, filters, screw conveyors (machinesTick).
|
||||||
- functions: engineIntake, fanBlow, machinesTick
|
- functions: engineIntake, fanBlow, machinesTick
|
||||||
|
|
||||||
### src/sim/conveyors.js (128 lines)
|
### src/sim/joins.js (125 lines)
|
||||||
How belts, filters and screw tubes move what is on or in them. What rests on a belt or filter rides along: the whole stack standing on the carrying surface steps one cell toward d together (belts run front of the line first, so the way ahead is clear). If a powder grain is blocked, the load above it stands still and the belt slips underneath. A filter's own material drops through it instead, unless it is already full.
|
Travel-directed transport ends. Precomputed probes; world/plan queries allocate nothing. End indices stay geometric: left/right, or up/down for vertical screws.
|
||||||
- functions: carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, setFilter, filterRun, roomFor, tubeSpread, tubeIntake
|
- functions: pieceEnds, joinCandidate, endJoin, pieceJoins, endAt, joinIsDrop, joinSnap, nearMiss
|
||||||
|
- variables: JOIN_ENDS, NO_PIECE
|
||||||
|
|
||||||
|
### src/sim/conveyors.js (212 lines)
|
||||||
|
How belts, filters and screw conveyors move what is on or in them. What rests on a belt or filter rides along: the whole stack standing on the carrying surface steps one cell toward d together (belts run front of the line first, so the way ahead is clear). If a powder grain is blocked, the load above it stands still and the belt slips underneath. A filter's own material drops through it instead, unless it is already full.
|
||||||
|
- functions: conveyorStep, carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, carryBeltsAndFilters, setFilter, filterRun, roomFor, screwSpread, screwIntake, screwAccepts, straightScrewNext, diagonalScrewNext
|
||||||
- variables: CARRY_DEPTH, filterR, filterL, filtersDirty
|
- variables: CARRY_DEPTH, filterR, filterL, filtersDirty
|
||||||
|
|
||||||
### src/sim/blocks.js (156 lines)
|
### src/sim/flow.js (26 lines)
|
||||||
|
Throughput for the info overlay (t49): how many grains each belt, filter and screw conveyor block hands on, per second of game time. Counting runs only while the overlay is on, so normal play pays one flag check per carried grain. Bars are not counted.
|
||||||
|
- functions: setFlowCounting, flowNote, flowCross, flowSlopeUnder, flowTick
|
||||||
|
- variables: flowOn, flowCount, flowRate
|
||||||
|
|
||||||
|
### src/sim/slope-conveyors.js (231 lines)
|
||||||
|
Cleats are rubber ribs standing up from a sloped belt's face, one every CLEAT_SPACING face cells. Loose grains treat them as solid (cleatMask). Nothing grips the bare face: grains slide down it until a cleat stops them. Each belt step moves every cleat one face cell along the belt, and the rib pushes whatever is in its way ahead of it, so the pocket in front rides along while anything heaped above the rib is left to roll back. If a rib can't push its way, its whole run stalls. Only uphill runs have cleats: downhill, the belt carries its load like a flat one.
|
||||||
|
- functions: ribMove, cleatFoot, ribCells, slopeUphill, slopeFaceAt, pushRow, scoop, pocketRoom, advanceCleat, slopeRunKey, slopeCleatsTick, rebuildCleatMask, expelFromRib, screenNudge, wakeCleatLoad
|
||||||
|
- variables: CLEAT_SPACING, CLEAT_HEIGHT, CLEAT_REACH, SCREEN_NUDGE_TICKS, cleatChunks, cleatMask, cleatCells, slopePhase, ribLog
|
||||||
|
|
||||||
|
### src/sim/blocks.js (235 lines)
|
||||||
Building blocks: placing, integrity, removing, lines and rectangles of blocks.
|
Building blocks: placing, integrity, removing, lines and rectangles of blocks.
|
||||||
- functions: blockOf, blockCellList, pieceCost, canPlaceBlock, placeBlock, blockIntegrity, removeWallCell, removeBlock, blockLine, blockRect, blockInReach, blockIs, placeBlocks
|
- functions: blockOf, blockCellList, pieceCost, setSlopeSeal, slopeSeal, slopeSealOwner, blockOwnsCell, clearStraySeals, wallCells, canPlaceBlock, placeBlock, blockIntegrity, removeWallCell, removeBlock, diagonalDrag, blockLine, blockRect, blockInReach, blockIs, placeBlocks
|
||||||
|
|
||||||
### src/sim/molds.js (317 lines)
|
### src/sim/molds.js (317 lines)
|
||||||
Molds: cavities for castings; ingot troughs empty themselves below. Removal returns set metal.
|
Molds: cavities for castings; ingot troughs empty themselves below. Removal returns set metal.
|
||||||
- functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, ejectIngot, checkMolds, moldAt, tally, collectMold, removeMold
|
- functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, ejectIngot, checkMolds, moldAt, tally, collectMold, removeMold
|
||||||
- variables: DROSS, grains, barWord
|
- variables: DROSS, grains, barWord
|
||||||
|
|
||||||
### src/sim/items.js (232 lines)
|
### src/sim/items.js (317 lines)
|
||||||
Bars and other loose objects (ents), and the bag: adding, taking, costs and refunds.
|
Bars and other loose objects (ents), and the bag: adding, taking, costs and refunds.
|
||||||
- functions: entSolidCell, entSupportCount, entFreeAt, entMark, spawnBar, removeEnt, entTubeAt, entTubeFree, entTubeMove, entTubeLane, entTubeEdge, entTubeEnter, entTubeTick, entTick, entsInCircle, bagAdd, bagTake, giveOrDrop, refundAt, canAfford, payCost, fmtMoney, costText, needText
|
- functions: entSolidCell, entSupportCount, entFreeAt, entMark, spawnBar, removeEnt, entScrewAt, entScrewFree, entScrewMove, entScrewLane, entScrewEdge, entScrewEnter, entScrewTick, entDiagonalScrewTick, entBeltScrewEnter, entSlopeBeltTick, entTick, entsInCircle, bagAdd, bagTake, giveOrDrop, refundAt, canAfford, payCost, fmtMoney, costText, needText
|
||||||
- variables: BAR_W, BAR_H, entCurM, droppedLast
|
- variables: BAR_W, BAR_H, entCurM, droppedLast
|
||||||
|
|
||||||
### src/sim/tools.js (52 lines)
|
### src/sim/tools.js (65 lines)
|
||||||
Tool helpers: reach, brushes, and plain digging.
|
Tool helpers: reach, brushes, and plain digging.
|
||||||
- functions: inReach, forBrush, drillReach, inDrillReach, toolDig
|
- functions: setBuildRange, inBuildRange, inReach, forBrush, drillReach, inDrillReach, toolDig
|
||||||
|
- variables: BUILD_MAX, buildBox
|
||||||
|
|
||||||
### src/sim/drill.js (161 lines)
|
### src/sim/drill.js (177 lines)
|
||||||
The resonance drill: tuned pulses that shatter one material.
|
The resonance drill: tuned pulses that shatter one material.
|
||||||
- functions: drillOrigin, pulseRay, builtCell, toolPulse, crumbleAround, crumbly, shatterCell, pulseTick
|
- functions: drillOrigin, pulseRay, builtCell, toolPulse, crumbleAround, crumbly, shatterCell, pulseTick, pingRadius, resonancePing
|
||||||
- variables: PULSE_W, PULSE_EVERY, PULSE_SPEED, PULSE_K, drillPulses, lastPulseTick, drillWidth, THIN_BONUS, CRUMBLE_MAX, THIN_BONUS_OFF
|
- variables: PULSE_W, PULSE_EVERY, PULSE_SPEED, PULSE_K, drillPulses, lastPulseTick, drillWidth, THIN_BONUS, CRUMBLE_MAX, THIN_BONUS_OFF, PING_VEINS
|
||||||
|
|
||||||
### src/sim/nature.js (168 lines)
|
### src/sim/nature.js (168 lines)
|
||||||
Trees (background layer), acorns, saplings, and waking freezing water.
|
Trees (background layer), acorns, saplings, and waking freezing water.
|
||||||
- functions: cutTree, treeHeld, findTreeBases, treeSupportTick, woodRoom, treeFall, coldTick, plantTick, setTreeCell, saplingTick, igniteTree
|
- functions: cutTree, treeHeld, findTreeBases, treeSupportTick, woodRoom, treeFall, coldTick, plantTick, setTreeCell, saplingTick, igniteTree
|
||||||
- variables: treeBases, saplings, treeDraw
|
- variables: treeBases, saplings, treeDraw
|
||||||
|
|
||||||
### src/sim/vacuum.js (172 lines)
|
### src/sim/vacuum.js (199 lines)
|
||||||
The vacuum, blowing grains out, grains in flight (flyers), and the flame tool. Material the vacuum would start on at the cursor: a bar, else the nearest loose grain.
|
The vacuum, blowing grains out, grains in flight (flyers), and the flame tool. Material the vacuum would start on at the cursor: a bar, else the nearest loose grain.
|
||||||
- functions: vacuumTargetAt, toolVacuum, nozzle, toolBlow, flyOpen, depositNear, bagTemp, landFlyer, flyTick, toolIgnite
|
- functions: vacuumTargetAt, toolVacuum, nozzle, toolBlow, flyOpen, depositNear, bagTemp, landFlyer, flyTick, toolIgnite
|
||||||
- variables: blowRR, FLY_G, FLY_Q
|
- variables: blowRR, FLY_G, FLY_Q
|
||||||
@@ -310,7 +343,7 @@ Player actions on built things: take back, place a machine or mold, gates, F to
|
|||||||
Blueprints: copy, cut, flip, cost and paste (as ghosts) a region of built pieces.
|
Blueprints: copy, cut, flip, cost and paste (as ghosts) a region of built pieces.
|
||||||
- functions: copyRegion, flipBlueprint, bpCost, pasteGhost, cutRegion, footprintBlocker, clearFootprint
|
- functions: copyRegion, flipBlueprint, bpCost, pasteGhost, cutRegion, footprintBlocker, clearFootprint
|
||||||
|
|
||||||
### src/sim/undo.js (87 lines)
|
### src/sim/undo.js (93 lines)
|
||||||
Undo (Ctrl+Z) for building, and the bot building ghosts.
|
Undo (Ctrl+Z) for building, and the bot building ghosts.
|
||||||
- functions: undoNote, undoPush, undoRecord, undoLast, clearAllGhosts, ghostTick, ghostTick0
|
- functions: undoNote, undoPush, undoRecord, undoLast, clearAllGhosts, ghostTick, ghostTick0
|
||||||
- variables: UNDO_MAX, undoStack, undoRec, ghostMissing, ghostIter
|
- variables: UNDO_MAX, undoStack, undoRec, ghostMissing, ghostIter
|
||||||
@@ -329,7 +362,7 @@ Weather: clouds from steam, rain and snow, and the world water count.
|
|||||||
- functions: wrnd, resetWeather, rainGap, worldWater, absorbSteam, spawnRain, rainAt, weatherTick
|
- functions: wrnd, resetWeather, rainGap, worldWater, absorbSteam, spawnRain, rainAt, weatherTick
|
||||||
- variables: WBAND, WS, weather
|
- variables: WBAND, WS, weather
|
||||||
|
|
||||||
### src/sim/tick.js (28 lines)
|
### src/sim/tick.js (29 lines)
|
||||||
simTick: one simulation step, in order.
|
simTick: one simulation step, in order.
|
||||||
- functions: simTick
|
- functions: simTick
|
||||||
|
|
||||||
@@ -342,7 +375,12 @@ World generation helpers: noise and removal of tiny hard rock specks.
|
|||||||
World generation: staged terrain, ore, caves and placement, with synchronous and async entry points.
|
World generation: staged terrain, ore, caves and placement, with synchronous and async entry points.
|
||||||
- functions: generateWorld, generateWorldAsync
|
- functions: generateWorld, generateWorldAsync
|
||||||
|
|
||||||
### src/sim/save.js (177 lines)
|
### src/sim/devconsole.js (113 lines)
|
||||||
|
Dev commands are pure simulation operations; the browser owns the panel and confirmation.
|
||||||
|
- functions: resetDevConsole, enableDevConsole, devMaterial, devCommand
|
||||||
|
- variables: devWorld, devLog, devFree, devSpeed, devPaused, devSteps, DEV_HELP, DEV_MATERIALS
|
||||||
|
|
||||||
|
### src/sim/save.js (183 lines)
|
||||||
Saving and loading a world (serializeWorld / deserializeWorld) and old-save migration.
|
Saving and loading a world (serializeWorld / deserializeWorld) and old-save migration.
|
||||||
- functions: serializeWorld, deserializeWorld, startLoadWake, wakeLoadArea, wakeLoadNearBot, loadWakeTick
|
- functions: serializeWorld, deserializeWorld, startLoadWake, wakeLoadArea, wakeLoadNearBot, loadWakeTick
|
||||||
- variables: SAVE_VERSION, TOFF
|
- variables: SAVE_VERSION, TOFF
|
||||||
@@ -352,29 +390,43 @@ Drawing (src/render/): browser only. This file: the renderer state R, colour tab
|
|||||||
- functions: glowIndex, thermIdx, pipeCellColor, pipeBodyCell, thinPipeColor, beltLike, buildSky, initRender, resizeRender, viewCells, updateCamera, snapCamera, screenToWorld, worldToScreenPx, renderMonoFont
|
- functions: glowIndex, thermIdx, pipeCellColor, pipeBodyCell, thinPipeColor, beltLike, buildSky, initRender, resizeRender, viewCells, updateCamera, snapCamera, screenToWorld, worldToScreenPx, renderMonoFont
|
||||||
- variables: R, GLOW_W, GLOW_R, GLOW_G, GLOW_B, THERM, PC, TREE_R, TREE_G, TREE_B, BG, SKY_R, SKY_G, SKY_B
|
- variables: R, GLOW_W, GLOW_R, GLOW_G, GLOW_B, THERM, PC, TREE_R, TREE_G, TREE_B, BG, SKY_R, SKY_G, SKY_B
|
||||||
|
|
||||||
### src/render/light.js (79 lines)
|
### src/render/light.js (103 lines)
|
||||||
Propagated coloured light from glowing and burning cells.
|
Propagated coloured light from glowing and burning cells.
|
||||||
- functions: computeLight
|
- functions: builtForLight, looseForLight, computeLight
|
||||||
|
|
||||||
### src/render/world.js (308 lines)
|
### src/render/slope-belts.js (167 lines)
|
||||||
|
Drawing only: the face stays on its simulation cells; the band hangs below it.
|
||||||
|
- functions: slopeBeltConnections, flatBeltCaps, prepareSlopeBelts, slopeBeltSegment, slopeBeltCorner, slopeBeltShape, slopeBeltAt, slopeBeltColor, screenLoaded, slopeWallShape, slopeWallAt, slopeDetailAt, slopePreviewCleat
|
||||||
|
- variables: slopeBeltJoins, slopeBeltHalo, beltCaps, slopeAcross, slopeAlong, slopeDistance, slopeWallOwner
|
||||||
|
|
||||||
|
### src/render/diagonal-screws.js (79 lines)
|
||||||
|
Screw casing geometry shared by both looks, ghosts and placement previews. smooth-look class of a screw conveyor cell by direction: 9 up, 10 right, 11 down, 12 left
|
||||||
|
- functions: diagonalScrewJoins, straightScrewJoins, diagonalScrewShape, diagonalScrewBlock
|
||||||
|
- variables: SCREW_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap
|
||||||
|
|
||||||
|
### src/render/world.js (404 lines)
|
||||||
Scale colour without changing the smooth-look class.
|
Scale colour without changing the smooth-look class.
|
||||||
- functions: fogColor, drawWorld
|
- functions: fogColor, drawWorld
|
||||||
- variables: TUBE_CLASS
|
|
||||||
|
|
||||||
### src/render/machines.js (242 lines)
|
### src/render/machines.js (256 lines)
|
||||||
Machine overlays: fans, engines, silo and the rest, drawn on top of the world.
|
Machine overlays: fans, engines, silo and the rest, drawn on top of the world.
|
||||||
- functions: cellRect, drawHazardRims, drawMachines, drawProcBlocks, drawSilo, drawSiloPixel, drawRackBlocks
|
- functions: cellRect, drawHazardRims, drawMachines, drawProcBlocks, drawSilo, drawSiloPixel, drawSiloWindow, drawRackBlocks
|
||||||
|
|
||||||
### src/render/overlays.js (229 lines)
|
### src/render/overlays.js (321 lines)
|
||||||
Other overlays: floating text, rain and snow, flow arrows, filter tags, molds, the bot, particles.
|
Other overlays: floating text, rain and snow, flow arrows, filter tags, molds, the bot, particles.
|
||||||
- functions: spawnFloater, drawRain, drawFlowArrows, drawRunOutline, drawFilterTags, drawFloaters, moldLabel, drawMolds, drawBot, spawnDebris, spawnSpark, spawnSuck, drawParticles
|
- functions: spawnFloater, drawRain, drawFlowArrows, drawJoinMark, drawJoinMarks, flowLoaded, drawFlowRates, pingImage, drawPing, drawRunOutline, drawFilterTags, drawFloaters, moldLabel, drawMolds, drawBot, spawnDebris, spawnSpark, spawnSuck, drawParticles
|
||||||
|
- variables: PING_SPEED, PING_FADE
|
||||||
|
|
||||||
### src/render/smooth.js (99 lines)
|
### src/render/smooth.js (99 lines)
|
||||||
Smooth look: WebGL setup, texture uploads, drawing and adaptive resolution.
|
Smooth look: WebGL setup, texture uploads, drawing and adaptive resolution.
|
||||||
- functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace
|
- functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace
|
||||||
- variables: SM, smBytes
|
- variables: SM, smBytes
|
||||||
|
|
||||||
### src/render/smooth-shaders.js (325 lines)
|
### src/render/slope-belt-shader.js (111 lines)
|
||||||
|
Smooth slope shading, kept beside the shared CPU geometry in slope-belts.js.
|
||||||
|
- variables: SM_SLOPE_HELPERS, SM_SLOPE_DRAW
|
||||||
|
|
||||||
|
### src/render/smooth-shaders.js (377 lines)
|
||||||
Smooth shaders: vertex and fragment programs for the rounded world look.
|
Smooth shaders: vertex and fragment programs for the rounded world look.
|
||||||
- variables: SM_VS, SM_FS
|
- variables: SM_VS, SM_FS
|
||||||
|
|
||||||
@@ -403,7 +455,7 @@ Shared player state; the world save remains a complete standalone fallback.
|
|||||||
- functions: careerState, validCareer, applyCareerState, loadCareer, claimName, openNewClaim, travel, renderClaimsTab
|
- functions: careerState, validCareer, applyCareerState, loadCareer, claimName, openNewClaim, travel, renderClaimsTab
|
||||||
- variables: career, claimBusy
|
- variables: career, claimBusy
|
||||||
|
|
||||||
### src/ui/session.js (157 lines)
|
### src/ui/session.js (158 lines)
|
||||||
Starting, loading and quitting a game; modal dialogs; the pause menu.
|
Starting, loading and quitting a game; modal dialogs; the pause menu.
|
||||||
- functions: showLoading, enterGame, setupNewWorld, startNewWorld, startFromSave, loadSavedWorld, quitToTitle, refreshContinue, anyModal, openModal, closeModal, closeAllModals, topModal, dialog, openPause, renderWorlds
|
- functions: showLoading, enterGame, setupNewWorld, startNewWorld, startFromSave, loadSavedWorld, quitToTitle, refreshContinue, anyModal, openModal, closeModal, closeAllModals, topModal, dialog, openPause, renderWorlds
|
||||||
|
|
||||||
@@ -430,16 +482,16 @@ Notebook blueprints: starter designs, saved library and code sharing.
|
|||||||
The tool hotbar and what is selected.
|
The tool hotbar and what is selected.
|
||||||
- functions: renderHotbar, toolId, steering, modShift, grainAt, selectTool, bagMaterials, blowList, blowAllList, blowingAll, piece
|
- functions: renderHotbar, toolId, steering, modShift, grainAt, selectTool, bagMaterials, blowList, blowAllList, blowingAll, piece
|
||||||
|
|
||||||
### src/ui/build-menu.js (104 lines)
|
### src/ui/build-menu.js (111 lines)
|
||||||
The build menu along the bottom: categories, piece tiles and their icons.
|
The build menu along the bottom: categories, piece tiles and their icons.
|
||||||
- functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar
|
- functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar
|
||||||
- variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu
|
- variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu
|
||||||
|
|
||||||
### src/ui/selection.js (96 lines)
|
### src/ui/selection.js (133 lines)
|
||||||
Tool selection: cycling, drill tuning, piece variants and costs.
|
Tool selection: cycling, drill tuning, piece variants and costs.
|
||||||
- functions: pieceSize, pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleBlow, pickFilterUnderCursor, toggleFilterMat, setBrush, pieceV, pieceExtra, filterBlockAt, chipCost, pieceCostNow
|
- functions: pieceSize, pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleBlow, pickFilterUnderCursor, toggleFilterMat, setBrush, pieceV, dragPlan, pieceExtra, filterBlockAt, chipCost, pieceCostNow, pieceCode, turnSlope
|
||||||
|
|
||||||
### src/ui/selection-pickers.js (59 lines)
|
### src/ui/selection-pickers.js (61 lines)
|
||||||
Selection pickers: filter rules, filter settings and wall materials. What a filter can hold: loose things and bars you have found, plus anything it already holds
|
Selection pickers: filter rules, filter settings and wall materials. What a filter can hold: loose things and bars you have found, plus anything it already holds
|
||||||
- functions: filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, openWallPicker
|
- functions: filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, openWallPicker
|
||||||
|
|
||||||
@@ -448,35 +500,39 @@ Selection panels: current tool card and bag materials.
|
|||||||
- functions: renderSelCard, renderBag
|
- functions: renderSelCard, renderBag
|
||||||
- variables: lastBagKey
|
- variables: lastBagKey
|
||||||
|
|
||||||
### src/ui/info.js (188 lines)
|
### src/ui/info.js (201 lines)
|
||||||
Discovery cards, hover info (I) and the cursor label.
|
Discovery cards, hover info (I) and the cursor label.
|
||||||
- functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel, pipeContentsText
|
- functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel, pipeContentsText
|
||||||
|
|
||||||
### src/ui/input.js (91 lines)
|
### src/ui/devconsole.js (42 lines)
|
||||||
|
Bottom panel; command dispatch and state live in sim/devconsole.js.
|
||||||
|
- functions: closeDevConsole, showDevConsole, toggleDevConsole, devPrint, runDevCommand
|
||||||
|
|
||||||
|
### src/ui/input.js (92 lines)
|
||||||
Keyboard and mouse listeners.
|
Keyboard and mouse listeners.
|
||||||
- functions: isTyping
|
- functions: isTyping
|
||||||
- variables: canvas
|
- variables: canvas
|
||||||
|
|
||||||
### src/ui/actions.js (239 lines)
|
### src/ui/actions.js (254 lines)
|
||||||
What input does: overlays, F, pick, copy / cut / paste, undo, building and tool use each frame. Which overlays are on, shown as a coloured frame round the screen and a named badge
|
What input does: overlays, F, pick, copy / cut / paste, undo, building and tool use each frame. Which overlays are on, shown as a coloured frame round the screen and a named badge
|
||||||
- functions: updateOverlayUI, toggleLens, doInteract, pickUnderCursor, aimBlock, stampOrigin, selModeHeld, startPaste, stopSelectAndPaste, plural, piecesIn, doUndo, endRmbRecord, dragDirFor, blockLabel, primaryPress, primaryRelease, secondaryPress, toolTick
|
- functions: updateOverlayUI, toggleLens, doInteract, pickUnderCursor, aimBlock, stampOrigin, selModeHeld, startPaste, stopSelectAndPaste, plural, piecesIn, doUndo, endRmbRecord, dragDirFor, blockLabel, primaryPress, primaryRelease, secondaryPress, firePing, toolTick
|
||||||
- variables: ovShown
|
- variables: ovShown, PING_RECHARGE
|
||||||
|
|
||||||
### src/ui/gamepad.js (60 lines)
|
### src/ui/gamepad.js (60 lines)
|
||||||
Gamepad input.
|
Gamepad input.
|
||||||
- functions: padPressed, pollPad
|
- functions: padPressed, pollPad
|
||||||
- variables: PAD
|
- variables: PAD
|
||||||
|
|
||||||
### src/ui/previews.js (190 lines)
|
### src/ui/previews.js (241 lines)
|
||||||
Cursor previews: shared grid helpers, air streaks and tool preview dispatch.
|
Cursor previews: shared grid helpers, air streaks and tool preview dispatch.
|
||||||
- functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews
|
- functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews, previewBlock
|
||||||
|
|
||||||
### src/ui/preview-drill.js (104 lines)
|
### src/ui/preview-drill.js (107 lines)
|
||||||
Drill previews: shaken rock cracks, pulse wavefronts and aiming fan. Rock the drill has shaken but not yet broken shows cracks, more and finer as it nears breaking. Each crack is a contour line of a noise field (marching squares, cell by cell), so the lines run on unbroken from one cell to the next and look like real fractures, not a pattern.
|
Drill previews: shaken rock cracks, pulse wavefronts and aiming fan. Rock the drill has shaken but not yet broken shows cracks, more and finer as it nears breaking. Each crack is a contour line of a noise field (marching squares, cell by cell), so the lines run on unbroken from one cell to the next and look like real fractures, not a pattern.
|
||||||
- functions: crackSeg, drawCracks, pulseRGB, fanPoints, drawDrillPulses, drawDrillAim
|
- functions: crackSeg, drawCracks, pulseRGB, fanPoints, drawDrillPulses, drawDrillAim
|
||||||
- variables: CRACK_N, CRACK_LV
|
- variables: CRACK_N, CRACK_LV
|
||||||
|
|
||||||
### src/ui/preview-selection.js (80 lines)
|
### src/ui/preview-selection.js (81 lines)
|
||||||
Selection previews: labelled tags, copy and cut boxes, and paste ghosts. a small labelled tag drawn on the world (screen pixels)
|
Selection previews: labelled tags, copy and cut boxes, and paste ghosts. a small labelled tag drawn on the world (screen pixels)
|
||||||
- functions: drawTag, drawSelection, drawPastePreview
|
- functions: drawTag, drawSelection, drawPastePreview
|
||||||
- variables: SEL_COL
|
- variables: SEL_COL
|
||||||
@@ -490,7 +546,7 @@ Shared world colours, minimap and full map.
|
|||||||
- functions: resetMap, mapArea, updateMap, drawMap, mapFit, drawFullMap, openMap, zoomMap, zoomMinimap
|
- functions: resetMap, mapArea, updateMap, drawMap, mapFit, drawFullMap, openMap, zoomMap, zoomMinimap
|
||||||
- variables: MAP_S, MAP
|
- variables: MAP_S, MAP
|
||||||
|
|
||||||
### src/ui/loop.js (134 lines)
|
### src/ui/loop.js (138 lines)
|
||||||
The frame loop and boot.
|
The frame loop and boot.
|
||||||
- functions: scanVisible, frame, boot
|
- functions: scanVisible, frame, boot
|
||||||
- variables: lastT, acc, lastRender, frameMs, simMs, fpsSm, ambientT
|
- variables: lastT, acc, lastRender, frameMs, simMs, fpsSm, ambientT
|
||||||
@@ -504,17 +560,29 @@ The frame loop and boot.
|
|||||||
- `test/t_bar_stacking.js`: Resting bars snap into pairs of grains and tip off narrow supports.
|
- `test/t_bar_stacking.js`: Resting bars snap into pairs of grains and tip off narrow supports.
|
||||||
- `test/t_basics.js`: Basics: drilling to rubble, the vacuum and blowing, walls, the ingot mold and bars, belts, blueprints, save/load.
|
- `test/t_basics.js`: Basics: drilling to rubble, the vacuum and blowing, walls, the ingot mold and bars, belts, blueprints, save/load.
|
||||||
- `test/t_build.js`: Building: undo (Ctrl+Z), cut, replace a machine with Ctrl, pasting ghosts, and the fan heat warning.
|
- `test/t_build.js`: Building: undo (Ctrl+Z), cut, replace a machine with Ctrl, pasting ghosts, and the fan heat warning.
|
||||||
- `test/t_conveyors.js`: Screw tubes that never carry liquid (it flows through), tube and belt throughput, filters.
|
- `test/t_conveyors.js`: Screw conveyors that never carry liquid (it flows through), screw and belt throughput, filters.
|
||||||
|
- `test/t_corner_seal.js`: Corner-touching square walls seal loose grains without changing free piles or worldgen.
|
||||||
|
- `test/t_dev_console.js`: Dev command validation, discovery, build costs, runtime controls and save metadata.
|
||||||
|
- `test/t_dev_console.py`: Dev console confirmation, keyboard isolation, HUD, time controls and saved badge.
|
||||||
|
- `test/t_diag_screws.js`: Diagonal screws: sealed four-lane corner transfers, bars, joins and persistence.
|
||||||
- `test/t_drill.js`: The resonance drill: pulses from the bot shake rock until it shatters. Widths trade area for speed, tuning shatters only one material and drains it out of the wall, built blocks are left alone, and the drill says why when nothing breaks.
|
- `test/t_drill.js`: The resonance drill: pulses from the bot shake rock until it shatters. Widths trade area for speed, tuning shatters only one material and drains it out of the wall, built blocks are left alone, and the drill says why when nothing breaks.
|
||||||
- `test/t_econ.js`: Economy: the dock (instant sale), orders, prices, the market dip, money for machines, alloys.
|
- `test/t_econ.js`: Economy: the dock (instant sale), orders, prices, the market dip, money for machines, alloys.
|
||||||
|
- `test/t_flow.js`: Throughput overlay (t49): grains per second per transport block, counted only while the overlay is on, never changing what the simulation does.
|
||||||
- `test/t_fog.js`: Fog: surface, sight, range, saves and simulation independence.
|
- `test/t_fog.js`: Fog: surface, sight, range, saves and simulation independence.
|
||||||
- `test/t_furnace_repeat.js`: The starter furnaces keep working when fired again and again without cleaning, however the charge goes in: mixed, ore first with coal on top, or blown in through the vent. Each case plays furnace_play.js, which builds the design and fires it like a player.
|
- `test/t_furnace_repeat.js`: The starter furnaces keep working when fired again and again without cleaning, however the charge goes in: mixed, ore first with coal on top, or blown in through the vent. Each case plays furnace_play.js, which builds the design and fires it like a player.
|
||||||
|
- `test/t_joins.js`: The oracle states the geometric rule independently of the precomputed neighbour tables.
|
||||||
|
- `test/t_light.js`: Run the renderer's lighting against real simulation data, with only canvas storage stubbed.
|
||||||
- `test/t_load.js`: Reload wakes nearby areas first, without losing distant water, fire or saved state.
|
- `test/t_load.js`: Reload wakes nearby areas first, without losing distant water, fire or saved state.
|
||||||
- `test/t_molds.js`: Molds and furnaces stay usable: ingots drop below and keep their stubs, plugs over molds are cleared, spoiled castings can be knocked out, grains never drop through a grate by teleporting, and slag lets the fire breathe.
|
- `test/t_molds.js`: Molds and furnaces stay usable: ingots drop below and keep their stubs, plugs over molds are cleared, spoiled castings can be knocked out, grains never drop through a grate by teleporting, and slag lets the fire breathe.
|
||||||
|
- `test/t_nozzle.js`: Blowing aims within the nozzle's reach, with room for the brush and settling.
|
||||||
- `test/t_physics.js`: Sand and other powders, molten metal and gases: heaps, sorting, sinking, setting, rising and fading. Water has its own suite (t_water.js). Tall worlds keep geothermal heat out of the way.
|
- `test/t_physics.js`: Sand and other powders, molten metal and gases: heaps, sorting, sinking, setting, rising and fading. Water has its own suite (t_water.js). Tall worlds keep geothermal heat out of the way.
|
||||||
- `test/t_pipes.js`: Pipes, pumps and outlets move liquids; the Grinder grinds waste rock for trace ore.
|
- `test/t_pipes.js`: Pipes, pumps and outlets move liquids; the Grinder grinds waste rock for trace ore.
|
||||||
- `test/t_racks.js`: Rack intake, FIFO storage, gate, removal and saved-state validation.
|
- `test/t_racks.js`: Rack intake, FIFO storage, gate, removal and saved-state validation.
|
||||||
|
- `test/t_silo_contents.js`: Stored snow keeps its identity and remains in the world's water budget.
|
||||||
- `test/t_sim_optimizations.js`: Optimized paths must match the full roof scan and the original heat loop exactly.
|
- `test/t_sim_optimizations.js`: Optimized paths must match the full roof scan and the original heat loop exactly.
|
||||||
|
- `test/t_slope_belt_art.js`: Drawing geometry against real parts, without changing transport.
|
||||||
|
- `test/t_slope_belts.js`: Sloped belts: transport, joins, sorting, persistence and the actual drag controls.
|
||||||
|
- `test/t_slopes.js`: Sloped walls: flow, sealed joints, building, refunds and persistence.
|
||||||
- `test/t_smooth.py`: The smooth look: the WebGL pass compiles, draws, falls back cleanly, and the thermal lens still works.
|
- `test/t_smooth.py`: The smooth look: the WebGL pass compiles, draws, falls back cleanly, and the thermal lens still works.
|
||||||
- `test/t_stone.js`: Uses for waste rock and sand: glass bricks from a fan furnace, the Saw, the silo and builders' orders.
|
- `test/t_stone.js`: Uses for waste rock and sand: glass bricks from a fan furnace, the Saw, the silo and builders' orders.
|
||||||
- `test/t_water.js`: Water conservation, settling, ledge pouring, boundaries, pipes and phase changes. Tall worlds keep geothermal heat out of the ponds.
|
- `test/t_water.js`: Water conservation, settling, ledge pouring, boundaries, pipes and phase changes. Tall worlds keep geothermal heat out of the ponds.
|
||||||
@@ -529,15 +597,25 @@ The frame loop and boot.
|
|||||||
- `tools/where.py`: Turn a line number in dist/index.html into the source file and line it came from.
|
- `tools/where.py`: Turn a line number in dist/index.html into the source file and line it came from.
|
||||||
- `tools/experiments/cast_sunk_mold.js`: Block-sized bit mold sunk into the ground with a stone furnace built over its funnel.
|
- `tools/experiments/cast_sunk_mold.js`: Block-sized bit mold sunk into the ground with a stone furnace built over its funnel.
|
||||||
- `tools/experiments/factory_cost.js`: What does a running factory cost when the player is somewhere else? Builds K self-running modules in a generated world, far from the bot, and measures simulation time per tick. Each module: a coal + copper-ore furnace burning in a stone-block box, a spring feeding a water wheel, a shaft carrying its power to a 10-block belt that hauls sand off a hopper.
|
- `tools/experiments/factory_cost.js`: What does a running factory cost when the player is somewhere else? Builds K self-running modules in a generated world, far from the bot, and measures simulation time per tick. Each module: a coal + copper-ore furnace burning in a stone-block box, a spring feeding a water wheel, a shaft carrying its power to a 10-block belt that hauls sand off a hopper.
|
||||||
- `tools/experiments/flow.js`: Throughput of belts and screw tubes: grains delivered per second. Whatever lands past the outlet is taken away, so the output pile never backs up into the line.
|
- `tools/experiments/flow.js`: Throughput of belts and screw conveyors: grains delivered per second. Whatever lands past the outlet is taken away, so the output pile never backs up into the line.
|
||||||
- `tools/experiments/furnace.js`: Furnace scenarios on a flat test world: builds a chamber, charges it with ore and coal, lights it and prints temperatures, burning cells and what has melted or set at intervals.
|
- `tools/experiments/furnace.js`: Furnace scenarios on a flat test world: builds a chamber, charges it with ore and coal, lights it and prints temperatures, burning cells and what has melted or set at intervals.
|
||||||
- `tools/experiments/section.js`: ASCII cross-section of a stone furnace firing, with a temperature digit per cell (0-9), printed at intervals. Usage: node tools/experiments/section.js [WALL_MATERIAL] (default STONE).
|
- `tools/experiments/section.js`: ASCII cross-section of a stone furnace firing, with a temperature digit per cell (0-9), printed at intervals. Usage: node tools/experiments/section.js [WALL_MATERIAL] (default STONE).
|
||||||
|
- `tools/experiments/slope-cost.js`: Many loaded slopes, with a fresh deterministic world for each timing sample.
|
||||||
|
- `tools/experiments/slope-flow.js`: Reproducible generous-feed throughput and settled pile angles for t42.
|
||||||
- `tools/experiments/smelter.js`: Smelter design experiments, built with the same block pieces a player uses. Layout (block rows, 4x4 grains each), on flat ground at row 25: roof with a vent ........ row g-CH-1 chamber, CH blocks tall .. charge of coal + copper ore inside grate floor ............. row g = 23 (liquids pass, powders do not) ingot mold .............. row 24, sitting on the ground side walls run from the roof down past the mold. Usage: node t_smelter.js [key=value ...]
|
- `tools/experiments/smelter.js`: Smelter design experiments, built with the same block pieces a player uses. Layout (block rows, 4x4 grains each), on flat ground at row 25: roof with a vent ........ row g-CH-1 chamber, CH blocks tall .. charge of coal + copper ore inside grate floor ............. row g = 23 (liquids pass, powders do not) ingot mold .............. row 24, sitting on the ground side walls run from the roof down past the mold. Usage: node t_smelter.js [key=value ...]
|
||||||
- `tools/experiments/starter.js`: Plays a starter design the way a player would: paste the blueprint, let the bot build it from the bag, blow an even ore/coal mix inside, light it, wait, knock out the castings. Usage: node tools/experiments/starter.js [design id] [ore] [coal]
|
- `tools/experiments/starter.js`: Plays a starter design the way a player would: paste the blueprint, let the bot build it from the bag, blow an even ore/coal mix inside, light it, wait, knock out the castings. Usage: node tools/experiments/starter.js [design id] [ore] [coal]
|
||||||
- `tools/experiments/worldgen.js`: World generation report: time to generate, how many cells of each material, where the bot starts, and what idle ticks and ticks after digging cost.
|
- `tools/experiments/worldgen.js`: World generation report: time to generate, how many cells of each material, where the bot starts, and what idle ticks and ticks after digging cost.
|
||||||
- `tools/scenes/bar-lift.js`: A mold drops copper bars onto a belt, through a screw lift and onto the upper belt.
|
- `tools/scenes/bar-lift.js`: A mold drops copper bars onto a belt, through a screw lift and onto the upper belt.
|
||||||
- `tools/scenes/belt-heap.js`: A belt feeding an upward screw: sand underneath a deep heap of copper ore. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/belt-heap.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/belt-heap.js)" --ticks 240 --zoom 7
|
- `tools/scenes/belt-heap.js`: A belt feeding an upward screw: sand underneath a deep heap of copper ore. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/belt-heap.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/belt-heap.js)" --ticks 240 --zoom 7
|
||||||
- `tools/scenes/clear-sky.js`: A clear work area left of the bot's starting point (the trade dock is to its right): open sky 160 grains wide and 40 tall, over a flat stone floor. Money and a bag of building stuff to build with. Sets FLOOR (the first row of the floor), BX, BY (the leftmost block of the area that stands on the floor) and CAM (the middle of the area).
|
- `tools/scenes/clear-sky.js`: A clear work area left of the bot's starting point (the trade dock is to its right): open sky 160 grains wide and 40 tall, over a flat stone floor. Money and a bag of building stuff to build with. Sets FLOOR (the first row of the floor), BX, BY (the leftmost block of the area that stands on the floor) and CAM (the middle of the area).
|
||||||
|
- `tools/scenes/joins.js`: Travel join grid: UR/DR/DL/UL, both ends, and all three neighbouring placements. Screw H and V, then belt H; each tries the side, above/below, and E. Self-contained, paused and empty. Use --zoom 4 --viewport 2100x1500 --no-light.
|
||||||
- `tools/scenes/mold-labels.js`: Run after clear-sky.js: filling, blocked, set, and hot machinery in one view.
|
- `tools/scenes/mold-labels.js`: Run after clear-sky.js: filling, blocked, set, and hot machinery in one view.
|
||||||
- `tools/scenes/silo.js`: Run after clear-sky.js; leave paused so the open gate keeps its demonstration charge.
|
- `tools/scenes/silo.js`: Run after clear-sky.js; leave paused so the open gate keeps its demonstration charge.
|
||||||
|
- `tools/scenes/slope-conveyors.js`: Loaded sidewall conveyor and screen. Use --js 'slopeScene(false)' (belt) or 'slopeScene(true)' (screen).
|
||||||
- `tools/scenes/stone-uses.js`: The stone and sand machines: a half-full silo with its gate open over a belt, a stone cutter fed with rubble, and cut stone and glass brick walls. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/stone.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/stone-uses.js)" --ticks 240 --zoom 7
|
- `tools/scenes/stone-uses.js`: The stone and sand machines: a half-full silo with its gate open over a belt, a stone cutter fed with rubble, and cut stone and glass brick walls. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/stone.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/stone-uses.js)" --ticks 240 --zoom 7
|
||||||
|
- `tools/scenes/t-screen-joint.js`: Downhill screen whose outlet belt sits level with its last block (the jam the owner saw).
|
||||||
|
- `tools/scenes/t33-ground.js`: Paused dug pocket: copper ore, rubble and dirt against stone, granite and dirt. Use --js "..." to set daylight or R.lens after this setup; CAM frames the pocket.
|
||||||
|
- `tools/scenes/t35-diagonal.js`: Run after clear-sky.js: a belt takes sand from a pile, up a diagonal screw to an upper belt.
|
||||||
|
- `tools/scenes/t35-joins.js`: Run after clear-sky.js. Paused, empty pipes make casing, flights and mouths easy to inspect.
|
||||||
|
- `tools/scenes/t36-funnel.js`: A twelve-block funnel, one block open at the bottom, draining 400 grains of sand.
|
||||||
|
- `tools/scenes/t40-sloped-belt.js`: Self-contained: a flat feed climbs, crosses the top, and runs downhill; a filter sorts below.
|
||||||
|
|||||||
+1
-1
@@ -130,5 +130,5 @@ python3 tools/shot.py shots/silo.png --setup tools/scenes/clear-sky.js \
|
|||||||
|
|
||||||
- `tools/perf.py`: renderer and tick times in the browser.
|
- `tools/perf.py`: renderer and tick times in the browser.
|
||||||
- `tools/experiments/factory_cost.js`: what running factories cost the simulation.
|
- `tools/experiments/factory_cost.js`: what running factories cost the simulation.
|
||||||
- `tools/experiments/flow.js`: belt and tube throughput in grains a second.
|
- `tools/experiments/flow.js`: belt and screw conveyor throughput in grains a second.
|
||||||
- `tools/experiments/worldgen.js`: material counts and timings of world generation.
|
- `tools/experiments/worldgen.js`: material counts and timings of world generation.
|
||||||
|
|||||||
@@ -4,6 +4,602 @@ What agents have done, newest first. Add an entry for every piece of work: the d
|
|||||||
note or request it was for, what changed, the files touched, the tests run, and anything left
|
note or request it was for, what changed, the files touched, the tests run, and anything left
|
||||||
open or not verified. Keep each entry short; the git history has the details.
|
open or not verified. Keep each entry short; the git history has the details.
|
||||||
|
|
||||||
|
## 2026-10-27: t75 ping recharge, natural ore colour, glints (Claude Code)
|
||||||
|
|
||||||
|
- 10 s recharge (`PING_RECHARGE`, `pingWait`) with a drill panel chip. `pingImage` draws veins in
|
||||||
|
their lit colour (base + per-grain noise) with a lighter rim; the ring is dimmer; up to 60 glints
|
||||||
|
per ping flash as the ring passes. Follow-up: veins fade by distance (lit 1.2 s, fade 0.8 s,
|
||||||
|
drawn in 6 alpha bands) and `drawPingRecharge` shows an arc around the bot with the drill. Files: `src/ui/actions.js`, `src/ui/selection-panels.js`,
|
||||||
|
`src/render/overlays.js`, `src/ui/loop.js`. Tests: full suite with --browser passed. The ring and glints were not
|
||||||
|
caught in a screenshot (they finish before the frame is taken).
|
||||||
|
|
||||||
|
## 2026-10-27: bag counted things its list didn't show (Claude Code)
|
||||||
|
|
||||||
|
- Owner's exported save: bag total 1,522 but the list showed nothing for 1,000 basalt (from the
|
||||||
|
dev console's give), 200 clay (source not traced) and 322 molten slag (vacuumed with the Flask;
|
||||||
|
not in BAG_ORDER). `bagAdd` now stores solids as their drill yield; `deserializeWorld` converts
|
||||||
|
solids already in a saved bag; `bagMaterials` appends anything outside BAG_ORDER; dev `give`
|
||||||
|
reports what it added and refuses gases and solids with no yield.
|
||||||
|
- Files: `src/sim/items.js`, `src/sim/save.js`, `src/ui/hud.js`, `src/sim/devconsole.js`,
|
||||||
|
`test/t_basics.js`, `test/t_dev_console.js`. Tests: `python3 test/run_all.py --browser` all passed.
|
||||||
|
|
||||||
|
## 2026-10-27: t50 resonance ping (Claude Code)
|
||||||
|
|
||||||
|
- Right click with the drill: `firePing` (`src/ui/actions.js`) calls `resonancePing` (`src/sim/drill.js`),
|
||||||
|
which lists natural vein cells (coal seam and the six ore veins) within 3x the bit's reach.
|
||||||
|
`drawPing` (`src/render/overlays.js`) draws an echo ring and the veins inside it over fog, from
|
||||||
|
a one-time offscreen image (one clipped drawImage per frame), fading over 4.5 s. 3 s recharge.
|
||||||
|
- Files: `src/sim/drill.js`, `src/ui/actions.js`, `src/render/overlays.js`, `src/ui/loop.js`,
|
||||||
|
`src/ui/notebook.js`, `src/ui/selection-panels.js`, `test/t_drill.js`, `docs/PROJECT_MAP.md`.
|
||||||
|
- Tests: 4 checks in `test/t_drill.js`; `python3 test/run_all.py --browser` all passed.
|
||||||
|
|
||||||
|
## 2026-10-27: t64 build range about the screen; out-of-range replace ghosts (Claude Code)
|
||||||
|
|
||||||
|
- `setBuildRange`/`inBuildRange` in `src/sim/tools.js`: the UI sets the view box each frame
|
||||||
|
(`src/ui/loop.js`); half-sizes clamped to REACH..BUILD_MAX (64..200 grains); with no view (tests)
|
||||||
|
a REACH box around the bot. Used by `blockInReach`, machine/mold placement and previews,
|
||||||
|
right-click take-back, hover reach, and ghost building for blocks, machines and molds.
|
||||||
|
- A replace (G) placed beyond range stores a replace flag on the ghost (`[code, v, extra, 1]`);
|
||||||
|
`ghostTick0` removes the old piece (refunded) and places the new one (charged), or re-sets a
|
||||||
|
filter's settings, when the bot comes in range.
|
||||||
|
- Files: `src/sim/tools.js`, `src/sim/blocks.js`, `src/sim/undo.js`, `src/ui/actions.js`,
|
||||||
|
`src/ui/previews.js`, `src/ui/info.js`, `src/ui/loop.js`, `test/t_build.js`, `docs/DESIGN.md`,
|
||||||
|
`docs/PROJECT_MAP.md`. Tests: `python3 test/run_all.py --browser` all passed. Not played by hand.
|
||||||
|
|
||||||
|
## 2026-10-27: t49 throughput overlay (Claude Code)
|
||||||
|
|
||||||
|
- The I overlay also shows grains per second at each line's open outlet, an orange glow on
|
||||||
|
transport blocks that are loaded but not moving, and the rate in the hover. `src/sim/flow.js`
|
||||||
|
counts block-edge crossings per carrier block while `flowOn` (set by the I toggle) and folds
|
||||||
|
them into a smoothed per-second rate once a second. Hooks: `carryColumn`, `machinesTick` screws,
|
||||||
|
`screenNudge`, uphill cleat runs (only runs that commit), and the uphill face drag.
|
||||||
|
- Not counted: bars. Not built: money per minute.
|
||||||
|
- Files: `src/sim/flow.js`, `src/manifest.json`, `src/sim/conveyors.js`, `src/sim/machine-tick.js`,
|
||||||
|
`src/sim/slope-conveyors.js`, `src/sim/tick.js`, `src/render/overlays.js`, `src/ui/loop.js`,
|
||||||
|
`src/ui/input.js`, `src/ui/info.js`, `src/ui/notebook.js`, `test/t_flow.js`, `docs/PROJECT_MAP.md`.
|
||||||
|
- Tests: `test/t_flow.js` (including a check that counting changes nothing in the simulation);
|
||||||
|
`python3 test/run_all.py --browser` all passed.
|
||||||
|
|
||||||
|
## 2026-10-27: cooled magma visible and drillable (Claude Code)
|
||||||
|
|
||||||
|
- Owner report: bag magma in a brick box left invisible basalt that couldn't be picked up,
|
||||||
|
drilled or built on; melted screws left similar bits. Cause: melts froze into non-natural
|
||||||
|
rock unless the liquid was natural, the drill only breaks natural rock, and the smooth look's
|
||||||
|
terrain field skipped non-natural solids.
|
||||||
|
- `src/sim/heat.js`: non-metal, non-glass melts freeze into natural rock; bag water into natural
|
||||||
|
ice. `src/sim/drill.js`: `crumbly` no longer needs F_NAT; `builtCell` protects slope corner cells.
|
||||||
|
`src/render/world.js`: the terrain field covers any terrain-class solid without a block or
|
||||||
|
machine. Note: the bot still passes through old stuck basalt until it is drilled (it only
|
||||||
|
collides with natural solids).
|
||||||
|
- Tests: 7 checks in `test/t_drill.js`; `python3 test/run_all.py --browser` all passed.
|
||||||
|
|
||||||
|
## 2026-10-27: t67 phase 4, "doesn't connect" marks (Claude Code)
|
||||||
|
|
||||||
|
- `nearMiss(bx, by, end, planned)` in `src/sim/joins.js`: an end with no join that would join
|
||||||
|
seamlessly, with a diagonal piece involved, one block up or down in empty space. Drop joins
|
||||||
|
count as connected; straight beside straight is never marked. It checks the moved block
|
||||||
|
through a small override view of `planned`, so ghost maps are never copied.
|
||||||
|
- Marks: a red ring with a slash at the end, on built pieces and ghosts (`drawJoinMarks` in
|
||||||
|
`src/render/overlays.js`, called every frame from `src/ui/loop.js`) and at the ends of a drag
|
||||||
|
preview (`src/ui/previews.js`). Hover says "doesn't connect: place it one block higher/lower"
|
||||||
|
(`src/ui/info.js`). This covers t58's mark for belts and the same cases for screws.
|
||||||
|
- Tests: 7 nearMiss checks in `test/t_joins.js`; `python3 test/run_all.py --browser` all passed.
|
||||||
|
`docs/PROJECT_MAP.md` regenerated.
|
||||||
|
|
||||||
|
## 2026-10-27: t67 phase 3 shared drag placement, and snapping (Claude Code)
|
||||||
|
|
||||||
|
- `dragPlan(P, a, b, shift)` in `src/ui/selection.js` gives code, direction, variant, blocks and
|
||||||
|
a planned map for every block piece. `primaryRelease` and the drag preview both use it (the
|
||||||
|
duplicated `blockLine(...)` expression and the screw/slope-only planned map are gone).
|
||||||
|
- Snapping: `joinSnap` in `src/sim/joins.js` moves a line one block up or down when its first
|
||||||
|
block would not join but would join seamlessly there with a diagonal piece involved, and
|
||||||
|
every block there is free. Fixes the owner's report that an uphill belt started beside a flat
|
||||||
|
belt on the same level did not join like screws do: it now snaps up to the seamless join. Also
|
||||||
|
covers t58's flat belt level with a downhill low end (snaps down) and a diagonal screw started
|
||||||
|
one block off. Straight beside straight never snaps. `joinIsDrop` (same-row feed into a slope's
|
||||||
|
low end) is now the one source for the drop, used by snapping and belt drawing.
|
||||||
|
- Files: `src/sim/joins.js`, `src/ui/selection.js`, `src/ui/actions.js`, `src/ui/previews.js`,
|
||||||
|
`src/render/slope-belts.js`, `test/t_joins.js`, `test/t_slope_belts.js`.
|
||||||
|
- Tests: `python3 test/run_all.py --browser` all passed; 9 snap checks plus a real
|
||||||
|
`primaryRelease` check. Open: phase 4's "doesn't connect" mark is not built yet.
|
||||||
|
|
||||||
|
## 2026-10-27: t66 dev console (Claude Code + Codex)
|
||||||
|
|
||||||
|
- Backtick opens a dev console in any build. The first time in a world it warns that the save
|
||||||
|
will be marked; accepting saves `dev: true` and a `devLog` (last 200 commands) in the world,
|
||||||
|
and the worlds list shows a "Dev console" badge. Commands: help, money, debt, give (display or
|
||||||
|
internal name, past the bag limit), bit, bag, unlock, tp (cursor or x y), free on|off, speed,
|
||||||
|
pause, step, weather clear|rain. Command logic is sim-side (`devCommand` in
|
||||||
|
`src/sim/devconsole.js`); the panel is `src/ui/devconsole.js`.
|
||||||
|
- Not built: `time` (no clean way to set the time of day) and undo (commands are logged instead).
|
||||||
|
Free building also skips `payCost`, so ghosts built later are free too; removing a piece built
|
||||||
|
for free still refunds its normal cost.
|
||||||
|
- Also fixed a flaky build-card check: the wall-material chip markup (`data-wall-picker=""`)
|
||||||
|
made the selection card rebuild its button every refresh.
|
||||||
|
- Files: `src/sim/devconsole.js`, `src/ui/devconsole.js`, `src/styles/devconsole.css`,
|
||||||
|
`src/shell.html`, `src/manifest.json`, `src/sim/items.js`, `src/sim/save.js`, `src/sim/world.js`,
|
||||||
|
`src/ui/input.js`, `src/ui/loop.js`, `src/ui/saves.js`, `src/ui/session.js`,
|
||||||
|
`src/ui/selection.js`, `src/ui/notebook.js`, `src/ui/build-menu.js`, tests
|
||||||
|
`t_dev_console.js`, `t_dev_console.py`, `test/run_all.py`, `docs/PROJECT_MAP.md`.
|
||||||
|
- Tests: `python3 test/run_all.py --browser` all passed. Built in a parallel worktree; Codex
|
||||||
|
hit its usage limit near the end and Claude finished it (the payCost fix).
|
||||||
|
|
||||||
|
## 2026-10-27: t67 belt drop drawn as two capped ends (Claude Code)
|
||||||
|
|
||||||
|
- A flat belt feeding an uphill belt's low end on the same row (and two uphill slopes side by
|
||||||
|
side) stays joined, but grains really drop about 3 grains onto the face there, so it now
|
||||||
|
draws as two capped ends instead of the bridge phase 2b drew (it overlapped and stepped
|
||||||
|
badly). `beltDrop` in `src/render/slope-belts.js` decides it for `slopeBeltConnections` and
|
||||||
|
`flatBeltCaps`; the shader and `world.js` packing went back to their pre-2b versions.
|
||||||
|
- Tests: `t_joins.js` and `t_smooth.py` drop checks updated; the existing sand test confirms
|
||||||
|
the same-row feed carries sand up. `python3 test/run_all.py --browser` all passed.
|
||||||
|
|
||||||
|
## 2026-10-27: t67 phase 2b, joins follow the travel direction (Claude Code + Codex)
|
||||||
|
|
||||||
|
- The owner tested phase 2: horizontal → uphill and vertical-up → diagonal didn't join like the
|
||||||
|
rest. New rule (DESIGN.md): a piece's outlet points at its block plus its travel vector, and
|
||||||
|
the next piece joins when it sits there travelling the same general way. Belts bend it where
|
||||||
|
grains can't follow: an uphill belt's top feeds a flat belt on the same row, and a flat belt
|
||||||
|
one block below an uphill low end still joins.
|
||||||
|
- `src/sim/joins.js` reworked around inlets and outlets; screw drawing, belt drawing, screw
|
||||||
|
grain flow and bars follow it. Straight-screw mouths no longer reach a diagonal across a
|
||||||
|
corner; straight-to-straight transfers (lift side-feeding) unchanged.
|
||||||
|
- Files: `src/sim/joins.js`, `src/sim/conveyors.js`, `src/sim/items.js`,
|
||||||
|
`src/render/diagonal-screws.js`, `src/render/slope-belts.js`, `src/render/slope-belt-shader.js`,
|
||||||
|
`src/render/world.js`, `src/ui/previews.js`, `tools/scenes/joins.js`, tests `t_joins.js`,
|
||||||
|
`t_diag_screws.js`, `t_slope_belt_art.js`, `t_smooth.py`, `docs/PROJECT_MAP.md`.
|
||||||
|
- Tests: `python3 test/run_all.py --browser` all passed.
|
||||||
|
- Open: Codex hit its usage limit before finishing; the belt drop drawing was finished in the
|
||||||
|
next entry. Not playtested.
|
||||||
|
|
||||||
|
## 2026-10-27: t67 phase 2, screw conveyors follow the join rule (Claude Code + Codex)
|
||||||
|
|
||||||
|
- Screw drawing, grain flow and bars now all go through `endJoin`. New joins: a horizontal in
|
||||||
|
E at a diagonal's low end, and a vertical in E at either end; both draw an elbow that bends
|
||||||
|
inside the straight piece's end block. Same-row high-end elbows are unchanged.
|
||||||
|
- No longer joined (moves nothing): a same-row horizontal at a low end, a vertical directly
|
||||||
|
above or below a diagonal's last block, and any other side contact. Joined belts and filters
|
||||||
|
take discharge on their carrying face at either end. A horizontal screw feeding the side of a
|
||||||
|
vertical lift still works (`straightScrewNext` keeps straight-to-straight transfers).
|
||||||
|
- The opposite-direction check is one helper, `screwAccepts`.
|
||||||
|
- Files: `src/render/diagonal-screws.js`, `src/render/smooth-shaders.js`, `src/render/world.js`,
|
||||||
|
`src/ui/previews.js`, `src/sim/conveyors.js`, `src/sim/machine-tick.js`, `src/sim/items.js`,
|
||||||
|
`src/sim/cells.js`, `tools/scenes/joins.js` (replaces `t62-joins.js`), `test/t_diag_screws.js`,
|
||||||
|
`test/t_smooth.py`, `docs/PROJECT_MAP.md`.
|
||||||
|
- Tests: `python3 test/run_all.py --browser` all passed; determinism diff changed only screw
|
||||||
|
checks. Screenshots: `shots/joins-*.png`.
|
||||||
|
- Open: inside the new elbows the flight stripes of both pieces overlap, which looks a little
|
||||||
|
busy in the smooth look. Layouts built on the removed joins need moving.
|
||||||
|
|
||||||
|
## 2026-10-27: t63 vacuum picks up grains in mid-air (Claude Code + Codex)
|
||||||
|
|
||||||
|
- `toolVacuum` now takes flyers (grains a screw launches or that were blown out) inside its
|
||||||
|
brush and reach before loose cells, with the same pick filter, flask rule, bag-full stop and
|
||||||
|
48-grain budget. `vacuumTargetAt` also considers them. Rain is skipped: it belongs to the
|
||||||
|
clouds, and taking it would make water.
|
||||||
|
- Files: `src/sim/vacuum.js`, `test/t_basics.js`. Built on a separate worktree branch in
|
||||||
|
parallel with t67 and fast-forwarded.
|
||||||
|
- Tests: `python3 test/run_all.py --browser` all passed; new checks only in the determinism diff.
|
||||||
|
|
||||||
|
## 2026-10-27: t67 phase 1, shared join module (Claude Code + Codex)
|
||||||
|
|
||||||
|
- Owner decision (DESIGN.md, replaces the t62 same-row rule): one join rule for every transport
|
||||||
|
piece. New DOM-free `src/sim/joins.js`: `pieceEnds`, `endJoin`, `pieceJoins`; rule tables are
|
||||||
|
compiled once and joins must be mutual. Belt drawing now uses it: `slopeBeltConnections`,
|
||||||
|
`flatBeltCaps` (now also for ghosts), and the pixel filter frame (`beltLike(b, end)`). Sloped
|
||||||
|
belt drag previews now pass a planned map.
|
||||||
|
- Deliberate drawing changes where the old belt rules disagreed: flat belt caps no longer
|
||||||
|
treat any same-row diagonal screw as joined, and filter frames open only at a real join.
|
||||||
|
- Files: `src/sim/joins.js`, `src/manifest.json`, `src/render/slope-belts.js`,
|
||||||
|
`src/render/setup.js`, `src/render/world.js`, `src/ui/previews.js`, `test/t_joins.js`,
|
||||||
|
`test/t_slope_belt_art.js`, `docs/PROJECT_MAP.md`, `docs/DESIGN.md`.
|
||||||
|
- Tests: `python3 test/run_all.py --browser` all passed; t_joins checks 4,232 placements,
|
||||||
|
symmetry, planned maps, and that belt sand flow agrees with the rule. Sim output unchanged.
|
||||||
|
- Open: screws still use their old rules until phase 2.
|
||||||
|
|
||||||
|
## 2026-10-27: t62 corner placements stay separate (Claude Code + Codex)
|
||||||
|
|
||||||
|
- Owner tried the corner join and found the jog odd; decision (recorded in DESIGN.md): only
|
||||||
|
same-row joins connect. Removed join kinds 5/6 and `straightScrewNext`; `diagonalScrewNext`
|
||||||
|
and `entDiagonalScrewTick` now continue across a corner only into a diagonal running the same
|
||||||
|
way, so a corner straight piece no longer receives grains or bars. Kept the shared geometry
|
||||||
|
file and the smooth-look elbow seam fix.
|
||||||
|
- Files: `src/render/diagonal-screws.js`, `src/render/smooth-shaders.js`, `src/sim/conveyors.js`,
|
||||||
|
`src/sim/items.js`, `src/sim/machine-tick.js`, `test/t_diag_screws.js`, `test/t_smooth.py`,
|
||||||
|
`tools/scenes/t62-joins.js`, `docs/PROJECT_MAP.md`, `docs/DESIGN.md`.
|
||||||
|
- Tests: `python3 test/run_all.py --browser` all passed; determinism diff changed only the
|
||||||
|
replaced corner checks.
|
||||||
|
|
||||||
|
## 2026-10-27: t62 screw conveyor joins (Claude Code + Codex)
|
||||||
|
|
||||||
|
- Straight screws touching a diagonal's end only at the corner now join, in both looks and in
|
||||||
|
ghosts/previews, and grains and bars cross the joint. Same-row joins were already elbows.
|
||||||
|
Also removed the seam across the top of connected elbows in the smooth look.
|
||||||
|
- Files: new `src/render/diagonal-screws.js` (join and casing geometry moved out of
|
||||||
|
`src/render/world.js`), `src/render/smooth-shaders.js`, `src/ui/previews.js`,
|
||||||
|
`src/sim/conveyors.js` (`straightScrewNext`), `src/sim/machine-tick.js`, `src/sim/items.js`,
|
||||||
|
`src/manifest.json`, `docs/PROJECT_MAP.md`, `tools/scenes/t62-joins.js`, tests
|
||||||
|
`test/t_diag_screws.js`, `test/t_smooth.py`.
|
||||||
|
- Tests: `python3 test/run_all.py --browser` all passed; determinism diff showed only timing
|
||||||
|
lines and the new checks.
|
||||||
|
- Open: the corner join doesn't check that the straight piece ends there, so a straight run
|
||||||
|
passing over the diagonal's corner also draws a jog. Not playtested by hand.
|
||||||
|
|
||||||
|
## 2026-10-27: t66 dev console in every build (Claude Code)
|
||||||
|
|
||||||
|
- Owner decision: the console ships in production too, with a warning when it's turned on that
|
||||||
|
the save gets marked (and future achievements won't unlock). Recorded in `docs/DESIGN.md`;
|
||||||
|
t66 gained subtasks for the warning, the save mark, and game speed and fun controls. No code
|
||||||
|
changed.
|
||||||
|
|
||||||
|
## 2026-10-27: t66 dev console added to Todo (Claude Code)
|
||||||
|
|
||||||
|
- Owner request: a dev console to add money, items and upgrades quickly for testing. Subtasks
|
||||||
|
cover how it opens (and hiding it from real players), money and debt, bag items, bit and bag
|
||||||
|
tiers, other shortcuts, and marking changed worlds. No code changed.
|
||||||
|
|
||||||
|
## 2026-10-27: Save compatibility rule dropped from AGENTS.md (Claude Code)
|
||||||
|
|
||||||
|
- Owner: old saves don't matter; only they play, for testing, and can start a new game.
|
||||||
|
`AGENTS.md` still said to keep saves loading (never renumber ids, add migrations), which
|
||||||
|
contradicted the DESIGN decision "No backward compatibility for now". It now matches. The t65
|
||||||
|
subtask no longer asks for saves to load. `AGENTS.md` is local (gitignored). No code changed.
|
||||||
|
|
||||||
|
## 2026-10-27: t65 Rust conversion added to Todo (Claude Code)
|
||||||
|
|
||||||
|
- Owner request: a task to convert the game to Rust for performance, after the base game is more
|
||||||
|
playable (sooner if performance becomes a problem). Subtasks: settle the scope (whole game or the
|
||||||
|
hot sim code; WebAssembly or native), profile for a baseline, keep tests and saves working. No
|
||||||
|
code changed.
|
||||||
|
|
||||||
|
## 2026-10-27: Shift-drag draws a line for transport pieces (Claude Code)
|
||||||
|
|
||||||
|
- Owner request: Shift shouldn't fill a rectangle for belts, conveyors and similar. New
|
||||||
|
`lineOnly(code)` in `src/sim/pieces.js` (belts, filters, screw conveyors, shafts, pipe pieces);
|
||||||
|
`primaryRelease` (`src/ui/actions.js`) and the drag preview (`src/ui/previews.js`) ignore Shift
|
||||||
|
for them, and `pieceCode` (`src/ui/selection.js`) no longer flattens a diagonal belt drag on
|
||||||
|
Shift. Build-menu "How to place" and the notebook say so; the notebook's stale "cleats up or
|
||||||
|
down" line now says uphill only.
|
||||||
|
- Tests: play.py checks a Shift-drag with a belt lays a 3-piece sloped line. Full
|
||||||
|
`run_all --browser` passes.
|
||||||
|
|
||||||
|
## 2026-10-27: t61 sticky materials slide freely on built surfaces (Claude Code)
|
||||||
|
|
||||||
|
- The owner's video showed clay lumps creeping down a sloped wall; removing a piece was
|
||||||
|
unrelated. Cause: STICK gates every diagonal step, so cohesive materials crawl on 45°. In
|
||||||
|
`powder()` (`src/sim/cells.js`) a grain resting on a built solid (not F_NAT) now always tries
|
||||||
|
the diagonal; the random draw is unchanged, so generated worlds are identical.
|
||||||
|
- Measured: a clay grain down a 20-piece built slope took 77 ticks, same as sand (before: never
|
||||||
|
arrived in 2000 ticks). Heaps on a built floor and on natural ground are unchanged.
|
||||||
|
- Test: `t_slopes.js` clay vs sand transit. `python3 test/run_all.py --browser`: ALL PASSED.
|
||||||
|
|
||||||
|
## 2026-10-27: Unremovable one-cell belt left after cutting slopes (Claude Code)
|
||||||
|
|
||||||
|
- The owner's exported save had a lone belt cell no block owned, so nothing could build there or
|
||||||
|
remove it. It was a sloped belt's seal: a seal may sit in a cell its neighbour leaves open (a
|
||||||
|
slope's open corner, or under a grate, screw or screen), but removal only let it go when the
|
||||||
|
neighbour was a sloped wall. Now `blockOwnsCell` decides (`src/sim/blocks.js`), and loading a
|
||||||
|
world clears belt and screen cells no block or seal owns (`clearStraySeals`, `save.js`).
|
||||||
|
- Found with a fuzz of random slope/belt/filter/wall/grate builds removed in random orders and by
|
||||||
|
cut (0 strays in 4000 runs after the fix). The owner's save loads clean and the spot is
|
||||||
|
buildable. Tests in `t_slope_belts.js` fail on the old code. `run_all --browser`: ALL PASSED.
|
||||||
|
|
||||||
|
## 2026-10-27: t42 feedback: downhill belts drop the sidewall too; new tasks (Claude Code)
|
||||||
|
|
||||||
|
- Downhill sloped belts now draw as plain belts: no far wall and no ribs, placed or as ghosts or
|
||||||
|
drag previews (`slopeWallAt`, ghost drawing in `render/world.js`, `previewBlock` now gets the
|
||||||
|
belt direction from `previews.js` and `preview-selection.js`). `slopeUphill` takes the
|
||||||
|
direction bits instead of a block.
|
||||||
|
- Test: `t_slope_belt_art.js` checks a downhill run draws no wall or ribs. Drag preview checked
|
||||||
|
headlessly with a counting canvas (downhill draws the band only); placed belts and ghosts
|
||||||
|
checked in screenshots. `python3 test/run_all.py --browser`: ALL PASSED. Rebuilt dist.
|
||||||
|
- Filed t59 (world longevity audit), t60 (renewable resources, discussion), t61 (sloped walls
|
||||||
|
sticky after removing a piece mid-flow: not reproduced headless, see its report), t62 (screw
|
||||||
|
conveyor horizontal/diagonal joins), t63 (vacuum falling grains).
|
||||||
|
|
||||||
|
## 2026-10-26: t42 feedback: uphill stall at a seam, no cleats downhill, slope pick (Claude Code)
|
||||||
|
|
||||||
|
- The owner's long uphill run stalled with a few grains, with a flashing seam between pieces 9
|
||||||
|
and 10. Cause: each block kept its own cleat phase, so a piece added to a running belt was out
|
||||||
|
of step; at the seam a rib started one or two cells behind another and pushed its pocket into
|
||||||
|
that rib's back, stalling the run for good. Every block of a run now takes the phase of the
|
||||||
|
run's lower-left block (`slopeCleatsTick`, memoised `slopeRunKey`).
|
||||||
|
- Owner decision: downhill belts have no cleats. They carry like a flat belt (`carryColumn`) and
|
||||||
|
draw only the sidewall; ribs, friction drag and pushes are uphill only (`slopeUphill`).
|
||||||
|
`pushRow`'s downhill levelling is gone. Downhill now 90 grains/s (was 38).
|
||||||
|
- Middle click on a sloped belt's band, where it is drawn into the empty block below, now picks
|
||||||
|
the belt (`pickUnderCursor` falls back to `slopeBeltAt`).
|
||||||
|
- Tests: `t_slope_belts.js` adds pieces added 3/7/11/13 ticks later (13 stalled before) and the
|
||||||
|
band pick (fails without the fix); downhill floor back to 60; cleat checks uphill only.
|
||||||
|
`t_slope_belt_art.js` draws ribs on uphill runs. `python3 test/run_all.py --browser`: ALL
|
||||||
|
PASSED. Screenshots of an up-and-down run in both looks opened. Rebuilt dist.
|
||||||
|
|
||||||
|
## 2026-10-26: t42 feedback: cleats that really hold and push their load (Claude Code)
|
||||||
|
|
||||||
|
- The owner saw ribs pass through heavy loads and the load climb as before. Rewrote the slope
|
||||||
|
physics in `src/sim/slope-conveyors.js`: no column carry on slopes; each step every rib moves
|
||||||
|
one face cell and pushes the rows of grains in its way (`pushRow`, front first, never between
|
||||||
|
two rib cells or into a machine); a contact-layer friction drag that gravity beats on 45°;
|
||||||
|
a boot-section scoop at the foot of uphill runs; a run stalls (all its moves undone) if a rib
|
||||||
|
can't push; grains under a newly placed rib are lifted out. Ribs stop short of built cells.
|
||||||
|
Slopes now step before flat belts (`machine-tick.js`), so a feeding belt finds the room.
|
||||||
|
- Drawing: ribs drawn by the smooth wall shader at the simulation phase (rubber bars with a
|
||||||
|
light cap); pixel ribs lighter with a cap; far wall a little lighter (`render/world.js`,
|
||||||
|
`slope-belt-shader.js`, `slope-belts.js`).
|
||||||
|
- Rates: uphill 78.7 (suite) to 86 grains/s (heavy probe), downhill 38, flat 90. Perf: 576
|
||||||
|
loaded slopes 1.03 ms/tick; 50 slopes in a generated world 2.19 vs 1.83 ms/tick without.
|
||||||
|
- Tests: `t_slope_belts.js` adds "no grain ever inside a rib under heavy feed", "a grain on the
|
||||||
|
bare face slides back to a rib, then rides up with it"; downhill rate floor lowered to 30.
|
||||||
|
`python3 test/run_all.py --browser`: ALL PASSED. Open: downhill rate; t58 (low-end joins).
|
||||||
|
|
||||||
|
## 2026-10-26: t42/t43 drawing and performance follow-up (Codex)
|
||||||
|
|
||||||
|
- Filled the far sidewall down to the belt, behind grains, with two-tone pixel corrugation
|
||||||
|
and a continuous smooth 45-degree band. Rubber ribs use the physics mask in all eight
|
||||||
|
phases. Fixed smooth wall seams; matched ghosts, cursor previews and menu icons.
|
||||||
|
Smooth screens retain tinted mesh, now between solid rails without the pixel overlay;
|
||||||
|
loaded-only shake and transport are unchanged.
|
||||||
|
- Empty cleats only wake nearby load; per-chunk flags bypass mask reads for distant powder.
|
||||||
|
Empty eight-block slope: 3 -> 0 moving areas after 120 ticks. Uphill sand/gravel/copper ore
|
||||||
|
71.3 grains/s, coal 71.2; exact sorting retained. Isolated 1f8a89d -> current, median five
|
||||||
|
generated falling-world samples: no slopes 2.006 -> 2.063 ms/tick; 50 slopes 1.952 -> 2.076.
|
||||||
|
Powder probe with cached zero flag: 45.02 -> 50.56 ms (2.14M calls); residual 12 percent
|
||||||
|
microbenchmark overhead, about 3 percent in the generated no-slope world. Existing loaded
|
||||||
|
576-block stress: 0.515 -> 1.064 ms/tick. slope-cost.js records the reproducible methodology;
|
||||||
|
raw results /tmp/t42-perf-{pre,after}-isolated.txt. Pre-t42 loaded slopes use old grip physics.
|
||||||
|
- Files: sim cells/slope-conveyors/world; render slope-belts/slope-belt-shader/world;
|
||||||
|
UI previews/build-menu; t_slope_belt_art.js, t_smooth.py, slope-cost.js; PROJECT_MAP,
|
||||||
|
DESIGN and t42/t43 reports. Reused enterFree for the ordinary entry rules.
|
||||||
|
- Final python3 test/run_all.py --browser: ALL PASSED (26 headless suites, 360 play,
|
||||||
|
16 smooth checks; art suite 57). Before/after suite outputs differ only by five new
|
||||||
|
checks (/tmp/t42-output.diff). Opened shots/t{42,43}-follow-{belt,screen}-{pixel,smooth}-z{4,8}.png;
|
||||||
|
rechecked smooth belt after fixing seams; opened t42-follow-ghosts-{pixel,smooth}-z8.png
|
||||||
|
with both updated menu icons visible. Added sleep, border/load wake, wall geometry,
|
||||||
|
grain colour and smooth grain-class checks. Rebuilt dist, regenerated map; both tasks Review.
|
||||||
|
Owner playtest and hardware GPU appearance remain unverified. No commit.
|
||||||
|
|
||||||
|
## 2026-10-26: t42: sidewall belts with moving cleats (Codex)
|
||||||
|
|
||||||
|
- Continued the uncommitted prototype. `sim/slope-conveyors.js` supplies moving barrier
|
||||||
|
lines (8 face cells apart, 6 grains tall), finite periodic pocket capacity and saved phase.
|
||||||
|
Removed slope grip; natural powder movement runs between steps and surplus rolls back.
|
||||||
|
Fixed the shared slope-to-screw mouth boundary so grains cannot fall under its casing.
|
||||||
|
Pixel/smooth drawing uses the actual mask, continuous dark far walls, matching previews/icons.
|
||||||
|
- Files: manifest; sim cells/conveyors/slope-conveyors/machine-tick/items/pieces/blocks/world/save;
|
||||||
|
render slope-belts/slope-belt-shader/world; UI actions/selection/selection-pickers/previews/
|
||||||
|
build-menu/notebook; slope test suites and play.py; slope-flow/slope-cost experiments,
|
||||||
|
slope-conveyors scene, PROJECT_MAP, DESIGN and t42/t43. Removed both requested scratch files.
|
||||||
|
- Reproducible `node tools/experiments/slope-flow.js`: sand, gravel, copper ore 71.3 and
|
||||||
|
coal 71.2 grains/s uphill, each 90.0 flat; 20-second window after 220 warm-up ticks.
|
||||||
|
Measured settled pile angles 33.7 degrees for all four. Final HEAD -> current stress measurement: 0.486 -> 1.038 ms/tick for 576 loaded
|
||||||
|
slope blocks (1024x512); no-slope powder microbenchmark, 2.14M calls: 45.07 -> 65.55 ms.
|
||||||
|
Median of five samples after warm-up; scripts use DG_HARNESS for the HEAD snapshot in
|
||||||
|
/tmp/t42-baseline. Barriers skip mask reads when none exist, but still have a CPU cost.
|
||||||
|
- Opened shots/t42-belt-{pixel,smooth}-z{4,8}.png. Headless checks pass, including pocket
|
||||||
|
overflow, collision masks, unstamped bare-face sliding, joins, screw mouths, water, bars,
|
||||||
|
save phase, all eight drawing phases and unchanged seed hashes 777/77/4242.
|
||||||
|
Full python3 test/run_all.py --browser: ALL PASSED (26 headless suites, 360 play
|
||||||
|
checks, 15 smooth checks). Rebuilt dist and regenerated PROJECT_MAP. Both tasks moved to
|
||||||
|
Review. The owner still decides whether the lower rate feels
|
||||||
|
right. Pocket movement is a discrete envelope, not a calculation of individual forces.
|
||||||
|
No commit.
|
||||||
|
|
||||||
|
## 2026-10-26: t43: inclined vibrating screens (Codex)
|
||||||
|
|
||||||
|
- Diagonal filters always run downhill through all placement/settings/undo/paste paths;
|
||||||
|
R leaves a picked slope unchanged. The picker shows fixed downhill travel rather than two
|
||||||
|
misleading direction buttons. Flat filters retain their original carry. Slope grains get
|
||||||
|
gravity plus one mesh-contact nudge every 4 ticks (15 steps/s); unselected bars use the
|
||||||
|
same cadence. No whole-column carry or between-step grip on slopes.
|
||||||
|
- Pixel and smooth mesh frames shake a little while loaded, with stable idle drawing.
|
||||||
|
Updated ghosts, icons, build text, notebook and the relevant sim/UI/render files listed
|
||||||
|
in t42 above. Added real browser drag/R/picker checks and art shake checks.
|
||||||
|
- Both shapes and both sorting modes deliver the complete 64 sand + 64 gravel mix exactly.
|
||||||
|
Generous-feed discharge is 60.0 grains/s down-left and 59.3 down-right; rest passes through.
|
||||||
|
Opened shots/t43-screen-{pixel,smooth}-z{4,8}.png. Headless and smooth suites pass; the
|
||||||
|
first browser run found a hover fixture above the mesh and a later timeout. Corrected the
|
||||||
|
coordinate and reset picked slope state between fixtures. All new browser checks pass.
|
||||||
|
A subsequent run caught an existing 200 ms silo HUD timing race; wait for the expected
|
||||||
|
HUD text before the same assertion. Final full browser run: ALL PASSED, 360 play
|
||||||
|
checks plus 15 smooth checks; all 26 headless suites pass. t43 moved to Review.
|
||||||
|
No uphill-save migration. No commit; owner playtest remains.
|
||||||
|
|
||||||
|
## 2026-10-26: t44: rename screw tubes to screw conveyors (Codex)
|
||||||
|
|
||||||
|
- Player text uses Screw conveyor; code uses screw/SCREW, including the piece id, part
|
||||||
|
constants, direction tables, transport helpers, shader variable, build icons and settings
|
||||||
|
access. Numeric ids and save/blueprint versions unchanged; no aliases or migrations added.
|
||||||
|
- Updated affected files across src/sim, src/render, src/ui, test and tools, plus README,
|
||||||
|
AGENTS and current DESIGN/TESTING/system docs. Regenerated PROJECT_MAP and built dist.
|
||||||
|
Renamed test/t_diag_tubes.js to test/t_diag_screws.js with git mv; no scene filename needed
|
||||||
|
renaming. Past log entries, docs/history and task title/Context preserved.
|
||||||
|
- All 26 suite outputs saved in /tmp/t44-before and /tmp/t44-after. Raw diff:
|
||||||
|
/tmp/t44-output.diff; name/timing-normalized diff: /tmp/t44-output-normalized.diff (empty).
|
||||||
|
No numeric output differences except ms timings; numeric literals unchanged in all 39
|
||||||
|
edited JavaScript/Python files. Only tube matches in scoped source/docs are the physical
|
||||||
|
casing description in pieces.js and casing/flight rendering comment in world.js.
|
||||||
|
- python3 test/run_all.py --browser: ALL PASSED (26 headless suites, 352 play checks,
|
||||||
|
15 smooth checks). Focused menu, tooltip, hover, notebook, screw blueprint-code round-trip
|
||||||
|
and both starter blueprint-code checks pass in pixel and smooth; no page errors.
|
||||||
|
Opened shots/t44-{menu,hover,notebook}-{pixel,smooth}.png and the play diagonal screenshot.
|
||||||
|
Temporary UI fixture initially lacked SX/SY; corrected to match shot.py before these checks.
|
||||||
|
- Task t44 moved to Review. Owner playtest remains. No commit.
|
||||||
|
|
||||||
|
## 2026-10-26: t38: show silo contents at the cursor and in a wide window (Codex)
|
||||||
|
|
||||||
|
- `src/ui/info.js`: contents were confined to the bottom info plate (hidden below 720 px),
|
||||||
|
omitted from cursor labels, and cone gaps lacked mach ownership. Hover now resolves the
|
||||||
|
full 12x24 outline and shows contents beside the cursor; physical footprints stay unchanged.
|
||||||
|
- `src/render/machines.js`: shared framed three-grain window in both looks, fixed pixel grain
|
||||||
|
noise, visible near-empty row, lifted dark fuel colours, and stencils fitted beside the glass.
|
||||||
|
`tools/scenes/silo.js` adds snow and one grain of coal. All 31 accepted powders checked.
|
||||||
|
- `test/t_silo_contents.js`: 5 checks confirm snow intake, identity, water accounting, warm
|
||||||
|
storage and release. `test/play.py` checks hover/label at five outline points empty/filled,
|
||||||
|
narrow HUD, fill pixels at zooms 2-12 and stable drawing for all powders in both looks.
|
||||||
|
- Opened and inspected `shots/t38-silos-{pixel,smooth}-z{2,4,8}.png`. Updated t38 and DESIGN;
|
||||||
|
regenerated PROJECT_MAP. Final `python3 test/run_all.py --browser`: ALL PASSED (26 headless
|
||||||
|
suites, 352 play checks, 15 smooth checks). Task moved to Review; owner playtest remains.
|
||||||
|
|
||||||
|
## 2026-10-26: t41: distant resonance-drill aim is normal (Codex)
|
||||||
|
|
||||||
|
- `src/ui/preview-drill.js`, `previews.js`: keep tuned fan and light crosshair, add a solid far
|
||||||
|
edge in pulse colour, remove the drill-only red reach circle. `info.js` omits drill distance
|
||||||
|
warnings; other tools retain their warning, including beside silo contents.
|
||||||
|
- `src/sim/drill.js`: removed only the distance status; too-hard/cannot-be-drilled wording and
|
||||||
|
`toolDig` are unchanged. Notebook drill text remains accurate; gamepad code untouched.
|
||||||
|
- `test/t_drill.js`: new distance regression failed before fix, now passes; two former warning
|
||||||
|
expectations updated intentionally. `test/play.py`: held mouse aim makes no toast/warning,
|
||||||
|
hard granite still toasts, no red reach circle, gamepad aim stays within reach.
|
||||||
|
- Opened and inspected `shots/t41-drill-beyond-reach.png`. Focused browser checks pass.
|
||||||
|
Initial full browser run exposed test timing/paused-input fixture mistakes, corrected without
|
||||||
|
changing gameplay. Final `python3 test/run_all.py --browser`: ALL PASSED (26 headless
|
||||||
|
suites, 352 play checks, 15 smooth checks). Updated t41 and DESIGN; moved to Review. No commit.
|
||||||
|
|
||||||
|
## 2026-10-26: t39: seal touching square-block corners (Codex)
|
||||||
|
|
||||||
|
- `cells.js`: six inline checks reject diagonal moves between two PH_SOLID neighbours, with
|
||||||
|
no new random calls. `worldgen.js` saves/disables/restores CORNER_SEAL alongside LSTEP/LFILM
|
||||||
|
while settling. No material ids, save fields or piece geometry changed.
|
||||||
|
- Reproduced two further leaks: free flyers (`vacuum.js`) and diagonal tube mouths pulling sand
|
||||||
|
between external walls (`conveyors.js`); blocked those steps only. Horizontal liquid scans
|
||||||
|
already check each intervening cell; vacuum collection removes grains into the bag. Bars
|
||||||
|
resolve ordinary movement by axis. Screw lane transport remains explicit and unchanged.
|
||||||
|
- `test/t_corner_seal.js`: 23 checks, including both staircase orientations for sand/water/steam,
|
||||||
|
all four diagonal intakes, a flyer, world hashes/flag restoration, and an open pile that remains
|
||||||
|
57x17 with identical material layout and random state when sealing is disabled.
|
||||||
|
- All 25 headless suites pass. Before/after outputs: `t_slopes` square-funnel outflow 379 -> 339
|
||||||
|
(more sand retained on steps); `t_drill` ore outside the wall 56 -> 55 (solid corners hold ore);
|
||||||
|
`t_world` awake chunks after 900 gameplay ticks following reload 148 -> 149 (different settling
|
||||||
|
after corner blocking). Disabling the checks restores drill/world baseline output exactly.
|
||||||
|
Other existing output is identical except timings. Evidence: /tmp/t39-before, /tmp/t39-after.
|
||||||
|
- Full mat/temp/flags hashes match original code for seeds 777/77/4242 at 512x384 and 2048x1024,
|
||||||
|
plus 424242 at 1024x640 (/tmp/t39-world-hashes-{before,after}.txt).
|
||||||
|
- Factory benchmark at 1024x640: idle 0.69 -> 0.68 ms/tick; 1/4/8 modules
|
||||||
|
2.05/2.36/2.65 -> 2.13/2.44/2.70 ms/tick. Timing is indicative: settling changes, and this
|
||||||
|
fixture reported zero machine-moved grains, so it does not prove loaded transport performance.
|
||||||
|
- Rebuilt dist and regenerated PROJECT_MAP. Inspected `shots/t39-corners.png` in pixel look:
|
||||||
|
sand rests along the upper steps and pours through the central outlet.
|
||||||
|
- Final `python3 test/run_all.py --browser`: ALL PASSED (25 headless suites, 323 play checks,
|
||||||
|
15 smooth checks). Updated t39 subtasks/report for Review and recorded the design decision.
|
||||||
|
No commit made; owner playtest remains.
|
||||||
|
|
||||||
|
## 2026-10-25: t40 art pass: sloped belts and filters drawn as one belt (Claude Code, Codex)
|
||||||
|
|
||||||
|
- Sloped runs draw as the flat belt (rollers, studs) or filter (tinted mesh) band turned to 45°,
|
||||||
|
continuous, with elbows into flat belts, filters and tubes and rounded free ends, in both looks
|
||||||
|
(`src/render/slope-belts.js`, new `slope-belt-shader.js`, `world.js`, `smooth-shaders.js`);
|
||||||
|
ghosts and icons match. Drawing only.
|
||||||
|
- `test/t_slope_belt_art.js`; `run_all.py --browser` ALL PASSED.
|
||||||
|
|
||||||
|
## 2026-10-25: t40: sloped belts and filters (Claude Code, Codex)
|
||||||
|
|
||||||
|
- Block codes 19-22 (`BP_BELT_UP/DOWN`, `BP_FILTER_UP/DOWN`, `src/sim/pieces.js`): slope-shaped
|
||||||
|
belts and filters whose 45° face runs, laid by a diagonal Belt or Filter drag. `conveyorStep`,
|
||||||
|
`holdSlopeColumn` and a shared front-first order for belts and filters in `src/sim/conveyors.js`;
|
||||||
|
bars in `items.js`; saves, blueprints, undo, hover, previews; `src/render/slope-belts.js`.
|
||||||
|
- 90 grains/s flat, up and down for belts and filters; a sloped filter sorted 64/64 with no loss.
|
||||||
|
Seeds 777, 77 and 4242 generate identically.
|
||||||
|
- `test/t_slope_belts.js` (32 checks); `run_all.py --browser` ALL PASSED. Art pass follows.
|
||||||
|
|
||||||
|
## 2026-10-25: t35 follow-up: diagonal tube elbows (Claude Code, Codex)
|
||||||
|
|
||||||
|
- Diagonal tubes are as thick as straight ones, and an end block joining a straight tube, belt or
|
||||||
|
filter draws an elbow at its centre, so the pipes meet centred and edge to edge; free ends get a
|
||||||
|
collar; ghosts match (`src/render/world.js`, `smooth-shaders.js`, `src/ui/previews.js`,
|
||||||
|
`preview-selection.js`). Scene `tools/scenes/t35-joins.js`.
|
||||||
|
- `test/t_diag_tubes.js`; `run_all.py --browser` ALL PASSED.
|
||||||
|
|
||||||
|
## 2026-10-25: t35/t36 follow-ups: wall drags make slopes, diagonal tube art (Claude Code, Codex)
|
||||||
|
|
||||||
|
- Wall piece: a diagonal drag lays slopes (`pieceCode`/`turnSlope` in `src/ui/selection.js`), T flips
|
||||||
|
a drag or cycles square and four corners for one block; the Sloped wall piece is gone from the
|
||||||
|
menu, slope codes map to the wall piece (`src/sim/pieces.js`); old slope saves load
|
||||||
|
(`test/fixtures/slopes-v2.json.gz`).
|
||||||
|
- Diagonal tubes draw as one continuous 45° pipe in both looks, overhanging empty cells at joints
|
||||||
|
(`src/render/world.js`, `smooth-shaders.js`). Drag ghosts lost the long side arrow; each ghost
|
||||||
|
block keeps its own chevron (`src/ui/previews.js`). The owner found the diagonal still meets
|
||||||
|
straight tubes off-centre; an elbow fix follows.
|
||||||
|
- `test/t_slopes.js`, `t_diag_tubes.js`, `play.py`; `run_all.py --browser` ALL PASSED.
|
||||||
|
|
||||||
|
## 2026-10-25: t37 follow-up: grains on built things cast no shadows (Claude Code, Codex)
|
||||||
|
|
||||||
|
- The owner saw a flickering shadow under a grinder fed by a stream: grains resting on its
|
||||||
|
housing for a moment shaded. `looseForLight` (`src/render/light.js`) now treats a loose stack
|
||||||
|
that rests on any `builtForLight` cell, or ends in air or gas, as see-through. Stacks on natural
|
||||||
|
ground or deeper than 12 cells still shade.
|
||||||
|
- `test/t_light.js` (a grinder stream checked for 180 ticks in both lighting modes);
|
||||||
|
`run_all.py --browser` ALL PASSED.
|
||||||
|
|
||||||
|
## 2026-10-25: t37 follow-up: falling grains cast no shadows (Claude Code, Codex)
|
||||||
|
|
||||||
|
- The owner saw a single falling grain darken a column of sky. `onBeltForLight` became
|
||||||
|
`looseForLight` (`src/render/light.js`): a loose stack that ends in air or gas within 12 cells
|
||||||
|
(falling) is see-through, as are belt and filter loads; stacks resting on any solid or a tube
|
||||||
|
part still shade.
|
||||||
|
- `test/t_light.js`; `run_all.py --browser` ALL PASSED.
|
||||||
|
|
||||||
|
## 2026-10-25: t37 follow-up: belt loads cast no shadows (Claude Code, Codex)
|
||||||
|
|
||||||
|
- The owner saw grains riding a belt overhead still shading the ground. `onBeltForLight(i)` in
|
||||||
|
`src/render/light.js` walks down through up to 12 loose cells to a belt or filter; such cells
|
||||||
|
are skipped by the light roof and pass light like air. `builtForLight` is unchanged, so the t33
|
||||||
|
loose-grain shading still applies to belt loads. Heaps on rock, silos and wall floors still shade.
|
||||||
|
- `test/t_light.js` (56 checks); `run_all.py --browser` ALL PASSED.
|
||||||
|
|
||||||
|
## 2026-10-25: t35: diagonal screw tubes (Claude Code, Codex)
|
||||||
|
|
||||||
|
- Parts `P_TUR`..`P_TUL` (11-14), `TUBE_PARTS`, and an `IS_TUBE` table replacing every
|
||||||
|
`P_TU..P_TR` range check (`src/sim/material-tables.js` and its users); tube variants 4-7;
|
||||||
|
diagonal drags for tubes; grains in `src/sim/conveyors.js`/`cells.js`, bars in `items.js`;
|
||||||
|
both looks, previews, hover, copy/paste, undo, blueprints, saves.
|
||||||
|
- `test/t_diag_tubes.js` (85 checks): 120 grains/s straight, up and down over ten blocks; joins,
|
||||||
|
no spills, water passes, bars both ways, controls, persistence. Existing suites unchanged and
|
||||||
|
passing; seeds unchanged. `run_all.py --browser` ALL PASSED. Screenshots
|
||||||
|
`shots/t35-diagonal-*.png`, scene `tools/scenes/t35-diagonal.js`.
|
||||||
|
|
||||||
|
## 2026-10-25: t36: sloped walls and diagonal wall lines (Claude Code, Codex)
|
||||||
|
|
||||||
|
- `BP_SLOPE_BL`..`BP_SLOPE_TR` (15-18) in `src/sim/pieces.js` (`isSlope`, `isWall`, `slopeCell`,
|
||||||
|
`slopeCode`); `src/sim/blocks.js`: half cost and refund, `slopeSeal` corner cell and
|
||||||
|
`slopeSealOwner`, `diagonalDrag` and `blockLine(.., diagonal)`. Build menu, T key
|
||||||
|
(`turnSlope`), previews, copy/paste, undo, blueprints, hover, both looks.
|
||||||
|
- `test/t_slopes.js`: funnel 400/400 sand (square steps 379/400), water 400/400, no leaks in all
|
||||||
|
four orientations from both sides, bars slide, every wall material; play.py menu and diagonal
|
||||||
|
drag. Seeds unchanged. `run_all.py --browser` ALL PASSED. Screenshots
|
||||||
|
`shots/t36-funnel-*.png`, scene `tools/scenes/t36-funnel.js`.
|
||||||
|
|
||||||
|
## 2026-10-25: t33: loose grains and solid ground look different (Claude Code, Codex)
|
||||||
|
|
||||||
|
- Smooth look (`src/render/smooth-shaders.js`): natural ground gets a soft seam where it meets
|
||||||
|
loose grains as well as air; packed grains keep the pebble shading and are slightly lighter.
|
||||||
|
Pixel look and thermal lens (`drawWorld()` in `src/render/world.js`): loose grains a touch
|
||||||
|
lighter and speckled, solid cells touching loose grains darker.
|
||||||
|
- Smooth frame 1.08 -> 1.10 ms. `t_smooth.py` checks the seam and the lighter grains;
|
||||||
|
`tools/scenes/t33-ground.js` for screenshots (`shots/t33/`).
|
||||||
|
- play.py: 'F on an automatic ingot mold stays quiet' counted toasts on screen, and failed
|
||||||
|
now and then when an older toast expired or a new one arrived in the same 150 ms; it now
|
||||||
|
records the toasts the key press makes (none). `run_all.py --browser` ALL PASSED apart from
|
||||||
|
that check, which passes in the new form.
|
||||||
|
|
||||||
|
## 2026-10-25: t37: buildings cast no shadows (Claude Code, Codex)
|
||||||
|
|
||||||
|
- `computeLight()` (`src/render/light.js`): a per-view-column light roof that skips player-built
|
||||||
|
cells (`builtForLight`: unnatural solids, block, machine or part cells), and built cells pass
|
||||||
|
light like air. `drawWorld()` stops marking built cells as shading terrain for the smooth look.
|
||||||
|
The simulation's `roofY` is untouched (built roofs still condense steam).
|
||||||
|
- `test/t_light.js`; screenshots `shots/t37-before-*.png`, `shots/t37-after-*.png`.
|
||||||
|
`run_all.py --browser` ALL PASSED.
|
||||||
|
|
||||||
|
## 2026-10-25: t34: the nozzle blows no further than it sucks (Claude Code, Codex)
|
||||||
|
|
||||||
|
- `toolBlow()` (`src/sim/vacuum.js`) aims at the capped point from `nozzle()` when the cursor is
|
||||||
|
out of reach (thrown bars already did); `drawPreviews()` draws the blow brush there, in the blow
|
||||||
|
colour rather than the out-of-reach red.
|
||||||
|
- `test/t_nozzle.js`: aiming at 3x and 5x the reach lands sand within reach + 16 grains (brush and
|
||||||
|
settling, measured 15); inside reach it still lands around the cursor. play.py: the preview
|
||||||
|
sits at the capped point. `run_all.py --browser` ALL PASSED. Screenshot
|
||||||
|
`shots/t34-blow-out-of-reach.png`.
|
||||||
|
|
||||||
## 2026-10-25: t21: a loaded world starts around the player first (Claude Code, Codex)
|
## 2026-10-25: t21: a loaded world starts around the player first (Claude Code, Codex)
|
||||||
|
|
||||||
- Owner: "lets speed up the first load speed. load in areas around the player first".
|
- Owner: "lets speed up the first load speed. load in areas around the player first".
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Built 2026-10-22; the uses for stone and sand were added 2026-10-23.
|
|||||||
- Code:
|
- Code:
|
||||||
- `src/sim/processors.js` (Refiner, Stone cutter, Silo);
|
- `src/sim/processors.js` (Refiner, Stone cutter, Silo);
|
||||||
- `src/sim/pipes.js`;
|
- `src/sim/pipes.js`;
|
||||||
- `src/sim/conveyors.js` and `src/sim/machine-tick.js` (belts and tubes);
|
- `src/sim/conveyors.js` and `src/sim/machine-tick.js` (belts and screw conveyors);
|
||||||
- `src/sim/economy.js` (prices, builders' orders).
|
- `src/sim/economy.js` (prices, builders' orders).
|
||||||
- Tests: `test/t_pipes.js`, `test/t_stone.js`, `test/t_conveyors.js`.
|
- Tests: `test/t_pipes.js`, `test/t_stone.js`, `test/t_conveyors.js`.
|
||||||
|
|
||||||
@@ -57,12 +57,12 @@ through another refiner.
|
|||||||
- **Water accounting.** Water in pipes counts in the world's water total, so the weather never
|
- **Water accounting.** Water in pipes counts in the world's water total, so the weather never
|
||||||
sends rain to replace it. Pipe contents are saved.
|
sends rain to replace it. Pipe contents are saved.
|
||||||
|
|
||||||
## Belts and screw tubes
|
## Belts and screw conveyors
|
||||||
- A heap on a belt moves as one: the whole stack, up to 96 grains deep, steps along together
|
- A heap on a belt moves as one: the whole stack, up to 96 grains deep, steps along together
|
||||||
instead of only its bottom layer. The owner reports that big piles still only move at the top
|
instead of only its bottom layer. The owner reports that big piles still only move at the top
|
||||||
(see NOTES.md).
|
(see NOTES.md).
|
||||||
- Screw tubes run at belt pace, moving every other tick.
|
- Screw conveyors run at belt pace, moving every other tick.
|
||||||
- A horizontal tube fed from a full hopper moves about 100–110 grains a second.
|
- A horizontal screw conveyor fed from a full hopper moves about 100–110 grains a second.
|
||||||
- A belt under its mouth keeps up, carrying a stream 3–5 grains deep at about the same rate.
|
- A belt under its mouth keeps up, carrying a stream 3–5 grains deep at about the same rate.
|
||||||
- A vertical lift brings up about 40–60 grains a second.
|
- A vertical lift brings up about 40–60 grains a second.
|
||||||
- `tools/experiments/flow.js` measures these.
|
- `tools/experiments/flow.js` measures these.
|
||||||
|
|||||||
+10
-1
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"shell": "shell.html",
|
"shell": "shell.html",
|
||||||
"css": [
|
"css": [
|
||||||
"styles/style.css"
|
"styles/style.css",
|
||||||
|
"styles/devconsole.css"
|
||||||
],
|
],
|
||||||
"sim": [
|
"sim": [
|
||||||
"sim/base.js",
|
"sim/base.js",
|
||||||
@@ -19,7 +20,10 @@
|
|||||||
"sim/racks.js",
|
"sim/racks.js",
|
||||||
"sim/pipes.js",
|
"sim/pipes.js",
|
||||||
"sim/machine-tick.js",
|
"sim/machine-tick.js",
|
||||||
|
"sim/joins.js",
|
||||||
"sim/conveyors.js",
|
"sim/conveyors.js",
|
||||||
|
"sim/flow.js",
|
||||||
|
"sim/slope-conveyors.js",
|
||||||
"sim/blocks.js",
|
"sim/blocks.js",
|
||||||
"sim/molds.js",
|
"sim/molds.js",
|
||||||
"sim/items.js",
|
"sim/items.js",
|
||||||
@@ -36,15 +40,19 @@
|
|||||||
"sim/tick.js",
|
"sim/tick.js",
|
||||||
"sim/worldgen-helpers.js",
|
"sim/worldgen-helpers.js",
|
||||||
"sim/worldgen.js",
|
"sim/worldgen.js",
|
||||||
|
"sim/devconsole.js",
|
||||||
"sim/save.js"
|
"sim/save.js"
|
||||||
],
|
],
|
||||||
"client": [
|
"client": [
|
||||||
"render/setup.js",
|
"render/setup.js",
|
||||||
"render/light.js",
|
"render/light.js",
|
||||||
|
"render/slope-belts.js",
|
||||||
|
"render/diagonal-screws.js",
|
||||||
"render/world.js",
|
"render/world.js",
|
||||||
"render/machines.js",
|
"render/machines.js",
|
||||||
"render/overlays.js",
|
"render/overlays.js",
|
||||||
"render/smooth.js",
|
"render/smooth.js",
|
||||||
|
"render/slope-belt-shader.js",
|
||||||
"render/smooth-shaders.js",
|
"render/smooth-shaders.js",
|
||||||
"ui/audio.js",
|
"ui/audio.js",
|
||||||
"ui/state.js",
|
"ui/state.js",
|
||||||
@@ -62,6 +70,7 @@
|
|||||||
"ui/selection-pickers.js",
|
"ui/selection-pickers.js",
|
||||||
"ui/selection-panels.js",
|
"ui/selection-panels.js",
|
||||||
"ui/info.js",
|
"ui/info.js",
|
||||||
|
"ui/devconsole.js",
|
||||||
"ui/input.js",
|
"ui/input.js",
|
||||||
"ui/actions.js",
|
"ui/actions.js",
|
||||||
"ui/gamepad.js",
|
"ui/gamepad.js",
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
// Screw casing geometry shared by both looks, ghosts and placement previews.
|
||||||
|
// smooth-look class of a screw conveyor cell by direction: 9 up, 10 right, 11 down, 12 left
|
||||||
|
const SCREW_CLASS = new Uint8Array(15); SCREW_CLASS[1] = 9; SCREW_CLASS[4] = 10; SCREW_CLASS[2] = 11; SCREW_CLASS[3] = 12;
|
||||||
|
for (let p = P_SCREW_UR; p <= P_SCREW_UL; p++) SCREW_CLASS[p] = 19 + p - P_SCREW_UR;
|
||||||
|
// Drawing only: the four transport lanes remain in the square block.
|
||||||
|
let diagonalJoins = new Uint8Array(0);
|
||||||
|
function diagonalScrewJoins(bx, by, v, planned = null) {
|
||||||
|
// Three bits per end: 1..4 edge exits, 5 horizontal in E, 6 vertical in E; bits 6/7 collars.
|
||||||
|
let joins = 0;
|
||||||
|
for (let end = 0; end < 2; end++) {
|
||||||
|
const join = endJoin(bx, by, end, planned);
|
||||||
|
if (join < 0) { joins |= 64 << end; continue; }
|
||||||
|
const nb = join >> 1, next = planned && planned.get(nb);
|
||||||
|
const code = next ? next[0] : blockP[nb], nv = next ? next[1] : blockV[nb];
|
||||||
|
if (pieceEnds(code, nv)[join & 1].diagonal) continue;
|
||||||
|
const horizontal = code !== BP_SCREW || (nv & 1);
|
||||||
|
const ny = (nb / BW) | 0, nx = nb % BW;
|
||||||
|
const kind = ny === by ? (nx > bx ? 2 : 4) : nx === bx ? (ny < by ? 1 : 3) : horizontal ? 5 : 6;
|
||||||
|
joins |= kind << (end * 3);
|
||||||
|
}
|
||||||
|
return joins;
|
||||||
|
}
|
||||||
|
// Only E elbows bend a straight block: 1 rising, 2 falling, three bits per end.
|
||||||
|
function straightScrewJoins(bx, by, v, planned = null) {
|
||||||
|
let joins = 0;
|
||||||
|
for (let end = 0; end < 2; end++) {
|
||||||
|
const join = endJoin(bx, by, end, planned);
|
||||||
|
if (join < 0) continue;
|
||||||
|
const nb = join >> 1, next = planned && planned.get(nb);
|
||||||
|
const code = next ? next[0] : blockP[nb], nv = next ? next[1] : blockV[nb];
|
||||||
|
if (code === BP_SCREW && nv >= 4 && ((nb / BW) | 0) !== by && nb % BW !== bx)
|
||||||
|
joins |= (nv === 4 || nv === 6 ? 1 : 2) << (end * 3);
|
||||||
|
}
|
||||||
|
return joins;
|
||||||
|
}
|
||||||
|
// Scratch coordinates shared by the pixel shading; no allocations in the grain loop.
|
||||||
|
let diagonalAcross = 0, diagonalAlong = 0, diagonalCap = 0;
|
||||||
|
function diagonalScrewShape(x, y, b, v, joins) {
|
||||||
|
const lx = x - (b % BW) * BS - 2, ly = y - ((b / BW) | 0) * BS - 2;
|
||||||
|
let best = Infinity;
|
||||||
|
for (let end = 0; end < 2; end++) {
|
||||||
|
const join = (joins >> (end * 3)) & 7, side = end ? 1 : -1;
|
||||||
|
const slope = v === 4 || v === 6 ? -1 : 1, straight = v < 4, vert = straight && !(v & 1);
|
||||||
|
const bent = straight && join;
|
||||||
|
const diagonal = straight ? bent : !join || join >= 5;
|
||||||
|
const grade = straight ? (join === 1 ? -1 : 1) : slope;
|
||||||
|
const dx = diagonal ? side * (vert ? grade : 1) * Math.SQRT1_2 : straight ? (vert ? 0 : side) : SCREW_DIRS[join - 1][0];
|
||||||
|
const dy = diagonal ? side * (vert ? 1 : grade) * Math.SQRT1_2 : straight ? (vert ? side : 0) : SCREW_DIRS[join - 1][1];
|
||||||
|
const along = lx * dx + ly * dy, across = -lx * dy + ly * dx;
|
||||||
|
const radius = Math.hypot(across, Math.min(0, along));
|
||||||
|
const cap = along - (diagonal ? 2 * Math.SQRT2 : 2);
|
||||||
|
const sd = Math.max(radius - 2, !straight && (joins & (64 << end)) ? cap : -10);
|
||||||
|
if (sd < best) {
|
||||||
|
best = sd; diagonalAcross = side * across < 0 ? -radius : radius;
|
||||||
|
const ox = (b % BW) * BS + 2, oy = ((b / BW) | 0) * BS + 2;
|
||||||
|
diagonalAlong = (straight ? (vert ? oy : ox) : (ox + oy * slope) * Math.SQRT1_2) + side * along;
|
||||||
|
diagonalCap = joins & (64 << end) ? cap : -10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
function diagonalScrewBlock(x, y, smooth) {
|
||||||
|
const i = y * W + x, own = (y >> 2) * BW + (x >> 2);
|
||||||
|
if (IS_SCREW[part[i]] && (blockV[own] >= 4 || diagonalJoins[own])) return own;
|
||||||
|
if (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || blockP[own]) return -1;
|
||||||
|
let best = smooth ? 0.75 : 0, found = -1;
|
||||||
|
for (let k = 0; k < 4; k++) {
|
||||||
|
const bx = (x >> 2) + (k === 0 ? -1 : k === 1 ? 1 : 0);
|
||||||
|
const by = (y >> 2) + (k === 2 ? -1 : k === 3 ? 1 : 0);
|
||||||
|
if (bx < 0 || by < 0 || bx >= BW || by >= BH) continue;
|
||||||
|
const b = by * BW + bx, v = blockV[b];
|
||||||
|
if (blockP[b] !== BP_SCREW || (v < 4 && !diagonalJoins[b])) continue;
|
||||||
|
const joins = diagonalJoins[b] === undefined ? (v >= 4 ? diagonalScrewJoins(bx, by, v) : straightScrewJoins(bx, by, v)) : diagonalJoins[b];
|
||||||
|
if (v < 4 && ((v & 1) ? (x >> 2) !== bx : (y >> 2) !== by)) continue;
|
||||||
|
const sd = diagonalScrewShape(x + 0.5, y + 0.5, b, v, joins);
|
||||||
|
if (sd <= best) { best = sd; found = b; }
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
+26
-2
@@ -2,6 +2,17 @@
|
|||||||
// ---------- Lighting ----------
|
// ---------- Lighting ----------
|
||||||
// Light is computed on a coarse grid (2 or 4 grains per cell), written into a
|
// Light is computed on a coarse grid (2 or 4 grains per cell), written into a
|
||||||
// small canvas, then the GPU upscales it smoothly and multiplies it over the world.
|
// small canvas, then the GPU upscales it smoothly and multiplies it over the world.
|
||||||
|
function builtForLight(i, x, y) {
|
||||||
|
return (PH[mat[i]] === PH_SOLID && !(flags[i] & F_NAT)) || blockP[(y >> 2) * BW + (x >> 2)] || mach[i] || part[i];
|
||||||
|
}
|
||||||
|
function looseForLight(i) {
|
||||||
|
for (let depth = 0; depth <= 12 && i < N; depth++, i += W) {
|
||||||
|
const m = mat[i], ph = PH[m];
|
||||||
|
if (builtForLight(i, i % W, (i / W) | 0)) return true;
|
||||||
|
if (ph !== PH_POWDER && ph !== PH_LIQUID) return m === EMPTY || ph === PH_GAS;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
function computeLight() {
|
function computeLight() {
|
||||||
const res = SET.lighting === 'fast' ? 4 : 2;
|
const res = SET.lighting === 'fast' ? 4 : 2;
|
||||||
const M = Math.ceil(28 / res);
|
const M = Math.ceil(28 / res);
|
||||||
@@ -13,6 +24,7 @@ function computeLight() {
|
|||||||
R.lightC.width = lw; R.lightC.height = lh;
|
R.lightC.width = lw; R.lightC.height = lh;
|
||||||
R.lightImg = R.lightX.createImageData(lw, lh);
|
R.lightImg = R.lightX.createImageData(lw, lh);
|
||||||
R.lightPx = new Uint32Array(R.lightImg.data.buffer);
|
R.lightPx = new Uint32Array(R.lightImg.data.buffer);
|
||||||
|
R.lightRoof = new Int32Array(lw);
|
||||||
}
|
}
|
||||||
R.lx0 = lx0; R.ly0 = ly0;
|
R.lx0 = lx0; R.ly0 = ly0;
|
||||||
const lr = R.lr, lg = R.lg, lb = R.lb, la = R.latt;
|
const lr = R.lr, lg = R.lg, lb = R.lb, la = R.latt;
|
||||||
@@ -20,15 +32,27 @@ function computeLight() {
|
|||||||
const P = res;
|
const P = res;
|
||||||
const aAir = Math.pow(0.978, P), aGas = Math.pow(0.96, P), aLiq = Math.pow(0.93, P), aSol = Math.pow(0.9, P);
|
const aAir = Math.pow(0.978, P), aGas = Math.pow(0.96, P), aLiq = Math.pow(0.93, P), aSol = Math.pow(0.9, P);
|
||||||
const skyK = SET.daylight * (1 - 0.32 * (R.rain || 0)); // overcast while it rains
|
const skyK = SET.daylight * (1 - 0.32 * (R.rain || 0)); // overcast while it rains
|
||||||
|
// Only sampled view columns, starting at the simulation roof; nothing below the view matters.
|
||||||
|
const roofEnd = Math.min(H, (ly0 + lh) * res), lightRoof = R.lightRoof;
|
||||||
|
for (let lx = 0; lx < lw; lx++) {
|
||||||
|
const x = (lx0 + lx) * res;
|
||||||
|
if (x < 0 || x >= W) continue;
|
||||||
|
let y = roofY[x];
|
||||||
|
for (; y < roofEnd; y++) {
|
||||||
|
const i = y * W + x, ph = PH[mat[i]];
|
||||||
|
if ((ph === PH_SOLID || ph === PH_POWDER || ph === PH_LIQUID) && !builtForLight(i, x, y) && !((ph === PH_POWDER || ph === PH_LIQUID) && looseForLight(i))) break;
|
||||||
|
}
|
||||||
|
lightRoof[lx] = y;
|
||||||
|
}
|
||||||
for (let ly = 0; ly < lh; ly++) {
|
for (let ly = 0; ly < lh; ly++) {
|
||||||
const wy = (ly0 + ly) * res;
|
const wy = (ly0 + ly) * res;
|
||||||
for (let lx = 0; lx < lw; lx++) {
|
for (let lx = 0; lx < lw; lx++) {
|
||||||
const wx = (lx0 + lx) * res, k = ly * lw + lx;
|
const wx = (lx0 + lx) * res, k = ly * lw + lx;
|
||||||
if (wx < 0 || wy < 0 || wx >= W || wy >= H) { la[k] = 0; lr[k] = lg[k] = lb[k] = 0; continue; }
|
if (wx < 0 || wy < 0 || wx >= W || wy >= H) { la[k] = 0; lr[k] = lg[k] = lb[k] = 0; continue; }
|
||||||
const i = wy * W + wx, m = mat[i], ph = PH[m];
|
const i = wy * W + wx, m = mat[i], ph = PH[m];
|
||||||
la[k] = ph === PH_EMPTY ? (bgm[i] ? aGas : aAir) : ph === PH_GAS ? aGas : ph === PH_LIQUID ? aLiq : m === GLASS || m === ICE ? aGas : aSol;
|
la[k] = builtForLight(i, wx, wy) || ((ph === PH_POWDER || ph === PH_LIQUID) && looseForLight(i)) ? aAir : ph === PH_EMPTY ? (bgm[i] ? aGas : aAir) : ph === PH_GAS ? aGas : ph === PH_LIQUID ? aLiq : m === GLASS || m === ICE ? aGas : aSol;
|
||||||
let r = 0, g = 0, b = 0;
|
let r = 0, g = 0, b = 0;
|
||||||
if (wy < roofY[wx]) { r = skyK; g = skyK * 0.98; b = skyK * 0.94; }
|
if (wy < lightRoof[lx]) { r = skyK; g = skyK * 0.98; b = skyK * 0.94; }
|
||||||
else if (wy < surfY[wx] && ph !== PH_SOLID) { const v = skyK * 0.9; r = v; g = v * 0.98; b = v * 0.95; }
|
else if (wy < surfY[wx] && ph !== PH_SOLID) { const v = skyK * 0.9; r = v; g = v * 0.98; b = v * 0.95; }
|
||||||
// emission from heat: hottest grain in the block
|
// emission from heat: hottest grain in the block
|
||||||
let T = 0;
|
let T = 0;
|
||||||
|
|||||||
+29
-15
@@ -119,7 +119,7 @@ function drawProcBlocks() {
|
|||||||
// glass that shows how full it is, a roof with the loading hatch. All in grain units from its corner.
|
// glass that shows how full it is, a roof with the loading hatch. All in grain units from its corner.
|
||||||
function drawSilo(ctx, mm, sx, sy, zp) {
|
function drawSilo(ctx, mm, sx, sy, zp) {
|
||||||
if (!R.smoothOn) { drawSiloPixel(ctx, mm, sx, sy, zp); return; }
|
if (!R.smoothOn) { drawSiloPixel(ctx, mm, sx, sy, zp); return; }
|
||||||
const X = u => sx + u * zp, Y = u => sy + u * zp, full = Math.min(1, (mm.sn || 0) / SILO_CAP);
|
const X = u => sx + u * zp, Y = u => sy + u * zp;
|
||||||
const steel = (x0, x1) => { const g = ctx.createLinearGradient(X(x0), 0, X(x1), 0); g.addColorStop(0, '#3b4249'); g.addColorStop(0.22, '#a9b3bb'); g.addColorStop(0.5, '#87919a'); g.addColorStop(1, '#353c43'); return g; };
|
const steel = (x0, x1) => { const g = ctx.createLinearGradient(X(x0), 0, X(x1), 0); g.addColorStop(0, '#3b4249'); g.addColorStop(0.22, '#a9b3bb'); g.addColorStop(0.5, '#87919a'); g.addColorStop(1, '#353c43'); return g; };
|
||||||
ctx.save();
|
ctx.save();
|
||||||
// legs, with short struts to the cone
|
// legs, with short struts to the cone
|
||||||
@@ -146,22 +146,16 @@ function drawSilo(ctx, mm, sx, sy, zp) {
|
|||||||
ctx.beginPath(); ctx.moveTo(X(1.3), Y(2)); ctx.lineTo(X(1.3), Y(19.8)); ctx.moveTo(X(2.3), Y(2)); ctx.lineTo(X(2.3), Y(19.8));
|
ctx.beginPath(); ctx.moveTo(X(1.3), Y(2)); ctx.lineTo(X(1.3), Y(19.8)); ctx.moveTo(X(2.3), Y(2)); ctx.lineTo(X(2.3), Y(19.8));
|
||||||
for (let yy = 2.6; yy < 19.8; yy += 1.1) { ctx.moveTo(X(1.3), Y(yy)); ctx.lineTo(X(2.3), Y(yy)); }
|
for (let yy = 2.6; yy < 19.8; yy += 1.1) { ctx.moveTo(X(1.3), Y(yy)); ctx.lineTo(X(2.3), Y(yy)); }
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
// the sight glass: fills from the bottom in the colour of what is inside
|
drawSiloWindow(ctx, mm, sx, sy, zp, false);
|
||||||
const gx = 9.1, gy0 = 3, gy1 = 18.8, gh = gy1 - gy0;
|
|
||||||
ctx.fillStyle = '#14181c'; ctx.fillRect(X(gx), Y(gy0), zp * 1.1, zp * gh);
|
|
||||||
if (mm.sn > 0) { ctx.fillStyle = `rgb(${COLR[mm.sm]},${COLG[mm.sm]},${COLB[mm.sm]})`; ctx.fillRect(X(gx + 0.15), Y(gy1 - gh * full), zp * 0.8, zp * gh * full); }
|
|
||||||
ctx.strokeStyle = '#c9d1d8'; ctx.lineWidth = Math.max(1, zp * 0.1);
|
|
||||||
ctx.strokeRect(X(gx), Y(gy0), zp * 1.1, zp * gh);
|
|
||||||
for (let k = 1; k < 4; k++) { const yy = Y(gy1 - gh * k / 4); ctx.beginPath(); ctx.moveTo(X(gx - 0.35), yy); ctx.lineTo(X(gx), yy); ctx.stroke(); }
|
|
||||||
// what it holds, stencilled on the drum
|
// what it holds, stencilled on the drum
|
||||||
if (zp >= 3) {
|
if (zp >= 3) {
|
||||||
renderMonoFont();
|
renderMonoFont();
|
||||||
const fs = Math.max(8, Math.round(zp * 1.15));
|
const fs = Math.max(8, Math.round(zp * 1.15));
|
||||||
ctx.font = `700 ${fs}px ${R.monoFont}`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
ctx.font = `700 ${fs}px ${R.monoFont}`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||||
ctx.fillStyle = 'rgba(20,24,28,0.75)';
|
ctx.fillStyle = 'rgba(20,24,28,0.75)';
|
||||||
ctx.fillText('SILO', X(5.6), Y(5));
|
ctx.fillText('SILO', X(4.5), Y(5), 5 * zp);
|
||||||
ctx.font = `600 ${Math.max(8, Math.round(zp * 0.95))}px ${R.monoFont}`;
|
ctx.font = `600 ${Math.max(8, Math.round(zp * 0.95))}px ${R.monoFont}`;
|
||||||
ctx.fillText(mm.sn ? mm.sn.toLocaleString('en-US') : 'EMPTY', X(5.6), Y(16.8));
|
ctx.fillText(mm.sn ? mm.sn.toLocaleString('en-US') : 'EMPTY', X(4.5), Y(16.8), 5 * zp);
|
||||||
if (mm.sn) { ctx.fillStyle = `rgb(${COLR[mm.sm]},${COLG[mm.sm]},${COLB[mm.sm]})`; ctx.fillRect(X(4.4), Y(17.9), zp * 2.4, zp * 0.55); }
|
if (mm.sn) { ctx.fillStyle = `rgb(${COLR[mm.sm]},${COLG[mm.sm]},${COLB[mm.sm]})`; ctx.fillRect(X(4.4), Y(17.9), zp * 2.4, zp * 0.55); }
|
||||||
}
|
}
|
||||||
// roof with a rim, and the loading hatch (its lid stands open)
|
// roof with a rim, and the loading hatch (its lid stands open)
|
||||||
@@ -173,7 +167,7 @@ function drawSilo(ctx, mm, sx, sy, zp) {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
function drawSiloPixel(ctx, mm, sx, sy, zp) {
|
function drawSiloPixel(ctx, mm, sx, sy, zp) {
|
||||||
const full = Math.min(1, (mm.sn || 0) / SILO_CAP), col = `rgb(${COLR[mm.sm]},${COLG[mm.sm]},${COLB[mm.sm]})`;
|
const col = `rgb(${COLR[mm.sm]},${COLG[mm.sm]},${COLB[mm.sm]})`;
|
||||||
const grain = (x, y, c) => { ctx.fillStyle = c; ctx.fillRect(sx + x * zp, sy + y * zp, zp, zp); };
|
const grain = (x, y, c) => { ctx.fillStyle = c; ctx.fillRect(sx + x * zp, sy + y * zp, zp, zp); };
|
||||||
// Steel shaded by column (light on the left, dark at the right edge), darker rib rows, and fixed
|
// Steel shaded by column (light on the left, dark at the right edge), darker rib rows, and fixed
|
||||||
// grain noise so it never shimmers. The drum is rows 2-19, the cone 20-22 narrowing to the gate.
|
// grain noise so it never shimmers. The drum is rows 2-19, the cone 20-22 narrowing to the gate.
|
||||||
@@ -191,8 +185,7 @@ function drawSiloPixel(ctx, mm, sx, sy, zp) {
|
|||||||
grain(1, 21, '#2f353b'); grain(10, 21, '#2f353b');
|
grain(1, 21, '#2f353b'); grain(10, 21, '#2f353b');
|
||||||
for (let x = 4; x < 8; x++) grain(x, 23, '#1c1f23');
|
for (let x = 4; x < 8; x++) grain(x, 23, '#1c1f23');
|
||||||
if (mm.open) for (let x = 8; x < 11; x++) grain(x, 23, '#c99a3c'); else for (let x = 3; x < 9; x++) grain(x, 23, '#6b737b');
|
if (mm.open) for (let x = 8; x < 11; x++) grain(x, 23, '#c99a3c'); else for (let x = 3; x < 9; x++) grain(x, 23, '#6b737b');
|
||||||
// the sight glass fills from the bottom in the colour of what is inside
|
drawSiloWindow(ctx, mm, sx, sy, zp, true);
|
||||||
for (let y = 3; y < 19; y++) grain(9, y, mm.sn > 0 && y >= 19 - Math.max(1, Math.round(16 * full)) ? col : '#14181c');
|
|
||||||
// roof: a rim, shoulders, the dark hatch and its lid standing open
|
// roof: a rim, shoulders, the dark hatch and its lid standing open
|
||||||
for (let x = 0; x < 12; x++) {
|
for (let x = 0; x < 12; x++) {
|
||||||
grain(x, 1, x >= 3 && x < 9 ? '#15181b' : '#5b646c');
|
grain(x, 1, x >= 3 && x < 9 ? '#15181b' : '#5b646c');
|
||||||
@@ -204,14 +197,35 @@ function drawSiloPixel(ctx, mm, sx, sy, zp) {
|
|||||||
renderMonoFont();
|
renderMonoFont();
|
||||||
ctx.save(); ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = 'rgba(20,24,28,0.75)';
|
ctx.save(); ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = 'rgba(20,24,28,0.75)';
|
||||||
ctx.font = `700 ${Math.max(8, Math.round(zp * 1.15))}px ${R.monoFont}`;
|
ctx.font = `700 ${Math.max(8, Math.round(zp * 1.15))}px ${R.monoFont}`;
|
||||||
ctx.fillText('SILO', sx + 5.6 * zp, sy + 5 * zp);
|
ctx.fillText('SILO', sx + 4.5 * zp, sy + 5 * zp, 5 * zp);
|
||||||
ctx.font = `600 ${Math.max(8, Math.round(zp * 0.95))}px ${R.monoFont}`;
|
ctx.font = `600 ${Math.max(8, Math.round(zp * 0.95))}px ${R.monoFont}`;
|
||||||
ctx.fillText(mm.sn ? mm.sn.toLocaleString('en-US') : 'EMPTY', sx + 5.6 * zp, sy + 16.8 * zp);
|
ctx.fillText(mm.sn ? mm.sn.toLocaleString('en-US') : 'EMPTY', sx + 4.5 * zp, sy + 16.8 * zp, 5 * zp);
|
||||||
if (mm.sn) { grain(4, 18, col); grain(5, 18, col); grain(6, 18, col); }
|
if (mm.sn) { grain(4, 18, col); grain(5, 18, col); grain(6, 18, col); }
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The same framed, three-grain window in both looks; one grain remains visible near empty.
|
||||||
|
function drawSiloWindow(ctx, mm, sx, sy, zp, pixel) {
|
||||||
|
const height = mm.sn > 0 ? Math.max(1, Math.round(16 * Math.min(1, mm.sn / SILO_CAP))) : 0;
|
||||||
|
ctx.fillStyle = '#c9d1d8'; ctx.fillRect(sx + 7 * zp, sy + 2 * zp, 5 * zp, 18 * zp);
|
||||||
|
ctx.fillStyle = '#14181c'; ctx.fillRect(sx + 8 * zp, sy + 3 * zp, 3 * zp, 16 * zp);
|
||||||
|
if (!height) return;
|
||||||
|
// Lift dark fuel slightly so coal stays distinct from the empty glass.
|
||||||
|
const lift = Math.max(COLR[mm.sm], COLG[mm.sm], COLB[mm.sm]) < 80 ? 32 : 0;
|
||||||
|
if (pixel) {
|
||||||
|
for (let y = 19 - height; y < 19; y++) for (let x = 8; x < 11; x++) {
|
||||||
|
const h = Math.imul(mm.x + x, 374761393) ^ Math.imul(mm.y + y, 668265263);
|
||||||
|
const n = lift + (((h ^ h >>> 13) >>> 0) % 3 - 1) * 8;
|
||||||
|
ctx.fillStyle = `rgb(${COLR[mm.sm] + n},${COLG[mm.sm] + n},${COLB[mm.sm] + n})`;
|
||||||
|
ctx.fillRect(sx + x * zp, sy + y * zp, zp, zp);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = `rgb(${COLR[mm.sm] + lift},${COLG[mm.sm] + lift},${COLB[mm.sm] + lift})`;
|
||||||
|
ctx.fillRect(sx + 8 * zp, sy + (19 - height) * zp, 3 * zp, height * zp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Shelves show each block's fill and materials; the bottom gate belongs to the column.
|
// Shelves show each block's fill and materials; the bottom gate belongs to the column.
|
||||||
function drawRackBlocks() {
|
function drawRackBlocks() {
|
||||||
const ctx = R.ctx;
|
const ctx = R.ctx;
|
||||||
|
|||||||
+129
-4
@@ -27,7 +27,7 @@ function drawRain() {
|
|||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
// Info overlay: a small arrow on every belt and screw tube block on screen, the way it moves things
|
// Info overlay: a small arrow on every belt and screw conveyor block on screen, the way it moves things
|
||||||
function drawFlowArrows() {
|
function drawFlowArrows() {
|
||||||
const ctx = R.ctx, zp = R.zp, v = viewCells();
|
const ctx = R.ctx, zp = R.zp, v = viewCells();
|
||||||
const bx0 = Math.max(0, Math.floor(R.camX / BS)), bx1 = Math.min(BW - 1, Math.floor((R.camX + v.w) / BS));
|
const bx0 = Math.max(0, Math.floor(R.camX / BS)), bx1 = Math.min(BW - 1, Math.floor((R.camX + v.w) / BS));
|
||||||
@@ -37,12 +37,12 @@ function drawFlowArrows() {
|
|||||||
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
||||||
for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
|
for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
|
||||||
const b = by * BW + bx, code = blockP[b];
|
const b = by * BW + bx, code = blockP[b];
|
||||||
if (code !== BP_BELT && code !== BP_TUBE && code !== BP_PUMP && code !== BP_OUTLET) continue;
|
if (code !== BP_BELT && code !== BP_SCREW && code !== BP_PUMP && code !== BP_OUTLET) continue;
|
||||||
const vv = blockV[b];
|
const vv = blockV[b];
|
||||||
const [dx, dy] = code === BP_BELT ? (vv ? [1, 0] : [-1, 0]) : code === BP_PUMP ? PUMP_V[vv & 3] : code === BP_OUTLET ? OUTLET_V[vv & 3] : [[0, -1], [1, 0], [0, 1], [-1, 0]][vv & 3];
|
const [dx, dy] = code === BP_BELT ? (vv ? [1, 0] : [-1, 0]) : code === BP_PUMP ? PUMP_V[vv & 3] : code === BP_OUTLET ? OUTLET_V[vv & 3] : SCREW_DIRS[vv & 7];
|
||||||
const c = worldToScreenPx(bx * BS + BS / 2, by * BS + BS / 2);
|
const c = worldToScreenPx(bx * BS + BS / 2, by * BS + BS / 2);
|
||||||
ctx.fillStyle = 'rgba(14,12,11,0.55)'; ctx.beginPath(); ctx.arc(c.x, c.y, r * 1.35, 0, Math.PI * 2); ctx.fill();
|
ctx.fillStyle = 'rgba(14,12,11,0.55)'; ctx.beginPath(); ctx.arc(c.x, c.y, r * 1.35, 0, Math.PI * 2); ctx.fill();
|
||||||
ctx.strokeStyle = code === BP_TUBE ? '#ffc58a' : '#e7dccb'; ctx.lineWidth = Math.max(1.2, r * 0.32);
|
ctx.strokeStyle = code === BP_SCREW ? '#ffc58a' : '#e7dccb'; ctx.lineWidth = Math.max(1.2, r * 0.32);
|
||||||
// a chevron pointing (dx, dy)
|
// a chevron pointing (dx, dy)
|
||||||
const px = -dy, py = dx;
|
const px = -dy, py = dx;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@@ -53,6 +53,131 @@ function drawFlowArrows() {
|
|||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
// "Doesn't connect": a red ring with a slash at a transport end that would join one block up or
|
||||||
|
// down (nearMiss), on built pieces, ghosts and the drag preview alike.
|
||||||
|
function drawJoinMark(bx, by, end, code, v) {
|
||||||
|
const e = pieceEnds(code, v)[end], ctx = R.ctx, r = Math.max(3, BS * R.zp * 0.24);
|
||||||
|
const c = worldToScreenPx(bx * BS + BS / 2 + e.dx * BS / 2, by * BS + BS / 2 + e.dy * BS / 2);
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = 'rgba(14,12,11,0.7)'; ctx.beginPath(); ctx.arc(c.x, c.y, r * 1.2, 0, Math.PI * 2); ctx.fill();
|
||||||
|
ctx.strokeStyle = '#ef6a55'; ctx.lineWidth = Math.max(1.2, r * 0.3);
|
||||||
|
ctx.beginPath(); ctx.arc(c.x, c.y, r * 0.75, 0, Math.PI * 2); ctx.stroke();
|
||||||
|
ctx.beginPath(); ctx.moveTo(c.x - r * 0.53, c.y + r * 0.53); ctx.lineTo(c.x + r * 0.53, c.y - r * 0.53); ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
function drawJoinMarks() {
|
||||||
|
const v = viewCells();
|
||||||
|
const bx0 = Math.max(0, Math.floor(R.camX / BS)), bx1 = Math.min(BW - 1, Math.floor((R.camX + v.w) / BS));
|
||||||
|
const by0 = Math.max(0, Math.floor(R.camY / BS)), by1 = Math.min(BH - 1, Math.floor((R.camY + v.h) / BS));
|
||||||
|
for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
|
||||||
|
const b = by * BW + bx, ghost = ghostBlocks.get(b), code = ghost ? ghost[0] : blockP[b], vv = ghost ? ghost[1] : blockV[b];
|
||||||
|
if (!code || !pieceEnds(code, vv).length) continue;
|
||||||
|
for (let end = 0; end < 2; end++) if (nearMiss(bx, by, end, ghostBlocks.size ? ghostBlocks : null)) drawJoinMark(bx, by, end, code, vv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Throughput (t49, info overlay): grains per second at the open outlet end of every line, and
|
||||||
|
// an orange glow on transport blocks that are loaded but not moving, where a line backs up.
|
||||||
|
function flowLoaded(b, code) {
|
||||||
|
const bx = b % BW, by = (b / BW) | 0;
|
||||||
|
let n = 0;
|
||||||
|
for (let k = 0; k < BS; k++) {
|
||||||
|
const x = bx * BS + k, i = code === BP_SCREW ? (by * BS + 1) * W + x : (beltFaceY(code, bx, by, x) - 1) * W + x;
|
||||||
|
if (i >= 0 && i < N && PH[mat[i]] === PH_POWDER) n++;
|
||||||
|
}
|
||||||
|
return n >= 3;
|
||||||
|
}
|
||||||
|
function drawFlowRates() {
|
||||||
|
const ctx = R.ctx, v = viewCells(), s = BS * R.zp;
|
||||||
|
const bx0 = Math.max(0, Math.floor(R.camX / BS)), bx1 = Math.min(BW - 1, Math.floor((R.camX + v.w) / BS));
|
||||||
|
const by0 = Math.max(0, Math.floor(R.camY / BS)), by1 = Math.min(BH - 1, Math.floor((R.camY + v.h) / BS));
|
||||||
|
ctx.save();
|
||||||
|
ctx.font = `600 ${Math.max(10, Math.min(14, s * 0.45))}px ui-monospace, monospace`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||||
|
for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
|
||||||
|
const b = by * BW + bx, code = blockP[b], ends = pieceEnds(code, blockV[b]);
|
||||||
|
if (!ends.length) continue;
|
||||||
|
const rate = flowRate[b] || 0;
|
||||||
|
if (rate < 0.5 && flowLoaded(b, code)) {
|
||||||
|
const p = worldToScreenPx(bx * BS, by * BS);
|
||||||
|
ctx.fillStyle = 'rgba(255,150,60,0.32)'; ctx.fillRect(p.x, p.y, s, s);
|
||||||
|
}
|
||||||
|
const out = ends[1].outlet ? 1 : 0;
|
||||||
|
if (endJoin(bx, by, out) >= 0) continue;
|
||||||
|
const e = ends[out], c = worldToScreenPx(bx * BS + BS / 2 + e.dx * BS * 0.95, by * BS + BS / 2 + e.dy * BS * 0.95 - (e.dy ? 0 : BS * 0.9));
|
||||||
|
const label = rate >= 10 ? `${Math.round(rate)}/s` : `${rate.toFixed(1)}/s`, tw = ctx.measureText(label).width;
|
||||||
|
ctx.fillStyle = 'rgba(14,12,11,0.8)'; ctx.fillRect(c.x - tw / 2 - 4, c.y - 9, tw + 8, 18);
|
||||||
|
ctx.fillStyle = rate > 0 ? '#e7dccb' : '#ef9a6a'; ctx.fillText(label, c.x, c.y + 1);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
// The resonance ping's echo: a faint ring spreading from the bot, the veins it found showing in
|
||||||
|
// their own lit colour inside it (with a slightly lighter rim so dark seams read against fog),
|
||||||
|
// a brief sparkle as the ring passes them; each vein fades a fixed time after the ring reached
|
||||||
|
// it, so the fade follows the ring outward. The veins are drawn once into a small image; each
|
||||||
|
// frame draws it in a few distance bands (fading ones at partial alpha) plus a few dozen sparkles.
|
||||||
|
const PING_SPEED = 160, PING_HOLD = 1200, PING_FADE = 800, PING_BANDS = 6, PING_GLINTS = 60, GLINT_MS = 450;
|
||||||
|
function pingImage(p) {
|
||||||
|
const size = p.r * 2 + 1, c = document.createElement('canvas'); c.width = c.height = size;
|
||||||
|
const g = c.getContext('2d'), img = g.createImageData(size, size), d = img.data, ox = Math.floor(p.x - p.r), oy = Math.floor(p.y - p.r);
|
||||||
|
const lit = (i, k, lift) => { const m = mat[i], v = (noiseB[i] - 128) * CVAR[m] / 128 + lift; d[k] = COLR[m] + v; d[k + 1] = COLG[m] + v; d[k + 2] = COLB[m] + v; d[k + 3] = 255; };
|
||||||
|
for (const i of p.cells) { const x = i % W - ox, y = ((i / W) | 0) - oy; if (x >= 0 && y >= 0 && x < size && y < size) lit(i, (y * size + x) * 4, 0); }
|
||||||
|
for (const i of p.cells) {
|
||||||
|
const x = i % W - ox, y = ((i / W) | 0) - oy, k = (y * size + x) * 4;
|
||||||
|
if (x <= 0 || y <= 0 || x >= size - 1 || y >= size - 1) continue;
|
||||||
|
if (!d[k - 1] || !d[k + 7] || !d[k - size * 4 + 3] || !d[k + size * 4 + 3]) lit(i, k, 45); // the rim
|
||||||
|
}
|
||||||
|
g.putImageData(img, 0, 0);
|
||||||
|
const step = Math.max(1, Math.ceil(p.cells.length / PING_GLINTS));
|
||||||
|
const glints = p.cells.filter((_, k) => k % step === 0).map(i => ({ x: i % W + 0.5, y: ((i / W) | 0) + 0.5, d: Math.hypot(i % W - p.x, ((i / W) | 0) - p.y), m: mat[i] }));
|
||||||
|
return { c, ox, oy, glints };
|
||||||
|
}
|
||||||
|
function drawPing() {
|
||||||
|
const p = GAME.ping;
|
||||||
|
if (!p) return;
|
||||||
|
const ms = performance.now() - p.t0, front = ms / 1000 * PING_SPEED, done = p.r / PING_SPEED * 1000;
|
||||||
|
if (ms > done + PING_HOLD + PING_FADE) { GAME.ping = null; return; }
|
||||||
|
if (!p.img) p.img = pingImage(p);
|
||||||
|
const ctx = R.ctx, zp = R.zp, c = worldToScreenPx(p.x, p.y), o = worldToScreenPx(p.img.ox, p.img.oy);
|
||||||
|
// inside `gone` the veins have faded out; from `full` out to the ring they are fully lit
|
||||||
|
const outer = Math.min(front, p.r + 1), full = (ms - PING_HOLD) / 1000 * PING_SPEED, gone = (ms - PING_HOLD - PING_FADE) / 1000 * PING_SPEED;
|
||||||
|
const band = (r0, r1, a) => {
|
||||||
|
if (r1 <= Math.max(0, r0) || a <= 0) return;
|
||||||
|
ctx.save(); ctx.beginPath(); ctx.arc(c.x, c.y, r1 * zp, 0, Math.PI * 2);
|
||||||
|
if (r0 > 0) ctx.arc(c.x, c.y, r0 * zp, 0, Math.PI * 2, true);
|
||||||
|
ctx.clip(); ctx.globalAlpha = a; ctx.imageSmoothingEnabled = false;
|
||||||
|
ctx.drawImage(p.img.c, o.x, o.y, p.img.c.width * zp, p.img.c.height * zp); ctx.restore();
|
||||||
|
};
|
||||||
|
band(Math.max(full, 0), outer, 1);
|
||||||
|
for (let k = 0; k < PING_BANDS; k++) {
|
||||||
|
const r0 = gone + (full - gone) * k / PING_BANDS, r1 = gone + (full - gone) * (k + 1) / PING_BANDS;
|
||||||
|
band(r0, Math.min(r1, outer), (k + 0.5) / PING_BANDS);
|
||||||
|
}
|
||||||
|
ctx.save();
|
||||||
|
if (front < p.r) {
|
||||||
|
ctx.strokeStyle = `rgba(160,225,255,${0.35 * (1 - front / p.r)})`; ctx.lineWidth = Math.max(1, zp * 0.4);
|
||||||
|
ctx.beginPath(); ctx.arc(c.x, c.y, front * zp, 0, Math.PI * 2); ctx.stroke();
|
||||||
|
}
|
||||||
|
// a four-point glint where the ring has just passed, growing then shrinking
|
||||||
|
for (const gl of p.img.glints) {
|
||||||
|
const t = (ms - gl.d / PING_SPEED * 1000) / GLINT_MS;
|
||||||
|
if (t < 0 || t > 1) continue;
|
||||||
|
const s = worldToScreenPx(gl.x, gl.y), k = Math.sin(t * Math.PI) * zp * 2.2, m = gl.m;
|
||||||
|
ctx.strokeStyle = `rgba(${Math.min(255, COLR[m] + 140)},${Math.min(255, COLG[m] + 140)},${Math.min(255, COLB[m] + 140)},${0.9 * Math.sin(t * Math.PI)})`;
|
||||||
|
ctx.lineWidth = Math.max(1, zp * 0.35);
|
||||||
|
ctx.beginPath(); ctx.moveTo(s.x - k, s.y); ctx.lineTo(s.x + k, s.y); ctx.moveTo(s.x, s.y - k); ctx.lineTo(s.x, s.y + k); ctx.stroke();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
// While the ping recharges, a thin arc around the bot fills up (only with the drill in hand).
|
||||||
|
function drawPingRecharge() {
|
||||||
|
if (toolId() !== 'drill' || GAME.pingTick === undefined || tick < GAME.pingTick) return;
|
||||||
|
const k = (tick - GAME.pingTick) / PING_RECHARGE;
|
||||||
|
if (k >= 1) return;
|
||||||
|
const ctx = R.ctx, c = worldToScreenPx(bot.x + bot.w / 2, bot.y + bot.h / 2), r = Math.max(bot.w, bot.h) * R.zp * 0.9;
|
||||||
|
ctx.save(); ctx.lineWidth = Math.max(2, R.zp * 0.5); ctx.lineCap = 'round';
|
||||||
|
ctx.strokeStyle = 'rgba(160,225,255,0.18)'; ctx.beginPath(); ctx.arc(c.x, c.y, r, 0, Math.PI * 2); ctx.stroke();
|
||||||
|
ctx.strokeStyle = 'rgba(160,225,255,0.7)'; ctx.beginPath(); ctx.arc(c.x, c.y, r, -Math.PI / 2, -Math.PI / 2 + k * Math.PI * 2); ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
// Outline a run of blocks: strong while it is being edited, faint when pointed at
|
// Outline a run of blocks: strong while it is being edited, faint when pointed at
|
||||||
function drawRunOutline(run, strong) {
|
function drawRunOutline(run, strong) {
|
||||||
if (!run || !run.length) return;
|
if (!run || !run.length) return;
|
||||||
|
|||||||
+2
-2
@@ -116,8 +116,8 @@ function thinPipeColor(x, y, anim) {
|
|||||||
}
|
}
|
||||||
PC[0] = r; PC[1] = g; PC[2] = bl;
|
PC[0] = r; PC[1] = g; PC[2] = bl;
|
||||||
}
|
}
|
||||||
// a block that is a belt or a filter: they join up into one belt
|
// A filter frame opens only where this end has a geometric partner.
|
||||||
function beltLike(b) { if (b < 0 || b >= NB) return false; const c = blockP[b]; return c === BP_BELT || c === BP_FILTER; }
|
function beltLike(b, end) { return endJoin(b % BW, (b / BW) | 0, end) >= 0; }
|
||||||
// standing trees: the trunk shows its dark bark, so it reads apart from the pale cut wood it drops
|
// standing trees: the trunk shows its dark bark, so it reads apart from the pale cut wood it drops
|
||||||
const TREE_R = new Uint8Array(NMAT), TREE_G = new Uint8Array(NMAT), TREE_B = new Uint8Array(NMAT);
|
const TREE_R = new Uint8Array(NMAT), TREE_G = new Uint8Array(NMAT), TREE_B = new Uint8Array(NMAT);
|
||||||
for (let m = 0; m < NMAT; m++) { TREE_R[m] = COLR[m]; TREE_G[m] = COLG[m]; TREE_B[m] = COLB[m]; }
|
for (let m = 0; m < NMAT; m++) { TREE_R[m] = COLR[m]; TREE_G[m] = COLG[m]; TREE_B[m] = COLB[m]; }
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
// Smooth slope shading, kept beside the shared CPU geometry in slope-belts.js.
|
||||||
|
const SM_SLOPE_HELPERS = `
|
||||||
|
// The same rounded strokes as slopeBeltShape; y/z carry distance along/across the band.
|
||||||
|
vec3 slopeStroke(vec2 p, vec2 a, vec2 b, float origin) {
|
||||||
|
vec2 ray = b - a; float len = length(ray); ray /= len;
|
||||||
|
float along = dot(p - a, ray), across = dot(p - a, vec2(-ray.y, ray.x));
|
||||||
|
vec2 q = abs(vec2(along - len * 0.5, across)) - vec2(len * 0.5 - 0.75, 1.1);
|
||||||
|
float sd = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - 0.75;
|
||||||
|
return vec3(sd, origin + along, across + 2.0);
|
||||||
|
}
|
||||||
|
vec3 slopeCorner(vec2 p, vec2 a, float origin) {
|
||||||
|
vec2 d = p - a; float radius = length(d);
|
||||||
|
return vec3(radius - 1.85, origin + d.x, 2.0 + d.y);
|
||||||
|
}
|
||||||
|
vec3 slopeShape(vec2 p, vec2 worldOrigin, float grade, int joins) {
|
||||||
|
if ((joins & 64) != 0) p.y -= 0.45;
|
||||||
|
float sy = grade < 0.0 ? 4.0 : 0.0;
|
||||||
|
vec2 a = vec2(-grade * 1.30814755, sy + 1.30814755), b = vec2(4.0 - grade * 1.30814755, 4.0 - sy + 1.30814755);
|
||||||
|
if ((joins & 1) != 0) { a.y = sy + 2.0; a.x = (a.y - sy - 2.61629509) / grade; }
|
||||||
|
else if ((joins & 4) != 0) a -= vec2(4.0, grade * 4.0);
|
||||||
|
if ((joins & 2) != 0) { b.y = 6.0 - sy; b.x = (b.y - sy - 2.61629509) / grade; }
|
||||||
|
else if ((joins & 8) != 0) b += vec2(4.0, grade * 4.0);
|
||||||
|
vec2 axis = vec2(1.0, grade) * 0.70710678;
|
||||||
|
vec3 best = slopeStroke(p, a, b, dot(worldOrigin + a, axis));
|
||||||
|
if ((joins & 1) != 0) {
|
||||||
|
vec3 h = slopeStroke(p, vec2(-2.0, a.y), a, worldOrigin.x - 2.0);
|
||||||
|
if (h.x < best.x) best = h;
|
||||||
|
h = slopeCorner(p, a, worldOrigin.x + a.x);
|
||||||
|
if (h.x < best.x) best = h;
|
||||||
|
}
|
||||||
|
if ((joins & 2) != 0) {
|
||||||
|
vec3 h = slopeStroke(p, b, vec2(6, b.y), worldOrigin.x + b.x);
|
||||||
|
if (h.x < best.x) best = h;
|
||||||
|
h = slopeCorner(p, b, worldOrigin.x + b.x);
|
||||||
|
if (h.x < best.x) best = h;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SM_SLOPE_DRAW = `
|
||||||
|
if (nc == 31 || nc == 32) {
|
||||||
|
vec4 tag = cellAt(tTree, c);
|
||||||
|
float grade = nc == 31 ? -1.0 : 1.0;
|
||||||
|
vec2 local = floor(tag.gb * 255.0 + 0.5) - 8.0 + fract(w);
|
||||||
|
float sy = grade < 0.0 ? 4.0 : 0.0;
|
||||||
|
float foot = (local.x + grade * (local.y - sy)) * 0.5;
|
||||||
|
float depth = (grade * local.x - local.y + sy) * 0.70710678;
|
||||||
|
int wallFlags = int(tag.a * 255.0 + 0.5);
|
||||||
|
float ends = max((wallFlags & 8) == 0 ? -foot : -100.0, (wallFlags & 16) == 0 ? foot - 4.0 : -100.0);
|
||||||
|
float sd = max(ends * 1.41421356, max(-depth, depth - ${CLEAT_HEIGHT.toFixed(1)}));
|
||||||
|
float phase = float(wallFlags & 7);
|
||||||
|
float wave = 0.5 + 0.5 * sin((foot + w.x - local.x - phase) * 3.14159265);
|
||||||
|
vec3 rubber = mix(vec3(0.20,0.22,0.25), vec3(0.25,0.27,0.31), wave);
|
||||||
|
C = mix(C, rubber, 1.0 - smoothstep(-px, px, sd));
|
||||||
|
// the ribs, where the simulation has them: rubber bars standing straight out of an uphill belt,
|
||||||
|
// one every ${CLEAT_SPACING} face cells, moved along by the belt's phase
|
||||||
|
float dirS = (wallFlags & 32) != 0 ? 1.0 : -1.0;
|
||||||
|
if (grade * dirS < 0.0) {
|
||||||
|
float rc = (w.x - local.x) + foot - (grade < 0.0 ? 1.0 : 0.0) - dirS * phase;
|
||||||
|
float off = (mod(rc + ${(CLEAT_SPACING / 2).toFixed(1)}, ${CLEAT_SPACING.toFixed(1)}) - ${(CLEAT_SPACING / 2).toFixed(1)}) * 1.41421356;
|
||||||
|
float ribTop = ${((2 * CLEAT_REACH + 1) * Math.SQRT1_2).toFixed(3)};
|
||||||
|
float rd = length(vec2(off, depth - clamp(depth, 0.0, ribTop))) - 0.5;
|
||||||
|
vec3 rib = mix(vec3(0.50,0.52,0.56), vec3(0.70,0.72,0.76), smoothstep(ribTop - 1.2, ribTop, depth));
|
||||||
|
rib *= mix(0.78, 1.0, smoothstep(-0.5, 0.2, off * -dirS * grade));
|
||||||
|
C = mix(C, rib, 1.0 - smoothstep(-px, px, rd));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slopes use the flat belt's band, rollers and mesh in coordinates along the run.
|
||||||
|
if ((nc == 2 || (nc >= 23 && nc <= 30)) &&
|
||||||
|
(int(cellAt(tTree, c).a * 255.0 + 0.5) & 128) != 0 &&
|
||||||
|
cellAt(tTree, c).r * 255.0 >= 22.5 && cellAt(tTree, c).r * 255.0 <= 30.5) {
|
||||||
|
int slopeClass = int(cellAt(tTree, c).r * 255.0 + 0.5);
|
||||||
|
int shape = (slopeClass - 23) / 2;
|
||||||
|
bool mesh = shape >= 2;
|
||||||
|
float grade = (shape == 0 || shape == 2) ? -1.0 : 1.0;
|
||||||
|
float dir = ((slopeClass - 23) % 2) == 1 ? 1.0 : -1.0;
|
||||||
|
vec4 tag = cellAt(tTree, c);
|
||||||
|
int joins = int(tag.a * 255.0 + 0.5);
|
||||||
|
vec2 local = floor(tag.gb * 255.0 + 0.5) - 8.0 + fract(w);
|
||||||
|
vec3 shapePos = slopeShape(local, w - local, grade, joins);
|
||||||
|
float sd = shapePos.x, axis = shapePos.y, depth = shapePos.z;
|
||||||
|
float time = !mesh && (joins & 16) != 0 ? uTime : 0.0;
|
||||||
|
vec2 lb = vec2(axis, depth), alongWorld = vec2(axis, depth);
|
||||||
|
float rg = step(0.72, fract((axis - dir * (depth < 2.0 ? 1.0 : -1.0) * time * 30.0) * 0.5));
|
||||||
|
vec3 belt;
|
||||||
|
if (-sd < (mesh ? 1.1 : 0.8)) {
|
||||||
|
belt = mesh ? mix(vec3(0.22,0.25,0.29), vec3(0.48,0.53,0.59), depth < 2.0 ? 0.85 : 0.35) : bandColor(lb, rg);
|
||||||
|
belt *= mix(0.55, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), -sd));
|
||||||
|
} else if (mesh) {
|
||||||
|
vec2 cg = abs(fract(alongWorld) - 0.5);
|
||||||
|
float bw2 = max(px * 0.9, 0.09);
|
||||||
|
float hole = 1.0 - smoothstep(0.5 - bw2 - px, 0.5 - bw2, max(cg.x, cg.y));
|
||||||
|
vec3 steel = dot(own.rgb, vec3(1)) > 0.01 ? mix(vec3(0.42, 0.44, 0.5), own.rgb, 0.4) : vec3(0.42, 0.44, 0.5);
|
||||||
|
belt = mix(steel * 0.78, C * 0.8, hole);
|
||||||
|
} else {
|
||||||
|
vec2 dv = vec2(mod(axis, 2.0) - 1.0, depth - 2.0);
|
||||||
|
float rd = length(dv);
|
||||||
|
belt = vec3(0.15, 0.16, 0.18);
|
||||||
|
if (rd < 0.95) {
|
||||||
|
float ang = atan(dv.y, dv.x) - dir * time * 30.0 / 0.95;
|
||||||
|
float spoke = smoothstep(0.55, 0.85, cos(ang * 3.0));
|
||||||
|
belt = mix(vec3(0.5, 0.36, 0.25), vec3(0.72, 0.53, 0.36), 0.5 + 0.5 * dot(normalize(dv + 1e-4), vec2(-0.6, -0.8)) * smoothstep(0.2, 0.95, rd));
|
||||||
|
belt *= mix(1.0, 0.72, spoke * smoothstep(0.25, 0.4, rd));
|
||||||
|
belt *= mix(1.0, 0.55, smoothstep(0.82, 0.95, rd));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
C = mix(C, belt, 1.0 - smoothstep(-px, px, sd));
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
// Drawing only: the face stays on its simulation cells; the band hangs below it.
|
||||||
|
let slopeBeltJoins = new Uint8Array(0), slopeBeltHalo = new Uint8Array(0), beltCaps = new Uint8Array(0);
|
||||||
|
let slopeAcross = 0, slopeAlong = 0, slopeDistance = 0;
|
||||||
|
function slopeBeltConnections(bx, by, code, planned = null) {
|
||||||
|
let joins = 0;
|
||||||
|
for (let end = 0; end < 2; end++) {
|
||||||
|
const join = endJoin(bx, by, end, planned);
|
||||||
|
if (join < 0) continue;
|
||||||
|
const nb = join >> 1, next = planned && planned.get(nb);
|
||||||
|
const slope = isSlopeBelt(next ? next[0] : blockP[nb]) &&
|
||||||
|
((nb / BW) | 0) - by === beltGrade(code) * (nb % BW - bx);
|
||||||
|
// A same-row feed into a low end drops onto its face: drawn as two capped ends, still joined.
|
||||||
|
if (!slope && joinIsDrop(bx, by, end, join, planned)) continue;
|
||||||
|
joins |= (slope ? 4 : 1) << end;
|
||||||
|
}
|
||||||
|
return joins;
|
||||||
|
}
|
||||||
|
function flatBeltCaps(b, planned = null) {
|
||||||
|
const bx = b % BW, by = (b / BW) | 0;
|
||||||
|
let caps = 0;
|
||||||
|
for (let end = 0; end < 2; end++) {
|
||||||
|
const join = endJoin(bx, by, end, planned);
|
||||||
|
if (join < 0 || joinIsDrop(bx, by, end, join, planned)) caps |= 1 << end;
|
||||||
|
}
|
||||||
|
return caps;
|
||||||
|
}
|
||||||
|
function prepareSlopeBelts(x0, y0, x1, y1) {
|
||||||
|
if (slopeBeltJoins.length !== NB) { slopeBeltJoins = new Uint8Array(NB); slopeBeltHalo = new Uint8Array(NB); beltCaps = new Uint8Array(NB); }
|
||||||
|
slopeBeltHalo.fill(0);
|
||||||
|
for (let by = Math.max(0, (y0 >> 2) - 2); by <= Math.min(BH - 1, (y1 >> 2) + 2); by++) {
|
||||||
|
for (let bx = Math.max(0, (x0 >> 2) - 2); bx <= Math.min(BW - 1, (x1 >> 2) + 2); bx++) {
|
||||||
|
const b = by * BW + bx, c = blockP[b];
|
||||||
|
if (c === BP_BELT || c === BP_FILTER) beltCaps[b] = flatBeltCaps(b);
|
||||||
|
if (!isSlopeBelt(c)) continue;
|
||||||
|
const face = beltFaceY(c, bx, by, bx * BS) * W + bx * BS;
|
||||||
|
slopeBeltJoins[b] = slopeBeltConnections(bx, by, c) | (netSat(face) > 0 ? 16 : 0) | (filterCfg.get(b)?.inv ? 32 : 0) | (isFilter(c) && netSat(face) > 0 && screenLoaded(b) && (tick & 2) ? 64 : 0);
|
||||||
|
for (let yy = Math.max(0, by - 2); yy <= Math.min(BH - 1, by + 1); yy++)
|
||||||
|
for (let xx = Math.max(0, bx - 1); xx <= Math.min(BW - 1, bx + 1); xx++) slopeBeltHalo[yy * BW + xx] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Rounded stroke, with coordinates along/across the winning segment for rollers and mesh.
|
||||||
|
function slopeBeltSegment(x, y, ax, ay, bx, by, origin) {
|
||||||
|
const dx = bx - ax, dy = by - ay, len = Math.hypot(dx, dy), ux = dx / len, uy = dy / len;
|
||||||
|
const along = (x - ax) * ux + (y - ay) * uy, across = -(x - ax) * uy + (y - ay) * ux;
|
||||||
|
const qx = Math.abs(along - len / 2) - len / 2 + 0.75, qy = Math.abs(across) - 1.1;
|
||||||
|
const sd = Math.hypot(Math.max(0, qx), Math.max(0, qy)) + Math.min(Math.max(qx, qy), 0) - 0.75;
|
||||||
|
if (sd < slopeDistance) { slopeDistance = sd; slopeAcross = across + 2; slopeAlong = origin + along; }
|
||||||
|
}
|
||||||
|
function slopeBeltCorner(x, y, ax, ay, origin) {
|
||||||
|
const dx = x - ax, dy = y - ay, radius = Math.hypot(dx, dy), sd = radius - 1.85;
|
||||||
|
if (sd < slopeDistance) { slopeDistance = sd; slopeAcross = 2 + dy; slopeAlong = origin + dx; }
|
||||||
|
}
|
||||||
|
function slopeBeltShape(x, y, b, joins = slopeBeltJoins[b], code = blockP[b]) {
|
||||||
|
const grade = beltGrade(code), ox = (b % BW) * BS, oy = ((b / BW) | 0) * BS;
|
||||||
|
x -= ox; y -= oy;
|
||||||
|
if (isFilter(code) && (joins & 64)) y -= 0.45;
|
||||||
|
const offset = 1.85 * Math.SQRT2, sy = grade < 0 ? 4 : 0;
|
||||||
|
const normal = 1.85 * Math.SQRT1_2;
|
||||||
|
let ax = -grade * normal, ay = sy + normal, bx = 4 - grade * normal, by = 4 - sy + normal;
|
||||||
|
if (joins & 1) { ay = sy + 2; ax = (ay - sy - offset) / grade; }
|
||||||
|
else if (joins & 4) { ax -= 4; ay -= grade * 4; }
|
||||||
|
if (joins & 2) { by = 6 - sy; bx = (by - sy - offset) / grade; }
|
||||||
|
else if (joins & 8) { bx += 4; by += grade * 4; }
|
||||||
|
slopeDistance = Infinity;
|
||||||
|
const origin = (ox + grade * oy) * Math.SQRT1_2;
|
||||||
|
slopeBeltSegment(x, y, ax, ay, bx, by, origin + (ax + grade * ay) * Math.SQRT1_2);
|
||||||
|
if (joins & 1) {
|
||||||
|
slopeBeltSegment(x, y, -2, ay, ax, ay, ox - 2);
|
||||||
|
slopeBeltCorner(x, y, ax, ay, ox + ax);
|
||||||
|
}
|
||||||
|
if (joins & 2) {
|
||||||
|
slopeBeltSegment(x, y, bx, by, 6, by, ox + bx);
|
||||||
|
slopeBeltCorner(x, y, bx, by, ox + bx);
|
||||||
|
}
|
||||||
|
return slopeDistance;
|
||||||
|
}
|
||||||
|
function slopeBeltAt(i, x, y, smooth = false) {
|
||||||
|
const own = (y >> 2) * BW + (x >> 2), code = blockP[own];
|
||||||
|
if (!slopeBeltHalo[own]) return -1;
|
||||||
|
const slope = isSlopeBelt(code);
|
||||||
|
if (!slope && (mach[i] || occ[i] || (mat[i] !== EMPTY && mat[i] !== BELTL && mat[i] !== BELTR))) return -1;
|
||||||
|
let seal = !slope && (mat[i] === BELTL || mat[i] === BELTR || part[i] === P_FILTER) ? slopeSealOwner(i) : -1;
|
||||||
|
if (seal >= 0 && !isSlopeBelt(blockP[seal])) seal = -1;
|
||||||
|
if (!slope && seal < 0 && (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || code)) return -1;
|
||||||
|
let best = smooth ? 0.8 : 0, found = slope ? own : seal;
|
||||||
|
for (let yy = Math.max(0, (y >> 2) - 1); yy <= Math.min(BH - 1, (y >> 2) + 1); yy++) {
|
||||||
|
for (let xx = Math.max(0, (x >> 2) - 1); xx <= Math.min(BW - 1, (x >> 2) + 1); xx++) {
|
||||||
|
const b = yy * BW + xx;
|
||||||
|
if (!isSlopeBelt(blockP[b])) continue;
|
||||||
|
const sd = slopeBeltShape(x + 0.5, y + 0.5, b);
|
||||||
|
if (sd < best) { best = sd; found = b; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
function slopeBeltColor(b, x, y, anim, br, bg, bb) {
|
||||||
|
const sd = slopeBeltShape(x + 0.5, y + 0.5, b);
|
||||||
|
if (sd > 1e-8) return (br | 0) | ((bg | 0) << 8) | ((bb | 0) << 16);
|
||||||
|
const c = blockP[b], joins = slopeBeltJoins[b], mesh = isFilter(c);
|
||||||
|
const dir = mesh ? (blockV[b] & 128 ? 1 : -1) : (blockV[b] ? 1 : -1);
|
||||||
|
const a = slopeAlong, d = slopeAcross, time = !mesh && joins & 16 ? anim : 0;
|
||||||
|
let r = 38, g = 41, blue = 46;
|
||||||
|
if (-sd < (mesh ? 1.1 : 0.8)) {
|
||||||
|
const stripe = (Math.floor((a - dir * (d < 2 ? 1 : -1) * (time >> 1)) / 2) & 1), k = d < 2 ? 1 : 0.72;
|
||||||
|
r = (stripe ? 92 : 56) * k; g = (stripe ? 96 : 58) * k; blue = (stripe ? 104 : 66) * k;
|
||||||
|
} else if (mesh) {
|
||||||
|
const f = filterCfg.get(b), m = f && f.m.length ? f.m[(Math.floor(a / 2) & 65535) % f.m.length] : 0;
|
||||||
|
if ((Math.floor(a) + Math.floor(d)) & 1) { r = br * 0.8; g = bg * 0.8; blue = bb * 0.8; }
|
||||||
|
else { r = m ? COLR[m] * 0.75 : 64; g = m ? COLG[m] * 0.75 : 68; blue = m ? COLB[m] * 0.75 : 76; }
|
||||||
|
} else {
|
||||||
|
const dx = ((a % 2 + 2) % 2) - 1, dy = d - 2;
|
||||||
|
if (dx * dx + dy * dy < 0.95 * 0.95) {
|
||||||
|
const spin = Math.cos((Math.atan2(dy, dx) - dir * time / 1.9) * 3) > 0.6;
|
||||||
|
r = spin ? 104 : 132; g = spin ? 94 : 120; blue = spin ? 84 : 108;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (r | 0) | ((g | 0) << 8) | ((blue | 0) << 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
function screenLoaded(b) {
|
||||||
|
const bx = b % BW, by = (b / BW) | 0, c = blockP[b];
|
||||||
|
for (let x = bx * BS; x < bx * BS + BS; x++) {
|
||||||
|
const i = (beltFaceY(c, bx, by, x) - 1) * W + x;
|
||||||
|
if (i >= 0 && (PH[mat[i]] === PH_POWDER || occ[i])) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Filled far wall, in belt coordinates. Project onto the face so ends stop at the slope ends.
|
||||||
|
let slopeWallOwner = -1;
|
||||||
|
function slopeWallShape(x, y, b, code = blockP[b]) {
|
||||||
|
const g = beltGrade(code), ox = b % BW * BS, oy = ((b / BW) | 0) * BS;
|
||||||
|
const sy = g < 0 ? BS : 0, lx = x - ox, ly = y - oy - sy;
|
||||||
|
const foot = (lx + g * ly) / 2, depth = (g * lx - ly) * Math.SQRT1_2;
|
||||||
|
return Math.max(-foot * Math.SQRT2, (foot - BS) * Math.SQRT2, -depth, depth - CLEAT_HEIGHT);
|
||||||
|
}
|
||||||
|
function slopeWallAt(x, y, smooth = false) {
|
||||||
|
slopeWallOwner = -1;
|
||||||
|
let best = smooth ? 0.8 : 0;
|
||||||
|
for (let by = Math.max(0, (y >> 2) - 1); by <= Math.min(BH - 1, (y >> 2) + 3); by++)
|
||||||
|
for (let bx = Math.max(0, (x >> 2) - 2); bx <= Math.min(BW - 1, (x >> 2) + 2); bx++) {
|
||||||
|
const b = by * BW + bx;
|
||||||
|
if (!isBelt(blockP[b]) || !isSlopeBelt(blockP[b]) || !slopeUphill(blockP[b], blockV[b])) continue; // downhill: a plain belt
|
||||||
|
const sd = slopeWallShape(x + 0.5, y + 0.5, b);
|
||||||
|
if (sd <= best) { best = sd; slopeWallOwner = b; }
|
||||||
|
}
|
||||||
|
return slopeWallOwner;
|
||||||
|
}
|
||||||
|
// The load occludes the far wall; ribs use exactly the simulation barrier raster.
|
||||||
|
function slopeDetailAt(i, x, y, smooth = false) {
|
||||||
|
slopeWallOwner = -1;
|
||||||
|
if (!slopeBeltHalo[(y >> 2) * BW + (x >> 2)] && !cleatMask[i]) return 0;
|
||||||
|
if (occ[i] || mach[i] || part[i]) return 0;
|
||||||
|
if (cleatMask[i] && PH[mat[i]] !== PH_SOLID) { slopeWallAt(x, y, smooth); return 1; }
|
||||||
|
if (mat[i] !== EMPTY && PH[mat[i]] !== PH_GAS) return 0;
|
||||||
|
return slopeWallAt(x, y, smooth) >= 0 ? 2 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function slopePreviewCleat(x, y, b, code) {
|
||||||
|
const bx = b % BW, by = (b / BW) | 0, g = beltGrade(code);
|
||||||
|
for (let h = 0; h <= CLEAT_REACH; h++) {
|
||||||
|
const foot = x - g * h;
|
||||||
|
if (foot >= bx * BS && foot < (bx + 1) * BS && foot % CLEAT_SPACING === 0 &&
|
||||||
|
y + h + 1 === beltFaceY(code, bx, by, foot)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -50,8 +50,8 @@ float beltSD(vec2 lb, bool endL, bool endR) {
|
|||||||
// The outline of the belt or filter in cell c. Belts and filters side by side are one belt, so
|
// The outline of the belt or filter in cell c. Belts and filters side by side are one belt, so
|
||||||
// the line only rounds off where the whole run ends.
|
// the line only rounds off where the whole run ends.
|
||||||
float beltLineSD(ivec2 c, vec2 lb) {
|
float beltLineSD(ivec2 c, vec2 lb) {
|
||||||
int nr = cls(cellAt(tCol, c + ivec2(4, 0))), nl = cls(cellAt(tCol, c - ivec2(4, 0)));
|
int caps = int(cellAt(tTree, c).a * 255.0 + 0.5);
|
||||||
return beltSD(lb, isBelt(nl) + isF(nl) < 0.5, isBelt(nr) + isF(nr) < 0.5);
|
return beltSD(lb, (caps & 1) != 0, (caps & 2) != 0);
|
||||||
}
|
}
|
||||||
// The rubber band of a belt or filter: 1 on a ridge. Ridges travel along the top run one way and
|
// The rubber band of a belt or filter: 1 on a ridge. Ridges travel along the top run one way and
|
||||||
// back along the bottom.
|
// back along the bottom.
|
||||||
@@ -61,6 +61,8 @@ float bandRidge(vec2 w, vec2 lb, float dir) {
|
|||||||
}
|
}
|
||||||
vec3 bandColor(vec2 lb, float rg) { return mix(vec3(0.2, 0.21, 0.24), vec3(0.34, 0.35, 0.39), rg) * (lb.y < 2.0 ? 1.0 : 0.72); }
|
vec3 bandColor(vec2 lb, float rg) { return mix(vec3(0.2, 0.21, 0.24), vec3(0.34, 0.35, 0.39), rg) * (lb.y < 2.0 ? 1.0 : 0.72); }
|
||||||
|
|
||||||
|
${SM_SLOPE_HELPERS}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
float zp = uView.z, px = 1.0 / zp;
|
float zp = uView.z, px = 1.0 / zp;
|
||||||
vec2 sp = vec2(gl_FragCoord.x, uView.w - gl_FragCoord.y);
|
vec2 sp = vec2(gl_FragCoord.x, uView.w - gl_FragCoord.y);
|
||||||
@@ -176,12 +178,16 @@ void main() {
|
|||||||
float band = 1.0 - smoothstep(0.5, 0.9, M);
|
float band = 1.0 - smoothstep(0.5, 0.9, M);
|
||||||
float lit = dot(nrm, normalize(vec2(-0.5, -1.0)));
|
float lit = dot(nrm, normalize(vec2(-0.5, -1.0)));
|
||||||
tc *= 1.0 + 0.42 * lit * band;
|
tc *= 1.0 + 0.42 * lit * band;
|
||||||
// the outline follows where ground and grains meet open space, never where rock meets gravel
|
// Keep the air outline; a softer, screen-width seam also separates rock from loose grains.
|
||||||
float dm = (M - 0.5) / fwM;
|
float dm = (M - 0.5) / fwM;
|
||||||
tc *= mix(0.4, 1.0, smoothstep(0.5, 1.4 + zp * 0.08, dm));
|
tc *= mix(0.4, 1.0, smoothstep(0.5, 1.4 + zp * 0.08, dm));
|
||||||
|
float dt = (F.r - 0.5) / fwT;
|
||||||
|
if (nc == 1 && pw > 0.0) tc *= mix(0.65, 1.0, smoothstep(0.0, 1.5, dt));
|
||||||
C = mix(C, tc, smoothstep(-0.6, 0.6, (F.r - 0.5) / fwT));
|
C = mix(C, tc, smoothstep(-0.6, 0.6, (F.r - 0.5) / fwT));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${SM_SLOPE_DRAW}
|
||||||
|
|
||||||
// ---- loose grains: little round pebbles; a pocket closed in by four grains is filled
|
// ---- loose grains: little round pebbles; a pocket closed in by four grains is filled
|
||||||
int np = int(isN(n0, 2) + isN(n1, 2) + isN(n2, 2) + isN(n3, 2));
|
int np = int(isN(n0, 2) + isN(n1, 2) + isN(n2, 2) + isN(n3, 2));
|
||||||
bool closed = isM(n0) + isM(n1) + isM(n2) + isM(n3) > 3.5;
|
bool closed = isM(n0) + isM(n1) + isM(n2) + isM(n3) > 3.5;
|
||||||
@@ -200,10 +206,13 @@ void main() {
|
|||||||
}
|
}
|
||||||
float cover = np == 4 ? 1.0 : 1.0 - smoothstep(1.0 - px / gr, 1.0 + px / gr, best);
|
float cover = np == 4 ? 1.0 : 1.0 - smoothstep(1.0 - px / gr, 1.0 + px / gr, best);
|
||||||
if (closed && F.r < 0.5) cover = 1.0;
|
if (closed && F.r < 0.5) cover = 1.0;
|
||||||
|
// Pebbles stop at the rock face so they cannot paint over its thin seam.
|
||||||
|
cover *= 1.0 - smoothstep(-0.6, 0.6, (F.r - 0.5) / fwT);
|
||||||
if (cover > 0.0) {
|
if (cover > 0.0) {
|
||||||
float r2 = min(1.0, dot(gd, gd));
|
float r2 = min(1.0, dot(gd, gd));
|
||||||
vec3 n3 = normalize(vec3(gd * 0.85, sqrt(1.0 - r2) + 0.25));
|
vec3 n3 = normalize(vec3(gd * 0.85, sqrt(1.0 - r2) + 0.25));
|
||||||
float sh = np == 4 ? 0.9 + 0.08 * vn(w * 0.9) : 0.8 + 0.3 * max(0.0, dot(n3, normalize(vec3(-0.45, -0.8, 0.6))));
|
float sh = 0.98 + 0.18 * max(0.0, dot(n3, normalize(vec3(-0.45, -0.8, 0.6))));
|
||||||
|
sh *= mix(1.0, 0.94, smoothstep(0.7 - px * 0.2, 1.15 + px * 0.2, best));
|
||||||
vec3 pc = (np == 4 ? (k0.rgb * wq.x + k1.rgb * wq.y + k2.rgb * wq.z + k3.rgb * wq.w) : gc) * sh;
|
vec3 pc = (np == 4 ? (k0.rgb * wq.x + k1.rgb * wq.y + k2.rgb * wq.z + k3.rgb * wq.w) : gc) * sh;
|
||||||
if (!closed) pc *= mix(1.0, 0.72, smoothstep(0.7, 1.0, best));
|
if (!closed) pc *= mix(1.0, 0.72, smoothstep(0.7, 1.0, best));
|
||||||
C = mix(C, pc, cover);
|
C = mix(C, pc, cover);
|
||||||
@@ -240,20 +249,52 @@ void main() {
|
|||||||
C = mix(C, bc, inside);
|
C = mix(C, bc, inside);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (nc >= 9 && nc <= 12) {
|
else if ((nc >= 9 && nc <= 12) || (nc >= 19 && nc <= 22)) {
|
||||||
// screw tube: a copper pipe; what is inside shows through, and the screw's flights turn
|
// screw conveyor: a copper pipe; what is inside shows through, and the screw's flights turn
|
||||||
bool vert = nc == 9 || nc == 11;
|
bool vert = nc == 9 || nc == 11;
|
||||||
float sp2 = nc == 9 ? -1.0 : nc == 10 ? 1.0 : nc == 11 ? 1.0 : -1.0;
|
float sp2 = nc == 9 ? -1.0 : nc == 10 ? 1.0 : nc == 11 ? 1.0 : -1.0;
|
||||||
float a = (vert ? fract(w.x * 0.25) : fract(w.y * 0.25)) * 4.0, along = vert ? w.y : w.x;
|
float a = (vert ? fract(w.x * 0.25) : fract(w.y * 0.25)) * 4.0, along = vert ? w.y : w.x;
|
||||||
|
float screwSD = -1.0, capRing = 0.0;
|
||||||
|
vec4 screwTag = cellAt(tTree, c);
|
||||||
|
int screwJoins = int(screwTag.a * 255.0 + 0.5);
|
||||||
|
// Straight bend tags use six bits; background trees use 128/255.
|
||||||
|
bool shaped = nc >= 19 || (screwJoins > 0 && screwJoins < 64);
|
||||||
|
if (shaped) {
|
||||||
|
vec2 d = vec2(1.0, nc == 19 || nc == 21 ? -1.0 : 1.0);
|
||||||
|
vec4 tag = screwTag;
|
||||||
|
vec2 local = floor(tag.gb * 255.0 + 0.5) - 8.0 + fract(w) - 2.0;
|
||||||
|
int joins = screwJoins;
|
||||||
|
screwSD = 100.0;
|
||||||
|
for (int end = 0; end < 2; end++) {
|
||||||
|
int join = (joins >> (end * 3)) & 7;
|
||||||
|
float side = end == 0 ? -1.0 : 1.0;
|
||||||
|
bool diagonal = nc >= 19 ? join == 0 || join >= 5 : join != 0;
|
||||||
|
float grade = nc >= 19 ? d.y : join == 1 ? -1.0 : 1.0;
|
||||||
|
vec2 ray = diagonal ? (vert ? vec2(side * grade, side) : vec2(side, side * grade)) * 0.70710678 :
|
||||||
|
nc < 19 ? (vert ? vec2(0, side) : vec2(side, 0)) :
|
||||||
|
join == 1 ? vec2(0, -1) : join == 2 ? vec2(1, 0) : join == 3 ? vec2(0, 1) : vec2(-1, 0);
|
||||||
|
float t = dot(local, ray), cross2 = dot(local, vec2(-ray.y, ray.x));
|
||||||
|
float radius = length(vec2(cross2, min(0.0, t))), cap = t - (diagonal ? 2.82842712 : 2.0);
|
||||||
|
// A connected mouth meets another opaque pipe; fading its edge leaves a seam.
|
||||||
|
float sd = max(radius - 2.0, (joins & (64 << end)) != 0 ? cap : -10.0);
|
||||||
|
if (sd < screwSD) {
|
||||||
|
screwSD = sd;
|
||||||
|
a = 2.0 + (side * cross2 < 0.0 ? -radius : radius);
|
||||||
|
along = (nc >= 19 ? dot(w - local, d) * 0.70710678 : vert ? w.y - local.y : w.x - local.x) + side * t;
|
||||||
|
capRing = (joins & (64 << end)) != 0 ? smoothstep(-0.55, -0.15, cap) : 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nc >= 19) sp2 = nc == 19 || nc == 20 ? 1.0 : -1.0;
|
||||||
|
}
|
||||||
float wall = 0.62;
|
float wall = 0.62;
|
||||||
vec3 tc;
|
vec3 tc;
|
||||||
if (a < wall || a > 4.0 - wall) {
|
if (a < wall || a > 4.0 - wall || capRing > 0.5) {
|
||||||
float u2 = a < wall ? a / wall : (4.0 - a) / wall; // 0 at the outside, 1 at the bore
|
float u2 = a < wall ? a / wall : (4.0 - a) / wall; // 0 at the outside, 1 at the bore
|
||||||
float cyl = sin(u2 * 1.5708);
|
float cyl = sin(clamp(u2, 0.0, 1.0) * 1.5708);
|
||||||
tc = vec3(0.62, 0.41, 0.27) * (0.6 + 0.55 * cyl);
|
tc = vec3(0.62, 0.41, 0.27) * (0.6 + 0.55 * cyl);
|
||||||
if (a < wall) tc *= 1.08;
|
if (a < wall) tc *= 1.08;
|
||||||
float ring = 1.0 - smoothstep(0.0, 0.3, min(fract(along * 0.25), 1.0 - fract(along * 0.25)) * 4.0);
|
float ring = 1.0 - smoothstep(0.0, 0.3, min(fract(along * 0.25), 1.0 - fract(along * 0.25)) * 4.0);
|
||||||
tc *= 1.0 - 0.3 * ring;
|
if (nc < 19) tc *= 1.0 - 0.3 * ring;
|
||||||
tc *= mix(0.5, 1.0, smoothstep(0.0, max(px * 1.3, 0.1), min(a, 4.0 - a)));
|
tc *= mix(0.5, 1.0, smoothstep(0.0, max(px * 1.3, 0.1), min(a, 4.0 - a)));
|
||||||
} else {
|
} else {
|
||||||
bool full = own.r + own.g + own.b > 0.0;
|
bool full = own.r + own.g + own.b > 0.0;
|
||||||
@@ -264,9 +305,13 @@ void main() {
|
|||||||
tc = mix(tc, vec3(0.6, 0.48, 0.38), flight * (full ? 0.35 : 0.8));
|
tc = mix(tc, vec3(0.6, 0.48, 0.38), flight * (full ? 0.35 : 0.8));
|
||||||
tc *= 1.0 - 0.25 * smoothstep(0.9, 1.3, abs(a - 2.0));
|
tc *= 1.0 - 0.25 * smoothstep(0.9, 1.3, abs(a - 2.0));
|
||||||
}
|
}
|
||||||
C = tc;
|
if (shaped) {
|
||||||
|
bool full = own.r + own.g + own.b > 0.0;
|
||||||
|
if (full) C = mix(C, own.rgb, 1.0 - smoothstep(0.36, 0.5, length(fract(w) - 0.5)));
|
||||||
|
C = mix(C, tc, 1.0 - smoothstep(2.0 - px, 2.0 + px, screwSD + 2.0));
|
||||||
|
} else C = tc;
|
||||||
}
|
}
|
||||||
else if (nc == 6) {
|
else if (nc == 6 || (nc >= 15 && nc <= 18)) {
|
||||||
vec2 lb = fract(w * 0.25) * 4.0, rb = 4.0 - lb;
|
vec2 lb = fract(w * 0.25) * 4.0, rb = 4.0 - lb;
|
||||||
float e = min(min(lb.x, lb.y), min(rb.x, rb.y));
|
float e = min(min(lb.x, lb.y), min(rb.x, rb.y));
|
||||||
vec3 tc = own.rgb * (0.92 + 0.14 * fbm(w * 0.55));
|
vec3 tc = own.rgb * (0.92 + 0.14 * fbm(w * 0.55));
|
||||||
@@ -274,7 +319,14 @@ void main() {
|
|||||||
float shd = max(1.0 - smoothstep(0.0, 0.8, rb.x), 1.0 - smoothstep(0.0, 0.8, rb.y));
|
float shd = max(1.0 - smoothstep(0.0, 0.8, rb.x), 1.0 - smoothstep(0.0, 0.8, rb.y));
|
||||||
tc *= 1.0 + 0.2 * hl - 0.26 * shd;
|
tc *= 1.0 + 0.2 * hl - 0.26 * shd;
|
||||||
tc *= mix(0.5, 1.0, smoothstep(px * 0.5, px * 0.5 + max(px * 1.2, 0.14), e));
|
tc *= mix(0.5, 1.0, smoothstep(px * 0.5, px * 0.5 + max(px * 1.2, 0.14), e));
|
||||||
C = tc;
|
if (nc >= 15) {
|
||||||
|
vec2 sl = lb;
|
||||||
|
if (nc == 16 || nc == 18) sl.x = 4.0 - sl.x;
|
||||||
|
if (nc >= 17) sl.y = 4.0 - sl.y;
|
||||||
|
float d = (sl.y - sl.x) * 0.70710678;
|
||||||
|
tc *= 1.0 + 0.2 * (1.0 - smoothstep(0.0, 0.6, d));
|
||||||
|
C = mix(C, tc, smoothstep(-px * 0.5, px * 0.5, d));
|
||||||
|
} else C = tc;
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (nc == 13 || nc == 14) {
|
else if (nc == 13 || nc == 14) {
|
||||||
|
|||||||
+120
-24
@@ -2,8 +2,6 @@
|
|||||||
function fogColor(c, k) { return (c & 0xff000000) | (((c >>> 16 & 255) * k) << 16) | (((c >>> 8 & 255) * k) << 8) | ((c & 255) * k); }
|
function fogColor(c, k) { return (c & 0xff000000) | (((c >>> 16 & 255) * k) << 16) | (((c >>> 8 & 255) * k) << 8) | ((c & 255) * k); }
|
||||||
// drawWorld: the per-grain pixel buffer (pixel look), also the input of the smooth look.
|
// drawWorld: the per-grain pixel buffer (pixel look), also the input of the smooth look.
|
||||||
// ---------- World pixels ----------
|
// ---------- World pixels ----------
|
||||||
// smooth-look class of a screw tube cell by direction: 9 up, 10 right, 11 down, 12 left
|
|
||||||
const TUBE_CLASS = new Uint8Array(9); TUBE_CLASS[1] = 9; TUBE_CLASS[4] = 10; TUBE_CLASS[2] = 11; TUBE_CLASS[3] = 12;
|
|
||||||
function drawWorld() {
|
function drawWorld() {
|
||||||
const bw = R.bw, bh = R.bh, px = R.px;
|
const bw = R.bw, bh = R.bh, px = R.px;
|
||||||
R.vx0 = Math.floor(R.camX); R.vy0 = Math.floor(R.camY);
|
R.vx0 = Math.floor(R.camX); R.vy0 = Math.floor(R.camY);
|
||||||
@@ -22,6 +20,14 @@ function drawWorld() {
|
|||||||
for (const i of moldCellsAt(mo.kind, mo.x, mo.y).walls) { const sx = (i % W) - vx0, sy = ((i / W) | 0) - vy0; if (sx >= 0 && sy >= 0 && sx < bw && sy < bh) mmask[sy * bw + sx] = 1; }
|
for (const i of moldCellsAt(mo.kind, mo.x, mo.y).walls) { const sx = (i % W) - vx0, sy = ((i / W) | 0) - vy0; if (sx >= 0 && sy >= 0 && sx < bw && sy < bh) mmask[sy * bw + sx] = 1; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (diagonalJoins.length !== NB) diagonalJoins = new Uint8Array(NB);
|
||||||
|
for (let by = Math.max(0, (vy0 >> 2) - 1); by <= Math.min(BH - 1, ((vy0 + bh) >> 2) + 1); by++) {
|
||||||
|
for (let bx = Math.max(0, (vx0 >> 2) - 1); bx <= Math.min(BW - 1, ((vx0 + bw) >> 2) + 1); bx++) {
|
||||||
|
const b = by * BW + bx;
|
||||||
|
if (blockP[b] === BP_SCREW) diagonalJoins[b] = blockV[b] >= 4 ? diagonalScrewJoins(bx, by, blockV[b]) : straightScrewJoins(bx, by, blockV[b]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prepareSlopeBelts(vx0, vy0, vx0 + bw, vy0 + bh);
|
||||||
const useFog = SET.fog;
|
const useFog = SET.fog;
|
||||||
const OUT = sm ? 0x04000000 : 0xff000000; // unexplored and off the map: black
|
const OUT = sm ? 0x04000000 : 0xff000000; // unexplored and off the map: black
|
||||||
const lighting = SET.lighting !== 'off' && !R.lens;
|
const lighting = SET.lighting !== 'off' && !R.lens;
|
||||||
@@ -59,12 +65,24 @@ function drawWorld() {
|
|||||||
}
|
}
|
||||||
const i = yw + x, m = mat[i], nz = noiseB[i] - 128;
|
const i = yw + x, m = mat[i], nz = noiseB[i] - 128;
|
||||||
const T = temp[i];
|
const T = temp[i];
|
||||||
|
// Also used by the thermal lens, which always draws in pixels.
|
||||||
|
let looseShade = 1;
|
||||||
|
if (!sm && m !== EMPTY && !builtForLight(i, x, y)) {
|
||||||
|
if (PH[m] === PH_POWDER) looseShade = 1.1 + nz * 0.00065;
|
||||||
|
else if (PH[m] === PH_SOLID && (
|
||||||
|
(x > 0 && PH[mat[i - 1]] === PH_POWDER && !part[i - 1]) ||
|
||||||
|
(x + 1 < W && PH[mat[i + 1]] === PH_POWDER && !part[i + 1]) ||
|
||||||
|
(y > 0 && PH[mat[i - W]] === PH_POWDER && !part[i - W]) ||
|
||||||
|
(y + 1 < H && PH[mat[i + W]] === PH_POWDER && !part[i + W])
|
||||||
|
)) looseShade = 0.72;
|
||||||
|
}
|
||||||
let r, g, b;
|
let r, g, b;
|
||||||
if (lens) {
|
if (lens) {
|
||||||
const ti = thermIdx(T);
|
const ti = thermIdx(T);
|
||||||
if (m === EMPTY) { r = tl[ti] * 0.55; g = tl[ti + 1] * 0.55; b = tl[ti + 2] * 0.55; }
|
if (m === EMPTY) { r = tl[ti] * 0.55; g = tl[ti + 1] * 0.55; b = tl[ti + 2] * 0.55; }
|
||||||
else { const v = (COLR[m] + COLG[m] + COLB[m]) / 3 * 0.25; r = tl[ti] * 0.78 + v; g = tl[ti + 1] * 0.78 + v; b = tl[ti + 2] * 0.78 + v; }
|
else { const v = (COLR[m] + COLG[m] + COLB[m]) / 3 * 0.25; r = tl[ti] * 0.78 + v; g = tl[ti + 1] * 0.78 + v; b = tl[ti + 2] * 0.78 + v; }
|
||||||
const p = part[i];
|
const p = part[i];
|
||||||
|
r *= looseShade; g *= looseShade; b *= looseShade;
|
||||||
if (p && ((x + y) & 1)) { r = r * 0.7 + 40; g = g * 0.7 + 40; b = b * 0.7 + 40; }
|
if (p && ((x + y) & 1)) { r = r * 0.7 + 40; g = g * 0.7 + 40; b = b * 0.7 + 40; }
|
||||||
if (fv < 255) { r *= fv / 255; g *= fv / 255; b *= fv / 255; }
|
if (fv < 255) { r *= fv / 255; g *= fv / 255; b *= fv / 255; }
|
||||||
px[row + bx] = 0xff000000 | ((b > 255 ? 255 : b) << 16) | ((g > 255 ? 255 : g) << 8) | (r > 255 ? 255 : r);
|
px[row + bx] = 0xff000000 | ((b > 255 ? 255 : b) << 16) | ((g > 255 ? 255 : g) << 8) | (r > 255 ? 255 : r);
|
||||||
@@ -93,6 +111,7 @@ function drawWorld() {
|
|||||||
else {
|
else {
|
||||||
const v = nz * CVAR[m] * (sm ? (PH[m] === PH_SOLID ? 0 : grain * 0.35) : grain) / 128;
|
const v = nz * CVAR[m] * (sm ? (PH[m] === PH_SOLID ? 0 : grain * 0.35) : grain) / 128;
|
||||||
r = COLR[m] + v; g = COLG[m] + v; b = COLB[m] + v;
|
r = COLR[m] + v; g = COLG[m] + v; b = COLB[m] + v;
|
||||||
|
r *= looseShade; g *= looseShade; b *= looseShade;
|
||||||
if (m === FIRE) { const f = (life[i] & 15) * 6; r = 255; g = 120 + f; b = 30 + f * 0.5; }
|
if (m === FIRE) { const f = (life[i] & 15) * 6; r = 255; g = 120 + f; b = 30 + f * 0.5; }
|
||||||
else if (m === WATER && !sm) { const s = ((x * 3 + y * 5 + (anim >> 2)) & 15) === 0 ? 14 : 0; r += s; g += s; b += s; }
|
else if (m === WATER && !sm) { const s = ((x * 3 + y * 5 + (anim >> 2)) & 15) === 0 ? 14 : 0; r += s; g += s; b += s; }
|
||||||
else if (m === BELTL || m === BELTR) {
|
else if (m === BELTL || m === BELTR) {
|
||||||
@@ -132,24 +151,38 @@ function drawWorld() {
|
|||||||
r += lift; g += lift; b += lift * 1.08;
|
r += lift; g += lift; b += lift * 1.08;
|
||||||
}
|
}
|
||||||
// built wall blocks read as tiles: light top-left edge, dark bottom-right edge
|
// built wall blocks read as tiles: light top-left edge, dark bottom-right edge
|
||||||
if (!sm && PH[m] === PH_SOLID && !(flags[i] & F_NAT) && blockP[(y >> 2) * BW + (x >> 2)] === BP_WALL) {
|
if (!sm && PH[m] === PH_SOLID && !(flags[i] & F_NAT) && isWall(blockP[(y >> 2) * BW + (x >> 2)])) {
|
||||||
const lx = x & 3, ly = y & 3;
|
const lx = x & 3, ly = y & 3;
|
||||||
if (lx === 0 || ly === 0) { r += 16; g += 16; b += 16; }
|
const wc = blockP[(y >> 2) * BW + (x >> 2)];
|
||||||
|
const edge = isSlope(wc) && (wc === BP_SLOPE_BL || wc === BP_SLOPE_TR ? lx === ly : lx + ly === 3);
|
||||||
|
if (edge || lx === 0 || ly === 0) { r += 16; g += 16; b += 16; }
|
||||||
else if (lx === 3 || ly === 3) { r -= 18; g -= 18; b -= 18; }
|
else if (lx === 3 || ly === 3) { r -= 18; g -= 18; b -= 18; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// field parts drawn over contents
|
// field parts drawn over contents
|
||||||
const p = part[i];
|
const sb = slopeBeltAt(i, x, y, sm);
|
||||||
if (p && sm && (p <= P_TR || p === P_FILTER)) { if (m === EMPTY || PH[m] === PH_GAS) { r = 0; g = 0; b = 0; } else { r = Math.max(1, r); } }
|
let p = part[i];
|
||||||
|
if (p === P_FILTER && (sb >= 0 || isSlopeBelt(blockP[slopeSealOwner(i)]))) p = 0;
|
||||||
|
const db = diagonalScrewBlock(x, y, sm);
|
||||||
|
let diagonal = false;
|
||||||
|
if (db >= 0) {
|
||||||
|
const v = blockV[db], joins = diagonalJoins[db];
|
||||||
|
const sd = diagonalScrewShape(x + 0.5, y + 0.5, db, v, joins);
|
||||||
|
p = sm || sd <= 0 ? SCREW_PARTS[v] : 0;
|
||||||
|
diagonal = !!p;
|
||||||
|
if (sm) tpx[row + bx] = (joins << 24) | ((y - ((db / BW) | 0) * BS + 8) << 16) | ((x - (db % BW) * BS + 8) << 8);
|
||||||
|
}
|
||||||
|
if (p && sm && (IS_SCREW[p] || p === P_FILTER)) { if (m === EMPTY || PH[m] === PH_GAS) { r = 0; g = 0; b = 0; } else { r = Math.max(1, r); } }
|
||||||
else if (p) {
|
else if (p) {
|
||||||
if (p <= P_TR) {
|
if (IS_SCREW[p]) {
|
||||||
const on = netSat(i) > 0;
|
const on = netSat(i) > 0;
|
||||||
const vert = p === P_TU || p === P_TD;
|
const vert = p === P_SCREW_U || p === P_SCREW_D;
|
||||||
const axis = vert ? y : x, across = vert ? x & 3 : y & 3;
|
const axis = diagonal ? Math.floor(diagonalAlong) : vert ? y : x;
|
||||||
const dir = (p === P_TU || p === P_TL) ? 1 : -1;
|
const across = diagonal ? diagonalAcross + 2 : vert ? x & 3 : y & 3;
|
||||||
if (across === 0 || across === 3) { // copper casing on the two long sides; its load shows through
|
const dir = diagonal && blockV[db] >= 4 ? (blockV[db] >= 6 ? 1 : -1) : (p === P_SCREW_U || p === P_SCREW_L) ? 1 : -1;
|
||||||
|
if (diagonal ? Math.abs(diagonalAcross) > 1.38 || diagonalCap > -0.55 : across === 0 || across === 3) { // copper casing on the two long sides; its load shows through
|
||||||
if (m === EMPTY) { r = 150; g = 96; b = 64; } else { r = r * 0.72 + 44; g = g * 0.72 + 26; b = b * 0.72 + 14; }
|
if (m === EMPTY) { r = 150; g = 96; b = 64; } else { r = r * 0.72 + 44; g = g * 0.72 + 26; b = b * 0.72 + 14; }
|
||||||
if ((axis & 3) === 0) { r -= 24; g -= 18; b -= 12; }
|
if (!diagonal && (axis & 3) === 0) { r -= 24; g -= 18; b -= 12; }
|
||||||
} else {
|
} else {
|
||||||
// screw flights: a diagonal stripe that walks along the tube when powered
|
// screw flights: a diagonal stripe that walks along the tube when powered
|
||||||
const ch = (((axis + across + dir * (on ? anim >> 1 : 0)) & 3) === 0);
|
const ch = (((axis + across + dir * (on ? anim >> 1 : 0)) & 3) === 0);
|
||||||
@@ -168,7 +201,7 @@ function drawWorld() {
|
|||||||
} else if (ly === 3) { // the belt's return run carries on under it, so belts and filters read as one belt
|
} else if (ly === 3) { // the belt's return run carries on under it, so belts and filters read as one belt
|
||||||
const st = ((x - dir * (anim >> 1)) >> 1) & 1;
|
const st = ((x - dir * (anim >> 1)) >> 1) & 1;
|
||||||
if (m === EMPTY || st) { r = (st ? 92 : 56) * 0.72; g = (st ? 96 : 58) * 0.72; b = (st ? 104 : 66) * 0.72; }
|
if (m === EMPTY || st) { r = (st ? 92 : 56) * 0.72; g = (st ? 96 : 58) * 0.72; b = (st ? 104 : 66) * 0.72; }
|
||||||
} else if ((lx === 0 && !beltLike(fb - 1)) || (lx === 3 && !beltLike(fb + 1))) { if (m === EMPTY || ((x + y) & 1)) { r = 64; g = 66; b = 74; } } // frame only where the line ends
|
} else if ((lx === 0 && !beltLike(fb, 0)) || (lx === 3 && !beltLike(fb, 1))) { if (m === EMPTY || ((x + y) & 1)) { r = 64; g = 66; b = 74; } } // frame only where the line ends
|
||||||
else if (m === EMPTY) { // mesh holes, tinted with what falls through them
|
else if (m === EMPTY) { // mesh holes, tinted with what falls through them
|
||||||
if ((x + y) & 1) { r = 40; g = 42; b = 48; }
|
if ((x + y) & 1) { r = 40; g = 42; b = 48; }
|
||||||
else if (inv) { r = 58; g = 74; b = 72; } // carry mode: everything else drops
|
else if (inv) { r = 58; g = 74; b = 72; } // carry mode: everything else drops
|
||||||
@@ -186,6 +219,20 @@ function drawWorld() {
|
|||||||
if (pipeBodyCell(x, y)) { thinPipeColor(x, y, anim); r = PC[0]; g = PC[1]; b = PC[2]; }
|
if (pipeBodyCell(x, y)) { thinPipeColor(x, y, anim); r = PC[0]; g = PC[1]; b = PC[2]; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const slopeArt = sb >= 0 && (m === EMPTY || PH[m] === PH_GAS || m === BELTL || m === BELTR);
|
||||||
|
if (slopeArt && !sm) {
|
||||||
|
const color = slopeBeltColor(sb, x, y, anim, br, bgc, bb);
|
||||||
|
r = color & 255; g = color >> 8 & 255; b = color >> 16 & 255;
|
||||||
|
}
|
||||||
|
const slopeDetail = slopeDetailAt(i, x, y, sm);
|
||||||
|
if (slopeDetail) {
|
||||||
|
const wave = slopeDetail === 2 && ((x + beltGrade(blockP[slopeWallOwner]) * y - slopePhase[slopeWallOwner]) & 3) < 2;
|
||||||
|
// ribs: light rubber with a pale cap; the far wall behind them: muted, corrugated rubber
|
||||||
|
const cap = slopeDetail === 1 && slopeWallOwner >= 0 && !cleatMask[i - W + beltGrade(blockP[slopeWallOwner])];
|
||||||
|
r = slopeDetail === 1 ? (cap ? 178 : 146) : wave ? 64 : 56;
|
||||||
|
g = slopeDetail === 1 ? (cap ? 182 : 150) : wave ? 68 : 60;
|
||||||
|
b = slopeDetail === 1 ? (cap ? 190 : 158) : wave ? 76 : 68;
|
||||||
|
}
|
||||||
// incandescence
|
// incandescence
|
||||||
if (T > 450 && m !== EMPTY && incand) {
|
if (T > 450 && m !== EMPTY && incand) {
|
||||||
const gi = glowIndex(PH[m] === PH_SOLID ? T + nz * 0.9 : m === MAGMA ? T + Math.sin(x * 0.19 + y * 0.11 + anim * 0.03 + nz * 0.04) * 55 - 30 : T), w = GLOW_W[gi];
|
const gi = glowIndex(PH[m] === PH_SOLID ? T + nz * 0.9 : m === MAGMA ? T + Math.sin(x * 0.19 + y * 0.11 + anim * 0.03 + nz * 0.04) * 55 - 30 : T), w = GLOW_W[gi];
|
||||||
@@ -201,25 +248,50 @@ function drawWorld() {
|
|||||||
// what kind of thing this grain is drawn as: 0 open, 1 terrain, 2 loose grain, 3 liquid,
|
// what kind of thing this grain is drawn as: 0 open, 1 terrain, 2 loose grain, 3 liquid,
|
||||||
// 4 built piece (drawn crisp), 5 smoke and flame, 6 built wall block (drawn as a tile)
|
// 4 built piece (drawn crisp), 5 smoke and flame, 6 built wall block (drawn as a tile)
|
||||||
let cl = 0;
|
let cl = 0;
|
||||||
const pt = p === P_PIPE && !pipeBodyCell(x, y) ? 0 : p; // the open parts of a pipe block show what is in them
|
const pt = (p === P_PIPE && !pipeBodyCell(x, y)) || (p === P_FILTER && sb >= 0) ? 0 : p; // the open parts of a pipe block show what is in them
|
||||||
if (pt >= P_TU && pt <= P_TR) cl = TUBE_CLASS[pt];
|
if (IS_SCREW[pt]) cl = SCREW_CLASS[pt];
|
||||||
else if (p === P_FILTER) {
|
else if (p === P_FILTER && sb < 0) {
|
||||||
// filter: 13 carries left, 14 right; the tree buffer carries its colour tag (first material) and mode
|
// filter: 13 carries left, 14 right; the tree buffer carries its colour tag (first material) and mode
|
||||||
const fb = (y >> 2) * BW + (x >> 2), fc = filterCfg.get(fb), fm = fc && fc.m.length ? fc.m[0] : 0;
|
const fb = (y >> 2) * BW + (x >> 2), fc = filterCfg.get(fb), fm = fc && fc.m.length ? fc.m[0] : 0;
|
||||||
cl = blockV[fb] & 128 ? 14 : 13;
|
cl = blockV[fb] & 128 ? 14 : 13;
|
||||||
tpx[row + bx] = ((fc && fc.inv ? 0x30 : 0x20) << 24) | (fm ? (COLB[fm] << 16) | (COLG[fm] << 8) | COLR[fm] : 0);
|
tpx[row + bx] = ((0x20 | (fc && fc.inv ? 0x10 : 0) | beltCaps[fb]) << 24) | (fm ? (COLB[fm] << 16) | (COLG[fm] << 8) | COLR[fm] : 0);
|
||||||
|
}
|
||||||
|
else if (m === BELTL || m === BELTR) {
|
||||||
|
cl = m === BELTL ? 7 : 8;
|
||||||
|
tpx[row + bx] = beltCaps[(y >> 2) * BW + (x >> 2)] << 24;
|
||||||
}
|
}
|
||||||
else if (m === BELTL || m === BELTR) cl = m === BELTL ? 7 : 8;
|
|
||||||
else if (pt || PARTMAT[m] || mmask[row + bx]) cl = 4;
|
else if (pt || PARTMAT[m] || mmask[row + bx]) cl = 4;
|
||||||
else if (m !== EMPTY) {
|
else if (m !== EMPTY) {
|
||||||
const ph = PH[m];
|
const ph = PH[m];
|
||||||
if (ph === PH_SOLID) cl = (!(flags[i] & F_NAT) && blockP[(y >> 2) * BW + (x >> 2)] === BP_WALL) ? 6 : 1;
|
if (ph === PH_SOLID) cl = (!(flags[i] & F_NAT) && isWall(blockP[(y >> 2) * BW + (x >> 2)])) ? 6 : 1;
|
||||||
else if (ph === PH_POWDER) cl = 2;
|
else if (ph === PH_POWDER) cl = 2;
|
||||||
else if (ph === PH_LIQUID) cl = 3;
|
else if (ph === PH_LIQUID) cl = 3;
|
||||||
else cl = 5;
|
else cl = 5;
|
||||||
}
|
}
|
||||||
|
const wallCode = blockP[(y >> 2) * BW + (x >> 2)];
|
||||||
|
if (isSlope(wallCode) && ((m === EMPTY && !slopeCell(wallCode, x & 3, y & 3)) || (PH[m] === PH_SOLID && !(flags[i] & F_NAT)))) {
|
||||||
|
cl = wallCode;
|
||||||
|
const wm = WALL_OF[blockV[(y >> 2) * BW + (x >> 2)]];
|
||||||
|
r = COLR[wm]; g = COLG[wm]; b = COLB[wm];
|
||||||
|
}
|
||||||
|
if (sb >= 0 && (slopeArt || PH[m] === PH_POWDER)) {
|
||||||
|
const code = blockP[sb], dir = isFilter(code) ? (blockV[sb] & 128 ? 1 : 0) : blockV[sb];
|
||||||
|
const slopeClass = 23 + (code - BP_BELT_UP) * 2 + dir;
|
||||||
|
if (slopeArt) cl = slopeClass;
|
||||||
|
const f = isFilter(code) && filterCfg.get(sb), fm = f && f.m.length ? f.m[(Math.floor((x + beltGrade(code) * y) / 2) & 65535) % f.m.length] : 0;
|
||||||
|
tpx[row + bx] = ((slopeBeltJoins[sb] | 128) << 24) | slopeClass |
|
||||||
|
((y - ((sb / BW) | 0) * BS + 8) << 16) | ((x - (sb % BW) * BS + 8) << 8);
|
||||||
|
if (slopeArt) { r = fm ? COLR[fm] : 0; g = fm ? COLG[fm] : 0; b = fm ? COLB[fm] : 0; }
|
||||||
|
}
|
||||||
|
if (slopeDetail && slopeWallOwner >= 0) {
|
||||||
|
// ribs and the far wall are both drawn by the wall shader, ribs from the same phase as the simulation
|
||||||
|
const wb = slopeWallOwner;
|
||||||
|
cl = beltGrade(blockP[wb]) < 0 ? 31 : 32;
|
||||||
|
tpx[row + bx] = ((128 | slopePhase[wb] | ((slopeBeltJoins[wb] & 12) << 1) | (blockV[wb] & 1 ? 32 : 0)) << 24) | cl |
|
||||||
|
((y - ((wb / BW) | 0) * BS + 8) << 16) | ((x - wb % BW * BS + 8) << 8);
|
||||||
|
}
|
||||||
top = cl << 24;
|
top = cl << 24;
|
||||||
fpx[row + bx] = (tb ? 0xff000000 : 0) | (cl === 5 ? 0xff0000 : 0) | (cl === 3 ? 0xff00 : 0) | (cl === 1 || cl === 4 || cl === 6 ? 0xff : 0);
|
fpx[row + bx] = (tb ? 0xff000000 : 0) | (cl === 5 ? 0xff0000 : 0) | (cl === 3 ? 0xff00 : 0) | (cl === 1 && !blockP[(y >> 2) * BW + (x >> 2)] && !mach[i] ? 0xff : 0); // terrain: any solid nothing owns, natural or not
|
||||||
}
|
}
|
||||||
if (fv < 255) {
|
if (fv < 255) {
|
||||||
const k = fv / 255; r *= k; g *= k; b *= k;
|
const k = fv / 255; r *= k; g *= k; b *= k;
|
||||||
@@ -248,7 +320,7 @@ function drawWorld() {
|
|||||||
px[sy * bw + sx] = (sm ? 0x04000000 : 0xff000000) | ((b > 255 ? 255 : b | 0) << 16) | ((g > 255 ? 255 : g | 0) << 8) | (r > 255 ? 255 : r | 0);
|
px[sy * bw + sx] = (sm ? 0x04000000 : 0xff000000) | ((b > 255 ? 255 : b | 0) << 16) | ((g > 255 ? 255 : g | 0) << 8) | (r > 255 ? 255 : r | 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// grains in flight from the vacuum nozzle, a tube or a belt (rain and snow are drawn over the top, see drawRain)
|
// grains in flight from the vacuum nozzle, a screw or a belt (rain and snow are drawn over the top, see drawRain)
|
||||||
if (!lens) for (const f of flyers) {
|
if (!lens) for (const f of flyers) {
|
||||||
if (f.rain || (useFog && fogSeen(f.x, f.y) < 64)) continue;
|
if (f.rain || (useFog && fogSeen(f.x, f.y) < 64)) continue;
|
||||||
const sx = Math.floor(f.x) - vx0, sy = Math.floor(f.y) - vy0;
|
const sx = Math.floor(f.x) - vx0, sy = Math.floor(f.y) - vy0;
|
||||||
@@ -266,12 +338,36 @@ function drawWorld() {
|
|||||||
const blocked = ghostBlocked.has(b), code = gv[0], v = gv[1];
|
const blocked = ghostBlocked.has(b), code = gv[0], v = gv[1];
|
||||||
let tr = 90, tg = 190, tb = 230;
|
let tr = 90, tg = 190, tb = 230;
|
||||||
if (blocked) { tr = 230; tg = 70; tb = 60; }
|
if (blocked) { tr = 230; tg = 70; tb = 60; }
|
||||||
else if (code === BP_WALL && WALL_OF[v]) { const wm = WALL_OF[v]; tr = (COLR[wm] + 90) / 2; tg = (COLG[wm] + 190) / 2; tb = (COLB[wm] + 230) / 2; }
|
else if (isWall(code) && WALL_OF[v]) { const wm = WALL_OF[v]; tr = (COLR[wm] + 90) / 2; tg = (COLG[wm] + 190) / 2; tb = (COLB[wm] + 230) / 2; }
|
||||||
for (let dy = 0; dy < BS; dy++) for (let dx = 0; dx < BS; dx++) {
|
const slope = isSlopeBelt(code), diagonal = code === BP_SCREW && (v >= 4 || straightScrewJoins(gbx, gby, v, ghostBlocks));
|
||||||
|
const joins = slope ? slopeBeltConnections(gbx, gby, code, ghostBlocks) : diagonal ? (v >= 4 ? diagonalScrewJoins(gbx, gby, v, ghostBlocks) : straightScrewJoins(gbx, gby, v, ghostBlocks)) : 0;
|
||||||
|
const caps = code === BP_BELT || code === BP_FILTER ? flatBeltCaps(b, ghostBlocks) : 3;
|
||||||
|
for (let dy = slope ? -9 : diagonal ? -4 : 0; dy < (slope || diagonal ? 8 : BS); dy++) for (let dx = slope ? -5 : diagonal ? -4 : 0; dx < (slope ? 9 : diagonal ? 8 : BS); dx++) {
|
||||||
|
let sd = 0;
|
||||||
|
if (diagonal) {
|
||||||
|
if (v < 4 && ((v & 1) ? dx < 0 || dx >= BS : dy < 0 || dy >= BS)) continue;
|
||||||
|
const x = gbx * BS + dx, y = gby * BS + dy;
|
||||||
|
if (x < 0 || y < 0 || x >= W || y >= H) continue;
|
||||||
|
const i = y * W + x, n = (y >> 2) * BW + (x >> 2);
|
||||||
|
if (n !== b && (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || blockP[n])) continue;
|
||||||
|
sd = diagonalScrewShape(x + 0.5, y + 0.5, b, v, joins);
|
||||||
|
if (sd > 0) continue;
|
||||||
|
}
|
||||||
|
if (slope) {
|
||||||
|
const x = gbx * BS + dx, y = gby * BS + dy;
|
||||||
|
if (x < 0 || y < 0 || x >= W || y >= H) continue;
|
||||||
|
const i = y * W + x, n = (y >> 2) * BW + (x >> 2);
|
||||||
|
if (n !== b && (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || blockP[n])) continue;
|
||||||
|
sd = slopeBeltShape(x + 0.5, y + 0.5, b, joins, code);
|
||||||
|
const up = !isFilter(code) && slopeUphill(code, v), rib = up && slopePreviewCleat(x, y, b, code);
|
||||||
|
if (sd > 0 && !rib && (!up || slopeWallShape(x + 0.5, y + 0.5, b, code) > 0)) continue;
|
||||||
|
if (rib) sd = -0.1;
|
||||||
|
}
|
||||||
|
if (isSlope(code) && !slopeCell(code, dx, dy)) continue;
|
||||||
const sx = gbx * BS + dx - vx0, sy = gby * BS + dy - vy0;
|
const sx = gbx * BS + dx - vx0, sy = gby * BS + dy - vy0;
|
||||||
if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue;
|
if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue;
|
||||||
const k = sy * bw + sx, c = px[k];
|
const k = sy * bw + sx, c = px[k];
|
||||||
const edge = dx === 0 || dy === 0 || dx === BS - 1 || dy === BS - 1;
|
const edge = slope || diagonal ? sd > -0.8 : (dx === 0 && (caps & 1)) || dy === 0 || (dx === BS - 1 && (caps & 2)) || dy === BS - 1;
|
||||||
const a = edge ? (((dx + dy + pulse) & 3) === 0 ? 0.85 : 0.6) : 0.28;
|
const a = edge ? (((dx + dy + pulse) & 3) === 0 ? 0.85 : 0.6) : 0.28;
|
||||||
const pr = c & 255, pg = (c >> 8) & 255, pb = (c >> 16) & 255;
|
const pr = c & 255, pg = (c >> 8) & 255, pb = (c >> 16) & 255;
|
||||||
px[k] = (sm ? 0x04000000 : 0xff000000) | ((pb + (tb - pb) * a) << 16) | ((pg + (tg - pg) * a) << 8) | (pr + (tr - pr) * a);
|
px[k] = (sm ? 0x04000000 : 0xff000000) | ((pb + (tb - pb) * a) << 16) | ((pg + (tg - pg) * a) << 8) | (pr + (tr - pr) * a);
|
||||||
|
|||||||
@@ -130,6 +130,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section id="dev-console" class="plate" aria-labelledby="dev-title" hidden>
|
||||||
|
<div class="dev-head"><strong id="dev-title">Dev console</strong><span class="note">World marked · help lists commands</span><button id="dev-close" class="iconbtn" aria-label="Close dev console">Close <kbd>Esc</kbd></button></div>
|
||||||
|
<div id="dev-output" role="log" aria-label="Command output" aria-live="polite"></div>
|
||||||
|
<form id="dev-form"><label for="dev-input">Command</label><input id="dev-input" type="text" autocomplete="off" spellcheck="false" placeholder="help"><button class="btn" type="submit">Run</button></form>
|
||||||
|
</section>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//__JS__
|
//__JS__
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+107
-28
@@ -2,42 +2,89 @@
|
|||||||
// ---------- Blocks ----------
|
// ---------- Blocks ----------
|
||||||
function blockOf(x, y) { return { bx: Math.floor(x / BS), by: Math.floor(y / BS) }; }
|
function blockOf(x, y) { return { bx: Math.floor(x / BS), by: Math.floor(y / BS) }; }
|
||||||
function blockCellList(bx, by) { const out = []; for (let y = by * BS; y < by * BS + BS; y++) for (let x = bx * BS; x < bx * BS + BS; x++) out.push(y * W + x); return out; }
|
function blockCellList(bx, by) { const out = []; for (let y = by * BS; y < by * BS + BS; y++) for (let x = bx * BS; x < bx * BS + BS; x++) out.push(y * W + x); return out; }
|
||||||
function pieceCost(code, v) { return code === BP_WALL ? wallCost(v, BS * BS) : PIECE_BY_CODE[code].cost; }
|
function pieceCost(code, v) { return isWall(code) ? wallCost(v, isSlope(code) ? 8 : BS * BS) : PIECE_BY_CODE[code].cost; }
|
||||||
function canPlaceBlock(bx, by) {
|
function setSlopeSeal(i, code, v) {
|
||||||
|
if (isFilter(code)) { part[i] = P_FILTER; wakeIdx(i); }
|
||||||
|
else {
|
||||||
|
setCell(i, isBelt(code) ? (v ? BELTR : BELTL) : WALL_OF[v], AMB[(i / W) | 0], 0);
|
||||||
|
if (isBelt(code)) markPow(i, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// One extra cell on the solid side seals diagonal crossings between successive triangles.
|
||||||
|
function slopeSeal(code, bx, by) {
|
||||||
|
code = slopeShape(code);
|
||||||
|
const x = bx * BS + (code === BP_SLOPE_BR || code === BP_SLOPE_TR ? 0 : 3);
|
||||||
|
const y = by * BS + (code >= BP_SLOPE_TL ? -1 : 4);
|
||||||
|
return y < 0 || y >= H ? -1 : y * W + x;
|
||||||
|
}
|
||||||
|
function slopeSealOwner(i) {
|
||||||
|
const x = i % W, y = (i / W) | 0, bx = x >> 2;
|
||||||
|
if (((x & 3) !== 0 && (x & 3) !== 3) || ((y & 3) !== 0 && (y & 3) !== 3)) return -1;
|
||||||
|
for (let by = (y >> 2) - 1; by <= (y >> 2) + 1; by += 2) {
|
||||||
|
if (by < 0 || by >= BH) continue;
|
||||||
|
const b = by * BW + bx, c = blockP[b];
|
||||||
|
if ((isSlope(c) || isSlopeBelt(c)) && slopeSeal(c, bx, by) === i) return b;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// Does block b's own piece fill cell i (with a part, or else with a material)? A slope's seal can
|
||||||
|
// sit in a cell its neighbour leaves open: a slope's open corner, or under a grate, screw or screen.
|
||||||
|
function blockOwnsCell(b, i, asPart) {
|
||||||
|
const c = blockP[b];
|
||||||
|
if (!c || ((isSlope(c) || isSlopeBelt(c)) && !slopeCell(c, (i % W) & 3, ((i / W) | 0) & 3))) return false;
|
||||||
|
return asPart ? isFilter(c) : !(isFilter(c) || c === BP_GRATE || c === BP_SCREW || c === BP_PIPE);
|
||||||
|
}
|
||||||
|
// Belt and screen cells that a removed slope left behind in an empty block (a removal bug in
|
||||||
|
// older worlds): nothing owns them, so nothing could build there or take them away.
|
||||||
|
function clearStraySeals() {
|
||||||
|
for (let i = 0; i < N; i++) {
|
||||||
|
if (mat[i] !== BELTL && mat[i] !== BELTR && part[i] !== P_FILTER) continue;
|
||||||
|
if (blockP[(((i / W) | 0) >> 2) * BW + ((i % W) >> 2)] || slopeSealOwner(i) >= 0) continue;
|
||||||
|
if (part[i] === P_FILTER) { part[i] = 0; wakeIdx(i); } else { setCell(i, EMPTY, temp[i], 0); markPow(i, false); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function wallCells(code, bx, by) {
|
||||||
|
return blockCellList(bx, by).filter(i => !(isSlope(code) || isSlopeBelt(code)) || slopeCell(code, (i % W) & 3, ((i / W) | 0) & 3));
|
||||||
|
}
|
||||||
|
function canPlaceBlock(bx, by, code = BP_WALL) {
|
||||||
if (bx < 0 || by < 0 || bx >= BW || by >= BH) return 'Out of bounds';
|
if (bx < 0 || by < 0 || bx >= BW || by >= BH) return 'Out of bounds';
|
||||||
if (blockP[by * BW + bx]) return 'Something is already built there';
|
if (blockP[by * BW + bx]) return 'Something is already built there';
|
||||||
for (const i of blockCellList(bx, by)) {
|
for (const i of blockCellList(bx, by)) {
|
||||||
if (mach[i] || part[i]) return 'Something is already built there';
|
if (mach[i] || (part[i] && slopeSealOwner(i) < 0)) return 'Something is already built there';
|
||||||
if (PH[mat[i]] === PH_SOLID) return 'Needs open space; clear the rock first.';
|
if (PH[mat[i]] === PH_SOLID && ((flags[i] & F_NAT) || slopeSealOwner(i) < 0)) return 'Needs open space; clear the rock first.';
|
||||||
if (occ[i]) return 'A bar is in the way';
|
if (occ[i]) return 'A bar is in the way';
|
||||||
}
|
}
|
||||||
|
if (isSlope(code) || isSlopeBelt(code)) {
|
||||||
|
const i = slopeSeal(code, bx, by);
|
||||||
|
if (i >= 0 && PH[mat[i]] !== PH_SOLID && (mach[i] || part[i] || occ[i])) return 'The slope corner is blocked.';
|
||||||
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
// v: wall material (loose id) for walls, direction for belts and tubes
|
// v: wall material (loose id) for walls, direction for belts and screws
|
||||||
function placeBlock(code, v, bx, by, free, extra) {
|
function placeBlock(code, v, bx, by, free, extra) {
|
||||||
const err = canPlaceBlock(bx, by); if (err) return err;
|
const err = canPlaceBlock(bx, by, code); if (err) return err;
|
||||||
if (code === BP_WALL && !WALL_OF[v]) return 'Pick a wall material with R';
|
if (isWall(code) && !WALL_OF[v]) return 'Pick a wall material with R';
|
||||||
const cost = pieceCost(code, v);
|
const cost = pieceCost(code, v);
|
||||||
if (!free) { if (!canAfford(cost, 1)) return needText(cost, 1); payCost(cost, 1); }
|
if (!free) { if (!canAfford(cost, 1)) return needText(cost, 1); payCost(cost, 1); }
|
||||||
const cells = blockCellList(bx, by);
|
const cells = isSlope(code) || isSlopeBelt(code) ? wallCells(code, bx, by) : blockCellList(bx, by);
|
||||||
if (code !== BP_PIPE) evictCells(cells, by * BS); // a pipe is thin: what is there stays and flows past it
|
if (code !== BP_PIPE) evictCells(cells, by * BS); // a pipe is thin: what is there stays and flows past it
|
||||||
for (const i of cells) {
|
for (const i of cells) {
|
||||||
const y = (i / W) | 0, x = i % W;
|
const y = (i / W) | 0, x = i % W;
|
||||||
if (code === BP_WALL) setCell(i, WALL_OF[v], AMB[y], 0);
|
if (isWall(code)) setCell(i, WALL_OF[v], AMB[y], 0);
|
||||||
else if (code === BP_BELT) { setCell(i, v === 0 ? BELTL : BELTR, AMB[y], 0); markPow(i, true); }
|
else if (isBelt(code)) { setCell(i, v === 0 ? BELTL : BELTR, AMB[y], 0); markPow(i, true); }
|
||||||
else if (code === BP_SHAFT) { setCell(i, SHAFT, AMB[y], 0); markPow(i, true); }
|
else if (code === BP_SHAFT) { setCell(i, SHAFT, AMB[y], 0); markPow(i, true); }
|
||||||
else if (code === BP_RACK) setCell(i, RACKB, AMB[y], 0);
|
else if (code === BP_RACK) setCell(i, RACKB, AMB[y], 0);
|
||||||
else if (isProcCode(code)) setCell(i, code === BP_GRIND ? GRINDER : code === BP_PRESS ? PRESSB : SAWB, AMB[y], 0);
|
else if (isProcCode(code)) setCell(i, code === BP_GRIND ? GRINDER : code === BP_PRESS ? PRESSB : SAWB, AMB[y], 0);
|
||||||
else if (code === BP_GATE) setCell(i, GATE, AMB[y], 0);
|
else if (code === BP_GATE) setCell(i, GATE, AMB[y], 0);
|
||||||
else if (code === BP_GRATE) { part[i] = P_GRATE; wakeXY(x, y); }
|
else if (code === BP_GRATE) { part[i] = P_GRATE; wakeXY(x, y); }
|
||||||
else if (code === BP_TUBE) { part[i] = [P_TU, P_TR, P_TD, P_TL][v & 3]; markPow(i, true); wakeXY(x, y); }
|
else if (code === BP_SCREW) { part[i] = SCREW_PARTS[v & 7]; markPow(i, true); wakeXY(x, y); }
|
||||||
else if (code === BP_FILTER) { part[i] = P_FILTER; wakeXY(x, y); }
|
else if (isFilter(code)) { part[i] = P_FILTER; wakeXY(x, y); }
|
||||||
else if (code === BP_PIPE) { part[i] = P_PIPE; wakeXY(x, y); }
|
else if (code === BP_PIPE) { part[i] = P_PIPE; wakeXY(x, y); }
|
||||||
else if (isPipeCode(code)) setCell(i, PIPE, AMB[y], 0);
|
else if (isPipeCode(code)) setCell(i, PIPE, AMB[y], 0);
|
||||||
}
|
}
|
||||||
if (isPipeCode(code)) pipesDirty = true;
|
if (isPipeCode(code)) pipesDirty = true;
|
||||||
blockP[by * BW + bx] = code; blockV[by * BW + bx] = v;
|
blockP[by * BW + bx] = code; blockV[by * BW + bx] = v;
|
||||||
if (code === BP_FILTER) { blockV[by * BW + bx] = v & 128; filterCfg.set(by * BW + bx, filterRec(extra)); filtersDirty = true; }
|
if (isFilter(code)) { blockV[by * BW + bx] = filterVariant(code, v); filterCfg.set(by * BW + bx, filterRec(extra)); filtersDirty = true; }
|
||||||
if (isProcCode(code)) procDirty = true;
|
if (isProcCode(code)) procDirty = true;
|
||||||
if (code === BP_RACK) {
|
if (code === BP_RACK) {
|
||||||
const b = by * BW + bx, s = newRack();
|
const b = by * BW + bx, s = newRack();
|
||||||
@@ -45,6 +92,10 @@ function placeBlock(code, v, bx, by, free, extra) {
|
|||||||
rackState.set(b, s);
|
rackState.set(b, s);
|
||||||
for (let q = rackTop(b); q <= rackBottom(b); q += BW) rackState.get(q).open = s.open;
|
for (let q = rackTop(b); q <= rackBottom(b); q += BW) rackState.get(q).open = s.open;
|
||||||
}
|
}
|
||||||
|
if (isSlope(code) || isSlopeBelt(code)) {
|
||||||
|
const i = slopeSeal(code, bx, by);
|
||||||
|
if (i >= 0 && PH[mat[i]] !== PH_SOLID && !part[i]) { evictCells([i], by * BS); setSlopeSeal(i, code, v); }
|
||||||
|
}
|
||||||
stats.placed++;
|
stats.placed++;
|
||||||
undoNote({ t: 'block+', bx, by, code });
|
undoNote({ t: 'block+', bx, by, code });
|
||||||
return '';
|
return '';
|
||||||
@@ -57,15 +108,15 @@ function blockIntegrity(b) {
|
|||||||
let n = 0;
|
let n = 0;
|
||||||
for (const i of blockCellList(bx, by)) {
|
for (const i of blockCellList(bx, by)) {
|
||||||
const m = mat[i], p = part[i];
|
const m = mat[i], p = part[i];
|
||||||
if (code === BP_WALL) { if (PH[m] === PH_SOLID && !(flags[i] & F_NAT) && !PARTMAT[m]) n++; }
|
if (isWall(code) && (!isSlope(code) || slopeCell(code, (i % W) & 3, ((i / W) | 0) & 3))) { if (PH[m] === PH_SOLID && !(flags[i] & F_NAT) && !PARTMAT[m]) n++; }
|
||||||
else if (code === BP_BELT) { if (m === BELTL || m === BELTR) n++; }
|
else if (isBelt(code)) { if (m === BELTL || m === BELTR) n++; }
|
||||||
else if (code === BP_SHAFT) { if (m === SHAFT) n++; }
|
else if (code === BP_SHAFT) { if (m === SHAFT) n++; }
|
||||||
else if (code === BP_RACK) { if (m === RACKB) n++; }
|
else if (code === BP_RACK) { if (m === RACKB) n++; }
|
||||||
else if (isProcCode(code)) { if (m === (code === BP_GRIND ? GRINDER : code === BP_PRESS ? PRESSB : SAWB)) n++; }
|
else if (isProcCode(code)) { if (m === (code === BP_GRIND ? GRINDER : code === BP_PRESS ? PRESSB : SAWB)) n++; }
|
||||||
else if (code === BP_GATE) { if (m === GATE || p === P_GOPEN) n++; }
|
else if (code === BP_GATE) { if (m === GATE || p === P_GOPEN) n++; }
|
||||||
else if (code === BP_GRATE) { if (p === P_GRATE) n++; }
|
else if (code === BP_GRATE) { if (p === P_GRATE) n++; }
|
||||||
else if (code === BP_TUBE) { if (p >= P_TU && p <= P_TR) n++; }
|
else if (code === BP_SCREW) { if (IS_SCREW[p]) n++; }
|
||||||
else if (code === BP_FILTER) { if (p === P_FILTER) n++; }
|
else if (isFilter(code)) { if (p === P_FILTER) n++; }
|
||||||
else if (isPipeCode(code)) { if (m === PIPE || p === P_PIPE) n++; }
|
else if (isPipeCode(code)) { if (m === PIPE || p === P_PIPE) n++; }
|
||||||
}
|
}
|
||||||
void v;
|
void v;
|
||||||
@@ -83,28 +134,35 @@ function removeWallCell(i, wall) {
|
|||||||
function removeBlock(bx, by) {
|
function removeBlock(bx, by) {
|
||||||
const b = by * BW + bx, code = blockP[b], v = blockV[b];
|
const b = by * BW + bx, code = blockP[b], v = blockV[b];
|
||||||
if (!code) return 0;
|
if (!code) return 0;
|
||||||
undoNote({ t: 'block-', bx, by, code, v, extra: code === BP_FILTER ? filterInfo(b) : undefined });
|
undoNote({ t: 'block-', bx, by, code, v, extra: isFilter(code) ? filterInfo(b) : undefined });
|
||||||
// a pipe taken out spills its share of what the line holds
|
// a pipe taken out spills its share of what the line holds
|
||||||
const spill = isPipeCode(code) ? pipeTakeShare(b) : null;
|
const spill = isPipeCode(code) ? pipeTakeShare(b) : null;
|
||||||
let n = 0, metal = 0;
|
let n = 0, metal = 0;
|
||||||
for (const i of blockCellList(bx, by)) {
|
for (const i of blockCellList(bx, by)) {
|
||||||
const m = mat[i], p = part[i];
|
const m = mat[i], p = part[i];
|
||||||
if (code === BP_WALL) {
|
if (isWall(code)) {
|
||||||
|
if (isSlope(code) && !slopeCell(code, (i % W) & 3, ((i / W) | 0) & 3)) continue;
|
||||||
if (PH[m] === PH_SOLID && !(flags[i] & F_NAT) && !PARTMAT[m] && m !== SPRING) {
|
if (PH[m] === PH_SOLID && !(flags[i] & F_NAT) && !PARTMAT[m] && m !== SPRING) {
|
||||||
metal += removeWallCell(i, v);
|
if (isSlope(code)) setCell(i, EMPTY, temp[i], 0);
|
||||||
|
else metal += removeWallCell(i, v);
|
||||||
n++;
|
n++;
|
||||||
}
|
}
|
||||||
} else if (isPipeCode(code)) {
|
} else if (isPipeCode(code)) {
|
||||||
if (p === P_PIPE) { part[i] = 0; wakeIdx(i); n++; }
|
if (p === P_PIPE) { part[i] = 0; wakeIdx(i); n++; }
|
||||||
else if (m === PIPE) { setCell(i, EMPTY, temp[i], 0); n++; }
|
else if (m === PIPE) { setCell(i, EMPTY, temp[i], 0); n++; }
|
||||||
} else if (code === BP_BELT || code === BP_SHAFT || code === BP_GATE || code === BP_RACK || isProcCode(code)) {
|
} else if (isBelt(code) || code === BP_SHAFT || code === BP_GATE || code === BP_RACK || isProcCode(code)) {
|
||||||
if (m === BELTL || m === BELTR || m === SHAFT || m === GATE || m === GRINDER || m === SAWB || m === PRESSB || m === RACKB) { setCell(i, EMPTY, temp[i], 0); markPow(i, false); n++; }
|
if (m === BELTL || m === BELTR || m === SHAFT || m === GATE || m === GRINDER || m === SAWB || m === PRESSB || m === RACKB) { setCell(i, EMPTY, temp[i], 0); markPow(i, false); n++; }
|
||||||
if (p === P_GOPEN) { part[i] = 0; n++; wakeIdx(i); }
|
if (p === P_GOPEN) { part[i] = 0; n++; wakeIdx(i); }
|
||||||
} else if (p) { part[i] = 0; markPow(i, false); wakeIdx(i); n++; }
|
} else if (p) { part[i] = 0; markPow(i, false); wakeIdx(i); n++; }
|
||||||
}
|
}
|
||||||
|
if (isSlope(code)) {
|
||||||
|
const cells = Math.floor(n * 8 / 10), refund = PH[v] === PH_ITEM ? Math.floor(cells / GRAINS_PER_BAR) : cells;
|
||||||
|
giveOrDrop(v, refund, bx * BS + 2, by * BS + 2);
|
||||||
|
|
||||||
|
}
|
||||||
if (code === BP_WALL && metal) giveOrDrop(BAR_OF[WALL_OF[v]] || BARCU, Math.floor(metal / GRAINS_PER_BAR), bx * BS + 2, by * BS + 2);
|
if (code === BP_WALL && metal) giveOrDrop(BAR_OF[WALL_OF[v]] || BARCU, Math.floor(metal / GRAINS_PER_BAR), bx * BS + 2, by * BS + 2);
|
||||||
else if (code !== BP_WALL && n >= 8) refundAt(PIECE_BY_CODE[code].cost, 1, bx * BS + 2, by * BS + 2);
|
else if (!isWall(code) && n >= 8) refundAt(PIECE_BY_CODE[code].cost, 1, bx * BS + 2, by * BS + 2);
|
||||||
if (code === BP_FILTER) { filtersDirty = true; filterCfg.delete(b); }
|
if (isFilter(code)) { filtersDirty = true; filterCfg.delete(b); }
|
||||||
if (isProcCode(code)) { procDirty = true; procState.delete(b); }
|
if (isProcCode(code)) { procDirty = true; procState.delete(b); }
|
||||||
if (code === BP_RACK) {
|
if (code === BP_RACK) {
|
||||||
const s = rackState.get(b);
|
const s = rackState.get(b);
|
||||||
@@ -112,15 +170,36 @@ function removeBlock(bx, by) {
|
|||||||
if (s) for (const [m, count] of s.q) giveOrDrop(m, count, bot.x, bot.y);
|
if (s) for (const [m, count] of s.q) giveOrDrop(m, count, bot.x, bot.y);
|
||||||
}
|
}
|
||||||
blockP[b] = 0; blockV[b] = 0;
|
blockP[b] = 0; blockV[b] = 0;
|
||||||
|
const cleared = blockCellList(bx, by);
|
||||||
|
if (isSlope(code) || isSlopeBelt(code)) {
|
||||||
|
const i = slopeSeal(code, bx, by);
|
||||||
|
if (i >= 0) {
|
||||||
|
const nb = (((i / W) | 0) >> 2) * BW + ((i % W) >> 2);
|
||||||
|
if (!blockOwnsCell(nb, i, isFilter(code)) && (mat[i] === (isBelt(code) ? (v ? BELTR : BELTL) : WALL_OF[v]) || (isFilter(code) && part[i] === P_FILTER)) && !(flags[i] & F_NAT)) { if (isFilter(code)) { part[i] = 0; wakeIdx(i); } else { setCell(i, EMPTY, temp[i], 0); if (isBelt(code)) markPow(i, false); } }
|
||||||
|
cleared.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const i of cleared) {
|
||||||
|
const owner = slopeSealOwner(i);
|
||||||
|
if (owner >= 0 && mat[i] === EMPTY && !part[i]) {
|
||||||
|
setSlopeSeal(i, blockP[owner], blockV[owner]);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (isPipeCode(code)) {
|
if (isPipeCode(code)) {
|
||||||
pipesDirty = true;
|
pipesDirty = true;
|
||||||
if (spill) { let k = spill.n; for (let y = by * BS + BS - 1; y >= by * BS && k > 0; y--) for (let x = bx * BS; x < bx * BS + BS && k > 0; x++) { const i = y * W + x; if (mat[i] === EMPTY) { setCell(i, spill.m, spill.T, 0); k--; } } }
|
if (spill) { let k = spill.n; for (let y = by * BS + BS - 1; y >= by * BS && k > 0; y--) for (let x = bx * BS; x < bx * BS + BS && k > 0; x++) { const i = y * W + x; if (mat[i] === EMPTY) { setCell(i, spill.m, spill.T, 0); k--; } } }
|
||||||
}
|
}
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
// Straight line of blocks along the dominant axis
|
// Wall drags near 45° snap to a diagonal; other lines follow the dominant axis.
|
||||||
function blockLine(bx0, by0, bx1, by1) {
|
function diagonalDrag(dx, dy) { return dx !== 0 && dy !== 0 && Math.min(Math.abs(dx), Math.abs(dy)) >= Math.max(Math.abs(dx), Math.abs(dy)) * 0.7; }
|
||||||
|
function blockLine(bx0, by0, bx1, by1, diagonal = false) {
|
||||||
const out = [];
|
const out = [];
|
||||||
|
const dx = bx1 - bx0, dy = by1 - by0;
|
||||||
|
if (diagonal && diagonalDrag(dx, dy)) {
|
||||||
|
for (let k = 0; k <= Math.max(Math.abs(dx), Math.abs(dy)); k++) out.push([bx0 + k * Math.sign(dx), by0 + k * Math.sign(dy)]);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
if (Math.abs(bx1 - bx0) >= Math.abs(by1 - by0)) { const s = bx1 >= bx0 ? 1 : -1; for (let x = bx0; ; x += s) { out.push([x, by0]); if (x === bx1) break; } }
|
if (Math.abs(bx1 - bx0) >= Math.abs(by1 - by0)) { const s = bx1 >= bx0 ? 1 : -1; for (let x = bx0; ; x += s) { out.push([x, by0]); if (x === bx1) break; } }
|
||||||
else { const s = by1 >= by0 ? 1 : -1; for (let y = by0; ; y += s) { out.push([bx0, y]); if (y === by1) break; } }
|
else { const s = by1 >= by0 ? 1 : -1; for (let y = by0; ; y += s) { out.push([bx0, y]); if (y === by1) break; } }
|
||||||
return out;
|
return out;
|
||||||
@@ -130,11 +209,11 @@ function blockRect(bx0, by0, bx1, by1) {
|
|||||||
for (let y = Math.min(by0, by1); y <= Math.max(by0, by1); y++) for (let x = Math.min(bx0, bx1); x <= Math.max(bx0, bx1); x++) out.push([x, y]);
|
for (let y = Math.min(by0, by1); y <= Math.max(by0, by1); y++) for (let x = Math.min(bx0, bx1); x <= Math.max(bx0, bx1); x++) out.push([x, y]);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
function blockInReach(bx, by) { return inReach(bx * BS + BS / 2, by * BS + BS / 2); }
|
function blockInReach(bx, by) { return inBuildRange(bx * BS + BS / 2, by * BS + BS / 2); }
|
||||||
// Is block b already exactly this piece (same kind, variant and, for a filter, settings)?
|
// Is block b already exactly this piece (same kind, variant and, for a filter, settings)?
|
||||||
function blockIs(b, code, v, extra) {
|
function blockIs(b, code, v, extra) {
|
||||||
if (blockP[b] !== code) return false;
|
if (blockP[b] !== code) return false;
|
||||||
if (code === BP_FILTER) return (blockV[b] & 128) === (v & 128) && sameFilter(filterInfo(b), cleanFilterCfg(extra));
|
if (isFilter(code)) return (blockV[b] & 128) === filterVariant(code, v) && sameFilter(filterInfo(b), cleanFilterCfg(extra));
|
||||||
return blockV[b] === v;
|
return blockV[b] === v;
|
||||||
}
|
}
|
||||||
// replace: a block already built in the way is taken down (and refunded) to make room
|
// replace: a block already built in the way is taken down (and refunded) to make room
|
||||||
@@ -145,7 +224,7 @@ function placeBlocks(code, v, list, extra, replace) {
|
|||||||
const b = by * BW + bx;
|
const b = by * BW + bx;
|
||||||
if (replace && blockP[b]) {
|
if (replace && blockP[b]) {
|
||||||
if (blockIs(b, code, v, extra)) continue;
|
if (blockIs(b, code, v, extra)) continue;
|
||||||
if (blockP[b] === BP_FILTER && code === BP_FILTER) { undoNote({ t: 'filter', bx, by, v: blockV[b], extra: filterInfo(b) }); blockV[b] = v & 128; setFilter(b, extra); filtersDirty = true; n++; continue; } // just re-set it
|
if (blockP[b] === code && isFilter(code)) { undoNote({ t: 'filter', bx, by, v: blockV[b], extra: filterInfo(b) }); blockV[b] = filterVariant(code, v); setFilter(b, extra); filtersDirty = true; n++; continue; } // just re-set it
|
||||||
if (!canAfford(pieceCost(code, v), 1)) { last = needText(pieceCost(code, v), 1); break; }
|
if (!canAfford(pieceCost(code, v), 1)) { last = needText(pieceCost(code, v), 1); break; }
|
||||||
removeBlock(bx, by);
|
removeBlock(bx, by);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function copyRegion(x0, y0, x1, y1) {
|
|||||||
const blocks = [], ms = [], ols = [];
|
const blocks = [], ms = [], ols = [];
|
||||||
for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
|
for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
|
||||||
const b = by * BW + bx;
|
const b = by * BW + bx;
|
||||||
if (blockP[b] && blockIntegrity(b) >= 8) blocks.push(blockP[b] === BP_FILTER ? [bx - bx0, by - by0, blockP[b], blockV[b], filterInfo(b)] : [bx - bx0, by - by0, blockP[b], blockV[b]]);
|
if (blockP[b] && blockIntegrity(b) >= 8) blocks.push(isFilter(blockP[b]) ? [bx - bx0, by - by0, blockP[b], blockV[b], filterInfo(b)] : [bx - bx0, by - by0, blockP[b], blockV[b]]);
|
||||||
}
|
}
|
||||||
for (const mm of machines) {
|
for (const mm of machines) {
|
||||||
if (!mm || PIECE_BY_ID[mm.type].fixed) continue;
|
if (!mm || PIECE_BY_ID[mm.type].fixed) continue;
|
||||||
@@ -23,11 +23,11 @@ function copyRegion(x0, y0, x1, y1) {
|
|||||||
}
|
}
|
||||||
function flipBlueprint(bp) {
|
function flipBlueprint(bp) {
|
||||||
const bw = bp.bw;
|
const bw = bp.bw;
|
||||||
const flipV = (code, v) => code === BP_BELT ? 1 - v : code === BP_FILTER ? v ^ 128 : code === BP_TUBE ? (v === 1 ? 3 : v === 3 ? 1 : v)
|
const flipV = (code, v) => isBelt(code) ? 1 - v : isFilter(code) ? v ^ 128 : code === BP_SCREW ? [0, 3, 2, 1, 7, 6, 5, 4][v & 7]
|
||||||
: code === BP_PUMP || code === BP_OUTLET ? (((v & 3) === 1 ? 3 : (v & 3) === 3 ? 1 : v & 3) | (v & 4)) : v;
|
: code === BP_PUMP || code === BP_OUTLET ? (((v & 3) === 1 ? 3 : (v & 3) === 3 ? 1 : v & 3) | (v & 4)) : v;
|
||||||
return {
|
return {
|
||||||
v: 2, bw, bh: bp.bh, name: bp.name,
|
v: 2, bw, bh: bp.bh, name: bp.name,
|
||||||
blocks: bp.blocks.map(([x, y, c, v, e]) => e ? [bw - 1 - x, y, c, flipV(c, v), e] : [bw - 1 - x, y, c, flipV(c, v)]),
|
blocks: bp.blocks.map(([x, y, c, v, e]) => e ? [bw - 1 - x, y, isSlopeBelt(c) ? c + (c & 1 ? 1 : -1) : isSlope(c) ? BP_SLOPE_BL + ((c - BP_SLOPE_BL) ^ 1) : c, flipV(c, v), e] : [bw - 1 - x, y, isSlopeBelt(c) ? c + (c & 1 ? 1 : -1) : isSlope(c) ? BP_SLOPE_BL + ((c - BP_SLOPE_BL) ^ 1) : c, flipV(c, v)]),
|
||||||
machines: bp.machines.map(([t, x, y, d]) => {
|
machines: bp.machines.map(([t, x, y, d]) => {
|
||||||
const P = PIECE_BY_ID[t];
|
const P = PIECE_BY_ID[t];
|
||||||
let nd = d;
|
let nd = d;
|
||||||
|
|||||||
+35
-17
@@ -36,10 +36,15 @@ function move(i, j, x, y, nx, ny) {
|
|||||||
// Can a grain of material m (phase ph) enter cell j moving by (dx,dy)?
|
// Can a grain of material m (phase ph) enter cell j moving by (dx,dy)?
|
||||||
// 0 = no, 1 = into empty, 2 = by displacing what is there.
|
// 0 = no, 1 = into empty, 2 = by displacing what is there.
|
||||||
function enter(m, ph, j, dx, dy) {
|
function enter(m, ph, j, dx, dy) {
|
||||||
|
if (ph === PH_POWDER && cleatCells.length && (cleatMask[j] || (dy > 0 && cleatMask[j - dx - dy * W]))) return 0;
|
||||||
|
return enterFree(m, ph, j, dx, dy);
|
||||||
|
}
|
||||||
|
// Powder already knows whether its chunk has barriers; other callers use enter.
|
||||||
|
function enterFree(m, ph, j, dx, dy) {
|
||||||
if (occ[j]) return 0;
|
if (occ[j]) return 0;
|
||||||
const p = part[j];
|
const p = part[j];
|
||||||
if (p !== 0) {
|
if (p !== 0) {
|
||||||
if (p <= P_TR) { if (ph === PH_POWDER && TDX[p] === -dx && TDY[p] === -dy) return 0; } // liquids and gases flow through freely
|
if (IS_SCREW[p]) { if (ph === PH_POWDER && !screwAccepts(p, dx, dy)) return 0; } // liquids and gases flow through freely
|
||||||
else if (p === P_GRATE) { if (ph === PH_POWDER || m === MSLAG) return 0; } // thick slag does not run through a grate
|
else if (p === P_GRATE) { if (ph === PH_POWDER || m === MSLAG) return 0; } // thick slag does not run through a grate
|
||||||
else if (p === P_INTAKE) { if (ph !== PH_GAS) return 0; }
|
else if (p === P_INTAKE) { if (ph !== PH_GAS) return 0; }
|
||||||
else if (p === P_FILTER) { if (ph === PH_POWDER && !filterPassAt(m, j)) return 0; }
|
else if (p === P_FILTER) { if (ph === PH_POWDER && !filterPassAt(m, j)) return 0; }
|
||||||
@@ -67,24 +72,37 @@ let SORTP = 0.06;
|
|||||||
const SORTP_FUEL = 0.003;
|
const SORTP_FUEL = 0.003;
|
||||||
// Liquids step up to LSTEP cells toward a drop in one tick (fewer the thicker they are), and a top-layer grain looks LFILM
|
// Liquids step up to LSTEP cells toward a drop in one tick (fewer the thicker they are), and a top-layer grain looks LFILM
|
||||||
// times as far for one (a two-times range strands the far end of a 200-grain-wide ledge).
|
// times as far for one (a two-times range strands the far end of a 200-grain-wide ledge).
|
||||||
// World generation settles with both at 1, so every seed keeps making the same world.
|
// World generation settles with both at 1 and corner sealing off, so seeds keep the same world.
|
||||||
let LSTEP = 8, LFILM = 4;
|
let LSTEP = 8, LFILM = 4;
|
||||||
function powder(i, x, y, m) {
|
let CORNER_SEAL = true;
|
||||||
|
function powder(i, x, y, m, barriers = cleatChunks[(y >> 5) * CW + (x >> 5)]) {
|
||||||
if (y >= H - 1) return;
|
if (y >= H - 1) return;
|
||||||
const b = i + W;
|
const b = i + W;
|
||||||
if (enter(m, PH_POWDER, b, 0, 1)) { move(i, b, x, y, x, y + 1); return; }
|
if ((!barriers || (!cleatMask[i] && !cleatMask[b])) && enterFree(m, PH_POWDER, b, 0, 1)) { move(i, b, x, y, x, y + 1); return; }
|
||||||
// a grain at least twice as dense works its way down through a loose pile. Unlit fuel
|
// a grain at least twice as dense works its way down through a loose pile. Unlit fuel
|
||||||
// packs too tight for that, so a mixed furnace charge stays mixed until the fire gets in.
|
// packs too tight for that, so a mixed furnace charge stays mixed until the fire gets in.
|
||||||
const t = mat[b];
|
const t = mat[b];
|
||||||
if (PH[t] === PH_POWDER && DENS[m] >= DENS[t] * 2 && (!FUEL[t] || (flags[b] & F_BURN)) && stamp[b] !== tickStamp && !occ[b] && part[b] !== P_GRATE && part[b] !== P_FILTER && rnd() < (FUEL[t] ? SORTP_FUEL : SORTP)) { move(i, b, x, y, x, y + 1); awake[(y >> 5) * CW + (x >> 5)] = 40; return; }
|
if (PH[t] === PH_POWDER && (!barriers || (!cleatMask[i] && !cleatMask[b])) && DENS[m] >= DENS[t] * 2 && (!FUEL[t] || (flags[b] & F_BURN)) && stamp[b] !== tickStamp && !occ[b] && part[b] !== P_GRATE && part[b] !== P_FILTER && rnd() < (FUEL[t] ? SORTP_FUEL : SORTP)) { move(i, b, x, y, x, y + 1); awake[(y >> 5) * CW + (x >> 5)] = 40; return; }
|
||||||
// a grain lying right on a belt or filter is held by it: the surface carries it, it does not slide off the edge
|
// a grain lying right on a belt or filter is held by it: the surface carries it, it does not slide off the edge
|
||||||
if (t === BELTL || t === BELTR || part[b] === P_FILTER) return;
|
if ((t === BELTL || t === BELTR || part[b] === P_FILTER) &&
|
||||||
if (rnd() < STICK[m]) {
|
!isSlopeBelt(blockP[((y + 1) >> 2) * BW + (x >> 2)])) return;
|
||||||
|
// Feed a receiving screw mouth sideways instead of falling under its bottom casing.
|
||||||
|
if (t === BELTL || t === BELTR) {
|
||||||
|
const b = ((y + 1) >> 2) * BW + (x >> 2);
|
||||||
|
const d = isSlopeBelt(blockP[b]) ? (blockV[b] & 1 ? 1 : -1) : 0;
|
||||||
|
if (d && x + d >= 0 && x + d < W && IS_SCREW[part[i + d]]) {
|
||||||
|
if ((!barriers || !cleatMask[i + d]) && enterFree(m, PH_POWDER, i + d, d, 0)) move(i, i + d, x, y, x + d, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// stickiness is grain against grain: on anything built (a wall, a slope, a belt) every material
|
||||||
|
// slides freely, so a chute carries clay as well as sand while clay heaps stay steep
|
||||||
|
if (rnd() < STICK[m] || (PH[t] === PH_SOLID && !(flags[b] & F_NAT))) {
|
||||||
const d = (RS & 2) ? 1 : -1;
|
const d = (RS & 2) ? 1 : -1;
|
||||||
let nx = x + d;
|
let nx = x + d;
|
||||||
if (nx >= 0 && nx < W && enter(m, PH_POWDER, b + d, d, 1)) { move(i, b + d, x, y, nx, y + 1); return; }
|
if (nx >= 0 && nx < W && (!CORNER_SEAL || (PH[mat[b]] !== PH_SOLID && (!barriers || !cleatMask[b])) || (PH[mat[i + d]] !== PH_SOLID && (!barriers || !cleatMask[i + d]))) && (!barriers || (!cleatMask[i] && !cleatMask[b + d])) && enterFree(m, PH_POWDER, b + d, d, 1)) { move(i, b + d, x, y, nx, y + 1); return; }
|
||||||
nx = x - d;
|
nx = x - d;
|
||||||
if (nx >= 0 && nx < W && enter(m, PH_POWDER, b - d, -d, 1)) { move(i, b - d, x, y, nx, y + 1); return; }
|
if (nx >= 0 && nx < W && (!CORNER_SEAL || (PH[mat[b]] !== PH_SOLID && (!barriers || !cleatMask[b])) || (PH[mat[i - d]] !== PH_SOLID && (!barriers || !cleatMask[i - d]))) && (!barriers || (!cleatMask[i] && !cleatMask[b - d])) && enterFree(m, PH_POWDER, b - d, -d, 1)) { move(i, b - d, x, y, nx, y + 1); return; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function passable(m, ph, j, dx, dy) {
|
function passable(m, ph, j, dx, dy) {
|
||||||
@@ -92,7 +110,7 @@ function passable(m, ph, j, dx, dy) {
|
|||||||
if (occ[j]) return false;
|
if (occ[j]) return false;
|
||||||
const p = part[j];
|
const p = part[j];
|
||||||
if (p !== 0) {
|
if (p !== 0) {
|
||||||
if (p <= P_TR) { if (ph === PH_POWDER && TDX[p] === -dx && TDY[p] === -dy) return false; }
|
if (IS_SCREW[p]) { if (ph === PH_POWDER && !screwAccepts(p, dx, dy)) return false; }
|
||||||
else if (p === P_GRATE) { if (ph === PH_POWDER || m === MSLAG) return false; }
|
else if (p === P_GRATE) { if (ph === PH_POWDER || m === MSLAG) return false; }
|
||||||
else if (p === P_INTAKE) { if (ph !== PH_GAS) return false; }
|
else if (p === P_INTAKE) { if (ph !== PH_GAS) return false; }
|
||||||
else if (p === P_FILTER) { if (ph === PH_POWDER && !filterPassAt(m, j)) return false; }
|
else if (p === P_FILTER) { if (ph === PH_POWDER && !filterPassAt(m, j)) return false; }
|
||||||
@@ -106,9 +124,9 @@ function liquid(i, x, y, m) {
|
|||||||
if (enter(m, PH_LIQUID, b, 0, 1)) { move(i, b, x, y, x, y + 1); return; }
|
if (enter(m, PH_LIQUID, b, 0, 1)) { move(i, b, x, y, x, y + 1); return; }
|
||||||
const d = (RS & 2) ? 1 : -1;
|
const d = (RS & 2) ? 1 : -1;
|
||||||
let nx = x + d;
|
let nx = x + d;
|
||||||
if (nx >= 0 && nx < W && enter(m, PH_LIQUID, b + d, d, 1)) { move(i, b + d, x, y, nx, y + 1); return; }
|
if (nx >= 0 && nx < W && (!CORNER_SEAL || PH[mat[b]] !== PH_SOLID || PH[mat[i + d]] !== PH_SOLID) && enter(m, PH_LIQUID, b + d, d, 1)) { move(i, b + d, x, y, nx, y + 1); return; }
|
||||||
nx = x - d;
|
nx = x - d;
|
||||||
if (nx >= 0 && nx < W && enter(m, PH_LIQUID, b - d, -d, 1)) { move(i, b - d, x, y, nx, y + 1); return; }
|
if (nx >= 0 && nx < W && (!CORNER_SEAL || PH[mat[b]] !== PH_SOLID || PH[mat[i - d]] !== PH_SOLID) && enter(m, PH_LIQUID, b - d, -d, 1)) { move(i, b - d, x, y, nx, y + 1); return; }
|
||||||
}
|
}
|
||||||
if (rnd() > FLUID[m]) return;
|
if (rnd() > FLUID[m]) return;
|
||||||
const sp = SPREAD[m] * (y === 0 || PH[mat[i - W]] !== PH_LIQUID ? LFILM : 1);
|
const sp = SPREAD[m] * (y === 0 || PH[mat[i - W]] !== PH_LIQUID ? LFILM : 1);
|
||||||
@@ -153,9 +171,9 @@ function gas(i, x, y, m) {
|
|||||||
if (enter(m, PH_GAS, u, 0, -1)) { move(i, u, x, y, x, y - 1); return; }
|
if (enter(m, PH_GAS, u, 0, -1)) { move(i, u, x, y, x, y - 1); return; }
|
||||||
const d = (RS & 2) ? 1 : -1;
|
const d = (RS & 2) ? 1 : -1;
|
||||||
let nx = x + d;
|
let nx = x + d;
|
||||||
if (nx >= 0 && nx < W && enter(m, PH_GAS, u + d, d, -1)) { move(i, u + d, x, y, nx, y - 1); return; }
|
if (nx >= 0 && nx < W && (!CORNER_SEAL || PH[mat[u]] !== PH_SOLID || PH[mat[i + d]] !== PH_SOLID) && enter(m, PH_GAS, u + d, d, -1)) { move(i, u + d, x, y, nx, y - 1); return; }
|
||||||
nx = x - d;
|
nx = x - d;
|
||||||
if (nx >= 0 && nx < W && enter(m, PH_GAS, u - d, -d, -1)) { move(i, u - d, x, y, nx, y - 1); return; }
|
if (nx >= 0 && nx < W && (!CORNER_SEAL || PH[mat[u]] !== PH_SOLID || PH[mat[i - d]] !== PH_SOLID) && enter(m, PH_GAS, u - d, -d, -1)) { move(i, u - d, x, y, nx, y - 1); return; }
|
||||||
if (rnd() < 0.6) {
|
if (rnd() < 0.6) {
|
||||||
nx = x + d;
|
nx = x + d;
|
||||||
if (nx >= 0 && nx < W && mat[i + d] === EMPTY && passable(m, PH_GAS, i + d, d, 0)) { move(i, i + d, x, y, nx, y); }
|
if (nx >= 0 && nx < W && mat[i + d] === EMPTY && passable(m, PH_GAS, i + d, d, 0)) { move(i, i + d, x, y, nx, y); }
|
||||||
@@ -187,7 +205,7 @@ function simMove() {
|
|||||||
const cx = ltr ? k : CW - 1 - k;
|
const cx = ltr ? k : CW - 1 - k;
|
||||||
if (!awake[rowBase + cx]) continue;
|
if (!awake[rowBase + cx]) continue;
|
||||||
roofDirty[cx] = 1;
|
roofDirty[cx] = 1;
|
||||||
const x0 = cx << 5, x1 = Math.min(W, x0 + 32);
|
const x0 = cx << 5, x1 = Math.min(W, x0 + 32), barriers = cleatChunks[rowBase + cx];
|
||||||
// alternate rows scan left to right and right to left, so nothing drifts one way
|
// alternate rows scan left to right and right to left, so nothing drifts one way
|
||||||
const dx = ltr ? 1 : -1, xEnd = ltr ? x1 : x0 - 1;
|
const dx = ltr ? 1 : -1, xEnd = ltr ? x1 : x0 - 1;
|
||||||
for (let x = ltr ? x0 : x1 - 1; x !== xEnd; x += dx) {
|
for (let x = ltr ? x0 : x1 - 1; x !== xEnd; x += dx) {
|
||||||
@@ -198,8 +216,8 @@ function simMove() {
|
|||||||
if (stamp[i] === tickStamp) continue;
|
if (stamp[i] === tickStamp) continue;
|
||||||
if (ph === PH_SOLID) continue;
|
if (ph === PH_SOLID) continue;
|
||||||
const p = part[i];
|
const p = part[i];
|
||||||
if (p >= P_TU && p <= P_TR && ph === PH_POWDER) continue; // a screw tube moves its own grains
|
if (IS_SCREW[p] && ph === PH_POWDER) continue; // a screw conveyor moves its own grains
|
||||||
if (ph === PH_POWDER) powder(i, x, y, m); else if (ph === PH_LIQUID) liquid(i, x, y, m); else gas(i, x, y, m);
|
if (ph === PH_POWDER) powder(i, x, y, m, barriers); else if (ph === PH_LIQUID) liquid(i, x, y, m); else gas(i, x, y, m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+108
-24
@@ -1,10 +1,27 @@
|
|||||||
// How belts, filters and screw tubes move what is on or in them.
|
// How belts, filters and screw conveyors move what is on or in them.
|
||||||
// What rests on a belt or filter rides along: the whole stack standing on the carrying surface
|
// What rests on a belt or filter rides along: the whole stack standing on the carrying surface
|
||||||
// steps one cell toward d together (belts run front of the line first, so the way ahead is
|
// steps one cell toward d together (belts run front of the line first, so the way ahead is
|
||||||
// clear). If a powder grain is blocked, the load above it stands still and the belt slips
|
// clear). If a powder grain is blocked, the load above it stands still and the belt slips
|
||||||
// underneath. A filter's own material drops through it instead, unless it is already full.
|
// underneath. A filter's own material drops through it instead, unless it is already full.
|
||||||
const CARRY_DEPTH = 96;
|
const CARRY_DEPTH = 96;
|
||||||
|
// Find the next carrying face at the same height or one grain above/below it.
|
||||||
|
function conveyorStep(x, y, d) {
|
||||||
|
const nx = x + d;
|
||||||
|
if (nx < 0 || nx >= W) return 0;
|
||||||
|
if ((x >> 2) === (nx >> 2)) return beltGrade(blockP[(y >> 2) * BW + (x >> 2)]) * d;
|
||||||
|
for (let dy = -1; dy <= 1; dy++) {
|
||||||
|
const yy = y + dy;
|
||||||
|
if (yy < 0 || yy >= H) continue;
|
||||||
|
const b = (yy >> 2) * BW + (nx >> 2), c = blockP[b], i = yy * W + nx;
|
||||||
|
if ((isBelt(c) || isFilter(c)) && beltFaceY(c, nx >> 2, yy >> 2, nx) === yy &&
|
||||||
|
(mat[i] === BELTL || mat[i] === BELTR || part[i] === P_FILTER)) return dy;
|
||||||
|
}
|
||||||
|
const b = (y >> 2) * BW + (x >> 2);
|
||||||
|
return beltGrade(blockP[b]) * d;
|
||||||
|
}
|
||||||
function carryColumn(a, d, pass) {
|
function carryColumn(a, d, pass) {
|
||||||
|
if (a < 0 || mat[a] === EMPTY) return;
|
||||||
|
const sy = ((a / W) | 0) + 1, step = conveyorStep(a % W, sy, d), carrier = (sy >> 2) * BW + ((a % W) >> 2);
|
||||||
let blocked = false;
|
let blocked = false;
|
||||||
for (let L = 0; L < CARRY_DEPTH; L++, a -= W) {
|
for (let L = 0; L < CARRY_DEPTH; L++, a -= W) {
|
||||||
if (a < 0) return;
|
if (a < 0) return;
|
||||||
@@ -20,18 +37,21 @@ function carryColumn(a, d, pass) {
|
|||||||
if (blocked) continue; // filters still sift above a stopped load
|
if (blocked) continue; // filters still sift above a stopped load
|
||||||
const x = a % W, nx = x + d;
|
const x = a % W, nx = x + d;
|
||||||
if (nx < 0 || nx >= W) return;
|
if (nx < 0 || nx >= W) return;
|
||||||
const j = a + d, tj = mat[j];
|
const j = a + d + step * W;
|
||||||
|
if (j < 0 || j >= N) return;
|
||||||
|
const tj = mat[j];
|
||||||
// grains on a wet belt push through the water rather than stopping at it
|
// grains on a wet belt push through the water rather than stopping at it
|
||||||
const wet = PH[tj] === PH_LIQUID && !occ[j] && stamp[j] !== tickStamp && (!part[j] || part[j] === P_FILTER || part[j] === P_GOPEN);
|
const wet = PH[tj] === PH_LIQUID && !occ[j] && stamp[j] !== tickStamp && (!part[j] || part[j] === P_FILTER || part[j] === P_GOPEN);
|
||||||
if (!wet && !enter(m, PH_POWDER, j, d, 0)) {
|
if (!wet && !enter(m, PH_POWDER, j, d, step)) {
|
||||||
if (!pass) return;
|
if (!pass) return;
|
||||||
blocked = true; continue;
|
blocked = true; continue;
|
||||||
}
|
}
|
||||||
const y = (a / W) | 0, bj = j + W;
|
const y = (a / W) | 0, bj = j + W;
|
||||||
// carried off the end with nothing under it: it leaves at belt speed and falls
|
// carried off the end with nothing under it: it leaves at belt speed and falls
|
||||||
if (mat[j] === EMPTY && !part[j] && bj < N && (mat[bj] === EMPTY || PH[mat[bj]] === PH_GAS) && !occ[bj] && !part[bj]) launchGrain(a, nx, y, d * 0.5, 0);
|
if (mat[j] === EMPTY && !part[j] && bj < N && (mat[bj] === EMPTY || PH[mat[bj]] === PH_GAS) && !occ[bj] && !part[bj]) launchGrain(a, nx, y + step, d * 0.5, step * 0.5);
|
||||||
else move(a, j, x, y, nx, y);
|
else move(a, j, x, y, nx, y + step);
|
||||||
stats.moved++;
|
stats.moved++;
|
||||||
|
if (flowOn && (x >> 2) !== (nx >> 2)) flowNote(carrier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Belts are perforated: liquid resting on one drips through to whatever open space is under it.
|
// Belts are perforated: liquid resting on one drips through to whatever open space is under it.
|
||||||
@@ -43,7 +63,7 @@ function beltDrain(b) {
|
|||||||
if (!passable(mat[a], PH_LIQUID, d, 0, 1)) return;
|
if (!passable(mat[a], PH_LIQUID, d, 0, 1)) return;
|
||||||
move(a, d, x, (a / W) | 0, x, (d / W) | 0);
|
move(a, d, x, (a / W) | 0, x, (d / W) | 0);
|
||||||
}
|
}
|
||||||
// Take a grain out of cell i and send it flying from (x, y): what leaves a belt or a tube mouth
|
// Take a grain out of cell i and send it flying from (x, y): what leaves a belt or a screw mouth
|
||||||
function launchGrain(i, x, y, vx, vy) {
|
function launchGrain(i, x, y, vx, vy) {
|
||||||
const m = mat[i], T = temp[i];
|
const m = mat[i], T = temp[i];
|
||||||
mat[i] = EMPTY; flags[i] = 0; life[i] = 0; stamp[i] = tickStamp;
|
mat[i] = EMPTY; flags[i] = 0; life[i] = 0; stamp[i] = tickStamp;
|
||||||
@@ -53,23 +73,48 @@ function launchGrain(i, x, y, vx, vy) {
|
|||||||
let filterR = [], filterL = [], filtersDirty = true;
|
let filterR = [], filterL = [], filtersDirty = true;
|
||||||
function rebuildFilters() {
|
function rebuildFilters() {
|
||||||
filtersDirty = false; filterR = []; filterL = [];
|
filtersDirty = false; filterR = []; filterL = [];
|
||||||
for (let b = 0; b < NB; b++) if (blockP[b] === BP_FILTER) (blockV[b] & 128 ? filterR : filterL).push(b);
|
for (let b = 0; b < NB; b++) if (isFilter(blockP[b])) (blockV[b] & 128 ? filterR : filterL).push(b);
|
||||||
// front of the line first, so a full run moves as one
|
// front of the line first, so a full run moves as one
|
||||||
filterR.sort((a, c) => (c % BW) - (a % BW)); filterL.sort((a, c) => (a % BW) - (c % BW));
|
filterR.sort((a, c) => (c % BW) - (a % BW)); filterL.sort((a, c) => (a % BW) - (c % BW));
|
||||||
}
|
}
|
||||||
function filterCarry(b, d) {
|
function filterCarry(b, d, carrying = true) {
|
||||||
if (blockP[b] !== BP_FILTER) return;
|
if (!isFilter(blockP[b])) return;
|
||||||
const bx = b % BW, y = ((b / BW) | 0) * BS;
|
const bx = b % BW, by = (b / BW) | 0, code = blockP[b], y = by * BS;
|
||||||
if (y < 1) return;
|
if (y < 1) return;
|
||||||
|
if (!carrying && !isSlopeBelt(code)) return;
|
||||||
const c = filterCfg.get(b), pass = c ? c.pass : null;
|
const c = filterCfg.get(b), pass = c ? c.pass : null;
|
||||||
for (let k = 0; k < BS; k++) {
|
for (let k = 0; k < BS; k++) {
|
||||||
const x = d > 0 ? bx * BS + BS - 1 - k : bx * BS + k, top = y * W + x;
|
const x = d > 0 ? bx * BS + BS - 1 - k : bx * BS + k, top = beltFaceY(code, bx, by, x) * W + x;
|
||||||
if (part[top] === P_FILTER) carryColumn(top - W, d, pass);
|
if (part[top] === P_FILTER) {
|
||||||
|
if (isSlopeBelt(code)) screenNudge(top, d, pass);
|
||||||
|
else if (carrying) carryColumn(top - W, d, pass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Belts and filters share one front-first order, including their flat/slope joins.
|
||||||
|
function carryBeltsAndFilters(belts, filters, d, bm, carrying) {
|
||||||
|
let k = 0, f = 0;
|
||||||
|
while (k < belts.length || f < filters.length) {
|
||||||
|
const fx = f < filters.length ? (filters[f] % BW) * BS + (d > 0 ? BS - 1 : 0) : 0;
|
||||||
|
if (f < filters.length && (k >= belts.length || (fx - belts[k] % W) * d > 0)) { filterCarry(filters[f++], d, carrying); continue; }
|
||||||
|
const b = belts[k++];
|
||||||
|
if (mat[b] !== bm || b < W) continue;
|
||||||
|
const x = b % W, y = (b / W) | 0, code = blockP[(y >> 2) * BW + (x >> 2)];
|
||||||
|
if (!carrying && !isSlopeBelt(code)) continue;
|
||||||
|
if (isSlopeBelt(code) && beltFaceY(code, x >> 2, y >> 2, x) !== y) continue;
|
||||||
|
if (slopeSealOwner(b) >= 0) continue;
|
||||||
|
const above = mat[b - W];
|
||||||
|
if (above === BELTL || above === BELTR || netSat(b) <= 0) continue;
|
||||||
|
if (carrying) {
|
||||||
|
if (PH[above] === PH_LIQUID) beltDrain(b);
|
||||||
|
// uphill, the cleats do the carrying
|
||||||
|
if (!isSlopeBelt(code) || !slopeUphill(code, blockV[(y >> 2) * BW + (x >> 2)])) carryColumn(b - W, d, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Change what a filter does; grains inside it that no longer pass are pushed out on top.
|
// Change what a filter does; grains inside it that no longer pass are pushed out on top.
|
||||||
function setFilter(b, cfg) {
|
function setFilter(b, cfg) {
|
||||||
if (blockP[b] !== BP_FILTER) return false;
|
if (!isFilter(blockP[b])) return false;
|
||||||
const rec = filterRec(cfg);
|
const rec = filterRec(cfg);
|
||||||
filterCfg.set(b, rec);
|
filterCfg.set(b, rec);
|
||||||
const bx = b % BW, by = (b / BW) | 0;
|
const bx = b % BW, by = (b / BW) | 0;
|
||||||
@@ -79,11 +124,13 @@ function setFilter(b, cfg) {
|
|||||||
}
|
}
|
||||||
// The run of filters in a row that touch block b and do the same thing it does
|
// The run of filters in a row that touch block b and do the same thing it does
|
||||||
function filterRun(b) {
|
function filterRun(b) {
|
||||||
if (blockP[b] !== BP_FILTER) return [];
|
if (!isFilter(blockP[b])) return [];
|
||||||
const f = filterInfo(b), dir = blockV[b] & 128, by = (b / BW) | 0, out = [b];
|
const f = filterInfo(b), dir = blockV[b] & 128, by = (b / BW) | 0, out = [b];
|
||||||
for (const s of [-1, 1]) for (let bx = (b % BW) + s; bx >= 0 && bx < BW; bx += s) {
|
for (const s of [-1, 1]) for (let bx = (b % BW) + s; bx >= 0 && bx < BW; bx += s) {
|
||||||
const c = by * BW + bx;
|
const yy = by + beltGrade(blockP[b]) * (bx - (b % BW));
|
||||||
if (blockP[c] !== BP_FILTER || (blockV[c] & 128) !== dir || !sameFilter(filterInfo(c), f)) break;
|
if (yy < 0 || yy >= BH) break;
|
||||||
|
const c = yy * BW + bx;
|
||||||
|
if (blockP[c] !== blockP[b] || (blockV[c] & 128) !== dir || !sameFilter(filterInfo(c), f)) break;
|
||||||
out.push(c);
|
out.push(c);
|
||||||
}
|
}
|
||||||
return out.sort((a, c) => a - c);
|
return out.sort((a, c) => a - c);
|
||||||
@@ -91,8 +138,8 @@ function filterRun(b) {
|
|||||||
|
|
||||||
// A cell a grain can be pushed into: empty, or liquid or gas that simply trades places with it
|
// A cell a grain can be pushed into: empty, or liquid or gas that simply trades places with it
|
||||||
function roomFor(j) { const t = mat[j]; return t === EMPTY || PH[t] === PH_GAS || PH[t] === PH_LIQUID; }
|
function roomFor(j) { const t = mat[j]; return t === EMPTY || PH[t] === PH_GAS || PH[t] === PH_LIQUID; }
|
||||||
// Cross one tube block, far lanes first; corners retain their liquid/gas swaps.
|
// Cross one screw block, far lanes first; corners retain their liquid/gas swaps.
|
||||||
function tubeSpread(c, x, y, sx, sy, intake = false) {
|
function screwSpread(c, x, y, sx, sy, intake = false) {
|
||||||
const p = part[c], bx = x >> 2, by = y >> 2;
|
const p = part[c], bx = x >> 2, by = y >> 2;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const nx = x + sx, ny = y + sy;
|
const nx = x + sx, ny = y + sy;
|
||||||
@@ -104,25 +151,62 @@ function tubeSpread(c, x, y, sx, sy, intake = false) {
|
|||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
// An empty screw cell draws in loose material from behind it or from its sides
|
// An empty screw cell draws in loose material from behind it or from its sides
|
||||||
// (not from the cell it pushes into), so a tube end sitting in a pile feeds itself.
|
// (not from the cell it pushes into), so a screw end sitting in a pile feeds itself.
|
||||||
function tubeIntake(i, p, dx, dy) {
|
function screwIntake(i, p, dx, dy) {
|
||||||
if (occ[i]) return;
|
if (occ[i]) return;
|
||||||
const x = i % W, y = (i / W) | 0;
|
const x = i % W, y = (i / W) | 0;
|
||||||
const cands = [[-dx, -dy], [dy, dx], [-dy, -dx]];
|
|
||||||
const start = (tick + i) % 3;
|
const start = (tick + i) % 3;
|
||||||
for (let c = 0; c < 3; c++) {
|
for (let c = 0; c < 3; c++) {
|
||||||
const [ox, oy] = cands[(start + c) % 3];
|
const k = (start + c) % 3, diagonal = dx && dy;
|
||||||
|
const ox = diagonal ? (k === 1 ? 0 : -dx) : k === 0 ? -dx : k === 1 ? dy : -dy;
|
||||||
|
const oy = diagonal ? (k === 0 ? 0 : -dy) : k === 0 ? -dy : k === 1 ? dx : -dx;
|
||||||
const nx = x + ox, ny = y + oy;
|
const nx = x + ox, ny = y + oy;
|
||||||
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
|
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
|
||||||
|
if (ox && oy && PH[mat[y * W + nx]] === PH_SOLID && PH[mat[ny * W + x]] === PH_SOLID) continue;
|
||||||
const j = ny * W + nx, m = mat[j];
|
const j = ny * W + nx, m = mat[j];
|
||||||
if (m === EMPTY || occ[j] || stamp[j] === tickStamp) continue;
|
if (m === EMPTY || occ[j] || stamp[j] === tickStamp) continue;
|
||||||
if (PH[m] !== PH_POWDER) continue; // the screw only takes loose material; liquids just flow through
|
if (PH[m] !== PH_POWDER) continue; // the screw only takes loose material; liquids just flow through
|
||||||
if (stamp[i] === tickStamp) return;
|
if (stamp[i] === tickStamp) return;
|
||||||
const pj = part[j];
|
const pj = part[j];
|
||||||
if (pj >= P_TU && pj <= P_TR) continue; // other tube cells move their own contents
|
if (IS_SCREW[pj]) continue; // other screw cells move their own contents
|
||||||
if (pj === P_GRATE || pj === P_FILTER) continue;
|
if (pj === P_GRATE || pj === P_FILTER) continue;
|
||||||
move(j, i, nx, ny, x, y);
|
move(j, i, nx, ny, x, y);
|
||||||
if (ox !== -dx || oy !== -dy) tubeSpread(i, x, y, -ox, -oy, true);
|
if (diagonal) screwSpread(i, x, y, 0, (y & 3) < 2 ? 1 : -1, true);
|
||||||
|
else if (ox !== -dx || oy !== -dy) screwSpread(i, x, y, -ox, -oy, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Powder may enter any screw except one pushing exactly against it.
|
||||||
|
function screwAccepts(p, dx, dy) {
|
||||||
|
return SCREW_DX[p] !== -dx || SCREW_DY[p] !== -dy;
|
||||||
|
}
|
||||||
|
// Straight mouths retain side-feeding into lifts; only diagonal transfers need a join.
|
||||||
|
function straightScrewNext(i, p) {
|
||||||
|
const dx = SCREW_DX[p], dy = SCREW_DY[p], x = i % W, y = (i / W) | 0;
|
||||||
|
const xx = x + dx, yy = y + dy, next = yy * W + xx;
|
||||||
|
if ((xx >> 2) === (x >> 2) && (yy >> 2) === (y >> 2)) return next;
|
||||||
|
const q = part[next];
|
||||||
|
if (!(SCREW_DX[q] && SCREW_DY[q])) return next;
|
||||||
|
const join = endJoin(x >> 2, y >> 2, dx + dy > 0 ? 1 : 0);
|
||||||
|
return join >= 0 && (join >> 1) === (yy >> 2) * BW + (xx >> 2) ? next : -1;
|
||||||
|
}
|
||||||
|
// Four lanes cross a block in four beats; only a mutual geometric join can receive them.
|
||||||
|
function diagonalScrewNext(i, p) {
|
||||||
|
const x = i % W, y = (i / W) | 0, dx = SCREW_DX[p], dy = SCREW_DY[p];
|
||||||
|
const xx = x + dx;
|
||||||
|
if (xx < 0 || xx >= W) return -1;
|
||||||
|
if ((xx >> 2) === (x >> 2)) return i + dx;
|
||||||
|
const yy = y + dy * BS, join = endJoin(x >> 2, y >> 2, dx > 0 ? 1 : 0);
|
||||||
|
if (join >= 0) {
|
||||||
|
const nb = join >> 1, nx = nb % BW * BS, ny = ((nb / BW) | 0) * BS;
|
||||||
|
if (blockP[nb] === BP_BELT || blockP[nb] === BP_FILTER) return (ny - 1) * W + xx;
|
||||||
|
if (blockP[nb] === BP_SCREW) {
|
||||||
|
const v = blockV[nb];
|
||||||
|
if (v >= 4) return yy * W + xx;
|
||||||
|
if (v & 1) return (ny + (y & 3)) * W + xx;
|
||||||
|
return (ny + (dy > 0 ? 0 : 3)) * W + nx + (y & 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return yy >= 0 && yy < H && !IS_SCREW[part[yy * W + xx]] ? yy * W + xx : -1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
// Dev commands are pure simulation operations; the browser owns the panel and confirmation.
|
||||||
|
let devWorld = false, devLog = [], devFree = false, devSpeed = 1, devPaused = false, devSteps = 0;
|
||||||
|
function resetDevConsole() {
|
||||||
|
devWorld = false; devLog = []; devFree = false; devSpeed = 1; devPaused = false; devSteps = 0;
|
||||||
|
}
|
||||||
|
function enableDevConsole() { devWorld = true; }
|
||||||
|
const DEV_HELP = [
|
||||||
|
'help — list commands',
|
||||||
|
'money <n> | money set <n> — add or set money; negative amounts allowed',
|
||||||
|
'debt <n> | debt clear — set or clear claim debt',
|
||||||
|
'give <material> [n] — add grains or items (default 1000); display or internal name, unique prefix',
|
||||||
|
'bit [tier] — list or set drill tiers (numbered from 0)',
|
||||||
|
'bag [tier] — list or set bag tiers (numbered from 0)',
|
||||||
|
'unlock — learn all materials and unlock progress rewards',
|
||||||
|
'tp [x y] — teleport in grains; no coordinates uses the cursor',
|
||||||
|
'free on|off — build without spending money or materials',
|
||||||
|
'speed <x> — speed multiplier (0.25, 0.5, 1, 2, 4; clamped to 0.1–8)',
|
||||||
|
'pause — toggle dev pause',
|
||||||
|
'step [n] — queue 1–10000 ticks while dev paused (default 1)',
|
||||||
|
'weather clear|rain — stop current rain or start a one-minute shower',
|
||||||
|
];
|
||||||
|
// Exact internal names win when a display name names a different material.
|
||||||
|
const DEV_MATERIALS = { EMPTY, GRASS, DIRT, LDIRT, SAND, GRAVEL, CLAY, STONE, GRANITE, BASALT, WOOD, LEAVES, COALV, COAL, CUV, CUORE, FEV, FEORE, ASH, SLAG, BRICK, GLASS, COPPER, IRON, STEEL, WATER, MAGMA, MCU, MFE, MST, MGL, STEAM, SMOKE, FIRE, BELTL, BELTR, SHAFT, GATE, HULL, SPRING, RSTONE, RGRAN, RBAS, LCLAY, LWOOD, RBRICK, CULLET, SCRCU, SCRFE, SCRST, BARCU, BARFE, BARST, ACORN, CHARCOAL, SNOW, ICE, SNV, SNORE, TIN, MSN, SCRSN, BARSN, ZNV, ZNORE, ZINC, MZN, SCRZN, BARZN, AGV, AGORE, SILVER, MAG, SCRAG, BARAG, AUV, AUORE, GOLD, MAU, SCRAU, BARAU, BRONZE, MBZ, SCRBZ, BARBZ, BRASS, MBS, SCRBS, BARBS, DOCKW, MSLAG, PIPE, BARGL, CUTST, BARSB, GRINDER, SAWB, PLANK, PRESSB, GREENB, BARBR, RACKB };
|
||||||
|
function devMaterial(name) {
|
||||||
|
name = name.toLowerCase();
|
||||||
|
const ids = Object.entries(DEV_MATERIALS);
|
||||||
|
const internal = ids.find(([key]) => key.toLowerCase() === name);
|
||||||
|
if (internal) return internal[1];
|
||||||
|
const exact = ids.filter(([, id]) => MD[id].name.toLowerCase() === name);
|
||||||
|
const matches = [...new Set((exact.length ? exact : ids.filter(([key, id]) => key.toLowerCase().startsWith(name) || MD[id].name.toLowerCase().startsWith(name))).map(([, id]) => id))];
|
||||||
|
return matches.length === 1 ? matches[0] : matches.length ? `Ambiguous material: ${matches.map(id => MD[id].name + ' (' + ids.find(([, m]) => m === id)[0] + ')').join(', ')}.` : 'Unknown material. Use a display name or internal name, such as copper ore or CUORE.';
|
||||||
|
}
|
||||||
|
function devCommand(text, cursor) {
|
||||||
|
if (typeof text !== 'string' || !text.trim()) return ['Type help to list commands.'];
|
||||||
|
text = text.trim();
|
||||||
|
const [cmd, ...args] = text.toLowerCase().split(/\s+/);
|
||||||
|
const usage = () => ['Usage: ' + DEV_HELP.find(s => s.startsWith(cmd + ' '))];
|
||||||
|
const number = s => s !== undefined && Number.isFinite(Number(s));
|
||||||
|
if (cmd === 'help' && !args.length) return DEV_HELP.slice();
|
||||||
|
if (!DEV_HELP.some(s => s.startsWith(cmd + ' '))) return ['Unknown command. Type help to list commands.'];
|
||||||
|
if (!devWorld) return ['Open the dev console and accept the world warning first.'];
|
||||||
|
let result;
|
||||||
|
switch (cmd) {
|
||||||
|
case 'money': {
|
||||||
|
const set = args[0] === 'set', value = Number(args[set ? 1 : 0]);
|
||||||
|
if (args.length !== (set ? 2 : 1) || !number(args[set ? 1 : 0]) || !number(String(set ? value : money + value))) return usage();
|
||||||
|
money = set ? value : money + value; result = 'Money: ' + fmtMoney(money); break;
|
||||||
|
}
|
||||||
|
case 'debt':
|
||||||
|
if (args.length !== 1 || (args[0] !== 'clear' && (!number(args[0]) || Number(args[0]) < 0))) return usage();
|
||||||
|
debt = args[0] === 'clear' ? 0 : Number(args[0]);
|
||||||
|
result = debt ? 'Claim debt: ' + fmtMoney(debt) : 'Claim paid off.'; break;
|
||||||
|
case 'give': {
|
||||||
|
if (!args.length) return usage();
|
||||||
|
let n = 1000;
|
||||||
|
if (number(args[args.length - 1])) n = Number(args.pop());
|
||||||
|
if (!args.length || !Number.isInteger(n) || n <= 0) return usage();
|
||||||
|
const m = devMaterial(args.join(' '));
|
||||||
|
if (typeof m === 'string') return [m];
|
||||||
|
if (m === EMPTY || PH[m] === PH_GAS || (PH[m] === PH_SOLID && !YLD[m])) return [`${MD[m].name} cannot go in the bag.`];
|
||||||
|
if (n > 2147483647 - bag[m]) return ['Too many: a material count cannot exceed 2147483647.'];
|
||||||
|
const into = PH[m] === PH_SOLID && YLD[m] ? YLD[m] : m;
|
||||||
|
bagAdd(m, n, true); result = `Added ${n} ${MD[into].name}${into !== m ? ` (as ${MD[m].name.toLowerCase()} breaks up)` : ''}.`; break;
|
||||||
|
}
|
||||||
|
case 'bit': case 'bag': {
|
||||||
|
const tiers = cmd === 'bit' ? BIT_TIERS : TANK_TIERS;
|
||||||
|
if (!args.length) return tiers.map((t, i) => `${i}: ${t.name}${t.cap ? ' (' + t.cap + ' grains)' : ''}`);
|
||||||
|
if (args.length !== 1 || !number(args[0]) || !Number.isInteger(Number(args[0]))) return usage();
|
||||||
|
const tier = Math.max(0, Math.min(tiers.length - 1, Number(args[0])));
|
||||||
|
player[cmd === 'bit' ? 'bitTier' : 'tankTier'] = tier;
|
||||||
|
result = `${cmd}: ${tier} — ${tiers[tier].name}`; break;
|
||||||
|
}
|
||||||
|
case 'unlock':
|
||||||
|
if (args.length) return usage();
|
||||||
|
seen.fill(1); player.lens = player.flask = true;
|
||||||
|
for (const m of MILESTONES) ledger[m.id] = ledger[m.id] || tick || 1;
|
||||||
|
result = 'All materials learned and progress rewards unlocked.'; break;
|
||||||
|
case 'tp': {
|
||||||
|
if (args.length !== 0 && (args.length !== 2 || !args.every(number))) return usage();
|
||||||
|
const x = args.length ? Number(args[0]) : cursor && cursor.x, y = args.length ? Number(args[1]) : cursor && cursor.y;
|
||||||
|
if (!Number.isFinite(x) || !Number.isFinite(y)) return ['No cursor position. Use tp <x> <y>.'];
|
||||||
|
bot.x = Math.max(0, Math.min(W - bot.w, x)); bot.y = Math.max(0, Math.min(H - bot.h, y)); bot.vx = bot.vy = 0;
|
||||||
|
fogLook(bot.x + bot.w / 2, bot.y + bot.h / 2);
|
||||||
|
result = `Bot: ${bot.x}, ${bot.y} grains`; break;
|
||||||
|
}
|
||||||
|
case 'free':
|
||||||
|
if (args.length !== 1 || !['on', 'off'].includes(args[0])) return usage();
|
||||||
|
devFree = args[0] === 'on'; result = 'Free building ' + args[0] + '.'; break;
|
||||||
|
case 'speed':
|
||||||
|
if (args.length !== 1 || !number(args[0])) return usage();
|
||||||
|
devSpeed = Math.max(0.1, Math.min(8, Number(args[0]))); result = 'Speed: ' + devSpeed + '×'; break;
|
||||||
|
case 'pause':
|
||||||
|
if (args.length) return usage();
|
||||||
|
devPaused = !devPaused; devSteps = 0; result = devPaused ? 'Dev paused. Use step or pause to resume.' : 'Dev pause off.'; break;
|
||||||
|
case 'step': {
|
||||||
|
const n = args.length ? Number(args[0]) : 1;
|
||||||
|
if (args.length > 1 || !Number.isInteger(n) || n < 1 || n > 10000 || devSteps + n > 10000) return usage();
|
||||||
|
if (!devPaused) return ['Use pause before step.'];
|
||||||
|
devSteps += n; result = `Queued ${n} ticks.`; break;
|
||||||
|
}
|
||||||
|
case 'weather':
|
||||||
|
if (args.length !== 1 || !['clear', 'rain'].includes(args[0])) return usage();
|
||||||
|
weather.shower = args[0] === 'rain' ? { x0: Math.max(0, bot.x - 160), x1: Math.min(W, bot.x + 160), rate: 2, ramp: 1, until: tick + 3600 } : null;
|
||||||
|
if (args[0] === 'clear') {
|
||||||
|
weather.raining.fill(0);
|
||||||
|
for (const f of flyers) if (f.rain) absorbSteam(f.x);
|
||||||
|
flyers = flyers.filter(f => !f.rain);
|
||||||
|
}
|
||||||
|
weather.next = tick + rainGap(); result = args[0] === 'rain' ? 'A one-minute shower started near the bot.' : 'Current rain cleared. Stored clouds may rain again.'; break;
|
||||||
|
default: return usage();
|
||||||
|
}
|
||||||
|
devLog.push([tick, text]); if (devLog.length > 200) devLog.shift();
|
||||||
|
return ['> ' + text, result];
|
||||||
|
}
|
||||||
+20
-4
@@ -28,7 +28,7 @@ function pulseRay(ox, oy, ang, Wd, f) {
|
|||||||
}
|
}
|
||||||
// Is a solid cell something the player built (or a part of a machine or mold)? Pulses leave those alone.
|
// Is a solid cell something the player built (or a part of a machine or mold)? Pulses leave those alone.
|
||||||
function builtCell(i, x, y) {
|
function builtCell(i, x, y) {
|
||||||
return PARTMAT[mat[i]] || part[i] || mach[i] || blockP[((y / BS) | 0) * BW + ((x / BS) | 0)] !== 0 || (!(flags[i] & F_NAT) && moldAt(x, y) !== null);
|
return PARTMAT[mat[i]] || part[i] || mach[i] || blockP[((y / BS) | 0) * BW + ((x / BS) | 0)] !== 0 || slopeSealOwner(i) >= 0 || (!(flags[i] & F_NAT) && moldAt(x, y) !== null);
|
||||||
}
|
}
|
||||||
// One press-and-hold step: fires a pulse when one is due. Returns { fired, status }.
|
// One press-and-hold step: fires a pulse when one is due. Returns { fired, status }.
|
||||||
function toolPulse(ax, ay, width, tune) {
|
function toolPulse(ax, ay, width, tune) {
|
||||||
@@ -43,7 +43,7 @@ function toolPulse(ax, ay, width, tune) {
|
|||||||
// every cell in the cone is hit, with no untouched stripes between the rays
|
// every cell in the cone is hit, with no untouched stripes between the rays
|
||||||
drillPulses.push({ ox: o.x, oy: o.y, ang, w: drillWidth, jit: rnd(), n: Wd.rays, damp: Wd.damp, en: new Float32Array(Wd.rays).fill(Wd.en), s: new Float32Array(Wd.rays), r: 0, range, tune: tune || 0 });
|
drillPulses.push({ ox: o.x, oy: o.y, ang, w: drillWidth, jit: rnd(), n: Wd.rays, damp: Wd.damp, en: new Float32Array(Wd.rays).fill(Wd.en), s: new Float32Array(Wd.rays), r: 0, range, tune: tune || 0 });
|
||||||
lastPulseTick = tick;
|
lastPulseTick = tick;
|
||||||
// say why nothing breaks: the first solid on the centre line is too hard, or out of reach
|
// say why nothing breaks: the first solid on the centre line is too hard
|
||||||
let status = '';
|
let status = '';
|
||||||
for (let k = 3; k < reach; k++) {
|
for (let k = 3; k < reach; k++) {
|
||||||
const x = Math.floor(o.x + dx / d * k), y = Math.floor(o.y + dy / d * k);
|
const x = Math.floor(o.x + dx / d * k), y = Math.floor(o.y + dy / d * k);
|
||||||
@@ -57,7 +57,6 @@ function toolPulse(ax, ay, width, tune) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!status && d > reach) status = `Out of reach; a ${Wd.name} pulse reaches ${reach} grains.`;
|
|
||||||
return { fired: true, status };
|
return { fired: true, status };
|
||||||
}
|
}
|
||||||
const THIN_BONUS = 0.75, CRUMBLE_MAX = 16;
|
const THIN_BONUS = 0.75, CRUMBLE_MAX = 16;
|
||||||
@@ -87,7 +86,8 @@ function crumbleAround(x, y) {
|
|||||||
for (const j of got) shatterCell(j, j % W, (j / W) | 0, mat[j]);
|
for (const j of got) shatterCell(j, j % W, (j / W) | 0, mat[j]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function crumbly(i) { const m = mat[i]; return PH[m] === PH_SOLID && (flags[i] & F_NAT) !== 0 && YLD[m] !== 0 && HARD[m] <= BIT_TIERS[player.bitTier].hard && !builtCell(i, i % W, (i / W) | 0); }
|
// Any solid nothing owns can be drilled: natural rock, and rock or metal that set loose in the world.
|
||||||
|
function crumbly(i) { const m = mat[i]; return PH[m] === PH_SOLID && YLD[m] !== 0 && HARD[m] <= BIT_TIERS[player.bitTier].hard && !builtCell(i, i % W, (i / W) | 0); }
|
||||||
function shatterCell(i, x, y, m) {
|
function shatterCell(i, x, y, m) {
|
||||||
const to = YLD[m];
|
const to = YLD[m];
|
||||||
setCell(i, to, temp[i], 0);
|
setCell(i, to, temp[i], 0);
|
||||||
@@ -159,3 +159,19 @@ function pulseTick() {
|
|||||||
}
|
}
|
||||||
drillPulses = drillPulses.filter(p => p.r < p.range + 3);
|
drillPulses = drillPulses.filter(p => p.r < p.range + 3);
|
||||||
}
|
}
|
||||||
|
// Resonance ping (t50): an echo from the bot that finds ore veins in the rock around it, under
|
||||||
|
// fog too. Reach grows with the bit. Only reads the world; the UI draws the echo.
|
||||||
|
const PING_VEINS = new Uint8Array(NMAT); for (const m of [COALV, CUV, FEV, SNV, ZNV, AGV, AUV]) PING_VEINS[m] = 1;
|
||||||
|
function pingRadius() { return BIT_TIERS[player.bitTier].reach * 3; }
|
||||||
|
// Natural vein cells within r of (cx, cy), and the materials found.
|
||||||
|
function resonancePing(cx, cy, r = pingRadius()) {
|
||||||
|
const cells = [], found = new Set(), r2 = r * r;
|
||||||
|
const x0 = Math.max(0, Math.floor(cx - r)), x1 = Math.min(W - 1, Math.ceil(cx + r));
|
||||||
|
const y0 = Math.max(0, Math.floor(cy - r)), y1 = Math.min(H - 1, Math.ceil(cy + r));
|
||||||
|
for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) {
|
||||||
|
const dx = x - cx, dy = y - cy, i = y * W + x, m = mat[i];
|
||||||
|
if (dx * dx + dy * dy > r2 || !PING_VEINS[m] || !(flags[i] & F_NAT)) continue;
|
||||||
|
cells.push(i); found.add(m);
|
||||||
|
}
|
||||||
|
return { cells, found: [...found], r, x: cx, y: cy };
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// Throughput for the info overlay (t49): how many grains each belt, filter and screw conveyor
|
||||||
|
// block hands on, per second of game time. Counting runs only while the overlay is on, so
|
||||||
|
// normal play pays one flag check per carried grain. Bars are not counted.
|
||||||
|
let flowOn = false, flowCount = new Uint16Array(0), flowRate = new Float32Array(0);
|
||||||
|
function setFlowCounting(on) {
|
||||||
|
flowOn = on;
|
||||||
|
if (flowCount.length !== NB) { flowCount = new Uint16Array(NB); flowRate = new Float32Array(NB); }
|
||||||
|
else if (!on) { flowCount.fill(0); flowRate.fill(0); }
|
||||||
|
}
|
||||||
|
// A grain left carrier block b across one of its edges.
|
||||||
|
function flowNote(b) { if (b >= 0 && b < flowCount.length && flowCount[b] < 65535) flowCount[b]++; }
|
||||||
|
// Count a move from cell a to cell j if it crosses into another column of blocks or row of blocks.
|
||||||
|
function flowCross(b, a, j) { if (((a % W) >> 2) !== ((j % W) >> 2) || ((a / W) >> 2) !== ((j / W) >> 2)) flowNote(b); }
|
||||||
|
// The sloped belt block carrying a grain at cell a: the slope under it in the same block column.
|
||||||
|
function flowSlopeUnder(a) {
|
||||||
|
const bx = (a % W) >> 2;
|
||||||
|
for (let by = ((a / W) | 0) >> 2, k = 0; k < 3 && by < BH; k++, by++) if (isSlopeBelt(blockP[by * BW + bx])) return by * BW + bx;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// Once a second, fold the count into a smoothed rate.
|
||||||
|
function flowTick() {
|
||||||
|
if (flowCount.length !== NB) setFlowCounting(flowOn);
|
||||||
|
if (tick % 60) return;
|
||||||
|
for (let b = 0; b < NB; b++) { if (flowCount[b] || flowRate[b]) { flowRate[b] = flowRate[b] * 0.5 + flowCount[b] * 0.5; if (flowRate[b] < 0.05) flowRate[b] = 0; } }
|
||||||
|
flowCount.fill(0);
|
||||||
|
}
|
||||||
+3
-2
@@ -130,7 +130,7 @@ function react(i, x, y, m) {
|
|||||||
if (rnd() < 0.02 - t * 0.004) {
|
if (rnd() < 0.02 - t * 0.004) {
|
||||||
const below = y < H - 1 ? PH[mat[i + W]] : PH_SOLID;
|
const below = y < H - 1 ? PH[mat[i + W]] : PH_SOLID;
|
||||||
const to = below === PH_EMPTY || below === PH_GAS ? SNOW : ICE;
|
const to = below === PH_EMPTY || below === PH_GAS ? SNOW : ICE;
|
||||||
mat[i] = to; life[i] = 0; flags[i] = fl & F_NAT; wakeXY(x, y); discover(to, x, y, WATER, 'freeze', 0);
|
mat[i] = to; life[i] = 0; flags[i] = to === ICE ? F_NAT : fl & F_NAT; wakeXY(x, y); discover(to, x, y, WATER, 'freeze', 0);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -143,7 +143,8 @@ function react(i, x, y, m) {
|
|||||||
if (L <= 0) {
|
if (L <= 0) {
|
||||||
const to = FRZTO[m];
|
const to = FRZTO[m];
|
||||||
mat[i] = to; life[i] = 0; temp[i] = fz - 1;
|
mat[i] = to; life[i] = 0; temp[i] = fz - 1;
|
||||||
flags[i] = (fl & F_NAT) | (CASTMETAL[m] ? F_CAST : 0); // natural magma freezes into natural rock
|
// metal and glass set into castings; any other melt cools into ordinary rock you can drill, wherever it came from
|
||||||
|
flags[i] = CASTMETAL[m] ? (fl & F_NAT) | F_CAST : PH[to] === PH_SOLID ? F_NAT : fl & F_NAT;
|
||||||
wakeXY(x, y); discover(to, x, y, m, 'freeze', fz);
|
wakeXY(x, y); discover(to, x, y, m, 'freeze', fz);
|
||||||
} else { life[i] = L; temp[i] = fz; }
|
} else { life[i] = L; temp[i] = fz; }
|
||||||
return;
|
return;
|
||||||
|
|||||||
+124
-38
@@ -5,7 +5,7 @@
|
|||||||
// its cells as solid (occ).
|
// its cells as solid (occ).
|
||||||
const BAR_W = 4, BAR_H = 2;
|
const BAR_W = 4, BAR_H = 2;
|
||||||
let entCurM = 0; // the bar being moved, so a filter can let its own kind of bar through
|
let entCurM = 0; // the bar being moved, so a filter can let its own kind of bar through
|
||||||
function entSolidCell(j) { const m = mat[j], p = part[j]; return occ[j] || PH[m] === PH_SOLID || PH[m] === PH_POWDER || (p !== 0 && (p <= P_GRATE || (p === P_FILTER && !filterPassAt(entCurM, j)))); }
|
function entSolidCell(j) { const m = mat[j], p = part[j]; return occ[j] || PH[m] === PH_SOLID || PH[m] === PH_POWDER || (p !== 0 && (IS_SCREW[p] || p === P_GRATE || (p === P_FILTER && !filterPassAt(entCurM, j)))); }
|
||||||
function entSupportCount(x, y) {
|
function entSupportCount(x, y) {
|
||||||
if (y + BAR_H >= H) return BAR_W;
|
if (y + BAR_H >= H) return BAR_W;
|
||||||
let n = 0;
|
let n = 0;
|
||||||
@@ -38,72 +38,157 @@ function spawnBar(m, x, y, vx, vy, T) {
|
|||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
function removeEnt(e) { entMark(e, 0); const k = ents.indexOf(e); if (k >= 0) ents.splice(k, 1); }
|
function removeEnt(e) { entMark(e, 0); const k = ents.indexOf(e); if (k >= 0) ents.splice(k, 1); }
|
||||||
// No transient tube state: even a loaded bar derives its ride from its eight cells.
|
// No transient screw state: even a loaded bar derives its ride from its eight cells.
|
||||||
function entTubeAt(x, y) {
|
function entScrewAt(x, y) {
|
||||||
if (x < 0 || y < 0 || x + BAR_W > W || y + BAR_H > H) return 0;
|
if (x < 0 || y < 0 || x + BAR_W > W || y + BAR_H > H) return 0;
|
||||||
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) {
|
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) {
|
||||||
const p = part[yy * W + xx]; if (p < P_TU || p > P_TR) return 0;
|
const p = part[yy * W + xx]; if (!IS_SCREW[p]) return 0;
|
||||||
}
|
}
|
||||||
return part[(y + 1) * W + x + 1];
|
return part[(y + 1) * W + x + 1];
|
||||||
}
|
}
|
||||||
function entTubeFree(x, y, tubeOnly = false) {
|
function entScrewFree(x, y, screwOnly = false) {
|
||||||
if (x < 0 || y < 0 || x + BAR_W > W || y + BAR_H > H) return false;
|
if (x < 0 || y < 0 || x + BAR_W > W || y + BAR_H > H) return false;
|
||||||
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) {
|
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) {
|
||||||
const j = yy * W + xx, p = part[j], tube = p >= P_TU && p <= P_TR;
|
const j = yy * W + xx, p = part[j], screw = IS_SCREW[p];
|
||||||
// gas and liquid don't stop a bar, in a tube or in the open
|
// gas and liquid don't stop a bar, in a screw or in the open
|
||||||
if (occ[j] || (mat[j] !== EMPTY && PH[mat[j]] !== PH_GAS && PH[mat[j]] !== PH_LIQUID) || (tubeOnly ? !tube : !tube && entSolidCell(j))) return false;
|
if (occ[j] || (mat[j] !== EMPTY && PH[mat[j]] !== PH_GAS && PH[mat[j]] !== PH_LIQUID) || (screwOnly ? !screw : !screw && entSolidCell(j))) return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Check the swept rectangle too: a snap must never jump through a grain or another bar.
|
// Check the swept rectangle too: a snap must never jump through a grain or another bar.
|
||||||
function entTubeMove(e, x, y, tubeOnly = false) {
|
function entScrewMove(e, x, y, screwOnly = false, mouth = false) {
|
||||||
if (!entTubeFree(x, y, tubeOnly)) return false;
|
if (!entScrewFree(x, y, screwOnly)) return false;
|
||||||
for (let yy = Math.min(e.iy, y); yy <= Math.max(e.iy, y); yy++)
|
if (mouth) {
|
||||||
for (let xx = Math.min(e.ix, x); xx <= Math.max(e.ix, x); xx++) if (!entTubeFree(xx, yy)) return false;
|
// Go around the receiving corner, checking both legs before moving.
|
||||||
|
const acrossY = y > e.iy ? e.iy : y, downX = y > e.iy ? x : e.ix;
|
||||||
|
for (let yy = Math.min(e.iy, y); yy <= Math.max(e.iy, y); yy++) if (!entScrewFree(downX, yy)) return false;
|
||||||
|
for (let xx = Math.min(e.ix, x); xx <= Math.max(e.ix, x); xx++) if (!entScrewFree(xx, acrossY)) return false;
|
||||||
|
} else for (let yy = Math.min(e.iy, y); yy <= Math.max(e.iy, y); yy++)
|
||||||
|
for (let xx = Math.min(e.ix, x); xx <= Math.max(e.ix, x); xx++) if (!entScrewFree(xx, yy)) return false;
|
||||||
e.x = e.ix = x; e.y = e.iy = y; e.vx = e.vy = 0; return true;
|
e.x = e.ix = x; e.y = e.iy = y; e.vx = e.vy = 0; return true;
|
||||||
}
|
}
|
||||||
function entTubeLane(y, iy) { return Math.max(y & ~3, Math.min((y & ~3) + 2, iy)); }
|
function entScrewLane(y, iy) { return Math.max(y & ~3, Math.min((y & ~3) + 2, iy)); }
|
||||||
function entTubeEdge(e, dx, dy, k) {
|
function entScrewEdge(e, dx, dy, k) {
|
||||||
const x = e.ix + (dx > 0 ? BAR_W : dx < 0 ? -1 : k), y = e.iy + (dy > 0 ? BAR_H : dy < 0 ? -1 : k);
|
const x = e.ix + (dx > 0 ? BAR_W : dx < 0 ? -1 : k), y = e.iy + (dy > 0 ? BAR_H : dy < 0 ? -1 : k);
|
||||||
return x < 0 || y < 0 || x >= W || y >= H ? -1 : y * W + x;
|
return x < 0 || y < 0 || x >= W || y >= H ? -1 : y * W + x;
|
||||||
}
|
}
|
||||||
function entTubeEnter(e, dx, dy) {
|
function entScrewEnter(e, dx, dy, mouth = false) {
|
||||||
for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
|
for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
|
||||||
const i = entTubeEdge(e, dx, dy, k);
|
const i = entScrewEdge(e, dx, dy, k);
|
||||||
if (i < 0) continue;
|
if (i < 0) continue;
|
||||||
const xx = i % W, yy = (i / W) | 0;
|
const xx = i % W, yy = (i / W) | 0;
|
||||||
const p = part[i]; if (p < P_TU || p > P_TR || (!dx && TDX[p]) || dx * TDX[p] + dy * TDY[p] < 0) continue;
|
const p = part[i]; if (!IS_SCREW[p] || (!dx && SCREW_DX[p]) || dx * SCREW_DX[p] + dy * SCREW_DY[p] < 0) continue;
|
||||||
// Vertical screws require the full width; horizontal screws use the nearest two-row lane.
|
// Vertical screws require the full width; horizontal screws use the nearest two-row lane.
|
||||||
const x = (xx & ~3), y = entTubeLane(yy, e.iy);
|
const x = (xx & ~3), y = entScrewLane(yy, e.iy);
|
||||||
if (entTubeMove(e, x, TDX[p] || dy ? y : e.iy, true)) return true;
|
if (entScrewMove(e, x, SCREW_DX[p] || dy ? y : e.iy, true, mouth)) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
function entTubeTick(e, p) {
|
function entScrewTick(e, p) {
|
||||||
e.vx = e.vy = 0; e.x = e.ix; e.y = e.iy;
|
e.vx = e.vy = 0; e.x = e.ix; e.y = e.iy;
|
||||||
const ci = (e.iy + 1) * W + e.ix + 1;
|
const ci = (e.iy + 1) * W + e.ix + 1;
|
||||||
if ((tick & 1) || netSat(ci) <= 0 || rnd() > netSat(ci)) return;
|
if ((tick & 1) || netSat(ci) <= 0 || rnd() > netSat(ci)) return;
|
||||||
const dx = TDX[p], dy = TDY[p];
|
const dx = SCREW_DX[p], dy = SCREW_DY[p];
|
||||||
|
if (dx && dy) { entDiagonalScrewTick(e, p); return; }
|
||||||
|
const frontX = e.ix + (dx > 0 ? BAR_W - 1 : 0), frontY = e.iy + (dy > 0 ? BAR_H - 1 : 0);
|
||||||
|
const front = frontY * W + frontX, dest = straightScrewNext(front, p);
|
||||||
|
if (dest < 0) return;
|
||||||
let x = e.ix + dx, y = e.iy + dy, next = 0, ax = 0, ay = 0;
|
let x = e.ix + dx, y = e.iy + dy, next = 0, ax = 0, ay = 0;
|
||||||
for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
|
for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
|
||||||
const i = entTubeEdge(e, dx, dy, k);
|
const i = entScrewEdge(e, dx, dy, k);
|
||||||
if (i < 0) return;
|
if (i < 0) return;
|
||||||
const xx = i % W, yy = (i / W) | 0;
|
const xx = i % W, yy = (i / W) | 0;
|
||||||
const q = part[i];
|
const q = part[i];
|
||||||
if (q >= P_TU && q <= P_TR && q !== p) { next = q; ax = xx; ay = yy; break; }
|
if (IS_SCREW[q] && q !== p) {
|
||||||
|
if (SCREW_DX[q] && SCREW_DY[q]) {
|
||||||
|
const join = endJoin(frontX >> 2, frontY >> 2, dx + dy > 0 ? 1 : 0);
|
||||||
|
if (join < 0 || (join >> 1) !== (yy >> 2) * BW + (xx >> 2)) return;
|
||||||
|
}
|
||||||
|
next = q; ax = xx; ay = yy; break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (next) {
|
if (next) {
|
||||||
if (TDX[next] === -dx && TDY[next] === -dy) return;
|
if (!screwAccepts(next, dx, dy)) return;
|
||||||
x = ax & ~3; y = entTubeLane(ay, e.iy);
|
x = ax & ~3; y = entScrewLane(ay, e.iy);
|
||||||
if (entTubeMove(e, x, y, true)) stats.moved++;
|
if (entScrewMove(e, x, y, true)) stats.moved++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (entTubeAt(x, y)) { if (entTubeMove(e, x, y, true)) stats.moved++; return; }
|
if (entScrewAt(x, y)) { if (entScrewMove(e, x, y, true)) stats.moved++; return; }
|
||||||
// Clear the mouth in one move so the next tick can derive ordinary falling from position.
|
// Clear the mouth in one move so the next tick can derive ordinary falling from position.
|
||||||
x = e.ix + dx * BAR_W; y = e.iy + dy * BAR_H;
|
x = e.ix + dx * BAR_W; y = e.iy + dy * BAR_H;
|
||||||
if (!entTubeFree(x, y) || entTubeAt(x, y)) return;
|
if (!entScrewFree(x, y) || entScrewAt(x, y)) return;
|
||||||
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) if (part[yy * W + xx]) return;
|
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) if (part[yy * W + xx]) return;
|
||||||
if (entTubeMove(e, x, y)) { e.vx = dx ? dx * 0.5 : (rnd() - 0.5) * 0.4; e.vy = dy * 0.6; stats.moved++; }
|
if (entScrewMove(e, x, y)) { e.vx = dx ? dx * 0.5 : (rnd() - 0.5) * 0.4; e.vy = dy * 0.6; stats.moved++; }
|
||||||
|
}
|
||||||
|
// A horizontal bar stays whole: one diagonal block per four screw beats.
|
||||||
|
function entDiagonalScrewTick(e, p) {
|
||||||
|
if (tick & 7) return;
|
||||||
|
const dx = SCREW_DX[p], dy = SCREW_DY[p], bx = e.ix & ~3, by = e.iy & ~3, lane = e.iy & 3;
|
||||||
|
const edge = (by + lane) * W + bx + (dx > 0 ? BS - 1 : 0);
|
||||||
|
const dest = diagonalScrewNext(edge, p);
|
||||||
|
if (dest < 0) return;
|
||||||
|
const x = (dest % W) & ~3;
|
||||||
|
let y = (dest / W) | 0;
|
||||||
|
const q = part[dest];
|
||||||
|
if (IS_SCREW[q]) {
|
||||||
|
if (SCREW_DX[q] * dx + SCREW_DY[q] * dy < 0) return;
|
||||||
|
y = Math.min((y & ~3) + BS - BAR_H, y);
|
||||||
|
if (entScrewMove(e, x, y, true)) stats.moved++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const join = endJoin(bx >> 2, by >> 2, dx > 0 ? 1 : 0);
|
||||||
|
if (join >= 0 && (blockP[join >> 1] === BP_BELT || blockP[join >> 1] === BP_FILTER)) y -= BAR_H - 1;
|
||||||
|
if (!entScrewFree(x, y)) return;
|
||||||
|
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) if (part[yy * W + xx]) return;
|
||||||
|
if (entScrewMove(e, x, y, false, dy < 0)) { e.vx = dx * 0.5; e.vy = dy * 0.6; stats.moved++; }
|
||||||
|
}
|
||||||
|
// A same-row flat belt drops its bar into a joined screw, just as grains fall off its face.
|
||||||
|
function entBeltScrewEnter(e) {
|
||||||
|
if (e.ix & 3) return false;
|
||||||
|
const y = e.iy + BAR_H;
|
||||||
|
if (y >= H || (y & 3)) return false;
|
||||||
|
const b = (y >> 2) * BW + (e.ix >> 2), c = blockP[b], v = blockV[b];
|
||||||
|
if (c !== BP_BELT && c !== BP_FILTER) return false;
|
||||||
|
if (c === BP_FILTER && filterPassAt(e.m, y * W + e.ix)) return false;
|
||||||
|
if (netSat(y * W + e.ix) <= 0) return false;
|
||||||
|
const d = c === BP_FILTER ? (v & 128 ? 1 : -1) : (v & 1 ? 1 : -1);
|
||||||
|
const join = endJoin(b % BW, (b / BW) | 0, d > 0 ? 1 : 0);
|
||||||
|
if (join < 0 || blockP[join >> 1] !== BP_SCREW) return false;
|
||||||
|
e.x = e.ix; e.y = e.iy; e.vx = e.vy = 0;
|
||||||
|
const nb = join >> 1;
|
||||||
|
if (!(tick & 1) && entScrewMove(e, nb % BW * BS, ((nb / BW) | 0) * BS, true, true)) stats.moved++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Bars stay level; their highest contact follows the face one grain per belt beat.
|
||||||
|
function entSlopeBeltTick(e) {
|
||||||
|
const bottom = e.iy + BAR_H;
|
||||||
|
if (bottom >= H) return false;
|
||||||
|
let d = 0, slope = false, screen = false;
|
||||||
|
for (let x = e.ix; x < e.ix + BAR_W; x++) {
|
||||||
|
const i = bottom * W + x, b = (bottom >> 2) * BW + (x >> 2), c = blockP[b];
|
||||||
|
if ((isBelt(c) || isFilter(c)) && entSolidCell(i)) {
|
||||||
|
d = isFilter(c) ? (blockV[b] & 128 ? 1 : -1) : (blockV[b] ? 1 : -1);
|
||||||
|
if (isSlopeBelt(c)) { slope = true; if (isFilter(c)) screen = true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!d) return false;
|
||||||
|
let face = H;
|
||||||
|
for (let x = e.ix + d; x < e.ix + d + BAR_W; x++) for (let dy = -1; dy <= 1; dy++) {
|
||||||
|
const y = bottom + dy;
|
||||||
|
if (x < 0 || x >= W || y < 0 || y >= H) continue;
|
||||||
|
const b = (y >> 2) * BW + (x >> 2), c = blockP[b];
|
||||||
|
if (!(isBelt(c) || isFilter(c)) || (isFilter(c) && filterPassAt(e.m, y * W + x))) continue;
|
||||||
|
if (beltFaceY(c, x >> 2, y >> 2, x) !== y) continue;
|
||||||
|
if (isSlopeBelt(c)) { slope = true; if (isFilter(c)) screen = true; }
|
||||||
|
face = Math.min(face, y);
|
||||||
|
}
|
||||||
|
if (!slope) return false;
|
||||||
|
e.x = e.ix; e.y = e.iy; e.vx = e.vy = 0;
|
||||||
|
if (screen ? tick % SCREEN_NUDGE_TICKS : tick & 1) return true;
|
||||||
|
const y = face === H ? e.iy : face - BAR_H, x = e.ix + d;
|
||||||
|
if (entFreeAt(x, y)) { e.vx = d * 0.5; e.vy = (y - e.iy) * 0.5; e.x = e.ix = x; e.y = e.iy = y; stats.moved++; }
|
||||||
|
else if (!entScrewEnter(e, d, 0)) entScrewEnter(e, d, Math.sign(y - e.iy), true);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
function entTick() {
|
function entTick() {
|
||||||
for (let k = ents.length - 1; k >= 0; k--) {
|
for (let k = ents.length - 1; k >= 0; k--) {
|
||||||
@@ -111,9 +196,9 @@ function entTick() {
|
|||||||
entMark(e, 0);
|
entMark(e, 0);
|
||||||
entCurM = e.m;
|
entCurM = e.m;
|
||||||
let ix = e.ix; const iy = e.iy;
|
let ix = e.ix; const iy = e.iy;
|
||||||
const tube = entTubeAt(ix, iy);
|
const screw = entScrewAt(ix, iy);
|
||||||
if (tube) entTubeTick(e, tube);
|
if (screw) entScrewTick(e, screw);
|
||||||
else {
|
else if (!entBeltScrewEnter(e) && !entSlopeBeltTick(e)) {
|
||||||
// what is underneath?
|
// what is underneath?
|
||||||
let belt = 0, bsat = 0, transport = false, support = iy + BAR_H >= H;
|
let belt = 0, bsat = 0, transport = false, support = iy + BAR_H >= H;
|
||||||
if (!support) {
|
if (!support) {
|
||||||
@@ -149,10 +234,10 @@ function entTick() {
|
|||||||
e.x += e.vx; e.y += e.vy;
|
e.x += e.vx; e.y += e.vy;
|
||||||
// step toward the new rounded position one cell at a time
|
// step toward the new rounded position one cell at a time
|
||||||
let tx = Math.round(e.x), ty = Math.round(e.y), cx = ix, cy = iy;
|
let tx = Math.round(e.x), ty = Math.round(e.y), cx = ix, cy = iy;
|
||||||
while (cx !== tx) { const s = Math.sign(tx - cx); if (entFreeAt(cx + s, cy)) cx += s; else if (belt && entTubeEnter(e, s, 0)) { cx = e.ix; cy = e.iy; e.x = cx; e.y = cy; break; } else { e.vx = 0; e.x = cx; break; } }
|
while (cx !== tx) { const s = Math.sign(tx - cx); if (entFreeAt(cx + s, cy)) cx += s; else if (belt && entScrewEnter(e, s, 0)) { cx = e.ix; cy = e.iy; e.x = cx; e.y = cy; break; } else { e.vx = 0; e.x = cx; break; } }
|
||||||
while (cy !== ty) { const s = Math.sign(ty - cy); if (entTubeAt(cx, cy)) break; if (entFreeAt(cx, cy + s)) cy += s; else if (entTubeEnter(e, 0, s)) { cx = e.ix; cy = e.iy; break; } else { e.vy = 0; e.y = cy; break; } }
|
while (cy !== ty) { const s = Math.sign(ty - cy); if (entScrewAt(cx, cy)) break; if (entFreeAt(cx, cy + s)) cy += s; else if (entScrewEnter(e, 0, s)) { cx = e.ix; cy = e.iy; break; } else { e.vy = 0; e.y = cy; break; } }
|
||||||
if (support && !belt && entTubeEnter(e, 0, 1)) { cx = e.ix; cy = e.iy; }
|
if (support && !belt && entScrewEnter(e, 0, 1)) { cx = e.ix; cy = e.iy; }
|
||||||
if (!entTubeAt(cx, cy) && !entFreeAt(cx, cy)) {
|
if (!entScrewAt(cx, cy) && !entFreeAt(cx, cy)) {
|
||||||
// something moved into us (a grain fell in): try to step up out of it
|
// something moved into us (a grain fell in): try to step up out of it
|
||||||
let freed = false;
|
let freed = false;
|
||||||
for (let u = 1; u < 6; u++) if (entFreeAt(cx, cy - u)) { cy -= u; e.y = cy; freed = true; break; }
|
for (let u = 1; u < 6; u++) if (entFreeAt(cx, cy - u)) { cy -= u; e.y = cy; freed = true; break; }
|
||||||
@@ -194,6 +279,7 @@ function entsInCircle(cx, cy, r) {
|
|||||||
|
|
||||||
// ---------- Bag ----------
|
// ---------- Bag ----------
|
||||||
function bagAdd(m, n, force) {
|
function bagAdd(m, n, force) {
|
||||||
|
if (PH[m] === PH_SOLID && YLD[m]) m = YLD[m]; // rock and metal go in as what drilling them gives
|
||||||
const room = force ? n : Math.max(0, bagCap() - bagTotal);
|
const room = force ? n : Math.max(0, bagCap() - bagTotal);
|
||||||
const k = Math.min(n, room);
|
const k = Math.min(n, room);
|
||||||
bag[m] += k; bagTotal += k;
|
bag[m] += k; bagTotal += k;
|
||||||
@@ -225,8 +311,8 @@ function giveOrDrop(m, n, x, y) {
|
|||||||
if (rest > 0) bagAdd(m, rest, true);
|
if (rest > 0) bagAdd(m, rest, true);
|
||||||
}
|
}
|
||||||
function refundAt(cost, mult, x, y) { for (const [m, n] of cost) giveOrDrop(m, n * mult, x, y); }
|
function refundAt(cost, mult, x, y) { for (const [m, n] of cost) giveOrDrop(m, n * mult, x, y); }
|
||||||
function canAfford(cost, mult) { for (const [m, n] of cost) if ((m === CASH ? money : bag[m]) < n * mult) return false; return true; }
|
function canAfford(cost, mult) { if (devFree) return true; for (const [m, n] of cost) if ((m === CASH ? money : bag[m]) < n * mult) return false; return true; }
|
||||||
function payCost(cost, mult) { for (const [m, n] of cost) { if (m === CASH) money -= n * mult; else bagTake(m, n * mult); } }
|
function payCost(cost, mult) { if (devFree) return; for (const [m, n] of cost) { if (m === CASH) money -= n * mult; else bagTake(m, n * mult); } }
|
||||||
function fmtMoney(v) { return '$' + Math.round(v).toLocaleString('en-US'); }
|
function fmtMoney(v) { return '$' + Math.round(v).toLocaleString('en-US'); }
|
||||||
function costText(cost, mult) { return cost.map(([m, n]) => { const k = n * (mult || 1); return m === CASH ? fmtMoney(k) : `${k} ${MD[m].name.toLowerCase()}${PH[m] === PH_ITEM && k !== 1 ? 's' : ''}`; }).join(' + '); }
|
function costText(cost, mult) { return cost.map(([m, n]) => { const k = n * (mult || 1); return m === CASH ? fmtMoney(k) : `${k} ${MD[m].name.toLowerCase()}${PH[m] === PH_ITEM && k !== 1 ? 's' : ''}`; }).join(' + '); }
|
||||||
function needText(cost, mult) { const c = cost.find(([m, n]) => (m === CASH ? money : bag[m]) < n * (mult || 1)); return c && c[0] === CASH ? `Costs ${fmtMoney(c[1] * (mult || 1))}; you have ${fmtMoney(money)}.` : 'Not enough ' + costText(c ? [c] : cost, mult) + '.'; }
|
function needText(cost, mult) { const c = cost.find(([m, n]) => (m === CASH ? money : bag[m]) < n * (mult || 1)); return c && c[0] === CASH ? `Costs ${fmtMoney(c[1] * (mult || 1))}; you have ${fmtMoney(money)}.` : 'Not enough ' + costText(c ? [c] : cost, mult) + '.'; }
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
// Travel-directed transport ends. Precomputed probes; world/plan queries allocate nothing.
|
||||||
|
// End indices stay geometric: left/right, or up/down for vertical screws.
|
||||||
|
const JOIN_ENDS = [[]];
|
||||||
|
for (const [kind, tx, ty] of [
|
||||||
|
...SCREW_DIRS.map(([x,y]) => [x && y ? 'screw-diagonal' : x ? 'screw-horizontal' : 'screw-vertical',x,y]),
|
||||||
|
['belt-flat',-1,0], ['belt-flat',1,0],
|
||||||
|
['belt-sloped',-1,1], ['belt-sloped',1,-1],
|
||||||
|
['belt-sloped',-1,-1], ['belt-sloped',1,1]
|
||||||
|
]) {
|
||||||
|
const outlet = (tx || ty) > 0 ? 1 : 0;
|
||||||
|
JOIN_ENDS.push([0,1].map(e => ({
|
||||||
|
dx: (e ? 1 : -1) * Math.abs(tx), dy: (e ? 1 : -1) * (tx ? ty * tx : 1),
|
||||||
|
diagonal: !!(tx && ty), kind, tx, ty, outlet: e === outlet, targets: []
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
function pieceEnds(code, v) {
|
||||||
|
if (code === BP_SCREW) return JOIN_ENDS[v >= 0 && v < 8 ? v + 1 : 0];
|
||||||
|
if (!isBelt(code) && !isFilter(code)) return JOIN_ENDS[0];
|
||||||
|
const d = isFilter(code) ? (v & 128 ? 1 : 0) : (v & 1);
|
||||||
|
return JOIN_ENDS[9 + d + (beltGrade(code) < 0 ? 2 : beltGrade(code) > 0 ? 4 : 0)];
|
||||||
|
}
|
||||||
|
for (const ends of JOIN_ENDS) for (let e = 0; e < ends.length; e++) {
|
||||||
|
const a = ends[e];
|
||||||
|
if (!a.outlet) continue;
|
||||||
|
for (const others of JOIN_ENDS) for (let n = 0; n < others.length; n++) {
|
||||||
|
const b = others[n], screw = a.kind.startsWith('screw'), otherScrew = b.kind.startsWith('screw');
|
||||||
|
if (b.outlet || a.tx * b.tx + a.ty * b.ty <= 0) continue;
|
||||||
|
if (screw !== otherScrew && (a.kind === 'screw-vertical' || b.kind === 'screw-vertical' ||
|
||||||
|
(a.diagonal && b.diagonal))) continue;
|
||||||
|
if (a.diagonal && b.diagonal && (a.tx !== b.tx || a.ty !== b.ty)) continue;
|
||||||
|
const dy = !screw && a.ty < 0 ? 0 : a.ty;
|
||||||
|
const offsets = [[a.tx, dy]];
|
||||||
|
// Grains on a flat belt below an uphill low end already reach its carrying face.
|
||||||
|
if (!screw && !otherScrew && !a.ty && b.ty < 0) offsets.push([a.tx, -1]);
|
||||||
|
for (const [dx, yy] of offsets) {
|
||||||
|
a.targets.push({ dx, dy: yy, ends: others, end: n });
|
||||||
|
b.targets.push({ dx: -dx, dy: -yy, ends, end: e });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const ends of JOIN_ENDS) for (const end of ends) {
|
||||||
|
const sameShape = t => t.ends[t.end].kind === end.kind &&
|
||||||
|
t.ends[t.end].tx === end.tx && t.ends[t.end].ty === end.ty;
|
||||||
|
end.targets.sort((a,b) => end.outlet ? Math.abs(a.dy - end.ty) - Math.abs(b.dy - end.ty) :
|
||||||
|
Number(sameShape(b)) - Number(sameShape(a)) || a.dy - b.dy || a.dx - b.dx);
|
||||||
|
}
|
||||||
|
// Result: neighbour block index * 2 + its end index; -1 means an open end.
|
||||||
|
function joinCandidate(bx, by, endIndex, planned) {
|
||||||
|
if (bx < 0 || by < 0 || bx >= BW || by >= BH) return -1;
|
||||||
|
const b = by * BW + bx, plan = planned && planned.get(b);
|
||||||
|
const ends = pieceEnds(plan ? plan[0] : blockP[b], plan ? plan[1] : blockV[b]);
|
||||||
|
const end = ends[endIndex];
|
||||||
|
if (!end) return -1;
|
||||||
|
for (let k = 0; k < end.targets.length; k++) {
|
||||||
|
const target = end.targets[k];
|
||||||
|
const x = bx + target.dx, y = by + target.dy;
|
||||||
|
if (x < 0 || y < 0 || x >= BW || y >= BH) continue;
|
||||||
|
const nb = y * BW + x, next = planned && planned.get(nb);
|
||||||
|
if (pieceEnds(next ? next[0] : blockP[nb], next ? next[1] : blockV[nb]) === target.ends)
|
||||||
|
return nb * 2 + target.end;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
function endJoin(bx, by, endIndex, planned = null) {
|
||||||
|
const join = joinCandidate(bx, by, endIndex, planned);
|
||||||
|
if (join < 0) return -1;
|
||||||
|
const nb = join >> 1;
|
||||||
|
// Competing neighbours cannot claim an end that chose a different partner.
|
||||||
|
return joinCandidate(nb % BW, (nb / BW) | 0, join & 1, planned) === (by * BW + bx) * 2 + endIndex ? join : -1;
|
||||||
|
}
|
||||||
|
function pieceJoins(bx, by, planned = null) {
|
||||||
|
return (endJoin(bx, by, 0, planned) >= 0 ? 1 : 0) | (endJoin(bx, by, 1, planned) >= 0 ? 2 : 0);
|
||||||
|
}
|
||||||
|
function endAt(b, end, planned = null) {
|
||||||
|
const p = planned && planned.get(b);
|
||||||
|
return pieceEnds(p ? p[0] : blockP[b], p ? p[1] : blockV[b])[end];
|
||||||
|
}
|
||||||
|
// A same-row join into a sloped belt's low end: the flat surface is level with the top of the
|
||||||
|
// slope's first block, so grains drop about 3 grains onto its face. Joined, but not seamless.
|
||||||
|
function joinIsDrop(bx, by, end, join, planned = null) {
|
||||||
|
if (join < 0 || (((join >> 1) / BW) | 0) !== by) return false;
|
||||||
|
const low = e => !!e && e.kind === 'belt-sloped' && e.dy > 0;
|
||||||
|
return low(endAt(by * BW + bx, end, planned)) || low(endAt(join >> 1, join & 1, planned));
|
||||||
|
}
|
||||||
|
// Rows to move a line so its first block joins: 0, -1 or +1. A line that already joins stays
|
||||||
|
// put; it moves only when one block up or down gives a seamless join involving a diagonal piece
|
||||||
|
// and every block there is free, so straight lines beside straight lines never jump.
|
||||||
|
function joinSnap(code, v, cells) {
|
||||||
|
if (!cells.length) return 0;
|
||||||
|
const joins = dy => {
|
||||||
|
const plan = new Map(cells.map(([x, y]) => [(y + dy) * BW + x, [code, v]]));
|
||||||
|
const x = cells[0][0], y = cells[0][1] + dy, own = pieceEnds(code, v);
|
||||||
|
for (let end = 0; end < own.length; end++) {
|
||||||
|
const j = endJoin(x, y, end, plan);
|
||||||
|
if (j < 0 || plan.has(j >> 1) || joinIsDrop(x, y, end, j, plan)) continue;
|
||||||
|
return own[end].diagonal || endAt(j >> 1, j & 1, plan).diagonal ? 2 : 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
if (!pieceEnds(code, v).length || joins(0)) return 0;
|
||||||
|
for (const dy of [-1, 1]) {
|
||||||
|
if (cells.every(([x, y]) => canPlaceBlock(x, y + dy, code) === '') && joins(dy) === 2) return dy;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// "Doesn't connect": an end with no join that would join seamlessly, with a diagonal piece
|
||||||
|
// involved, if its block sat one higher (-1) or lower (+1) in empty space. 0 when no near miss.
|
||||||
|
// A drop join counts as connected. The moved block is tried through a small override view of
|
||||||
|
// `planned`, so ghost maps are never copied.
|
||||||
|
const NO_PIECE = [0, 0];
|
||||||
|
function nearMiss(bx, by, end, planned = null) {
|
||||||
|
const b = by * BW + bx, p = planned && planned.get(b), code = p ? p[0] : blockP[b], v = p ? p[1] : blockV[b];
|
||||||
|
const own = pieceEnds(code, v);
|
||||||
|
if (!own[end] || endJoin(bx, by, end, planned) >= 0) return 0;
|
||||||
|
const entry = [code, v];
|
||||||
|
for (const dy of [-1, 1]) {
|
||||||
|
const y = by + dy, nb = y * BW + bx;
|
||||||
|
if (y < 0 || y >= BH || blockP[nb] || (planned && planned.get(nb))) continue;
|
||||||
|
const view = { get: k => k === b ? NO_PIECE : k === nb ? entry : planned ? planned.get(k) : undefined };
|
||||||
|
view.has = k => view.get(k) !== undefined;
|
||||||
|
const j = endJoin(bx, y, end, view);
|
||||||
|
if (j >= 0 && !joinIsDrop(bx, y, end, j, view) && (own[end].diagonal || endAt(j >> 1, j & 1, view).diagonal)) return dy;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+28
-35
@@ -1,4 +1,4 @@
|
|||||||
// Per-tick machine work: steam engines, fans, belts, filters, screw tubes (machinesTick).
|
// Per-tick machine work: steam engines, fans, belts, filters, screw conveyors (machinesTick).
|
||||||
function engineIntake(mm) {
|
function engineIntake(mm) {
|
||||||
for (const i of mm.intake) {
|
for (const i of mm.intake) {
|
||||||
if (mat[i] === STEAM) {
|
if (mat[i] === STEAM) {
|
||||||
@@ -69,7 +69,7 @@ function machinesTick() {
|
|||||||
for (let n = 1; n < nets.length; n++) {
|
for (let n = 1; n < nets.length; n++) {
|
||||||
const net = nets[n];
|
const net = nets[n];
|
||||||
let s = 0; for (const p of net.prods) s += p.out;
|
let s = 0; for (const p of net.prods) s += p.out;
|
||||||
let d = net.nb * BELT_POWER + net.nt * TUBE_POWER;
|
let d = net.nb * BELT_POWER + net.nt * SCREW_POWER;
|
||||||
for (const f of net.fans) if (f.on) d += FAN_POWER;
|
for (const f of net.fans) if (f.on) d += FAN_POWER;
|
||||||
net.supply = s; net.demand = d;
|
net.supply = s; net.demand = d;
|
||||||
net.sat = POWER_FREE ? 1 : d > 0 ? Math.min(1, s / d) : 0;
|
net.sat = POWER_FREE ? 1 : d > 0 ? Math.min(1, s / d) : 0;
|
||||||
@@ -87,33 +87,22 @@ function machinesTick() {
|
|||||||
mm.hot = Math.round(hotT);
|
mm.hot = Math.round(hotT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// belts and filters move what rests on them every other tick: half a grain per tick
|
// Belts step every other tick; sloped belts carry only their finite pockets.
|
||||||
if ((tick & 1) === 0) {
|
if (filtersDirty) rebuildFilters();
|
||||||
for (let pass = 0; pass < 2; pass++) {
|
// slopes first, so a flat belt feeding one finds the room the slope has just made
|
||||||
const list = pass ? beltR : beltL, d = pass ? 1 : -1, bm = pass ? BELTR : BELTL;
|
slopeCleatsTick();
|
||||||
for (let k = 0; k < list.length; k++) {
|
carryBeltsAndFilters(beltL, filterL, -1, BELTL, (tick & 1) === 0);
|
||||||
const b = list[k];
|
carryBeltsAndFilters(beltR, filterR, 1, BELTR, (tick & 1) === 0);
|
||||||
if (mat[b] !== bm || b < W) continue;
|
// screws: every loose grain inside steps one cell along, front of the line first, on the same
|
||||||
const above = mat[b - W];
|
|
||||||
if (above === BELTL || above === BELTR) continue; // only the top surface carries
|
|
||||||
if (netSat(b) <= 0) continue;
|
|
||||||
if (PH[above] === PH_LIQUID) beltDrain(b);
|
|
||||||
carryColumn(b - W, d, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (filtersDirty) rebuildFilters();
|
|
||||||
for (const b of filterR) filterCarry(b, 1);
|
|
||||||
for (const b of filterL) filterCarry(b, -1);
|
|
||||||
}
|
|
||||||
// tubes: every loose grain inside steps one cell along, front of the line first, on the same
|
|
||||||
// beat as belts (every other tick), so a block-wide screw carries as much as a block-deep belt
|
// beat as belts (every other tick), so a block-wide screw carries as much as a block-deep belt
|
||||||
if ((tick & 1) === 0) for (let p = P_TU; p <= P_TR; p++) {
|
if ((tick & 1) === 0) for (let p = 1; p < IS_SCREW.length; p++) {
|
||||||
const list = tubeLists[p], dx = TDX[p], dy = TDY[p], dj = dy * W + dx, flat = dy === 0;
|
if (!IS_SCREW[p]) continue;
|
||||||
|
const list = screwLists[p], dx = SCREW_DX[p], dy = SCREW_DY[p], flat = dy === 0;
|
||||||
for (let k = 0; k < list.length; k++) {
|
for (let k = 0; k < list.length; k++) {
|
||||||
const i = list[k];
|
const i = list[k];
|
||||||
if (part[i] !== p || occ[i]) continue;
|
if (part[i] !== p || occ[i]) continue;
|
||||||
const m = mat[i];
|
const m = mat[i];
|
||||||
if (m === EMPTY || PH[m] !== PH_POWDER) { if (m === EMPTY || PH[m] !== PH_SOLID) tubeIntake(i, p, dx, dy); continue; }
|
if (m === EMPTY || PH[m] !== PH_POWDER) { if (m === EMPTY || PH[m] !== PH_SOLID) screwIntake(i, p, dx, dy); continue; }
|
||||||
if (stamp[i] === tickStamp) continue;
|
if (stamp[i] === tickStamp) continue;
|
||||||
if (temp[i] > 1000 && rnd() < 0.02) {
|
if (temp[i] > 1000 && rnd() < 0.02) {
|
||||||
part[i] = 0; markPow(i, false); emit({ t: 'warn', x: i % W, y: (i / W) | 0, text: 'melted' }); wakeIdx(i);
|
part[i] = 0; markPow(i, false); emit({ t: 'warn', x: i % W, y: (i / W) | 0, text: 'melted' }); wakeIdx(i);
|
||||||
@@ -124,7 +113,7 @@ function machinesTick() {
|
|||||||
const x = i % W, y = (i / W) | 0;
|
const x = i % W, y = (i / W) | 0;
|
||||||
// a sideways screw carries its load along the bottom: grains settle into empty lanes below
|
// a sideways screw carries its load along the bottom: grains settle into empty lanes below
|
||||||
if (flat && y + 1 < H && part[i + W] === p && roomFor(i + W) && !occ[i + W]) { move(i, i + W, x, y, x, y + 1); continue; }
|
if (flat && y + 1 < H && part[i + W] === p && roomFor(i + W) && !occ[i + W]) { move(i, i + W, x, y, x, y + 1); continue; }
|
||||||
if (!flat) {
|
if (!flat && !dx) {
|
||||||
let own = 0, shifted = false;
|
let own = 0, shifted = false;
|
||||||
const end = dy < 0 ? (y & ~3) : (y | 3);
|
const end = dy < 0 ? (y & ~3) : (y | 3);
|
||||||
for (let yy = y; dy < 0 ? yy >= end : yy <= end; yy += dy) if (PH[mat[yy * W + x]] === PH_POWDER) own++;
|
for (let yy = y; dy < 0 ? yy >= end : yy <= end; yy += dy) if (PH[mat[yy * W + x]] === PH_POWDER) own++;
|
||||||
@@ -140,24 +129,28 @@ function machinesTick() {
|
|||||||
}
|
}
|
||||||
if (shifted) continue;
|
if (shifted) continue;
|
||||||
}
|
}
|
||||||
const nx = x + dx, ny = y + dy;
|
if (x + dx < 0 || x + dx >= W || y + dy < 0 || y + dy >= H) continue;
|
||||||
|
const dest = dx && dy ? diagonalScrewNext(i, p) : straightScrewNext(i, p);
|
||||||
|
if (dest < 0) continue;
|
||||||
|
const nx = dest % W, ny = (dest / W) | 0;
|
||||||
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
|
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
|
||||||
const j = i + dj;
|
const j = dest;
|
||||||
if (occ[j]) continue;
|
if (occ[j]) continue;
|
||||||
const pj = part[j], tube = pj >= P_TU && pj <= P_TR;
|
const pj = part[j], screw = IS_SCREW[pj];
|
||||||
if (tube && TDX[pj] === -dx && TDY[pj] === -dy) continue;
|
if (screw && !screwAccepts(pj, dx, dy)) continue;
|
||||||
const t = mat[j];
|
const t = mat[j];
|
||||||
if (!roomFor(j)) continue;
|
if (!roomFor(j)) continue;
|
||||||
if (pj === P_GRATE || pj === P_INTAKE || (pj === P_FILTER && !filterPassAt(m, j))) continue;
|
if (pj === P_GRATE || pj === P_INTAKE || (pj === P_FILTER && !filterPassAt(m, j))) continue;
|
||||||
if (!tube && !pj && t === EMPTY) {
|
if (!screw && !pj && t === EMPTY) {
|
||||||
// out of the mouth: the screw spits it clear, so the next grain is never blocked by the last
|
// out of the mouth: the screw spits it clear, so the next grain is never blocked by the last
|
||||||
launchGrain(i, nx, ny, dy ? (rnd() - 0.5) * (dy < 0 ? 0.9 : 0.1) : dx * (0.42 + rnd() * 0.16), dy ? dy * (0.5 + rnd() * 0.2) : rnd() * 0.08);
|
launchGrain(i, nx, ny, dy && !dx ? (rnd() - 0.5) * (dy < 0 ? 0.9 : 0.1) : dx * (0.42 + rnd() * 0.16), dy ? dy * (0.5 + rnd() * 0.2) : rnd() * 0.08);
|
||||||
stats.moved++; continue;
|
stats.moved++; if (flowOn) flowCross((y >> 2) * BW + (x >> 2), i, j); continue;
|
||||||
}
|
}
|
||||||
move(i, j, x, y, nx, ny); stats.moved++;
|
move(i, j, x, y, nx, ny); stats.moved++;
|
||||||
if (tube && pj !== p) {
|
if (flowOn) flowCross((y >> 2) * BW + (x >> 2), i, j);
|
||||||
// turning a corner: keep going across the new tube's width so every lane fills
|
if (screw && pj !== p) {
|
||||||
tubeSpread(j, nx, ny, dx, dy);
|
// turning a corner: keep going across the new screw's width so every lane fills
|
||||||
|
screwSpread(j, nx, ny, dx, dy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-6
@@ -10,7 +10,7 @@ let nextPowerBuild = 0;
|
|||||||
function rebuildPower() {
|
function rebuildPower() {
|
||||||
powerDirty = false;
|
powerDirty = false;
|
||||||
for (const i of pcells) netId[i] = 0;
|
for (const i of pcells) netId[i] = 0;
|
||||||
nets = [null]; beltL = []; beltR = []; tubeLists = [[], [], [], [], []];
|
nets = [null]; beltL = []; beltR = []; screwLists = Array.from({ length: IS_SCREW.length }, () => []);
|
||||||
const stack = [];
|
const stack = [];
|
||||||
for (const s of pcells) {
|
for (const s of pcells) {
|
||||||
if (netId[s]) continue;
|
if (netId[s]) continue;
|
||||||
@@ -23,8 +23,13 @@ function rebuildPower() {
|
|||||||
const i = stack.pop();
|
const i = stack.pop();
|
||||||
const m = mat[i], p = part[i];
|
const m = mat[i], p = part[i];
|
||||||
if (m === BELTL) { net.nb++; beltL.push(i); } else if (m === BELTR) { net.nb++; beltR.push(i); }
|
if (m === BELTL) { net.nb++; beltL.push(i); } else if (m === BELTR) { net.nb++; beltR.push(i); }
|
||||||
if (p >= P_TU && p <= P_TR) { net.nt++; tubeLists[p].push(i); }
|
if (IS_SCREW[p]) { net.nt++; screwLists[p].push(i); }
|
||||||
const x = i % W;
|
const x = i % W, y = (i / W) | 0;
|
||||||
|
// Corner-touching screws share power, without connecting unrelated diagonal machinery.
|
||||||
|
if (IS_SCREW[p] && SCREW_DX[p] && SCREW_DY[p]) for (let d = -1; d <= 1; d += 2) {
|
||||||
|
const xx = x + d * SCREW_DX[p], yy = y + d * SCREW_DY[p], j = yy * W + xx;
|
||||||
|
if (xx >= 0 && xx < W && yy >= 0 && yy < H && part[j] === p && isPow[j] && !netId[j]) { netId[j] = id; stack.push(j); }
|
||||||
|
}
|
||||||
let j;
|
let j;
|
||||||
if (x > 0) { j = i - 1; if (isPow[j] && !netId[j]) { netId[j] = id; stack.push(j); } }
|
if (x > 0) { j = i - 1; if (isPow[j] && !netId[j]) { netId[j] = id; stack.push(j); } }
|
||||||
if (x < W - 1) { j = i + 1; if (isPow[j] && !netId[j]) { netId[j] = id; stack.push(j); } }
|
if (x < W - 1) { j = i + 1; if (isPow[j] && !netId[j]) { netId[j] = id; stack.push(j); } }
|
||||||
@@ -39,9 +44,13 @@ function rebuildPower() {
|
|||||||
if (!net) continue;
|
if (!net) continue;
|
||||||
if (mm.type === 'fan') net.fans.push(mm); else if (mm.type === 'wheel' || mm.type === 'engine') net.prods.push(mm);
|
if (mm.type === 'fan') net.fans.push(mm); else if (mm.type === 'wheel' || mm.type === 'engine') net.prods.push(mm);
|
||||||
}
|
}
|
||||||
beltL.sort((a, b) => a - b); beltR.sort((a, b) => b - a);
|
beltL.sort((a, b) => (a % W) - (b % W) || a - b); beltR.sort((a, b) => (b % W) - (a % W) || b - a);
|
||||||
tubeLists[P_TU].sort((a, b) => a - b); tubeLists[P_TD].sort((a, b) => b - a);
|
for (const p of SCREW_PARTS) {
|
||||||
tubeLists[P_TL].sort((a, b) => a - b); tubeLists[P_TR].sort((a, b) => b - a);
|
screwLists[p].sort((a, b) => {
|
||||||
|
if (SCREW_DX[p] && SCREW_DY[p]) return ((b % W) - (a % W)) * SCREW_DX[p];
|
||||||
|
return (SCREW_DY[p] || SCREW_DX[p]) < 0 ? a - b : b - a;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function netSat(i) { if (POWER_FREE) return 1; const n = nets[netId[i]]; return n ? n.sat : 0; }
|
function netSat(i) { if (POWER_FREE) return 1; const n = nets[netId[i]]; return n ? n.sat : 0; }
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,13 @@ const SOLIDMETAL = new Uint8Array(NMAT); for (const m of [COPPER, IRON, STEEL, T
|
|||||||
|
|
||||||
// ---------- Flags & parts ----------
|
// ---------- Flags & parts ----------
|
||||||
const F_NAT = 1, F_CAST = 2, F_BURN = 4;
|
const F_NAT = 1, F_CAST = 2, F_BURN = 4;
|
||||||
const P_NONE = 0, P_TU = 1, P_TD = 2, P_TL = 3, P_TR = 4, P_GRATE = 5, P_GOPEN = 6, P_WHEEL = 7, P_INTAKE = 8, P_FILTER = 9, P_PIPE = 10;
|
const P_NONE = 0, P_SCREW_U = 1, P_SCREW_D = 2, P_SCREW_L = 3, P_SCREW_R = 4, P_GRATE = 5, P_GOPEN = 6, P_WHEEL = 7, P_INTAKE = 8, P_FILTER = 9, P_PIPE = 10, P_SCREW_UR = 11, P_SCREW_DR = 12, P_SCREW_DL = 13, P_SCREW_UL = 14;
|
||||||
const TDX = [0, 0, 0, -1, 1], TDY = [0, -1, 1, 0, 0];
|
const SCREW_PARTS = [P_SCREW_U, P_SCREW_R, P_SCREW_D, P_SCREW_L, P_SCREW_UR, P_SCREW_DR, P_SCREW_DL, P_SCREW_UL];
|
||||||
|
const SCREW_DIRS = [[0, -1], [1, 0], [0, 1], [-1, 0], [1, -1], [1, 1], [-1, 1], [-1, -1]];
|
||||||
|
const IS_SCREW = new Uint8Array(15), SCREW_DX = new Int8Array(15), SCREW_DY = new Int8Array(15);
|
||||||
|
for (let v = 0; v < SCREW_PARTS.length; v++) {
|
||||||
|
const p = SCREW_PARTS[v]; IS_SCREW[p] = 1; SCREW_DX[p] = SCREW_DIRS[v][0]; SCREW_DY[p] = SCREW_DIRS[v][1];
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- Tool tiers ----------
|
// ---------- Tool tiers ----------
|
||||||
// reach: how far from the bot the drill works. Harder rock takes longer, and
|
// reach: how far from the bot the drill works. Harder rock takes longer, and
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ def(SLAG, { name: 'Slag', ph: PH_POWDER, col: [76, 66, 64], v: 34, dens: 2.4, k:
|
|||||||
def(MSLAG, { name: 'Molten slag', ph: PH_LIQUID, col: [232, 112, 40], v: 16, dens: 3.6, k: 0.02, cap: 1.2, hard: 0, freeze: 1000, freezeTo: SLAG, fluid: 0.5, spread: 10, liqT: 1150, desc: 'Smelting waste. Trickles down through the charge but is too thick to pass a grate, so it pools there and sets into slag. Knocking out the mold below rakes it away.' });
|
def(MSLAG, { name: 'Molten slag', ph: PH_LIQUID, col: [232, 112, 40], v: 16, dens: 3.6, k: 0.02, cap: 1.2, hard: 0, freeze: 1000, freezeTo: SLAG, fluid: 0.5, spread: 10, liqT: 1150, desc: 'Smelting waste. Trickles down through the charge but is too thick to pass a grate, so it pools there and sets into slag. Knocking out the mold below rakes it away.' });
|
||||||
def(BRICK, { name: 'Brick', col: [168, 76, 56], v: 16, k: 0.004, hard: 3, melt: 1750, meltTo: MAGMA, yld: RBRICK, desc: 'Fired clay. Insulates and survives 1750°C.' });
|
def(BRICK, { name: 'Brick', col: [168, 76, 56], v: 16, k: 0.004, hard: 3, melt: 1750, meltTo: MAGMA, yld: RBRICK, desc: 'Fired clay. Insulates and survives 1750°C.' });
|
||||||
def(GLASS, { name: 'Glass', col: [178, 216, 224], v: 6, k: 0.006, hard: 2, melt: 1300, meltTo: MGL, alpha: 0.45, yld: CULLET, desc: 'Clear. Lets light through.' });
|
def(GLASS, { name: 'Glass', col: [178, 216, 224], v: 6, k: 0.006, hard: 2, melt: 1300, meltTo: MGL, alpha: 0.45, yld: CULLET, desc: 'Clear. Lets light through.' });
|
||||||
def(COPPER, { name: 'Copper', col: [200, 122, 76], v: 8, k: 0.04, cap: 1.5, hard: 4, melt: 1085, meltTo: MCU, yld: SCRCU, desc: 'Conducts heat well. Belts, tubes, fans, engines.' });
|
def(COPPER, { name: 'Copper', col: [200, 122, 76], v: 8, k: 0.04, cap: 1.5, hard: 4, melt: 1085, meltTo: MCU, yld: SCRCU, desc: 'Conducts heat well. Belts, screw conveyors, fans, engines.' });
|
||||||
def(IRON, { name: 'Iron', col: [128, 131, 138], v: 8, k: 0.035, cap: 1.5, hard: 5, melt: 1538, meltTo: MFE, yld: SCRFE, desc: 'Strong metal. Melts at 1538°C.' });
|
def(IRON, { name: 'Iron', col: [128, 131, 138], v: 8, k: 0.035, cap: 1.5, hard: 5, melt: 1538, meltTo: MFE, yld: SCRFE, desc: 'Strong metal. Melts at 1538°C.' });
|
||||||
def(STEEL, { name: 'Steel', col: [172, 180, 192], v: 6, k: 0.035, cap: 1.5, hard: 6, melt: 1450, meltTo: MST, yld: SCRST, desc: 'Iron refined by blowing air through it while molten. The best bit material.' });
|
def(STEEL, { name: 'Steel', col: [172, 180, 192], v: 6, k: 0.035, cap: 1.5, hard: 6, melt: 1450, meltTo: MST, yld: SCRST, desc: 'Iron refined by blowing air through it while molten. The best bit material.' });
|
||||||
def(WATER, { name: 'Water', ph: PH_LIQUID, col: [44, 100, 194], v: 6, dens: 1, k: 0.02, cap: 4, hard: 0, boil: 100, boilTo: STEAM, fluid: 1, spread: 48, alpha: 0.78, liqT: 20, bag: true, desc: 'Boils at 100°C. Steam that rises into the open sky comes back down as rain.' });
|
def(WATER, { name: 'Water', ph: PH_LIQUID, col: [44, 100, 194], v: 6, dens: 1, k: 0.02, cap: 4, hard: 0, boil: 100, boilTo: STEAM, fluid: 1, spread: 48, alpha: 0.78, liqT: 20, bag: true, desc: 'Boils at 100°C. Steam that rises into the open sky comes back down as rain.' });
|
||||||
@@ -77,7 +77,7 @@ def(CULLET, { name: 'Glass shards', ph: PH_POWDER, col: [196, 228, 236], v: 22,
|
|||||||
def(SCRCU, { name: 'Copper scrap', ph: PH_POWDER, col: [192, 118, 74], v: 36, dens: 5, k: 0.035, cap: 1.5, hard: 0, stick: 0.7, melt: 1085, meltTo: MCU, bag: true, desc: 'Copper that set outside a mold. Melt it again and cast it.' });
|
def(SCRCU, { name: 'Copper scrap', ph: PH_POWDER, col: [192, 118, 74], v: 36, dens: 5, k: 0.035, cap: 1.5, hard: 0, stick: 0.7, melt: 1085, meltTo: MCU, bag: true, desc: 'Copper that set outside a mold. Melt it again and cast it.' });
|
||||||
def(SCRFE, { name: 'Iron scrap', ph: PH_POWDER, col: [124, 126, 132], v: 30, dens: 5, k: 0.03, cap: 1.5, hard: 0, stick: 0.7, melt: 1538, meltTo: MFE, bag: true, desc: 'Iron that set outside a mold. Melt it again and cast it.' });
|
def(SCRFE, { name: 'Iron scrap', ph: PH_POWDER, col: [124, 126, 132], v: 30, dens: 5, k: 0.03, cap: 1.5, hard: 0, stick: 0.7, melt: 1538, meltTo: MFE, bag: true, desc: 'Iron that set outside a mold. Melt it again and cast it.' });
|
||||||
def(SCRST, { name: 'Steel scrap', ph: PH_POWDER, col: [168, 176, 188], v: 28, dens: 5, k: 0.03, cap: 1.5, hard: 0, stick: 0.7, melt: 1450, meltTo: MST, bag: true, desc: 'Steel that set outside a mold. Melt it again and cast it.' });
|
def(SCRST, { name: 'Steel scrap', ph: PH_POWDER, col: [168, 176, 188], v: 28, dens: 5, k: 0.03, cap: 1.5, hard: 0, stick: 0.7, melt: 1450, meltTo: MST, bag: true, desc: 'Steel that set outside a mold. Melt it again and cast it.' });
|
||||||
def(BARCU, { name: 'Copper bar', ph: PH_ITEM, col: [214, 132, 82], v: 0, dens: 8.9, k: 0, hard: 0, melt: 1085, meltTo: MCU, bag: true, desc: 'A cast bar. Belts, screw tubes, fans and engines are made from these.' });
|
def(BARCU, { name: 'Copper bar', ph: PH_ITEM, col: [214, 132, 82], v: 0, dens: 8.9, k: 0, hard: 0, melt: 1085, meltTo: MCU, bag: true, desc: 'A cast bar. Belts, screw conveyors, fans and engines are made from these.' });
|
||||||
def(BARFE, { name: 'Iron bar', ph: PH_ITEM, col: [140, 143, 150], v: 0, dens: 7.9, k: 0, hard: 0, melt: 1538, meltTo: MFE, bag: true, desc: 'A cast bar of iron.' });
|
def(BARFE, { name: 'Iron bar', ph: PH_ITEM, col: [140, 143, 150], v: 0, dens: 7.9, k: 0, hard: 0, melt: 1538, meltTo: MFE, bag: true, desc: 'A cast bar of iron.' });
|
||||||
def(BARST, { name: 'Steel bar', ph: PH_ITEM, col: [184, 192, 204], v: 0, dens: 7.9, k: 0, hard: 0, melt: 1450, meltTo: MST, bag: true, desc: 'A cast bar of steel.' });
|
def(BARST, { name: 'Steel bar', ph: PH_ITEM, col: [184, 192, 204], v: 0, dens: 7.9, k: 0, hard: 0, melt: 1450, meltTo: MST, bag: true, desc: 'A cast bar of steel.' });
|
||||||
def(ACORN, { name: 'Acorn', ph: PH_POWDER, col: [150, 104, 56], v: 22, dens: 0.9, k: 0.006, hard: 0, stick: 0.6, ign: 300, flame: 700, flameF: 900, fuel: 30, burnTo: ASH, ashP: 0.3, bag: true, desc: 'A tree seed. Left on dirt or grass under open sky, it sprouts and grows into a tree.' });
|
def(ACORN, { name: 'Acorn', ph: PH_POWDER, col: [150, 104, 56], v: 22, dens: 0.9, k: 0.006, hard: 0, stick: 0.6, ign: 300, flame: 700, flameF: 900, fuel: 30, burnTo: ASH, ashP: 0.3, bag: true, desc: 'A tree seed. Left on dirt or grass under open sky, it sprouts and grows into a tree.' });
|
||||||
|
|||||||
+46
-10
@@ -4,8 +4,36 @@
|
|||||||
// Everything you build snaps to a grid of 4x4-grain blocks. The world itself
|
// Everything you build snaps to a grid of 4x4-grain blocks. The world itself
|
||||||
// stays grain-level; only built pieces snap.
|
// stays grain-level; only built pieces snap.
|
||||||
const BS = 4;
|
const BS = 4;
|
||||||
const BP_WALL = 1, BP_BELT = 2, BP_TUBE = 3, BP_SHAFT = 4, BP_GRATE = 5, BP_GATE = 6, BP_FILTER = 7, BP_PIPE = 8, BP_PUMP = 9, BP_OUTLET = 10, BP_GRIND = 11, BP_SAW = 12, BP_PRESS = 13, BP_RACK = 14;
|
const BP_WALL = 1, BP_BELT = 2, BP_SCREW = 3, BP_SHAFT = 4, BP_GRATE = 5, BP_GATE = 6, BP_FILTER = 7, BP_PIPE = 8, BP_PUMP = 9, BP_OUTLET = 10, BP_GRIND = 11, BP_SAW = 12, BP_PRESS = 13, BP_RACK = 14;
|
||||||
|
const BP_SLOPE_BL = 15, BP_SLOPE_BR = 16, BP_SLOPE_TL = 17, BP_SLOPE_TR = 18;
|
||||||
|
// The shape is independent of the direction: reversing a belt never flips its face.
|
||||||
|
const BP_BELT_UP = 19, BP_BELT_DOWN = 20, BP_FILTER_UP = 21, BP_FILTER_DOWN = 22;
|
||||||
|
function isSlopeBelt(c) { return c >= BP_BELT_UP && c <= BP_FILTER_DOWN; }
|
||||||
|
function isFilter(c) { return c === BP_FILTER || c === BP_FILTER_UP || c === BP_FILTER_DOWN; }
|
||||||
|
function isBelt(c) { return c === BP_BELT || c === BP_BELT_UP || c === BP_BELT_DOWN; }
|
||||||
|
function beltGrade(c) { return c === BP_BELT_UP || c === BP_FILTER_UP ? -1 : c === BP_BELT_DOWN || c === BP_FILTER_DOWN ? 1 : 0; }
|
||||||
|
function slopeShape(c) { return isSlopeBelt(c) ? (beltGrade(c) < 0 ? BP_SLOPE_BR : BP_SLOPE_BL) : c; }
|
||||||
|
function beltFaceY(c, bx, by, x) { return by * BS + (beltGrade(c) < 0 ? 3 - (x - bx * BS) : beltGrade(c) > 0 ? x - bx * BS : 0); }
|
||||||
|
function filterVariant(c, v) { return beltGrade(c) ? (beltGrade(c) > 0 ? 128 : 0) : v & 128; }
|
||||||
|
function beltDirection(c, v) {
|
||||||
|
const d = isFilter(c) ? (filterVariant(c, v) & 128 ? 1 : -1) : (v ? 1 : -1), g = beltGrade(c);
|
||||||
|
return g ? (g * d < 0 ? 'up-' : 'down-') + (d > 0 ? 'right' : 'left') : d > 0 ? 'right' : 'left';
|
||||||
|
}
|
||||||
|
function isSlope(c) { return c >= BP_SLOPE_BL && c <= BP_SLOPE_TR; }
|
||||||
|
function isWall(c) { return c === BP_WALL || isSlope(c); }
|
||||||
|
function slopeCell(c, x, y) {
|
||||||
|
c = slopeShape(c);
|
||||||
|
if (c === BP_SLOPE_BR || c === BP_SLOPE_TR) x = 3 - x;
|
||||||
|
if (c === BP_SLOPE_TL || c === BP_SLOPE_TR) y = 3 - y;
|
||||||
|
return y >= x;
|
||||||
|
}
|
||||||
|
function slopeCode(dx, dy, orientation) {
|
||||||
|
if (!diagonalDrag(dx, dy)) return BP_SLOPE_BL + orientation;
|
||||||
|
return dx * dy > 0 ? (orientation >= 2 ? BP_SLOPE_TR : BP_SLOPE_BL) : (orientation >= 2 ? BP_SLOPE_TL : BP_SLOPE_BR);
|
||||||
|
}
|
||||||
function isPipeCode(c) { return c === BP_PIPE || c === BP_PUMP || c === BP_OUTLET; }
|
function isPipeCode(c) { return c === BP_PIPE || c === BP_PUMP || c === BP_OUTLET; }
|
||||||
|
// pieces that carry something along a run: a Shift-drag still draws a line, never a filled rectangle
|
||||||
|
function lineOnly(c) { return isBelt(c) || isFilter(c) || c === BP_SCREW || c === BP_SHAFT || isPipeCode(c); }
|
||||||
// Filters: each filter block has a set of materials and a mode. In drop mode (inv 0)
|
// Filters: each filter block has a set of materials and a mode. In drop mode (inv 0)
|
||||||
// the chosen materials fall through and everything else rides along on top; in carry
|
// the chosen materials fall through and everything else rides along on top; in carry
|
||||||
// mode (inv 1) the chosen ones ride along and everything else falls through. The block
|
// mode (inv 1) the chosen ones ride along and everything else falls through. The block
|
||||||
@@ -18,7 +46,12 @@ function filterRec(c) {
|
|||||||
for (let m = 1; m < NMAT; m++) if (filterable(m)) pass[m] = (k.m.includes(m) ? 1 : 0) ^ k.inv;
|
for (let m = 1; m < NMAT; m++) if (filterable(m)) pass[m] = (k.m.includes(m) ? 1 : 0) ^ k.inv;
|
||||||
return { m: k.m, inv: k.inv, pass };
|
return { m: k.m, inv: k.inv, pass };
|
||||||
}
|
}
|
||||||
function filterPassAt(m, j) { const c = filterCfg.get((((j / W) | 0) >> 2) * BW + ((j % W) >> 2)); return c ? c.pass[m] : 0; }
|
function filterPassAt(m, j) {
|
||||||
|
let b = (((j / W) | 0) >> 2) * BW + ((j % W) >> 2);
|
||||||
|
if (!isFilter(blockP[b])) { const owner = slopeSealOwner(j); if (owner >= 0) b = owner; }
|
||||||
|
const c = filterCfg.get(b);
|
||||||
|
return c ? c.pass[m] : 0;
|
||||||
|
}
|
||||||
function filterInfo(b) { const c = filterCfg.get(b); return c ? { m: c.m.slice(), inv: c.inv } : { m: [], inv: 0 }; }
|
function filterInfo(b) { const c = filterCfg.get(b); return c ? { m: c.m.slice(), inv: c.inv } : { m: [], inv: 0 }; }
|
||||||
function sameFilter(a, b) { return a.inv === b.inv && a.m.length === b.m.length && a.m.every((m, k) => m === b.m[k]); }
|
function sameFilter(a, b) { return a.inv === b.inv && a.m.length === b.m.length && a.m.every((m, k) => m === b.m[k]); }
|
||||||
// loose material -> the solid it becomes in a wall block
|
// loose material -> the solid it becomes in a wall block
|
||||||
@@ -38,13 +71,13 @@ const POWER_FREE = true;
|
|||||||
const CASH = 255; // stands for money in cost lists
|
const CASH = 255; // stands for money in cost lists
|
||||||
const PIECES = [
|
const PIECES = [
|
||||||
{ id: 'wall', code: BP_WALL, name: 'Wall block', kind: 'block', variants: 'wall',
|
{ id: 'wall', code: BP_WALL, name: 'Wall block', kind: 'block', variants: 'wall',
|
||||||
desc: 'A solid block. R picks what it is made of: 16 grains of rubble, lumps or chunks, or 2 bars of metal.' },
|
desc: 'A solid block. Diagonal drags make a sloped face; T flips it. For one block, T cycles square and four slope corners. R picks the material: a square costs 16 grains or 2 bars; a slope costs 8 grains or 1 bar.' },
|
||||||
{ id: 'belt', code: BP_BELT, name: 'Belt', kind: 'block', cost: [[CASH, 12]], dirs: ['left', 'right'],
|
{ id: 'belt', code: BP_BELT, name: 'Belt', kind: 'block', cost: [[CASH, 12]], dirs: ['left', 'right'],
|
||||||
desc: 'Carries loose grains and bars resting on top of it, left or right, in a layer up to a block deep. Water and other liquids drain through it.' },
|
desc: 'Carries loose grains and bars resting on top of it, left or right, in a layer up to a block deep. Water and other liquids drain through it. Drag diagonally for a sidewall belt. Moving cleats lift pockets of grains; extra grains roll back.' },
|
||||||
{ id: 'tube', code: BP_TUBE, name: 'Screw tube', kind: 'block', cost: [[CASH, 25]], dirs: ['up', 'right', 'down', 'left'],
|
{ id: 'screw', code: BP_SCREW, name: 'Screw conveyor', kind: 'block', cost: [[CASH, 25]], dirs: ['up', 'right', 'down', 'left', 'up-right', 'down-right', 'down-left', 'up-left'],
|
||||||
desc: 'A turning screw that draws in loose grains and whole bars and pushes them along, four grains wide. Water and other liquids run straight through it; the screw never carries them. Stack them pointing up to lift ore out of a mine, and turn the top one sideways to unload. Copper softens around 1000°C.' },
|
desc: 'A turning screw inside a tube that draws in loose grains and whole bars and pushes them along, four grains wide. Water and other liquids run straight through it; the screw never carries them. Drag a line to set its direction, including diagonals. R turns a single screw conveyor. Join either end to screw conveyors or belts. Copper softens around 1000°C.' },
|
||||||
{ id: 'filter', code: BP_FILTER, name: 'Filter', kind: 'block', cost: [[CASH, 40]], dirs: ['left', 'right'], filter: true,
|
{ id: 'filter', code: BP_FILTER, name: 'Filter', kind: 'block', cost: [[CASH, 40]], dirs: ['left', 'right'], filter: true,
|
||||||
desc: 'A stretch of belt with a mesh in it. Put it in a line of belts and it joins them as one belt. Pick materials and a mode: drop them through and carry everything else along, or carry them and drop the rest. Liquids drain through.' },
|
desc: 'A shaking mesh that joins belts. Pick materials and a mode: drop them through and carry everything else along, or carry them and drop the rest. Liquids drain through. Drag diagonally for a downhill screen. Grains slide down while it shakes; R keeps a picked slope downhill.' },
|
||||||
{ id: 'shaft', code: BP_SHAFT, name: 'Shaft', kind: 'block', cost: [[LWOOD, 8]], hidden: POWER_FREE,
|
{ id: 'shaft', code: BP_SHAFT, name: 'Shaft', kind: 'block', cost: [[LWOOD, 8]], hidden: POWER_FREE,
|
||||||
desc: 'Carries power between machines. Wooden, so keep it away from fire.' },
|
desc: 'Carries power between machines. Wooden, so keep it away from fire.' },
|
||||||
{ id: 'grate', code: BP_GRATE, name: 'Grate', kind: 'block', cost: [[LCLAY, 8]],
|
{ id: 'grate', code: BP_GRATE, name: 'Grate', kind: 'block', cost: [[LCLAY, 8]],
|
||||||
@@ -70,7 +103,7 @@ const PIECES = [
|
|||||||
{ id: 'fan', name: 'Fan', kind: 'machine', bw: 1, bh: 1, cost: [[CASH, 60]], dirs: ['right', 'down', 'left', 'up'],
|
{ id: 'fan', name: 'Fan', kind: 'machine', bw: 1, bh: 1, cost: [[CASH, 60]], dirs: ['right', 'down', 'left', 'up'],
|
||||||
desc: 'Blows gas and forces air into fires, making them far hotter: its stream reaches 28 grains and passes through grates but not walls. Its copper body melts at 1085°C, so keep it outside the furnace, a block clear of the wall, blowing in through a grate. Press F to switch it on or off.' },
|
desc: 'Blows gas and forces air into fires, making them far hotter: its stream reaches 28 grains and passes through grates but not walls. Its copper body melts at 1085°C, so keep it outside the furnace, a block clear of the wall, blowing in through a grate. Press F to switch it on or off.' },
|
||||||
{ id: 'silo', name: 'Silo', kind: 'machine', bw: 3, bh: 6, cost: [[CASH, 180]],
|
{ id: 'silo', name: 'Silo', kind: 'machine', bw: 3, bh: 6, cost: [[CASH, 180]],
|
||||||
desc: 'A steel bin on legs for one loose material: up to 4,000 grains go in through the hatch in its roof (drop them in from a belt or tube). Its gauge shows how full it is. Press F to open the gate in its cone and let it out underneath, about 60 grains a second; F again shuts it. The first material in decides what it holds until it is empty.' },
|
desc: 'A steel bin on legs for one loose material: up to 4,000 grains go in through the hatch in its roof (drop them in from a belt or screw conveyor). Its gauge shows how full it is. Press F to open the gate in its cone and let it out underneath, about 60 grains a second; F again shuts it. The first material in decides what it holds until it is empty.' },
|
||||||
{ id: 'engine', name: 'Steam engine', kind: 'machine', bw: 3, bh: 2, cost: [[BARCU, 10]], dirs: ['exhaust right', 'exhaust left'], hidden: POWER_FREE,
|
{ id: 'engine', name: 'Steam engine', kind: 'machine', bw: 3, bh: 2, cost: [[BARCU, 10]], dirs: ['exhaust right', 'exhaust left'], hidden: POWER_FREE,
|
||||||
desc: 'Steam rising into the intake slots on its underside drives it. Condensate drips out the exhaust side.' },
|
desc: 'Steam rising into the intake slots on its underside drives it. Condensate drips out the exhaust side.' },
|
||||||
{ id: 'dock', name: 'Trade dock', kind: 'machine', bw: 6, bh: 4, cost: [], hidden: true, fixed: true,
|
{ id: 'dock', name: 'Trade dock', kind: 'machine', bw: 6, bh: 4, cost: [], hidden: true, fixed: true,
|
||||||
@@ -84,8 +117,11 @@ const PIECES = [
|
|||||||
];
|
];
|
||||||
const PIECE_BY_ID = {}, PIECE_BY_CODE = [];
|
const PIECE_BY_ID = {}, PIECE_BY_CODE = [];
|
||||||
PIECES.forEach((p, i) => { p.index = i; PIECE_BY_ID[p.id] = p; if (p.code) PIECE_BY_CODE[p.code] = p; if (p.kind === 'machine') { p.w = p.bw * BS; p.h = p.bh * BS; } });
|
PIECES.forEach((p, i) => { p.index = i; PIECE_BY_ID[p.id] = p; if (p.code) PIECE_BY_CODE[p.code] = p; if (p.kind === 'machine') { p.w = p.bw * BS; p.h = p.bh * BS; } });
|
||||||
// power per grain of belt / tube (a block is 16 grains)
|
for (let c = BP_SLOPE_BL; c <= BP_SLOPE_TR; c++) PIECE_BY_CODE[c] = PIECE_BY_ID.wall;
|
||||||
const BELT_POWER = 0.03, TUBE_POWER = 0.125, FAN_POWER = 6;
|
for (const c of [BP_BELT_UP, BP_BELT_DOWN]) PIECE_BY_CODE[c] = PIECE_BY_ID.belt;
|
||||||
|
for (const c of [BP_FILTER_UP, BP_FILTER_DOWN]) PIECE_BY_CODE[c] = PIECE_BY_ID.filter;
|
||||||
|
// power per grain of belt / screw (a block is 16 grains)
|
||||||
|
const BELT_POWER = 0.03, SCREW_POWER = 0.125, FAN_POWER = 6;
|
||||||
const WHEEL_K = 0.32, ENGINE_K = 40;
|
const WHEEL_K = 0.32, ENGINE_K = 40;
|
||||||
|
|
||||||
const MOLDS = {
|
const MOLDS = {
|
||||||
|
|||||||
+2
-2
@@ -30,14 +30,14 @@ const MILESTONES = [
|
|||||||
{ id: 'copper', title: 'Molten copper', msg: 'Molten copper sinks to the bottom of the charge. Get it into a mold before it sets.', hint: 'Copper ore melts at 1085°C. An open fire loses too much heat, so wall coal and ore in with blocks and leave a small vent at the top for smoke. Mix ore and coal about half and half: Ctrl+click coal and copper ore in the bag to blow a mix. Right click blows an even mix.' },
|
{ id: 'copper', title: 'Molten copper', msg: 'Molten copper sinks to the bottom of the charge. Get it into a mold before it sets.', hint: 'Copper ore melts at 1085°C. An open fire loses too much heat, so wall coal and ore in with blocks and leave a small vent at the top for smoke. Mix ore and coal about half and half: Ctrl+click coal and copper ore in the bag to blow a mix. Right click blows an even mix.' },
|
||||||
{ id: 'bars', title: 'Copper bars', msg: 'Bars sell for far more than the ore in them, and make metal wall blocks.', hint: 'Build an Ingot mold (Build, 3) right under the furnace with a floor of clay Grates between them: molten copper drips through, while the charge and the thick slag stay up. When the bars set, they drop out of the bottom of the mold onto whatever is under it; a belt carries them off. Leave space below, or pick up the bars with the Pneumatic nozzle. Ingot molds empty themselves: bars, junk and scrap drop below, slag is thrown away and the grate above is raked. Removing any mold returns its set metal to your bag: whole bars as bars, the rest as scrap. Copper that sets in the furnace melts again next firing.' },
|
{ id: 'bars', title: 'Copper bars', msg: 'Bars sell for far more than the ore in them, and make metal wall blocks.', hint: 'Build an Ingot mold (Build, 3) right under the furnace with a floor of clay Grates between them: molten copper drips through, while the charge and the thick slag stay up. When the bars set, they drop out of the bottom of the mold onto whatever is under it; a belt carries them off. Leave space below, or pick up the bars with the Pneumatic nozzle. Ingot molds empty themselves: bars, junk and scrap drop below, slag is thrown away and the grate above is raked. Removing any mold returns its set metal to your bag: whole bars as bars, the rest as scrap. Copper that sets in the furnace melts again next firing.' },
|
||||||
{ id: 'bit1', title: 'Copper drill bit', hint: 'Cast copper in a Bit mold. Building the furnace right on top works. A Grate over the funnel keeps the charge out. Let it set, then press F.', reward: 'Copper bit. You can now dig iron ore.' },
|
{ id: 'bit1', title: 'Copper drill bit', hint: 'Cast copper in a Bit mold. Building the furnace right on top works. A Grate over the funnel keeps the charge out. Let it set, then press F.', reward: 'Copper bit. You can now dig iron ore.' },
|
||||||
{ id: 'auto', title: 'Moving parts', msg: 'Machines can now haul for you. Feed furnaces automatically, and lift ore out of your mines.', hint: 'Belts carry grains and bars resting on them. Screw tubes push loose material along (never liquids); stacked pointing up they lift it out of a mine. A Filter lets one material fall through and carries the rest along, so you can sort ore from rubble. Move 300 grains with machines.' },
|
{ id: 'auto', title: 'Moving parts', msg: 'Machines can now haul for you. Feed furnaces automatically, and lift ore out of your mines.', hint: 'Belts carry grains and bars resting on them. Screw conveyors push loose material along (never liquids); stacked pointing up they lift it out of a mine. A Filter lets one material fall through and carries the rest along, so you can sort ore from rubble. Move 300 grains with machines.' },
|
||||||
{ id: 'glass', title: 'Glass', hint: 'Sand melts near 1300°C. Still air will not get a fire that hot: point a Fan into the fuel. Molten glass runs through a grate like metal, so cast it in an Ingot mold for glass bricks, which sell well.', reward: 'Flask unlocked. The nozzle can suck up and blow out liquids.' },
|
{ id: 'glass', title: 'Glass', hint: 'Sand melts near 1300°C. Still air will not get a fire that hot: point a Fan into the fuel. Molten glass runs through a grate like metal, so cast it in an Ingot mold for glass bricks, which sell well.', reward: 'Flask unlocked. The nozzle can suck up and blow out liquids.' },
|
||||||
{ id: 'iron', title: 'Molten iron', msg: 'Iron is hard to keep molten. A brick mold right under the hot zone works best.', hint: 'Iron ore melts at 1538°C. That needs forced air, and stone slumps at 1250°C, so build the furnace from brick.' },
|
{ id: 'iron', title: 'Molten iron', msg: 'Iron is hard to keep molten. A brick mold right under the hot zone works best.', hint: 'Iron ore melts at 1538°C. That needs forced air, and stone slumps at 1250°C, so build the furnace from brick.' },
|
||||||
{ id: 'bit2', title: 'Iron drill bit', hint: 'Cast iron in a Bit mold. A stone mold melts at iron temperatures, so build it from brick.', reward: 'Iron bit. Granite is now diggable.' },
|
{ id: 'bit2', title: 'Iron drill bit', hint: 'Cast iron in a Bit mold. A stone mold melts at iron temperatures, so build it from brick.', reward: 'Iron bit. Granite is now diggable.' },
|
||||||
{ id: 'steel', title: 'Steel', msg: 'Cast it in a brick Bit mold for the best drill bit there is.', hint: 'Blow air through molten iron. Aim a Fan at a pool of it and the excess carbon burns off, turning it into steel.' },
|
{ id: 'steel', title: 'Steel', msg: 'Cast it in a brick Bit mold for the best drill bit there is.', hint: 'Blow air through molten iron. Aim a Fan at a pool of it and the excess carbon burns off, turning it into steel.' },
|
||||||
{ id: 'bit3', title: 'Steel drill bit', hint: 'Cast steel in a brick Bit mold.', reward: 'Steel bit. Basalt is now diggable.' },
|
{ id: 'bit3', title: 'Steel drill bit', hint: 'Cast steel in a brick Bit mold.', reward: 'Steel bit. Basalt is now diggable.' },
|
||||||
{ id: 'tank', title: 'Bigger bag', hint: 'Cast any metal in a Tank mold and knock it out to carry more.' },
|
{ id: 'tank', title: 'Bigger bag', hint: 'Cast any metal in a Tank mold and knock it out to carry more.' },
|
||||||
{ id: 'sale', title: 'First sale', msg: 'Money buys machines: belts, screw tubes and fans. Refined goods pay far more than raw.', hint: 'Blow, belt or lift goods into the trade dock near where you started. They are paid for the moment they drop through the grate in its floor.' },
|
{ id: 'sale', title: 'First sale', msg: 'Money buys machines: belts, screw conveyors and fans. Refined goods pay far more than raw.', hint: 'Blow, belt or lift goods into the trade dock near where you started. They are paid for the moment they drop through the grate in its floor.' },
|
||||||
{ id: 'order', title: 'First order filled', msg: 'Bigger buyers notice you as you fill more orders.', hint: 'Buyers post orders on the Market page: an amount of one good by a deadline, at well over market price. Goods dropped into the dock fill an open order first.' },
|
{ id: 'order', title: 'First order filled', msg: 'Bigger buyers notice you as you fill more orders.', hint: 'Buyers post orders on the Market page: an amount of one good by a deadline, at well over market price. Goods dropped into the dock fill an open order first.' },
|
||||||
{ id: 'alloy', title: 'An alloy', msg: 'Alloys sell for far more than the metals that went into them.', hint: 'Tin ore lies a little deeper than copper, zinc deeper again. Melt tin into a pool of molten copper and it mixes into bronze; zinc makes brass.' },
|
{ id: 'alloy', title: 'An alloy', msg: 'Alloys sell for far more than the metals that went into them.', hint: 'Tin ore lies a little deeper than copper, zinc deeper again. Melt tin into a pool of molten copper and it mixes into bronze; zinc makes brass.' },
|
||||||
{ id: 'precious', title: 'Precious metal', hint: 'Silver lies deep in the granite, gold deeper still. Their ore sells high even raw, and much higher as bars.' },
|
{ id: 'precious', title: 'Precious metal', hint: 'Silver lies deep in the granite, gold deeper still. Their ore sells high even raw, and much higher as bars.' },
|
||||||
|
|||||||
+13
-5
@@ -3,12 +3,14 @@
|
|||||||
const SAVE_VERSION = 2;
|
const SAVE_VERSION = 2;
|
||||||
const TOFF = 64; // temperatures are saved with an offset so sub-zero snow and ice survive
|
const TOFF = 64; // temperatures are saved with an offset so sub-zero snow and ice survive
|
||||||
function serializeWorld() {
|
function serializeWorld() {
|
||||||
// grains in flight from a tube or belt land where they are (rain in the air simply carries on)
|
// grains in flight from a screw or belt land where they are (rain in the air simply carries on)
|
||||||
for (const f of flyers) if (!f.bag && !f.rain) landFlyer(f, Math.max(0, Math.min(W - 1, Math.floor(f.x))), Math.max(0, Math.min(H - 1, Math.floor(f.y))));
|
for (const f of flyers) if (!f.bag && !f.rain) landFlyer(f, Math.max(0, Math.min(W - 1, Math.floor(f.x))), Math.max(0, Math.min(H - 1, Math.floor(f.y))));
|
||||||
flyers = flyers.filter(f => f.bag || f.rain);
|
flyers = flyers.filter(f => f.bag || f.rain);
|
||||||
const t16 = new Uint16Array(N);
|
const t16 = new Uint16Array(N);
|
||||||
for (let i = 0; i < N; i++) { const v = (temp[i] + TOFF) * 16; t16[i] = v < 0 ? 0 : v > 65535 ? 65535 : v; }
|
for (let i = 0; i < N; i++) { const v = (temp[i] + TOFF) * 16; t16[i] = v < 0 ? 0 : v > 65535 ? 65535 : v; }
|
||||||
const meta = {
|
const meta = {
|
||||||
|
dev: devWorld, devLog: devLog.map(e => e.slice()),
|
||||||
|
slopePhases: Array.from(slopePhase),
|
||||||
v: SAVE_VERSION, seed: worldSeed, W, H, tick, surfBase, underD, shallowD, lapse, tOff: TOFF,
|
v: SAVE_VERSION, seed: worldSeed, W, H, tick, surfBase, underD, shallowD, lapse, tOff: TOFF,
|
||||||
saplings: saplings.map(s => [s.x, s.y, s.age, s.th, s.seed]),
|
saplings: saplings.map(s => [s.x, s.y, s.age, s.th, s.seed]),
|
||||||
economy: { money, debt, earned, recent: Array.from(mkRecent, v => Math.round(v)), log: shipLog, orders, ordersDone, nextOrderT, orderSeq },
|
economy: { money, debt, earned, recent: Array.from(mkRecent, v => Math.round(v)), log: shipLog, orders, ordersDone, nextOrderT, orderSeq },
|
||||||
@@ -31,6 +33,8 @@ function serializeWorld() {
|
|||||||
function deserializeWorld(meta, buffers) {
|
function deserializeWorld(meta, buffers) {
|
||||||
if (meta.v !== SAVE_VERSION) throw new Error('This world was saved by an older version of the game and cannot be loaded.');
|
if (meta.v !== SAVE_VERSION) throw new Error('This world was saved by an older version of the game and cannot be loaded.');
|
||||||
alloc(meta.W, meta.H);
|
alloc(meta.W, meta.H);
|
||||||
|
devWorld = meta.dev === true;
|
||||||
|
devLog = devWorld && Array.isArray(meta.devLog) ? meta.devLog.filter(e => Array.isArray(e) && Number.isSafeInteger(e[0]) && e[0] >= 0 && typeof e[1] === 'string').slice(-200).map(e => [e[0], e[1]]) : [];
|
||||||
worldSeed = meta.seed;
|
worldSeed = meta.seed;
|
||||||
if (meta.surfBase) { surfBase = meta.surfBase; underD = meta.underD; }
|
if (meta.surfBase) { surfBase = meta.surfBase; underD = meta.underD; }
|
||||||
shallowD = meta.shallowD || 0; lapse = meta.lapse || 0.36;
|
shallowD = meta.shallowD || 0; lapse = meta.lapse || 0.36;
|
||||||
@@ -55,8 +59,8 @@ function deserializeWorld(meta, buffers) {
|
|||||||
}
|
}
|
||||||
// pipes used to be solid copper blocks; now they are thin and let things past
|
// pipes used to be solid copper blocks; now they are thin and let things past
|
||||||
for (let b = 0; b < NB; b++) if (blockP[b] === BP_PIPE) for (const i of blockCellList(b % BW, (b / BW) | 0)) if (mat[i] === PIPE) { mat[i] = EMPTY; part[i] = P_PIPE; }
|
for (let b = 0; b < NB; b++) if (blockP[b] === BP_PIPE) for (const i of blockCellList(b % BW, (b / BW) | 0)) if (mat[i] === PIPE) { mat[i] = EMPTY; part[i] = P_PIPE; }
|
||||||
for (const [b, m, inv] of meta.filters || []) if (blockP[b] === BP_FILTER) filterCfg.set(b, filterRec({ m, inv }));
|
for (const [b, m, inv] of meta.filters || []) if (isFilter(blockP[b])) filterCfg.set(b, filterRec({ m, inv }));
|
||||||
for (let b = 0; b < NB; b++) if (blockP[b] === BP_FILTER) {
|
for (let b = 0; b < NB; b++) if (isFilter(blockP[b])) {
|
||||||
// the first filters kept one material in their block variant
|
// the first filters kept one material in their block variant
|
||||||
if (!filterCfg.has(b)) filterCfg.set(b, filterRec({ m: [blockV[b] & 127], inv: 0 }));
|
if (!filterCfg.has(b)) filterCfg.set(b, filterRec({ m: [blockV[b] & 127], inv: 0 }));
|
||||||
blockV[b] &= 128;
|
blockV[b] &= 128;
|
||||||
@@ -86,7 +90,7 @@ function deserializeWorld(meta, buffers) {
|
|||||||
const p = savedPart[i];
|
const p = savedPart[i];
|
||||||
if (p === P_WHEEL || p === P_INTAKE) continue;
|
if (p === P_WHEEL || p === P_INTAKE) continue;
|
||||||
part[i] = p;
|
part[i] = p;
|
||||||
if (p >= P_TU && p <= P_TR) markPow(i, true);
|
if (IS_SCREW[p]) markPow(i, true);
|
||||||
}
|
}
|
||||||
for (let i = 0; i < N; i++) { const m = mat[i]; if (m === BELTL || m === BELTR || m === SHAFT) markPow(i, true); }
|
for (let i = 0; i < N; i++) { const m = mat[i]; if (m === BELTL || m === BELTR || m === SHAFT) markPow(i, true); }
|
||||||
// Old machines removed from the game are refunded.
|
// Old machines removed from the game are refunded.
|
||||||
@@ -102,6 +106,7 @@ function deserializeWorld(meta, buffers) {
|
|||||||
if (!dock()) placeDock((W >> 1) + 34); // worlds from before the economy get their trade dock too
|
if (!dock()) placeDock((W >> 1) + 34); // worlds from before the economy get their trade dock too
|
||||||
ghostBlocks = new Map(meta.ghostBlocks || []); ghostMachines = meta.ghostMachines || []; ghostMolds = meta.ghostMolds || [];
|
ghostBlocks = new Map(meta.ghostBlocks || []); ghostMachines = meta.ghostMachines || []; ghostMolds = meta.ghostMolds || [];
|
||||||
tick = meta.tick || 0;
|
tick = meta.tick || 0;
|
||||||
|
if (meta.slopePhases) slopePhase.set(meta.slopePhases);
|
||||||
updateRoof(); resetWeather();
|
updateRoof(); resetWeather();
|
||||||
if (meta.weather) {
|
if (meta.weather) {
|
||||||
const mw = meta.weather;
|
const mw = meta.weather;
|
||||||
@@ -112,12 +117,15 @@ function deserializeWorld(meta, buffers) {
|
|||||||
bot.x = meta.bot.x; bot.y = meta.bot.y; bot.lamp = meta.bot.lamp !== false; bot.vx = bot.vy = 0;
|
bot.x = meta.bot.x; bot.y = meta.bot.y; bot.lamp = meta.bot.lamp !== false; bot.vx = bot.vy = 0;
|
||||||
bag = new Int32Array(NMAT); meta.bag.forEach((v, k) => { if (k < NMAT) bag[k] = v; });
|
bag = new Int32Array(NMAT); meta.bag.forEach((v, k) => { if (k < NMAT) bag[k] = v; });
|
||||||
for (const m of meta.flyers || []) if (m < NMAT) bag[m]++; // grains that were mid-air when saved
|
for (const m of meta.flyers || []) if (m < NMAT) bag[m]++; // grains that were mid-air when saved
|
||||||
|
// solids saved in the bag (older saves) come back as what drilling them gives
|
||||||
|
for (let k = 0; k < NMAT; k++) if (bag[k] && PH[k] === PH_SOLID && YLD[k]) { bag[YLD[k]] += bag[k]; bag[k] = 0; }
|
||||||
bagTotal = 0; for (let k = 0; k < NMAT; k++) bagTotal += bag[k];
|
bagTotal = 0; for (let k = 0; k < NMAT; k++) bagTotal += bag[k];
|
||||||
Object.assign(player, meta.player);
|
Object.assign(player, meta.player);
|
||||||
seen = new Uint8Array(NMAT); meta.seen.forEach((v, k) => { if (k < NMAT) seen[k] = v; });
|
seen = new Uint8Array(NMAT); meta.seen.forEach((v, k) => { if (k < NMAT) seen[k] = v; });
|
||||||
ledger = meta.ledger || {};
|
ledger = meta.ledger || {};
|
||||||
Object.assign(stats, meta.stats || {});
|
Object.assign(stats, meta.stats || {});
|
||||||
powerDirty = true; rebuildPower(); updateRoof();
|
clearStraySeals();
|
||||||
|
powerDirty = true; rebuildPower(); slopeCleatsTick(false); updateRoof();
|
||||||
if (!weather.base) weather.base = worldWater(); // older saves: take the water it has now as its starting amount
|
if (!weather.base) weather.base = worldWater(); // older saves: take the water it has now as its starting amount
|
||||||
if (!meta.specks) clearHardSpecks(true); // older saves: tidy away tiny specks of too-hard rock once
|
if (!meta.specks) clearHardSpecks(true); // older saves: tidy away tiny specks of too-hard rock once
|
||||||
rebuildPipes();
|
rebuildPipes();
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
// Cleats are rubber ribs standing up from a sloped belt's face, one every CLEAT_SPACING face cells.
|
||||||
|
// Loose grains treat them as solid (cleatMask). Nothing grips the bare face: grains slide down it
|
||||||
|
// until a cleat stops them. Each belt step moves every cleat one face cell along the belt, and the
|
||||||
|
// rib pushes whatever is in its way ahead of it, so the pocket in front rides along while anything
|
||||||
|
// heaped above the rib is left to roll back. If a rib can't push its way, its whole run stalls.
|
||||||
|
// Only uphill runs have cleats: downhill, the belt carries its load like a flat one.
|
||||||
|
const CLEAT_SPACING = 8, CLEAT_HEIGHT = 6; // spacing in face cells, height in grains
|
||||||
|
const CLEAT_REACH = Math.floor(CLEAT_HEIGHT * Math.SQRT1_2);
|
||||||
|
const SCREEN_NUDGE_TICKS = 4; // one downhill contact step per four ticks (15 steps/s), plus gravity
|
||||||
|
let cleatChunks = new Uint8Array(0);
|
||||||
|
let cleatMask = new Uint8Array(0), cleatCells = [], slopePhase = new Uint8Array(0);
|
||||||
|
let ribLog = []; // the swaps one run's ribs made this step, undone if the run stalls
|
||||||
|
function ribMove(a, b) { move(a, b, a % W, (a / W) | 0, b % W, (b / W) | 0); ribLog.push(a, b); }
|
||||||
|
function cleatFoot(x, b, phase = slopePhase[b]) {
|
||||||
|
const d = blockV[b] & 1 ? 1 : -1;
|
||||||
|
return ((x - d * phase) % CLEAT_SPACING + CLEAT_SPACING) % CLEAT_SPACING === 0;
|
||||||
|
}
|
||||||
|
// The rib standing on face cell (x, y) of a slope with grade g, foot first. Where its end would
|
||||||
|
// reach into something built (a screw conveyor, the next belt, a wall), the rib stops short: -1.
|
||||||
|
function ribCells(x, y, g) {
|
||||||
|
const out = [];
|
||||||
|
let open = true;
|
||||||
|
for (let h = 0; h <= CLEAT_REACH; h++) {
|
||||||
|
const xx = x + g * h, yy = y - h - 1, j = yy * W + xx;
|
||||||
|
if (xx < 0 || xx >= W || yy < 0 || yy >= H || part[j] || mach[j] || PH[mat[j]] === PH_SOLID) open = false;
|
||||||
|
out.push(open ? j : -1);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function slopeUphill(c, v) { return beltGrade(c) * (v & 1 ? 1 : -1) < 0; } // v: the belt's direction
|
||||||
|
// Is (x, y) a face cell of a sloped belt with grade g?
|
||||||
|
function slopeFaceAt(x, y, g) {
|
||||||
|
if (x < 0 || x >= W || y < 0 || y >= H) return false;
|
||||||
|
const c = blockP[(y >> 2) * BW + (x >> 2)];
|
||||||
|
return isBelt(c) && isSlopeBelt(c) && beltGrade(c) === g && beltFaceY(c, x >> 2, y >> 2, x) === y;
|
||||||
|
}
|
||||||
|
// Push the row of loose grains starting at p one cell along (dx, dy), front first. False if the
|
||||||
|
// row runs into something it can't enter.
|
||||||
|
function pushRow(p, dx, dy) {
|
||||||
|
const row = [];
|
||||||
|
let q = p;
|
||||||
|
for (let k = 0; k < CARRY_DEPTH; k++) {
|
||||||
|
if (PH[mat[q]] !== PH_POWDER || occ[q] || part[q] || (k && cleatMask[q])) break; // a machine moves its own contents
|
||||||
|
row.push(q);
|
||||||
|
const x = q % W + dx, y = ((q / W) | 0) + dy;
|
||||||
|
if (x < 0 || x >= W || y < 0 || y >= H) return false;
|
||||||
|
if (cleatMask[q + dx] && cleatMask[q + dy * W]) return false; // never between two cells of a rib
|
||||||
|
q = y * W + x;
|
||||||
|
}
|
||||||
|
const front = row[row.length - 1], m = mat[front];
|
||||||
|
if (cleatMask[q] || !(passable(m, PH_POWDER, q, dx, 0) || (PH[mat[q]] === PH_LIQUID && !occ[q] && !part[q]))) return false;
|
||||||
|
ribMove(front, q);
|
||||||
|
for (let k = row.length - 2; k >= 0; k--) ribMove(row[k], row[k] + dx + dy * W);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// At the foot of an uphill run, a rib coming up around the end roller scoops what lies on the
|
||||||
|
// feeding belt behind it, one pocket long and as high as the rib, into the pocket in front of it.
|
||||||
|
function scoop(x, y, d, rib) {
|
||||||
|
for (let k = 1; k < CLEAT_SPACING; k++) {
|
||||||
|
const xx = x - d * k;
|
||||||
|
if (xx < 0 || xx >= W) break;
|
||||||
|
for (let yy = y; yy >= y - Math.min(k, CLEAT_REACH) && yy >= 0; yy--) {
|
||||||
|
const a = yy * W + xx, m = mat[a];
|
||||||
|
if (PH[m] !== PH_POWDER || occ[a] || cleatMask[a]) continue;
|
||||||
|
const to = pocketRoom(rib, d, m);
|
||||||
|
if (to < 0) return;
|
||||||
|
ribMove(a, to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The lowest open cell in the pocket just in front of a rib, or -1 if it is full.
|
||||||
|
function pocketRoom(rib, d, m) {
|
||||||
|
for (let h = 0; h < rib.length; h++) {
|
||||||
|
if (rib[h] < 0) continue;
|
||||||
|
for (let t = 1; t < CLEAT_SPACING; t++) {
|
||||||
|
const j = rib[h] + d * t, jx = rib[h] % W + d * t;
|
||||||
|
if (jx < 0 || jx >= W) break;
|
||||||
|
if (PH[mat[j]] === PH_SOLID || cleatMask[j]) break;
|
||||||
|
if (passable(m, PH_POWDER, j, d, 0)) return j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// Move the rib that now stands on face cell (x - d) to (x, y), pushing what is in its way.
|
||||||
|
function advanceCleat(x, y, g, d) {
|
||||||
|
const st = g * d, ox = x - d, oy = y - st;
|
||||||
|
const fresh = ribCells(x, y, g), old = slopeFaceAt(ox, oy, g) ? ribCells(ox, oy, g) : null;
|
||||||
|
// the cells the rib sweeps: those between the old and the new rib, and the new rib itself
|
||||||
|
const sweep = [];
|
||||||
|
if (old) {
|
||||||
|
for (const j of old) if (j >= 0) cleatMask[j] = 0;
|
||||||
|
for (let h = 0; h < old.length; h++) {
|
||||||
|
const j = old[h];
|
||||||
|
if (j >= 0) {
|
||||||
|
sweep.push(j + d);
|
||||||
|
if (h < old.length - 1) sweep.push(j + st * W);
|
||||||
|
}
|
||||||
|
if (fresh[h] >= 0) sweep.push(fresh[h]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const j of fresh) if (j >= 0) sweep.push(j);
|
||||||
|
if (st < 0) scoop(x, y, d, fresh);
|
||||||
|
}
|
||||||
|
let clear = true;
|
||||||
|
for (const s of sweep) {
|
||||||
|
if (s < 0 || s >= N || PH[mat[s]] !== PH_POWDER || occ[s] || part[s] || pushRow(s, d, st)) continue;
|
||||||
|
clear = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for (const j of fresh) if (j >= 0) { cleatMask[j] = 1; cleatCells.push(j); } // the rebuild clears it if this run stalls
|
||||||
|
return clear;
|
||||||
|
}
|
||||||
|
// A run of sloped belt is one belt: the block at its lower-left end names it.
|
||||||
|
function slopeRunKey(b, memo) {
|
||||||
|
if (memo.has(b)) return memo.get(b);
|
||||||
|
const c = blockP[b], g = beltGrade(c), v = blockV[b] & 1, path = [b];
|
||||||
|
let bx = b % BW, by = (b / BW) | 0, key = -1;
|
||||||
|
while (bx > 0 && by - g >= 0 && by - g < BH && blockP[(by - g) * BW + bx - 1] === c && (blockV[(by - g) * BW + bx - 1] & 1) === v) {
|
||||||
|
bx--; by -= g;
|
||||||
|
if (memo.has(by * BW + bx)) { key = memo.get(by * BW + bx); break; }
|
||||||
|
path.push(by * BW + bx);
|
||||||
|
}
|
||||||
|
if (key < 0) key = by * BW + bx;
|
||||||
|
for (const p of path) memo.set(p, key);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
function slopeCleatsTick(advance = true) {
|
||||||
|
const memo = new Map(), stalled = new Set();
|
||||||
|
// the whole run shares the phase of its lower-left block, so a piece added to a running belt
|
||||||
|
// takes up its cleat spacing instead of starting a rib of its own at the seam
|
||||||
|
let resynced = false;
|
||||||
|
for (const list of [beltR, beltL]) for (const i of list) {
|
||||||
|
const x = i % W, y = (i / W) | 0, b = (y >> 2) * BW + (x >> 2), c = blockP[b];
|
||||||
|
if ((x & 3) || !isBelt(c) || !isSlopeBelt(c) || beltFaceY(c, x >> 2, y >> 2, x) !== y) continue;
|
||||||
|
const p = slopePhase[slopeRunKey(b, memo)];
|
||||||
|
if (slopePhase[b] !== p) { slopePhase[b] = p; resynced = true; }
|
||||||
|
}
|
||||||
|
if (resynced) rebuildCleatMask();
|
||||||
|
if (advance && !(tick & 1)) {
|
||||||
|
// each run moves all its ribs or none: if one can't clear its path, the run stalls and every
|
||||||
|
// grain its ribs moved goes back. Front of the line first, so a rib pushes into room the rib
|
||||||
|
// ahead of it has just made.
|
||||||
|
const runs = new Map();
|
||||||
|
for (const list of [beltR, beltL]) for (const i of list) {
|
||||||
|
const x = i % W, y = (i / W) | 0, b = (y >> 2) * BW + (x >> 2), c = blockP[b];
|
||||||
|
if (!isBelt(c) || !isSlopeBelt(c) || !slopeUphill(c, blockV[b]) || beltFaceY(c, x >> 2, y >> 2, x) !== y || netSat(i) <= 0) continue;
|
||||||
|
// lift the run's ribs off; each is put back where it moves to, and a rib leaving the top is gone
|
||||||
|
if (cleatFoot(x, b)) for (const j of ribCells(x, y, beltGrade(c))) if (j >= 0) cleatMask[j] = 0;
|
||||||
|
if (!cleatFoot(x, b, (slopePhase[b] + 1) % CLEAT_SPACING)) continue;
|
||||||
|
const key = slopeRunKey(b, memo);
|
||||||
|
if (!runs.has(key)) runs.set(key, []);
|
||||||
|
runs.get(key).push(x, y, beltGrade(c), blockV[b] & 1 ? 1 : -1);
|
||||||
|
}
|
||||||
|
for (const [key, feet] of runs) {
|
||||||
|
ribLog.length = 0;
|
||||||
|
let ok = true;
|
||||||
|
for (let k = 0; k < feet.length && ok; k += 4) ok = advanceCleat(feet[k], feet[k + 1], feet[k + 2], feet[k + 3]);
|
||||||
|
if (ok) { if (flowOn) for (let k = 0; k < ribLog.length; k += 2) flowCross(flowSlopeUnder(ribLog[k]), ribLog[k], ribLog[k + 1]); continue; }
|
||||||
|
stalled.add(key);
|
||||||
|
for (let k = ribLog.length - 2; k >= 0; k -= 2) { swapCells(ribLog[k], ribLog[k + 1]); wakeIdx(ribLog[k]); wakeIdx(ribLog[k + 1]); }
|
||||||
|
}
|
||||||
|
ribLog.length = 0;
|
||||||
|
for (const list of [beltR, beltL]) for (const i of list) {
|
||||||
|
const x = i % W, y = (i / W) | 0, b = (y >> 2) * BW + (x >> 2), c = blockP[b];
|
||||||
|
if ((x & 3) === 0 && isBelt(c) && isSlopeBelt(c) && beltFaceY(c, x >> 2, y >> 2, x) === y && netSat(i) > 0 && !stalled.has(slopeRunKey(b, memo)))
|
||||||
|
slopePhase[b] = (slopePhase[b] + 1) % CLEAT_SPACING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rebuildCleatMask();
|
||||||
|
if (advance && !(tick & 1)) {
|
||||||
|
// the running face drags the grains lying right on it; unless something below holds them,
|
||||||
|
// they slide back down between steps, which on 45° is faster than the belt runs
|
||||||
|
for (const list of [beltR, beltL]) for (const i of list) {
|
||||||
|
const x = i % W, y = (i / W) | 0, b = (y >> 2) * BW + (x >> 2), c = blockP[b], a = i - W;
|
||||||
|
if (!isBelt(c) || !isSlopeBelt(c) || !slopeUphill(c, blockV[b]) || beltFaceY(c, x >> 2, y >> 2, x) !== y || netSat(i) <= 0) continue;
|
||||||
|
if (a < 0 || PH[mat[a]] !== PH_POWDER || occ[a] || cleatMask[a] || stamp[a] === tickStamp) continue;
|
||||||
|
const d = blockV[b] & 1 ? 1 : -1, st = conveyorStep(x, y, d), j = a + d + st * W;
|
||||||
|
if (x + d < 0 || x + d >= W || j < 0 || cleatMask[j] || !passable(mat[a], PH_POWDER, j, d, st)) continue;
|
||||||
|
const bj = j + W;
|
||||||
|
// dragged off an open end with nothing under it: it leaves at belt speed and falls
|
||||||
|
if (mat[j] === EMPTY && !part[j] && bj < N && (mat[bj] === EMPTY || PH[mat[bj]] === PH_GAS) && !occ[bj] && !part[bj]) launchGrain(a, x + d, y - 1 + st, d * 0.5, st * 0.5);
|
||||||
|
else move(a, j, x, y - 1, x + d, y - 1 + st);
|
||||||
|
if (flowOn) flowCross(b, a, j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The barriers, rebuilt from the phases: ribs that left the top of a slope are gone.
|
||||||
|
function rebuildCleatMask() {
|
||||||
|
for (const i of cleatCells) { cleatMask[i] = 0; wakeCleatLoad(i); }
|
||||||
|
cleatChunks.fill(0);
|
||||||
|
cleatCells.length = 0;
|
||||||
|
for (const list of [beltR, beltL]) for (const i of list) {
|
||||||
|
const x = i % W, y = (i / W) | 0, b = (y >> 2) * BW + (x >> 2), c = blockP[b];
|
||||||
|
if (!isBelt(c) || !isSlopeBelt(c) || !slopeUphill(c, blockV[b]) || beltFaceY(c, x >> 2, y >> 2, x) !== y || !cleatFoot(x, b)) continue;
|
||||||
|
for (const j of ribCells(x, y, beltGrade(c))) if (j >= 0) cleatCells.push(j);
|
||||||
|
}
|
||||||
|
for (const i of cleatCells) {
|
||||||
|
cleatMask[i] = 1; wakeCleatLoad(i);
|
||||||
|
if (PH[mat[i]] === PH_POWDER && !occ[i]) expelFromRib(i);
|
||||||
|
const x = i % W, y = (i / W) | 0;
|
||||||
|
for (let cy = Math.max(0, (y - 1) >> 5); cy <= Math.min(CH - 1, (y + 1) >> 5); cy++)
|
||||||
|
for (let cx = Math.max(0, (x - 1) >> 5); cx <= Math.min(CW - 1, (x + 1) >> 5); cx++) cleatChunks[cy * CW + cx] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A rib that appears where grains already lie (a belt built under a heap, a loaded save) lifts
|
||||||
|
// each one to the nearest open cell above it.
|
||||||
|
function expelFromRib(i) {
|
||||||
|
for (let q = i - W, k = 0; q >= 0 && k < 24; q -= W, k++) {
|
||||||
|
if (cleatMask[q] || part[q] || occ[q]) continue;
|
||||||
|
if (mat[q] === EMPTY || PH[mat[q]] === PH_GAS) { move(i, q, i % W, (i / W) | 0, q % W, (q / W) | 0); return; }
|
||||||
|
if (PH[mat[q]] !== PH_POWDER) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A screen shakes only the grains touching its mesh, never the whole column above it.
|
||||||
|
function screenNudge(top, d, pass) {
|
||||||
|
if (tick % SCREEN_NUDGE_TICKS) return;
|
||||||
|
const i = top - W, m = mat[i];
|
||||||
|
if (i < 0 || PH[m] !== PH_POWDER || stamp[i] === tickStamp || occ[i]) return;
|
||||||
|
if (pass && pass[m] && enter(m, PH_POWDER, top, 0, 1)) return;
|
||||||
|
const j = i + d + W;
|
||||||
|
if (i % W + d < 0 || i % W + d >= W || j >= N) return;
|
||||||
|
if (enter(m, PH_POWDER, j, d, 1)) { move(i, j, i % W, (i / W) | 0, j % W, (j / W) | 0); if (flowOn) flowCross(((top / W) >> 2) * BW + ((top % W) >> 2), i, j); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only loose load can react to a barrier appearing or disappearing.
|
||||||
|
function wakeCleatLoad(i) {
|
||||||
|
const x = i % W, y = (i / W) | 0;
|
||||||
|
for (let yy = Math.max(0, y - 1); yy <= Math.min(H - 1, y + 1); yy++)
|
||||||
|
for (let xx = Math.max(0, x - 1); xx <= Math.min(W - 1, x + 1); xx++)
|
||||||
|
if (PH[mat[yy * W + xx]] === PH_POWDER || occ[yy * W + xx]) wakeXY(xx, yy);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ function simTick() {
|
|||||||
if (tick % 8 === 0) wakeBotPowder();
|
if (tick % 8 === 0) wakeBotPowder();
|
||||||
simMove();
|
simMove();
|
||||||
heatPass();
|
heatPass();
|
||||||
|
if (flowOn) flowTick();
|
||||||
for (let c = 0; c < NCH; c++) if (awake[c]) awake[c]--;
|
for (let c = 0; c < NCH; c++) if (awake[c]) awake[c]--;
|
||||||
if (tick % 15 === 0) checkMolds();
|
if (tick % 15 === 0) checkMolds();
|
||||||
if (tick % 30 === 0) checkLedger();
|
if (tick % 30 === 0) checkLedger();
|
||||||
|
|||||||
+14
-1
@@ -1,5 +1,18 @@
|
|||||||
// Tool helpers: reach, brushes, and plain digging.
|
// Tool helpers: reach, brushes, and plain digging.
|
||||||
// ---------- Tools ----------
|
// ---------- Tools ----------
|
||||||
|
// Building reaches about as far as the screen shows (t64): the view box the UI sets each frame
|
||||||
|
// (setBuildRange), at least REACH from its centre and at most BUILD_MAX; anything beyond becomes
|
||||||
|
// a blueprint ghost. With no view (tests, headless), a REACH box around the bot.
|
||||||
|
const BUILD_MAX = 200;
|
||||||
|
let buildBox = null;
|
||||||
|
function setBuildRange(cx, cy, w, h) {
|
||||||
|
const hw = Math.max(REACH, Math.min(BUILD_MAX, w / 2)), hh = Math.max(REACH, Math.min(BUILD_MAX, h / 2));
|
||||||
|
if (!buildBox) buildBox = { cx, cy, hw, hh }; else { buildBox.cx = cx; buildBox.cy = cy; buildBox.hw = hw; buildBox.hh = hh; }
|
||||||
|
}
|
||||||
|
function inBuildRange(x, y) {
|
||||||
|
if (!buildBox) { const dx = x - (bot.x + bot.w / 2), dy = y - (bot.y + bot.h / 2); return Math.abs(dx) <= REACH && Math.abs(dy) <= REACH; }
|
||||||
|
return Math.abs(x - buildBox.cx) <= buildBox.hw && Math.abs(y - buildBox.cy) <= buildBox.hh;
|
||||||
|
}
|
||||||
function inReach(x, y) { const dx = x - (bot.x + bot.w / 2), dy = y - (bot.y + bot.h / 2); return dx * dx + dy * dy <= REACH * REACH; }
|
function inReach(x, y) { const dx = x - (bot.x + bot.w / 2), dy = y - (bot.y + bot.h / 2); return dx * dx + dy * dy <= REACH * REACH; }
|
||||||
function forBrush(cx, cy, r, fn) {
|
function forBrush(cx, cy, r, fn) {
|
||||||
const r2 = (r + 0.5) * (r + 0.5);
|
const r2 = (r + 0.5) * (r + 0.5);
|
||||||
@@ -34,7 +47,7 @@ function toolDig(cx, cy, r) {
|
|||||||
const b = blockP[((y / BS) | 0) * BW + ((x / BS) | 0)];
|
const b = blockP[((y / BS) | 0) * BW + ((x / BS) | 0)];
|
||||||
setCell(i, to, temp[i], 0);
|
setCell(i, to, temp[i], 0);
|
||||||
discover(to, x, y, m, 'dig');
|
discover(to, x, y, m, 'dig');
|
||||||
if (b === BP_WALL && !blockIntegrity(((y / BS) | 0) * BW + ((x / BS) | 0))) blockP[((y / BS) | 0) * BW + ((x / BS) | 0)] = 0;
|
if (isWall(b) && !blockIntegrity(((y / BS) | 0) * BW + ((x / BS) | 0))) removeBlock((x / BS) | 0, (y / BS) | 0);
|
||||||
dug++;
|
dug++;
|
||||||
if ((dug & 3) === 0) emit({ t: 'debris', x, y, m });
|
if ((dug & 3) === 0) emit({ t: 'debris', x, y, m });
|
||||||
} else digp[i] = p;
|
} else digp[i] = p;
|
||||||
|
|||||||
+14
-8
@@ -22,8 +22,8 @@ function undoLast() {
|
|||||||
for (let k = rec.ops.length - 1; k >= 0; k--) {
|
for (let k = rec.ops.length - 1; k >= 0; k--) {
|
||||||
const o = rec.ops[k];
|
const o = rec.ops[k];
|
||||||
if (o.t === 'block+') { if (blockP[o.by * BW + o.bx] === o.code) removeBlock(o.bx, o.by); }
|
if (o.t === 'block+') { if (blockP[o.by * BW + o.bx] === o.code) removeBlock(o.bx, o.by); }
|
||||||
else if (o.t === 'block-') { const r = placeBlock(o.code, o.code === BP_FILTER ? o.v & 128 : o.v, o.bx, o.by, false, o.extra); if (r) fail(r); }
|
else if (o.t === 'block-') { const r = placeBlock(o.code, isFilter(o.code) ? o.v & 128 : o.v, o.bx, o.by, false, o.extra); if (r) fail(r); }
|
||||||
else if (o.t === 'filter') { const b = o.by * BW + o.bx; if (blockP[b] === BP_FILTER) { blockV[b] = o.v & 128; setFilter(b, o.extra); filtersDirty = true; } }
|
else if (o.t === 'filter') { const b = o.by * BW + o.bx; if (isFilter(blockP[b])) { blockV[b] = o.v & 128; setFilter(b, o.extra); filtersDirty = true; } }
|
||||||
else if (o.t === 'mach+') { const mm = machines.find(m => m && m.type === o.type && m.x === o.bx * BS && m.y === o.by * BS); if (mm && siloFull(mm)) fail('empty the silo first'); else if (mm) removeMachine(mm.id, true, false); }
|
else if (o.t === 'mach+') { const mm = machines.find(m => m && m.type === o.type && m.x === o.bx * BS && m.y === o.by * BS); if (mm && siloFull(mm)) fail('empty the silo first'); else if (mm) removeMachine(mm.id, true, false); }
|
||||||
else if (o.t === 'mach-') { const r = placeMachineAt(o.type, o.dir, o.bx, o.by); if (r) fail(r); else { const mm = machines.find(m => m && m.type === o.type && m.x === o.bx * BS && m.y === o.by * BS); if (mm) mm.on = o.on !== false; } }
|
else if (o.t === 'mach-') { const r = placeMachineAt(o.type, o.dir, o.bx, o.by); if (r) fail(r); else { const mm = machines.find(m => m && m.type === o.type && m.x === o.bx * BS && m.y === o.by * BS); if (mm) mm.on = o.on !== false; } }
|
||||||
else if (o.t === 'mold+') { const mo = molds.find(m => m.kind === o.kind && m.x === o.bx * BS && m.y === o.by * BS); if (mo) removeMold(mo); }
|
else if (o.t === 'mold+') { const mo = molds.find(m => m.kind === o.kind && m.x === o.bx * BS && m.y === o.by * BS); if (mo) removeMold(mo); }
|
||||||
@@ -54,14 +54,20 @@ function ghostTick0() {
|
|||||||
while (budget > 0 && checks-- > 0 && ghostBlocks.size) {
|
while (budget > 0 && checks-- > 0 && ghostBlocks.size) {
|
||||||
let nx = ghostIter.next();
|
let nx = ghostIter.next();
|
||||||
if (nx.done) { ghostIter = ghostBlocks.entries(); nx = ghostIter.next(); if (nx.done) break; }
|
if (nx.done) { ghostIter = ghostBlocks.entries(); nx = ghostIter.next(); if (nx.done) break; }
|
||||||
const [b, [code, v, e]] = nx.value;
|
const [b, [code, v, e, swap]] = nx.value;
|
||||||
const bx = b % BW, by = (b / BW) | 0;
|
const bx = b % BW, by = (b / BW) | 0;
|
||||||
if (blockP[b] === code && blockV[b] === (code === BP_FILTER ? v & 128 : v)) {
|
if (blockP[b] === code && blockV[b] === (isFilter(code) ? v & 128 : v)) {
|
||||||
if (code === BP_FILTER && e && !sameFilter(filterInfo(b), cleanFilterCfg(e))) setFilter(b, e); // same piece, new settings
|
if (isFilter(code) && e && !sameFilter(filterInfo(b), cleanFilterCfg(e))) setFilter(b, e); // same piece, new settings
|
||||||
ghostBlocks.delete(b); ghostBlocked.delete(b); continue;
|
ghostBlocks.delete(b); ghostBlocked.delete(b); continue;
|
||||||
}
|
}
|
||||||
if (!blockInReach(bx, by)) continue;
|
if (!blockInReach(bx, by)) continue;
|
||||||
const err = canPlaceBlock(bx, by);
|
// a replacing ghost takes down what is there (refunded) and builds itself, like a replace in reach
|
||||||
|
if (swap && blockP[b]) {
|
||||||
|
if (blockP[b] === code && isFilter(code)) { blockV[b] = filterVariant(code, v); setFilter(b, e); filtersDirty = true; ghostBlocks.delete(b); ghostBlocked.delete(b); budget--; continue; }
|
||||||
|
if (!canAfford(pieceCost(code, v), 1)) { ghostMissing++; continue; }
|
||||||
|
removeBlock(bx, by);
|
||||||
|
}
|
||||||
|
const err = canPlaceBlock(bx, by, code);
|
||||||
if (err) { if (err.startsWith('Needs')) ghostBlocked.add(b); continue; }
|
if (err) { if (err.startsWith('Needs')) ghostBlocked.add(b); continue; }
|
||||||
if (!canAfford(pieceCost(code, v), 1)) { ghostMissing++; continue; }
|
if (!canAfford(pieceCost(code, v), 1)) { ghostMissing++; continue; }
|
||||||
placeBlock(code, v, bx, by, false, e);
|
placeBlock(code, v, bx, by, false, e);
|
||||||
@@ -70,7 +76,7 @@ function ghostTick0() {
|
|||||||
}
|
}
|
||||||
for (let k = ghostMachines.length - 1; k >= 0 && budget > 0; k--) {
|
for (let k = ghostMachines.length - 1; k >= 0 && budget > 0; k--) {
|
||||||
const g = ghostMachines[k], P = PIECE_BY_ID[g.type];
|
const g = ghostMachines[k], P = PIECE_BY_ID[g.type];
|
||||||
if (!inReach(g.bx * BS + P.w / 2, g.by * BS + P.h / 2)) continue;
|
if (!inBuildRange(g.bx * BS + P.w / 2, g.by * BS + P.h / 2)) continue;
|
||||||
if (canPlaceMachine(g.type, g.bx, g.by)) continue;
|
if (canPlaceMachine(g.type, g.bx, g.by)) continue;
|
||||||
if (!canAfford(P.cost, 1)) { ghostMissing++; continue; }
|
if (!canAfford(P.cost, 1)) { ghostMissing++; continue; }
|
||||||
placeMachineAt(g.type, g.dir, g.bx, g.by); ghostMachines.splice(k, 1); budget--;
|
placeMachineAt(g.type, g.dir, g.bx, g.by); ghostMachines.splice(k, 1); budget--;
|
||||||
@@ -78,7 +84,7 @@ function ghostTick0() {
|
|||||||
}
|
}
|
||||||
for (let k = ghostMolds.length - 1; k >= 0 && budget > 0; k--) {
|
for (let k = ghostMolds.length - 1; k >= 0 && budget > 0; k--) {
|
||||||
const g = ghostMolds[k], md = MOLDS[g.kind];
|
const g = ghostMolds[k], md = MOLDS[g.kind];
|
||||||
if (!inReach(g.bx * BS + md.w / 2, g.by * BS + md.h / 2)) continue;
|
if (!inBuildRange(g.bx * BS + md.w / 2, g.by * BS + md.h / 2)) continue;
|
||||||
if (canPlaceMold(g.kind, g.bx, g.by)) continue;
|
if (canPlaceMold(g.kind, g.bx, g.by)) continue;
|
||||||
if (!canAfford(wallCost(g.wall, md.walls), 1)) { ghostMissing++; continue; }
|
if (!canAfford(wallCost(g.wall, md.walls), 1)) { ghostMissing++; continue; }
|
||||||
addMold(g.kind, g.bx, g.by, g.wall, false); ghostMolds.splice(k, 1); budget--;
|
addMold(g.kind, g.bx, g.by, g.wall, false); ghostMolds.splice(k, 1); budget--;
|
||||||
|
|||||||
+32
-5
@@ -2,11 +2,19 @@
|
|||||||
// Material the vacuum would start on at the cursor: a bar, else the nearest loose grain.
|
// Material the vacuum would start on at the cursor: a bar, else the nearest loose grain.
|
||||||
function vacuumTargetAt(cx, cy, r) {
|
function vacuumTargetAt(cx, cy, r) {
|
||||||
for (const e of entsInCircle(cx, cy, Math.max(1, r))) return e.m;
|
for (const e of entsInCircle(cx, cy, Math.max(1, r))) return e.m;
|
||||||
|
r = Math.max(1, r);
|
||||||
let best = 0, bd = 1e9;
|
let best = 0, bd = 1e9;
|
||||||
forBrush(cx, cy, Math.max(1, r), (x, y, i) => {
|
forBrush(cx, cy, Math.max(1, r), (x, y, i) => {
|
||||||
const m = mat[i], ph = PH[m];
|
const m = mat[i], ph = PH[m];
|
||||||
if (ph === PH_POWDER || (ph === PH_LIQUID && player.flask)) { const d = (x - cx) * (x - cx) + (y - cy) * (y - cy); if (d < bd) { bd = d; best = m; } }
|
if (ph === PH_POWDER || (ph === PH_LIQUID && player.flask)) { const d = (x - cx) * (x - cx) + (y - cy) * (y - cy); if (d < bd) { bd = d; best = m; } }
|
||||||
});
|
});
|
||||||
|
for (const f of flyers) {
|
||||||
|
if (f.rain) continue; // Rain belongs to the clouds; collecting it would create water.
|
||||||
|
const ph = PH[f.m];
|
||||||
|
if (!(ph === PH_POWDER || (ph === PH_LIQUID && player.flask))) continue;
|
||||||
|
const x = Math.floor(f.x), y = Math.floor(f.y), dx = x - cx, dy = y - cy, d = dx * dx + dy * dy;
|
||||||
|
if (d <= (r + 0.5) * (r + 0.5) && d < bd && inReach(x, y)) { bd = d; best = f.m; }
|
||||||
|
}
|
||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
// Vacuum: picks up loose grains, liquids (with the Flask) and bars for which pick(m) is true.
|
// Vacuum: picks up loose grains, liquids (with the Flask) and bars for which pick(m) is true.
|
||||||
@@ -18,11 +26,23 @@ function toolVacuum(cx, cy, r, pick) {
|
|||||||
bagAdd(e.m, 1); removeEnt(e); got++; if (!first) first = e.m;
|
bagAdd(e.m, 1); removeEnt(e); got++; if (!first) first = e.m;
|
||||||
emit({ t: 'suck', x: e.ix + 2, y: e.iy + 1, m: e.m });
|
emit({ t: 'suck', x: e.ix + 2, y: e.iy + 1, m: e.m });
|
||||||
}
|
}
|
||||||
if (!full) forBrush(cx, cy, r, (x, y, i) => {
|
if (!full) for (let k = flyers.length - 1; k >= 0; k--) {
|
||||||
|
const f = flyers[k], m = f.m, ph = PH[m];
|
||||||
|
if (f.rain) continue; // Rain belongs to the clouds; collecting it would create water.
|
||||||
|
if (!(ph === PH_POWDER || (ph === PH_LIQUID && player.flask)) || !pick(m)) continue;
|
||||||
|
const x = Math.floor(f.x), y = Math.floor(f.y), dx = x - cx, dy = y - cy;
|
||||||
|
if (dx * dx + dy * dy > (r + 0.5) * (r + 0.5) || !inReach(x, y)) continue;
|
||||||
|
if (bagTotal >= bagCap()) { full = true; break; }
|
||||||
|
if (!first) first = m;
|
||||||
|
bagAdd(m, 1); flyers[k] = flyers[flyers.length - 1]; flyers.pop(); got++;
|
||||||
|
if (got & 1) emit({ t: 'suck', x, y, m });
|
||||||
|
if (--budget <= 0) break;
|
||||||
|
}
|
||||||
|
if (!full && budget > 0) forBrush(cx, cy, r, (x, y, i) => {
|
||||||
const m = mat[i], ph = PH[m];
|
const m = mat[i], ph = PH[m];
|
||||||
if (!(ph === PH_POWDER || (ph === PH_LIQUID && player.flask))) return;
|
if (!(ph === PH_POWDER || (ph === PH_LIQUID && player.flask))) return;
|
||||||
if (!pick(m)) return;
|
if (!pick(m)) return;
|
||||||
if (part[i] >= P_TU && part[i] <= P_TR) return;
|
if (IS_SCREW[part[i]]) return;
|
||||||
if (bagTotal >= bagCap()) { full = true; return false; }
|
if (bagTotal >= bagCap()) { full = true; return false; }
|
||||||
if (!first) first = m;
|
if (!first) first = m;
|
||||||
bagAdd(m, 1); setCell(i, EMPTY, AMB[y], 0); got++;
|
bagAdd(m, 1); setCell(i, EMPTY, AMB[y], 0); got++;
|
||||||
@@ -31,7 +51,7 @@ function toolVacuum(cx, cy, r, pick) {
|
|||||||
});
|
});
|
||||||
return { got, m: first, full, status: full ? 'Bag full; blow material out or cast a bigger tank.' : '' };
|
return { got, m: first, full, status: full ? 'Bag full; blow material out or cast a bigger tank.' : '' };
|
||||||
}
|
}
|
||||||
// Blow: grains leave the bot's nozzle and fly to the cursor, where they drop.
|
// Blow: grains leave the bot's nozzle and arc toward the cursor, within reach.
|
||||||
// Liquids need the Flask. Bars are thrown, one at a time.
|
// Liquids need the Flask. Bars are thrown, one at a time.
|
||||||
function nozzle(tx, ty) {
|
function nozzle(tx, ty) {
|
||||||
const cx = bot.x + bot.w / 2, cy = bot.y + bot.h / 2;
|
const cx = bot.x + bot.w / 2, cy = bot.y + bot.h / 2;
|
||||||
@@ -65,6 +85,10 @@ function toolBlow(cx, cy, r, m, mix) {
|
|||||||
if (!e) return { placed: 0, status: 'No room to throw a bar from here.' };
|
if (!e) return { placed: 0, status: 'No room to throw a bar from here.' };
|
||||||
bagTake(m, 1); return { placed: 1, status: '' };
|
bagTake(m, 1); return { placed: 1, status: '' };
|
||||||
}
|
}
|
||||||
|
if (!inReach(cx, cy)) {
|
||||||
|
cx = nz.ox + nz.dx * nz.dist - 0.5;
|
||||||
|
cy = nz.oy + nz.dy * nz.dist - 0.5;
|
||||||
|
}
|
||||||
if (flyers.length > 3000) return { placed: 0, status: '' };
|
if (flyers.length > 3000) return { placed: 0, status: '' };
|
||||||
// nozzle 0 is a trickle for careful work, the widest a torrent
|
// nozzle 0 is a trickle for careful work, the widest a torrent
|
||||||
const rate = 1 + r + ((r * r) >> 1);
|
const rate = 1 + r + ((r * r) >> 1);
|
||||||
@@ -73,7 +97,7 @@ function toolBlow(cx, cy, r, m, mix) {
|
|||||||
for (let k = 0; k < n; k++) {
|
for (let k = 0; k < n; k++) {
|
||||||
const gm = pickM ? pickM() : m;
|
const gm = pickM ? pickM() : m;
|
||||||
if (!gm) break;
|
if (!gm) break;
|
||||||
// aim each grain at a spot in the brush around the cursor, on an arc that passes
|
// aim each grain at a spot in the brush around the target, on an arc that passes
|
||||||
// through it; past that point gravity carries it on until it hits something
|
// through it; past that point gravity carries it on until it hits something
|
||||||
const a = rnd() * Math.PI * 2, rr = Math.sqrt(rnd()) * (r + 0.5);
|
const a = rnd() * Math.PI * 2, rr = Math.sqrt(rnd()) * (r + 0.5);
|
||||||
const tx = cx + 0.5 + Math.cos(a) * rr, ty = cy + 0.5 + Math.sin(a) * rr;
|
const tx = cx + 0.5 + Math.cos(a) * rr, ty = cy + 0.5 + Math.sin(a) * rr;
|
||||||
@@ -142,7 +166,10 @@ function flyTick() {
|
|||||||
if (cx < 0 || cx >= W) { f.vx = 0; continue; } // the world's edge stops it; it drops from there
|
if (cx < 0 || cx >= W) { f.vx = 0; continue; } // the world's edge stops it; it drops from there
|
||||||
if (cy >= H) { landFlyer(f, px, H - 1); done = true; break; }
|
if (cy >= H) { landFlyer(f, px, H - 1); done = true; break; }
|
||||||
if (cy < 0) { f.x = nx; f.y = ny; continue; }
|
if (cy < 0) { f.x = nx; f.y = ny; continue; }
|
||||||
const open = flyOpen(f.m, cy * W + cx);
|
// A free flyer must not cut across the point where two solid cells meet.
|
||||||
|
const sealed = cx !== px && cy !== py && px >= 0 && px < W && py >= 0 && py < H &&
|
||||||
|
PH[mat[py * W + cx]] === PH_SOLID && PH[mat[cy * W + px]] === PH_SOLID;
|
||||||
|
const open = !sealed && flyOpen(f.m, cy * W + cx);
|
||||||
if (!f.free) {
|
if (!f.free) {
|
||||||
// still inside whatever the bot is flying through: pass until the first open cell
|
// still inside whatever the bot is flying through: pass until the first open cell
|
||||||
// a blocked nozzle spills: the grain backs up into the nearest open space
|
// a blocked nozzle spills: the grain backs up into the nearest open space
|
||||||
|
|||||||
+6
-3
@@ -17,7 +17,7 @@ let awake, hot, hotLvl, powderArea, roofDirty;
|
|||||||
let loadWakeOrder = null, loadWakePending = null, loadWakeIndex = 0;
|
let loadWakeOrder = null, loadWakePending = null, loadWakeIndex = 0;
|
||||||
let tick = 0, tickStamp = 1;
|
let tick = 0, tickStamp = 1;
|
||||||
let machines = [null], molds = [], nets = [null], pcells = new Set(), powerDirty = true;
|
let machines = [null], molds = [], nets = [null], pcells = new Set(), powerDirty = true;
|
||||||
let beltL = [], beltR = [], tubeLists = [[], [], [], [], []];
|
let beltL = [], beltR = [], screwLists = Array.from({ length: IS_SCREW.length }, () => []);
|
||||||
let brokenMachines = [];
|
let brokenMachines = [];
|
||||||
let ghostMachines = [], ghostBlocked = new Set();
|
let ghostMachines = [], ghostBlocked = new Set();
|
||||||
const EV = [];
|
const EV = [];
|
||||||
@@ -39,23 +39,26 @@ function discover(m, x, y, from, how, T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function alloc(w, h) {
|
function alloc(w, h) {
|
||||||
|
resetDevConsole();
|
||||||
W = w; H = h; N = w * h;
|
W = w; H = h; N = w * h;
|
||||||
fogAlloc();
|
fogAlloc();
|
||||||
CW = Math.ceil(w / 32); CH = Math.ceil(h / 32); NCH = CW * CH;
|
CW = Math.ceil(w / 32); CH = Math.ceil(h / 32); NCH = CW * CH;
|
||||||
|
cleatMask = new Uint8Array(N); cleatCells = [];
|
||||||
mat = new Uint8Array(N); temp = new Float32Array(N); life = new Uint8Array(N); flags = new Uint8Array(N);
|
mat = new Uint8Array(N); temp = new Float32Array(N); life = new Uint8Array(N); flags = new Uint8Array(N);
|
||||||
stamp = new Uint8Array(N); part = new Uint8Array(N); mach = new Uint16Array(N); digp = new Uint8Array(N);
|
stamp = new Uint8Array(N); part = new Uint8Array(N); mach = new Uint16Array(N); digp = new Uint8Array(N);
|
||||||
noiseB = new Uint8Array(N); bgm = new Uint8Array(N); airS = new Uint16Array(N); netId = new Uint16Array(N); isPow = new Uint8Array(N); occ = new Uint8Array(N);
|
noiseB = new Uint8Array(N); bgm = new Uint8Array(N); airS = new Uint16Array(N); netId = new Uint16Array(N); isPow = new Uint8Array(N); occ = new Uint8Array(N);
|
||||||
BW = Math.ceil(w / BS); BH = Math.ceil(h / BS); NB = BW * BH; blockP = new Uint8Array(NB); blockV = new Uint8Array(NB); filtersDirty = true; filterCfg = new Map(); procDirty = true; procState.clear(); rackState.clear();
|
BW = Math.ceil(w / BS); BH = Math.ceil(h / BS); NB = BW * BH; blockP = new Uint8Array(NB); blockV = new Uint8Array(NB); slopePhase = new Uint8Array(NB); filtersDirty = true; filterCfg = new Map(); procDirty = true; procState.clear(); rackState.clear();
|
||||||
ents = []; flyers = []; ghostBlocks = new Map(); ghostMolds = []; treeBases = []; saplings = [];
|
ents = []; flyers = []; ghostBlocks = new Map(); ghostMolds = []; treeBases = []; saplings = [];
|
||||||
surfBase = Math.round(Math.min(h * 0.27, 175)); underD = h - surfBase; shallowD = 0; lapse = 0.36;
|
surfBase = Math.round(Math.min(h * 0.27, 175)); underD = h - surfBase; shallowD = 0; lapse = 0.36;
|
||||||
surfY = new Int16Array(w); roofY = new Int16Array(w); dirtY = new Int16Array(w); graniteY = new Int16Array(w); basaltY = new Int16Array(w); magmaY = new Int16Array(w);
|
surfY = new Int16Array(w); roofY = new Int16Array(w); dirtY = new Int16Array(w); graniteY = new Int16Array(w); basaltY = new Int16Array(w); magmaY = new Int16Array(w);
|
||||||
AMB = new Float32Array(h);
|
AMB = new Float32Array(h);
|
||||||
loadWakeOrder = loadWakePending = null; loadWakeIndex = 0;
|
loadWakeOrder = loadWakePending = null; loadWakeIndex = 0;
|
||||||
|
cleatChunks = new Uint8Array(NCH);
|
||||||
awake = new Uint8Array(NCH); hot = new Uint8Array(NCH); hotLvl = new Uint8Array(NCH);
|
awake = new Uint8Array(NCH); hot = new Uint8Array(NCH); hotLvl = new Uint8Array(NCH);
|
||||||
roofDirty = new Uint8Array(CW).fill(1);
|
roofDirty = new Uint8Array(CW).fill(1);
|
||||||
powderArea = new Uint8Array(NCH).fill(1); // unknown areas get one visit before being ruled out
|
powderArea = new Uint8Array(NCH).fill(1); // unknown areas get one visit before being ruled out
|
||||||
machines = [null]; molds = []; nets = [null]; pcells = new Set(); powerDirty = true;
|
machines = [null]; molds = []; nets = [null]; pcells = new Set(); powerDirty = true;
|
||||||
beltL = []; beltR = []; tubeLists = [[], [], [], [], []]; brokenMachines = [];
|
beltL = []; beltR = []; screwLists = Array.from({ length: IS_SCREW.length }, () => []); brokenMachines = [];
|
||||||
ghostMachines = []; ghostBlocked = new Set();
|
ghostMachines = []; ghostBlocked = new Set();
|
||||||
tick = 0; tickStamp = 1; drillPulses = []; lastPulseTick = -99; undoStack = []; undoRec = null;
|
tick = 0; tickStamp = 1; drillPulses = []; lastPulseTick = -99; undoStack = []; undoRec = null;
|
||||||
pipeNets = []; pipeNetAt = null; pipesDirty = true;
|
pipeNets = []; pipeNetAt = null; pipesDirty = true;
|
||||||
|
|||||||
+2
-2
@@ -245,7 +245,7 @@ function* worldSteps(seedIn, w, h) {
|
|||||||
yield [0.75, 'Settling loose ground'];
|
yield [0.75, 'Settling loose ground'];
|
||||||
// settle loose material at the water and sorting speeds the seeds were made with
|
// settle loose material at the water and sorting speeds the seeds were made with
|
||||||
awake.fill(3);
|
awake.fill(3);
|
||||||
const lstep = LSTEP, lfilm = LFILM, sortp = SORTP; LSTEP = LFILM = 1; SORTP = 0.003;
|
const lstep = LSTEP, lfilm = LFILM, sortp = SORTP, cornerSeal = CORNER_SEAL; LSTEP = LFILM = 1; SORTP = 0.003; CORNER_SEAL = false;
|
||||||
for (let k = 0; k < 260; k++) {
|
for (let k = 0; k < 260; k++) {
|
||||||
tick++; tickStamp = (tickStamp % 255) + 1;
|
tick++; tickStamp = (tickStamp % 255) + 1;
|
||||||
simMove();
|
simMove();
|
||||||
@@ -253,7 +253,7 @@ function* worldSteps(seedIn, w, h) {
|
|||||||
if (!any) break;
|
if (!any) break;
|
||||||
if ((k & 31) === 0) yield [0.75 + 0.24 * k / 260, 'Settling loose ground'];
|
if ((k & 31) === 0) yield [0.75 + 0.24 * k / 260, 'Settling loose ground'];
|
||||||
}
|
}
|
||||||
LSTEP = lstep; LFILM = lfilm; SORTP = sortp;
|
LSTEP = lstep; LFILM = lfilm; SORTP = sortp; CORNER_SEAL = cornerSeal;
|
||||||
yield [0.99, 'Finishing'];
|
yield [0.99, 'Finishing'];
|
||||||
// re-ambient after settling, then start clean
|
// re-ambient after settling, then start clean
|
||||||
for (let i = 0; i < N; i++) { const m = mat[i]; if (m !== MAGMA) temp[i] = AMB[(i / w) | 0]; life[i] = PH[m] === PH_LIQUID && FRZ[m] ? 255 : 0; }
|
for (let i = 0; i < N; i++) { const m = mat[i]; if (m !== MAGMA) temp[i] = AMB[(i / w) | 0]; life[i] = PH[m] === PH_LIQUID && FRZ[m] ? 255 : 0; }
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#dev-console {
|
||||||
|
position: fixed; z-index: 15; bottom: 0; left: 0; right: 0;
|
||||||
|
padding: 12px 18px; border-radius: 0; border-top: 2px solid var(--patina);
|
||||||
|
background: rgba(15, 13, 12, 0.97); max-height: 55vh;
|
||||||
|
}
|
||||||
|
.dev-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
||||||
|
.dev-head strong { color: var(--patina); }
|
||||||
|
.dev-head button { margin-left: auto; }
|
||||||
|
#dev-output {
|
||||||
|
height: 160px; max-height: 28vh; overflow-y: auto; margin: 8px 0;
|
||||||
|
font: 12px/1.6 var(--font-mono); white-space: pre-wrap; overflow-wrap: anywhere;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
#dev-form { display: flex; align-items: center; gap: 10px; }
|
||||||
|
#dev-input { flex: 1; min-width: 0; font-family: var(--font-mono); }
|
||||||
|
.dev-badge { display: inline-block; padding: 1px 6px; border: 1px solid var(--patina-dim); border-radius: 4px; color: var(--patina); font-size: 11px; }
|
||||||
+37
-20
@@ -27,16 +27,16 @@ function doInteract() {
|
|||||||
if (fb >= 0) {
|
if (fb >= 0) {
|
||||||
if (!inReach(ax, ay)) { setStatus('Out of reach.'); playSfx('error'); return; }
|
if (!inReach(ax, ay)) { setStatus('Out of reach.'); playSfx('error'); return; }
|
||||||
const run = filterRun(fb);
|
const run = filterRun(fb);
|
||||||
openFilterPicker(Object.assign(filterInfo(fb), { dir: blockV[fb] & 128 ? 1 : 0 }), f => {
|
openFilterPicker(Object.assign(filterInfo(fb), { dir: blockV[fb] & 128 ? 1 : 0, code: blockP[fb] }), f => {
|
||||||
applyFilterRun(run, f);
|
applyFilterRun(run, f);
|
||||||
GAME.filterCfg = { m: f.m.slice(), inv: f.inv }; GAME.pieceDir.filter = f.dir;
|
GAME.filterCfg = { m: f.m.slice(), inv: f.inv }; GAME.pieceDir.filter = f.dir;
|
||||||
setStatus(`${run.length} ${run.length === 1 ? 'filter' : 'filters'} updated, carrying ${f.dir ? 'right' : 'left'}.`, 'info');
|
setStatus(`${run.length} ${run.length === 1 ? 'filter' : 'filters'} updated, carrying ${beltDirection(blockP[fb], f.dir ? 128 : 0)}.`, 'info');
|
||||||
}, run.length > 1 ? `Filter (${run.length} in this row)` : 'Filter');
|
}, run.length > 1 ? `Filter (${run.length} in this row)` : 'Filter');
|
||||||
GAME.editRun = run;
|
GAME.editRun = run;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (toolId() === 'build' && pieceUsesWall(piece())) { openWallPicker(); return; }
|
if (toolId() === 'build' && pieceUsesWall(piece())) { openWallPicker(); return; }
|
||||||
if (toolId() === 'build' && piece().filter) { openFilterPicker(Object.assign({}, GAME.filterCfg, { dir: GAME.pieceDir.filter || 0 }), f => { GAME.filterCfg = { m: f.m, inv: f.inv }; GAME.pieceDir.filter = f.dir; }, 'New filters'); return; }
|
if (toolId() === 'build' && piece().filter) { openFilterPicker(Object.assign({}, GAME.filterCfg, { dir: GAME.pieceDir.filter || 0, code: pieceCode(piece()) }), f => { GAME.filterCfg = { m: f.m, inv: f.inv }; GAME.pieceDir.filter = f.dir; }, 'New filters'); return; }
|
||||||
let msg = '';
|
let msg = '';
|
||||||
outer: for (let r = 0; r <= 2; r++) for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) {
|
outer: for (let r = 0; r <= 2; r++) for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) {
|
||||||
if (!inReach(ax + dx, ay + dy)) continue;
|
if (!inReach(ax + dx, ay + dy)) continue;
|
||||||
@@ -51,17 +51,20 @@ function doInteract() {
|
|||||||
function pickUnderCursor() {
|
function pickUnderCursor() {
|
||||||
const ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y);
|
const ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y);
|
||||||
if (ax < 0 || ay < 0 || ax >= W || ay >= H) return;
|
if (ax < 0 || ay < 0 || ax >= W || ay >= H) return;
|
||||||
const i = ay * W + ax, m = mat[i], bi = ((ay / BS) | 0) * BW + ((ax / BS) | 0);
|
const i = ay * W + ax, m = mat[i];
|
||||||
let pid = null;
|
let bi = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pid = null;
|
||||||
|
// a sloped belt's band is drawn across its block's edge, so the belt you see can lie in the next block
|
||||||
|
if (!blockP[bi] && !ghostBlocks.has(bi)) { const sb = slopeBeltAt(i, ax, ay, R.smoothOn); if (sb >= 0) bi = sb; }
|
||||||
const ghost = ghostBlocks.get(bi);
|
const ghost = ghostBlocks.get(bi);
|
||||||
if (mach[i] && machines[mach[i]]) { const mm = machines[mach[i]]; pid = mm.type; if (PIECE_BY_ID[pid].dirs) GAME.pieceDir[pid] = mm.dir; }
|
if (mach[i] && machines[mach[i]]) { const mm = machines[mach[i]]; pid = mm.type; if (PIECE_BY_ID[pid].dirs) GAME.pieceDir[pid] = mm.dir; }
|
||||||
else if (moldAt(ax, ay)) { const mo = moldAt(ax, ay); pid = mo.kind + 'mold'; GAME.wallMat = mo.wall; }
|
else if (moldAt(ax, ay)) { const mo = moldAt(ax, ay); pid = mo.kind + 'mold'; GAME.wallMat = mo.wall; }
|
||||||
else if (blockP[bi] || ghost) {
|
else if (blockP[bi] || ghost) {
|
||||||
const code = blockP[bi] || ghost[0], v = blockP[bi] ? blockV[bi] : ghost[1];
|
const code = blockP[bi] || ghost[0], v = blockP[bi] ? blockV[bi] : ghost[1];
|
||||||
const P = PIECE_BY_CODE[code]; pid = P.id;
|
const P = PIECE_BY_CODE[code]; pid = P.id;
|
||||||
if (P.code === BP_WALL) { GAME.wallMat = v; }
|
if (isBelt(code) || isFilter(code)) { GAME.pieceShape = GAME.pieceShape || {}; GAME.pieceShape[P.id] = code; }
|
||||||
|
if (isWall(code)) { GAME.wallMat = v; GAME.pieceDir.wall = isSlope(code) ? code - BP_SLOPE_BL + 1 : 0; }
|
||||||
else if (P.filter) { GAME.filterCfg = blockP[bi] ? filterInfo(bi) : cleanFilterCfg(ghost[2]); GAME.pieceDir[P.id] = v >> 7; }
|
else if (P.filter) { GAME.filterCfg = blockP[bi] ? filterInfo(bi) : cleanFilterCfg(ghost[2]); GAME.pieceDir[P.id] = v >> 7; }
|
||||||
else if (P.dirs) GAME.pieceDir[P.id] = v & 3;
|
else if (P.dirs) GAME.pieceDir[P.id] = v % P.dirs.length;
|
||||||
}
|
}
|
||||||
if (pid) {
|
if (pid) {
|
||||||
const P = PIECE_BY_ID[pid];
|
const P = PIECE_BY_ID[pid];
|
||||||
@@ -94,12 +97,14 @@ function doUndo() {
|
|||||||
}
|
}
|
||||||
// right-click take-backs while the button is held count as one thing to undo
|
// right-click take-backs while the button is held count as one thing to undo
|
||||||
function endRmbRecord() { if (GAME.rmbRec) { const r = GAME.rmbRec; GAME.rmbRec = null; const n = r.ops.length; r.label = `taking back ${plural(n, 'piece')}`; undoPush(r); } }
|
function endRmbRecord() { if (GAME.rmbRec) { const r = GAME.rmbRec; GAME.rmbRec = null; const n = r.ops.length; r.label = `taking back ${plural(n, 'piece')}`; undoPush(r); } }
|
||||||
// Belts, filters and tubes run the way you drag them; a single click uses the direction R sets
|
// Belts, filters and screws run the way you drag them; a single click uses the direction R sets
|
||||||
function dragDirFor(P, a, b) {
|
function dragDirFor(P, a, b) {
|
||||||
if (!a || !P.dirs || !(P.id === 'belt' || P.id === 'tube' || P.filter)) return -1;
|
if (!a || !P.dirs || !(P.id === 'belt' || P.id === 'screw' || P.filter)) return -1;
|
||||||
const dx = b.bx - a.bx, dy = b.by - a.by;
|
const dx = b.bx - a.bx, dy = b.by - a.by;
|
||||||
if (!dx && !dy) return -1;
|
if (!dx && !dy) return -1;
|
||||||
if (P.id === 'tube') return Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0);
|
if ((P.id === 'belt' || P.filter) && diagonalDrag(dx, dy)) return P.filter ? (dx * dy > 0 ? 1 : 0) : (dx > 0 ? 1 : 0);
|
||||||
|
if (P.id === 'screw' && diagonalDrag(dx, dy)) return dx > 0 ? (dy < 0 ? 4 : 5) : (dy > 0 ? 6 : 7);
|
||||||
|
if (P.id === 'screw') return Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0);
|
||||||
return Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 1 : 0) : -1;
|
return Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 1 : 0) : -1;
|
||||||
}
|
}
|
||||||
function blockLabel(P, rec) {
|
function blockLabel(P, rec) {
|
||||||
@@ -126,7 +131,7 @@ function primaryPress(shift) {
|
|||||||
else {
|
else {
|
||||||
const { bw, bh } = pieceSize(P);
|
const { bw, bh } = pieceSize(P);
|
||||||
const { bx, by } = stampOrigin(bw, bh);
|
const { bx, by } = stampOrigin(bw, bh);
|
||||||
if (!inReach(bx * BS + bw * BS / 2, by * BS + bh * BS / 2)) { setStatus('Out of reach.'); playSfx('error'); return; }
|
if (!inBuildRange(bx * BS + bw * BS / 2, by * BS + bh * BS / 2)) { setStatus('Out of reach.'); playSfx('error'); return; }
|
||||||
const place = () => P.kind === 'mold' ? placeMoldAt(P.mold, bx, by, GAME.wallMat) : placeMachineAt(P.id, GAME.pieceDir[P.id] || 0, bx, by);
|
const place = () => P.kind === 'mold' ? placeMoldAt(P.mold, bx, by, GAME.wallMat) : placeMachineAt(P.id, GAME.pieceDir[P.id] || 0, bx, by);
|
||||||
const check = () => P.kind === 'mold' ? canPlaceMold(P.mold, bx, by) : canPlaceMachine(P.id, bx, by);
|
const check = () => P.kind === 'mold' ? canPlaceMold(P.mold, bx, by) : canPlaceMachine(P.id, bx, by);
|
||||||
let err = '';
|
let err = '';
|
||||||
@@ -161,21 +166,20 @@ function primaryRelease(shift) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (t === 'build' && GAME.drag) {
|
if (t === 'build' && GAME.drag) {
|
||||||
const P = piece(), b = aimBlock();
|
const P = piece(), plan = dragPlan(P, GAME.drag, aimBlock(), shift), { code, v, cells: list } = plan;
|
||||||
const list = shift ? blockRect(GAME.drag.bx, GAME.drag.by, b.bx, b.by) : blockLine(GAME.drag.bx, GAME.drag.by, b.bx, b.by);
|
if (plan.dd >= 0) GAME.pieceDir[P.id] = plan.dd;
|
||||||
const dd = dragDirFor(P, GAME.drag, b);
|
const ex = pieceExtra(P);
|
||||||
if (dd >= 0) GAME.pieceDir[P.id] = dd;
|
|
||||||
const v = pieceV(P), ex = pieceExtra(P);
|
|
||||||
// blocks past the bot's reach become ghosts it builds when it gets close, so a line can be any length
|
// blocks past the bot's reach become ghosts it builds when it gets close, so a line can be any length
|
||||||
const near = list.filter(([bx, by]) => blockInReach(bx, by)), far = list.filter(([bx, by]) => !blockInReach(bx, by));
|
const near = list.filter(([bx, by]) => blockInReach(bx, by)), far = list.filter(([bx, by]) => !blockInReach(bx, by));
|
||||||
let ghosts = 0;
|
let ghosts = 0;
|
||||||
const r = undoRecord(rec => blockLabel(P, rec) + (piecesIn(rec, 'paste') ? ' (and its ghosts)' : ''), () => {
|
const r = undoRecord(rec => blockLabel(P, rec) + (piecesIn(rec, 'paste') ? ' (and its ghosts)' : ''), () => {
|
||||||
const res = placeBlocks(P.code, v, near, ex, GAME.replace);
|
const res = placeBlocks(code, v, near, ex, GAME.replace);
|
||||||
const g = { t: 'paste', blocks: [], machines: [], molds: [] };
|
const g = { t: 'paste', blocks: [], machines: [], molds: [] };
|
||||||
for (const [bx, by] of far) {
|
for (const [bx, by] of far) {
|
||||||
const b = by * BW + bx;
|
const b = by * BW + bx;
|
||||||
if (bx < 0 || by < 0 || bx >= BW || by >= BH || blockIs(b, P.code, v, ex) || ghostBlocks.has(b)) continue;
|
if (bx < 0 || by < 0 || bx >= BW || by >= BH || blockIs(b, code, v, ex) || ghostBlocks.has(b)) continue;
|
||||||
ghostBlocks.set(b, ex ? [P.code, v, ex] : [P.code, v]); g.blocks.push([bx, by, P.code]); ghosts++;
|
// held G over a different piece: the ghost replaces it when the bot comes in range
|
||||||
|
ghostBlocks.set(b, GAME.replace && blockP[b] ? [code, v, ex, 1] : ex ? [code, v, ex] : [code, v]); g.blocks.push([bx, by, code]); ghosts++;
|
||||||
}
|
}
|
||||||
if (ghosts) undoNote(g);
|
if (ghosts) undoNote(g);
|
||||||
return res;
|
return res;
|
||||||
@@ -193,9 +197,22 @@ function secondaryPress(shift) {
|
|||||||
if (GAME.selDrag) { GAME.selDrag = null; hudDirty = true; GAME.rmbConsumed = true; }
|
if (GAME.selDrag) { GAME.selDrag = null; hudDirty = true; GAME.rmbConsumed = true; }
|
||||||
else if (GAME.paste) { GAME.paste = false; hudDirty = true; playSfx('ui'); GAME.rmbConsumed = true; }
|
else if (GAME.paste) { GAME.paste = false; hudDirty = true; playSfx('ui'); GAME.rmbConsumed = true; }
|
||||||
else if (t === 'build' && GAME.drag) { GAME.drag = null; GAME.dragDir = -1; GAME.rmbConsumed = true; }
|
else if (t === 'build' && GAME.drag) { GAME.drag = null; GAME.dragDir = -1; GAME.rmbConsumed = true; }
|
||||||
|
else if (t === 'drill') { firePing(); GAME.rmbConsumed = true; }
|
||||||
else { GAME.rmbConsumed = false; if (t === 'build') { endRmbRecord(); GAME.rmbRec = { label: '', ops: [] }; } }
|
else { GAME.rmbConsumed = false; if (t === 'build') { endRmbRecord(); GAME.rmbRec = { label: '', ops: [] }; } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Right click with the drill: a resonance ping. The echo is drawn by drawPing (overlays.js).
|
||||||
|
const PING_RECHARGE = 600;
|
||||||
|
// Seconds until the ping is ready again (0: ready).
|
||||||
|
function pingWait() { return GAME.pingTick !== undefined && tick >= GAME.pingTick ? Math.max(0, Math.ceil((PING_RECHARGE - (tick - GAME.pingTick)) / 60)) : 0; }
|
||||||
|
function firePing() {
|
||||||
|
if (pingWait()) { setStatus(`The ping is recharging: ${pingWait()} s.`); playSfx('error'); return; }
|
||||||
|
const p = resonancePing(bot.x + bot.w / 2, bot.y + bot.h / 2);
|
||||||
|
GAME.ping = Object.assign(p, { t0: performance.now() }); GAME.pingTick = tick;
|
||||||
|
playSfx('pulse', 2);
|
||||||
|
const names = p.found.map(m => MD[m].name.toLowerCase());
|
||||||
|
setStatus(names.length ? `Ping: ${names.slice(0, 3).join(', ')}${names.length > 3 ? ' and more' : ''} nearby.` : `Ping: no veins within ${Math.round(p.r / BS)} blocks.`, 'info');
|
||||||
|
}
|
||||||
// continuous actions, once per simulation tick
|
// continuous actions, once per simulation tick
|
||||||
function toolTick() {
|
function toolTick() {
|
||||||
const t = toolId(), ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y);
|
const t = toolId(), ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y);
|
||||||
@@ -227,7 +244,7 @@ function toolTick() {
|
|||||||
if (r.placed) { playSfx('blow'); hudDirty = true; } else if (r.status) setStatus(r.status);
|
if (r.placed) { playSfx('blow'); hudDirty = true; } else if (r.status) setStatus(r.status);
|
||||||
} else if (t === 'build') {
|
} else if (t === 'build') {
|
||||||
const key = `${(ax / BS) | 0},${(ay / BS) | 0}`;
|
const key = `${(ax / BS) | 0},${(ay / BS) | 0}`;
|
||||||
if (!GAME.rmbSeen.has(key) && inReach(ax, ay)) {
|
if (!GAME.rmbSeen.has(key) && inBuildRange(ax, ay)) {
|
||||||
GAME.rmbSeen.add(key);
|
GAME.rmbSeen.add(key);
|
||||||
const keep = undoRec; undoRec = GAME.rmbRec;
|
const keep = undoRec; undoRec = GAME.rmbRec;
|
||||||
const msg = removePieceAt(ax, ay);
|
const msg = removePieceAt(ax, ay);
|
||||||
|
|||||||
+18
-11
@@ -2,7 +2,7 @@
|
|||||||
// ---------- Build menu ----------
|
// ---------- Build menu ----------
|
||||||
const BUILD_CATS = [
|
const BUILD_CATS = [
|
||||||
['Structure', ['wall', 'grate', 'gate']],
|
['Structure', ['wall', 'grate', 'gate']],
|
||||||
['Moving', ['belt', 'filter', 'tube']],
|
['Moving', ['belt', 'filter', 'screw']],
|
||||||
['Liquids', ['pipe', 'pump', 'outlet']],
|
['Liquids', ['pipe', 'pump', 'outlet']],
|
||||||
['Machines', ['fan', 'grinder', 'saw', 'press', 'rack', 'silo']],
|
['Machines', ['fan', 'grinder', 'saw', 'press', 'rack', 'silo']],
|
||||||
['Molds', ['ingotmold', 'bitmold', 'tankmold']],
|
['Molds', ['ingotmold', 'bitmold', 'tankmold']],
|
||||||
@@ -12,9 +12,9 @@ const PIECE_ICONS = {
|
|||||||
wall: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M3 12h18M9 5v7M15 12v7"/>',
|
wall: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M3 12h18M9 5v7M15 12v7"/>',
|
||||||
grate: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M7.5 5v14M12 5v14M16.5 5v14"/>',
|
grate: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M7.5 5v14M12 5v14M16.5 5v14"/>',
|
||||||
gate: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M9 5v14"/><path d="M12 12h6M16 9.5l2.5 2.5-2.5 2.5"/>',
|
gate: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M9 5v14"/><path d="M12 12h6M16 9.5l2.5 2.5-2.5 2.5"/>',
|
||||||
belt: '<rect x="2" y="9" width="20" height="8" rx="2"/><circle cx="7" cy="13" r="2"/><circle cx="17" cy="13" r="2"/><path d="M9 5h7M14 3l2 2-2 2"/>',
|
belt: '<g transform="rotate(-45 12 12)"><rect x="2" y="7" width="20" height="10" rx="2"/><path d="M4 9h16M4 15h16M3 7V3Q5 1 7 3T11 3T15 3T19 3H21V7M7 7V3M13 7V3M19 7V3"/><circle cx="6" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="18" cy="12" r="2"/></g>',
|
||||||
filter: '<rect x="2" y="10" width="20" height="8" rx="2"/><path d="M7 10v8M12 10v8M17 10v8M2 14h20"/><path d="M12 2v5M10 5l2 2 2-2"/>',
|
filter: '<g transform="rotate(45 12 12)"><rect x="2" y="7" width="20" height="10" rx="2"/><path d="M4 9h16M4 15h16M6 9v6M10 9v6M14 9v6M18 9v6M4 12h16"/></g>',
|
||||||
tube: '<rect x="3" y="6" width="18" height="12" rx="2"/><path d="M6.5 18l3.5-12M11.5 18l3.5-12M16.5 18l3-10"/>',
|
screw: '<rect x="3" y="6" width="18" height="12" rx="2"/><path d="M6.5 18l3.5-12M11.5 18l3.5-12M16.5 18l3-10"/>',
|
||||||
pipe: '<path d="M2 10h20M2 14h20"/><path d="M6 7.5v9M18 7.5v9"/>',
|
pipe: '<path d="M2 10h20M2 14h20"/><path d="M6 7.5v9M18 7.5v9"/>',
|
||||||
pump: '<circle cx="11" cy="14" r="6"/><path d="M11 14l3.5-2.5M11 14l-1 4.2M11 14l-4-1.2"/><path d="M15.5 9.5L20 5M16 5h4v4"/>',
|
pump: '<circle cx="11" cy="14" r="6"/><path d="M11 14l3.5-2.5M11 14l-1 4.2M11 14l-4-1.2"/><path d="M15.5 9.5L20 5M16 5h4v4"/>',
|
||||||
outlet: '<path d="M2 7h12a4 4 0 0 1 4 4v3"/><path d="M2 11h10a2 2 0 0 1 2 2v1"/><path d="M15 18v1.5M18 20v1.5M12 20v1.5"/>',
|
outlet: '<path d="M2 7h12a4 4 0 0 1 4 4v3"/><path d="M2 11h10a2 2 0 0 1 2 2v1"/><path d="M15 18v1.5M18 20v1.5M12 20v1.5"/>',
|
||||||
@@ -28,7 +28,12 @@ const PIECE_ICONS = {
|
|||||||
bitmold: '<path d="M3 9h10l7 3-7 3H3z"/><path d="M6.5 9v6M9.5 9v6"/>',
|
bitmold: '<path d="M3 9h10l7 3-7 3H3z"/><path d="M6.5 9v6M9.5 9v6"/>',
|
||||||
tankmold: '<rect x="5" y="7" width="14" height="13" rx="3"/><path d="M9 4h6v3M8.5 12h7"/>',
|
tankmold: '<rect x="5" y="7" width="14" height="13" rx="3"/><path d="M9 4h6v3M8.5 12h7"/>',
|
||||||
};
|
};
|
||||||
function pieceIcon(id) { return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round" aria-hidden="true">${PIECE_ICONS[id] || ''}</svg>`; }
|
function pieceIcon(id) {
|
||||||
|
const v = GAME.drag && GAME.dragDir >= 0 ? GAME.dragDir : GAME.pieceDir.screw || 0;
|
||||||
|
const body = PIECE_ICONS[id] || '';
|
||||||
|
const icon = id === 'screw' && v >= 4 ? `<g transform="rotate(${v === 4 || v === 6 ? -45 : 45} 12 12)">${body}</g>` : body;
|
||||||
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round" aria-hidden="true">${icon}</svg>`;
|
||||||
|
}
|
||||||
function catOf(id) { const c = BUILD_CATS.findIndex(([, ids]) => ids.includes(id)); return c < 0 ? 0 : c; }
|
function catOf(id) { const c = BUILD_CATS.findIndex(([, ids]) => ids.includes(id)); return c < 0 ? 0 : c; }
|
||||||
let lastBuildKey = '', lastBuildMenu = '';
|
let lastBuildKey = '', lastBuildMenu = '';
|
||||||
function selectBuildCat(c) {
|
function selectBuildCat(c) {
|
||||||
@@ -44,13 +49,15 @@ function cycleBuildCat(d) {
|
|||||||
}
|
}
|
||||||
function buildVariantChips(P) {
|
function buildVariantChips(P) {
|
||||||
const dir = P.dirs ? P.dirs[GAME.pieceDir[P.id] || 0] : null;
|
const dir = P.dirs ? P.dirs[GAME.pieceDir[P.id] || 0] : null;
|
||||||
// while dragging a line of belts, filters or tubes, the direction is the drag's
|
// while dragging a line of belts, filters or screws, the direction is the drag's
|
||||||
const dragging = GAME.drag && GAME.dragDir >= 0 && P.dirs;
|
const dragging = GAME.drag && GAME.dragDir >= 0 && P.dirs;
|
||||||
const glyph = d => ({ left: '←', right: '→', up: '↑', down: '↓' })[d] || '';
|
const glyph = d => ({ left: '←', right: '→', up: '↑', down: '↓', 'up-right': '↗', 'down-right': '↘', 'down-left': '↙', 'up-left': '↖' })[d] || '';
|
||||||
const dirChip = !P.dirs ? '' : dragging ? `<span class="chip ok">${glyph(P.dirs[GAME.dragDir])} ${esc(P.dirs[GAME.dragDir])} · from your drag</span>` : `<span class="chip hot">${glyph(dir)} ${esc(dir)} · R</span>`;
|
const code = pieceCode(P), direction = dragging ? GAME.dragDir : GAME.pieceDir[P.id] || 0;
|
||||||
|
const slopeDir = isSlopeBelt(code) ? beltDirection(code, P.filter ? direction << 7 : direction) : '';
|
||||||
|
const dirChip = !P.dirs ? '' : dragging ? `<span class="chip ok">${glyph(slopeDir || P.dirs[GAME.dragDir])} ${esc(slopeDir || P.dirs[GAME.dragDir])} · from your drag</span>` : `<span class="chip hot">${glyph(slopeDir || dir)} ${esc(slopeDir || dir)} · R</span>`;
|
||||||
const fc = GAME.filterCfg;
|
const fc = GAME.filterCfg;
|
||||||
const sws = ms => ms.slice(0, 6).map(m => `<span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:2px;background:${rgbOf(m)}"></span>`).join('') + (ms.length > 6 ? `+${ms.length - 6}` : '');
|
const sws = ms => ms.slice(0, 6).map(m => `<span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:2px;background:${rgbOf(m)}"></span>`).join('') + (ms.length > 6 ? `+${ms.length - 6}` : '');
|
||||||
return pieceUsesWall(P) ? `<button class="chip hot" data-wall-picker><span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:4px;background:${rgbOf(WALL_OF[GAME.wallMat])}"></span>${esc(MD[WALL_OF[GAME.wallMat]].name.toLowerCase())} · R · F</button>`
|
return pieceUsesWall(P) ? `<button class="chip hot" data-wall-picker=""><span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:4px;background:${rgbOf(WALL_OF[GAME.wallMat])}"></span>${esc(MD[WALL_OF[GAME.wallMat]].name.toLowerCase())} · R · F</button>${P.code === BP_WALL ? `<span class="chip hot">${['square', '◣ lower left', '◢ lower right', '◤ upper left', '◥ upper right'][isSlope(pieceCode(P)) ? pieceCode(P) - BP_SLOPE_BL + 1 : 0]} · T · turn / flip</span>` : ''}`
|
||||||
: P.filter ? `<span class="chip hot">${fc.inv ? 'carries' : 'drops'} ${fc.m.length ? sws(fc.m) : 'nothing'} · F</span>${dirChip}`
|
: P.filter ? `<span class="chip hot">${fc.inv ? 'carries' : 'drops'} ${fc.m.length ? sws(fc.m) : 'nothing'} · F</span>${dirChip}`
|
||||||
: dir ? dirChip : '';
|
: dir ? dirChip : '';
|
||||||
}
|
}
|
||||||
@@ -62,7 +69,7 @@ function renderBuildBar() {
|
|||||||
const P = piece(), cat = GAME.buildCat != null ? GAME.buildCat : catOf(P.id), hov = GAME.bbHover;
|
const P = piece(), cat = GAME.buildCat != null ? GAME.buildCat : catOf(P.id), hov = GAME.bbHover;
|
||||||
const ids = BUILD_CATS[cat][1], tip = PIECE_BY_ID[GAME.usingPad ? P.id : hov && ids.includes(hov) ? hov : ''];
|
const ids = BUILD_CATS[cat][1], tip = PIECE_BY_ID[GAME.usingPad ? P.id : hov && ids.includes(hov) ? hov : ''];
|
||||||
const afford = ids.map(id => canAfford(pieceCostNow(PIECE_BY_ID[id]), 1) ? 1 : 0).join('');
|
const afford = ids.map(id => canAfford(pieceCostNow(PIECE_BY_ID[id]), 1) ? 1 : 0).join('');
|
||||||
const key = [cat, P.id, tip && tip.id, afford, GAME.wallMat, Math.round(money), GAME.pieceDir[P.id], GAME.filterCfg.inv, GAME.filterCfg.m.join('.'), innerWidth, innerHeight, SET.uiScale].join('|');
|
const key = [cat, P.id, tip && tip.id, afford, GAME.wallMat, Math.round(money), GAME.pieceDir[P.id], GAME.dragDir, GAME.previewCode, GAME.filterCfg.inv, GAME.filterCfg.m.join('.'), innerWidth, innerHeight, SET.uiScale].join('|');
|
||||||
if (key === lastBuildKey) return;
|
if (key === lastBuildKey) return;
|
||||||
lastBuildKey = key;
|
lastBuildKey = key;
|
||||||
let h = '<div class="bb-head">' + BUILD_CATS.map(([name], k) => `<button class="bb-tab${k === cat ? ' on' : ''}" data-cat="${k}">${esc(name)}</button>`).join('')
|
let h = '<div class="bb-head">' + BUILD_CATS.map(([name], k) => `<button class="bb-tab${k === cat ? ' on' : ''}" data-cat="${k}">${esc(name)}</button>`).join('')
|
||||||
@@ -80,7 +87,7 @@ function renderBuildBar() {
|
|||||||
if (!tip) return;
|
if (!tip) return;
|
||||||
const size = tip.kind === 'machine' ? `${tip.bw}×${tip.bh} blocks` : tip.kind === 'mold' ? `${MOLDS[tip.mold].bw}×${MOLDS[tip.mold].bh} blocks` : '1 block';
|
const size = tip.kind === 'machine' ? `${tip.bw}×${tip.bh} blocks` : tip.kind === 'mold' ? `${MOLDS[tip.mold].bw}×${MOLDS[tip.mold].bh} blocks` : '1 block';
|
||||||
pop.innerHTML = `<div class="bb-title">${pieceIcon(tip.id)}<b>${esc(tip.name)}</b></div><div class="bb-chips">${chipCost(pieceCostNow(tip))}<span class="chip">${size}</span>${tip.power ? `<span class="chip">${tip.power} power</span>` : ''}</div>
|
pop.innerHTML = `<div class="bb-title">${pieceIcon(tip.id)}<b>${esc(tip.name)}</b></div><div class="bb-chips">${chipCost(pieceCostNow(tip))}<span class="chip">${size}</span>${tip.power ? `<span class="chip">${tip.power} power</span>` : ''}</div>
|
||||||
<div class="bb-desc">${esc(tip.desc)}</div><div class="bb-place"><b>How to place</b> ${tip.kind === 'block' ? 'Drag for a line, Shift-drag for a rectangle; out of reach becomes ghosts.' : 'Click to place.'}${tip.dirs && tip.kind === 'block' ? ' A dragged line runs the way you drag it; a single click uses <kbd>R</kbd>.' : tip.dirs ? ' <kbd>R</kbd> turns it.' : pieceUsesWall(tip) ? ' <kbd>R</kbd> cycles materials; <kbd>F</kbd> opens the material menu.' : ''}</div>
|
<div class="bb-desc">${esc(tip.desc)}</div><div class="bb-place"><b>How to place</b> ${tip.kind === 'block' ? (lineOnly(tip.code) ? 'Drag for a line' : 'Drag for a line, Shift-drag for a rectangle') + '; out of reach becomes ghosts.' : 'Click to place.'}${tip.dirs && tip.kind === 'block' ? ' A dragged line runs the way you drag it; a single click uses <kbd>R</kbd>.' : tip.dirs ? ' <kbd>R</kbd> turns it.' : pieceUsesWall(tip) ? ' <kbd>R</kbd> cycles materials; <kbd>F</kbd> opens the material menu.' : ''}</div>
|
||||||
${tip.filter ? `<div class="bb-place">New ones will ${esc(filterSentence(GAME.filterCfg))}. <kbd>F</kbd> sets materials and mode (or click materials in the bag, or <kbd>T</kbd> over one); <kbd>F</kbd> on a placed filter changes its whole row.</div>` : ''}
|
${tip.filter ? `<div class="bb-place">New ones will ${esc(filterSentence(GAME.filterCfg))}. <kbd>F</kbd> sets materials and mode (or click materials in the bag, or <kbd>T</kbd> over one); <kbd>F</kbd> on a placed filter changes its whole row.</div>` : ''}
|
||||||
<div class="bb-place">hold <kbd>G</kbd> replace · <kbd>Ctrl</kbd>+<kbd>Z</kbd> undo · right click take back · middle mouse pick · hold <kbd>C</kbd>/<kbd>X</kbd> copy/cut · <kbd>V</kbd> paste</div>`;
|
<div class="bb-place">hold <kbd>G</kbd> replace · <kbd>Ctrl</kbd>+<kbd>Z</kbd> undo · right click take back · middle mouse pick · hold <kbd>C</kbd>/<kbd>X</kbd> copy/cut · <kbd>V</kbd> paste</div>`;
|
||||||
const tile = bar.querySelector(`[data-piece="${tip.id}"]`); if (!tile) { pop.hidden = true; return; }
|
const tile = bar.querySelector(`[data-piece="${tip.id}"]`); if (!tile) { pop.hidden = true; return; }
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// Bottom panel; command dispatch and state live in sim/devconsole.js.
|
||||||
|
function closeDevConsole() {
|
||||||
|
$('dev-console').hidden = true;
|
||||||
|
$('dev-input').blur();
|
||||||
|
}
|
||||||
|
function showDevConsole() {
|
||||||
|
$('dev-console').hidden = false;
|
||||||
|
if (!$('dev-output').children.length) devPrint(['Type help to list commands. Backtick or Escape closes this panel.']);
|
||||||
|
$('dev-input').focus();
|
||||||
|
}
|
||||||
|
function toggleDevConsole() {
|
||||||
|
if (!GAME.running || GAME.loading) return;
|
||||||
|
if (!$('dev-console').hidden) { closeDevConsole(); return; }
|
||||||
|
if (devWorld) { showDevConsole(); return; }
|
||||||
|
dialog('Enable dev console?', '<p>This save will be permanently marked as a dev-console world. If achievements are added later, they will not unlock in this world.</p><p>You can change money, materials, upgrades and game speed. These commands are logged but cannot be undone.</p>', [
|
||||||
|
{ label: 'Cancel' },
|
||||||
|
{ label: 'Enable dev console', cls: 'primary', fn: () => {
|
||||||
|
enableDevConsole(); closeModal('m-dialog'); showDevConsole(); saveWorld('main'); return true;
|
||||||
|
} },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
function devPrint(lines) {
|
||||||
|
const log = $('dev-output');
|
||||||
|
for (const line of lines) { const row = document.createElement('div'); row.textContent = line; log.appendChild(row); }
|
||||||
|
while (log.children.length > 400) log.firstChild.remove();
|
||||||
|
log.scrollTop = log.scrollHeight;
|
||||||
|
}
|
||||||
|
function runDevCommand(text) {
|
||||||
|
devPrint(devCommand(text, GAME.aim));
|
||||||
|
hudDirty = true; lastBagKey = ''; lastBuildKey = '';
|
||||||
|
renderWallet();
|
||||||
|
if (!$('m-notebook').hidden) renderNotebook();
|
||||||
|
}
|
||||||
|
$('dev-close').onclick = closeDevConsole;
|
||||||
|
$('dev-input').onfocus = () => { keys.clear(); GAME.lmb = GAME.rmb = false; GAME.drag = null; GAME.replace = false; };
|
||||||
|
$('dev-input').onkeydown = e => {
|
||||||
|
if (e.key === 'Escape' || e.key === '`') { e.preventDefault(); e.stopPropagation(); closeDevConsole(); }
|
||||||
|
};
|
||||||
|
$('dev-form').onsubmit = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = $('dev-input'); runDevCommand(input.value); input.value = ''; input.focus();
|
||||||
|
};
|
||||||
+7
-1
@@ -26,7 +26,13 @@ function selectTool(k) {
|
|||||||
if (k === GAME.tool) return;
|
if (k === GAME.tool) return;
|
||||||
GAME.tool = k; GAME.drag = null; hudDirty = true; renderHotbar(); playSfx('ui');
|
GAME.tool = k; GAME.drag = null; hudDirty = true; renderHotbar(); playSfx('ui');
|
||||||
}
|
}
|
||||||
function bagMaterials() { return BAG_ORDER.filter(m => bag[m] > 0); }
|
// The bag's usual order first, then anything else it holds, so nothing in it is ever hidden.
|
||||||
|
const IN_BAG_ORDER = new Uint8Array(NMAT); for (const m of BAG_ORDER) IN_BAG_ORDER[m] = 1;
|
||||||
|
function bagMaterials() {
|
||||||
|
const l = BAG_ORDER.filter(m => bag[m] > 0);
|
||||||
|
for (let m = 1; m < NMAT; m++) if (bag[m] > 0 && !IN_BAG_ORDER[m]) l.push(m);
|
||||||
|
return l;
|
||||||
|
}
|
||||||
function blowList() { return [...GAME.blow].filter(m => bag[m] > 0); }
|
function blowList() { return [...GAME.blow].filter(m => bag[m] > 0); }
|
||||||
// Shift + right click: an even mix of everything loose in the bag (bars never mix)
|
// Shift + right click: an even mix of everything loose in the bag (bars never mix)
|
||||||
function blowAllList() { return bagMaterials().filter(m => PH[m] !== PH_ITEM && (PH[m] !== PH_LIQUID || player.flask)); }
|
function blowAllList() { return bagMaterials().filter(m => PH[m] !== PH_ITEM && (PH[m] !== PH_LIQUID || player.flask)); }
|
||||||
|
|||||||
+28
-15
@@ -81,17 +81,23 @@ function hoverInfo(ax, ay) {
|
|||||||
if (ax < 0 || ay < 0 || ax >= W || ay >= H) return { t1: '', t2: ['Outside the world'], warn: '', air: true };
|
if (ax < 0 || ay < 0 || ax >= W || ay >= H) return { t1: '', t2: ['Outside the world'], warn: '', air: true };
|
||||||
const i = ay * W + ax, m = mat[i];
|
const i = ay * W + ax, m = mat[i];
|
||||||
let warn = '';
|
let warn = '';
|
||||||
let t1 = MD[m].name, t2 = [];
|
let t1 = MD[m].name, t2 = [], contents = '';
|
||||||
const p = part[i], id = mach[i];
|
const p = part[i];
|
||||||
|
// Cone gaps have no machine cells; hover still belongs to the silo's whole outline.
|
||||||
|
const silo = machines.find(mm => mm && mm.type === 'silo' && ax >= mm.x && ax < mm.x + mm.w && ay >= mm.y && ay < mm.y + mm.h);
|
||||||
|
const id = silo ? silo.id : mach[i];
|
||||||
const b = blockP[((ay / BS) | 0) * BW + ((ax / BS) | 0)];
|
const b = blockP[((ay / BS) | 0) * BW + ((ax / BS) | 0)];
|
||||||
const bar = occ[i] ? ents.find(e => ax >= e.ix && ax < e.ix + BAR_W && ay >= e.iy && ay < e.iy + BAR_H) : null;
|
const bar = occ[i] ? ents.find(e => ax >= e.ix && ax < e.ix + BAR_W && ay >= e.iy && ay < e.iy + BAR_H) : null;
|
||||||
if (bar) { t1 = MD[bar.m].name; t2.push(fmtT(bar.T)); }
|
if (bar && !silo) { t1 = MD[bar.m].name; t2.push(fmtT(bar.T)); }
|
||||||
else if (id && machines[id]) {
|
else if (id && machines[id]) {
|
||||||
const mm = machines[id], P = PIECE_BY_ID[mm.type]; t1 = P.name;
|
const mm = machines[id], P = PIECE_BY_ID[mm.type]; t1 = P.name;
|
||||||
if (mm.type === 'dock') { t2.push('sells what drops through its grate', `${orders.length} open ${orders.length === 1 ? 'order' : 'orders'}`); }
|
if (mm.type === 'dock') { t2.push('sells what drops through its grate', `${orders.length} open ${orders.length === 1 ? 'order' : 'orders'}`); }
|
||||||
if (mm.type === 'wheel' || mm.type === 'engine') t2.push(`making ${mm.out.toFixed(1)} power`);
|
if (mm.type === 'wheel' || mm.type === 'engine') t2.push(`making ${mm.out.toFixed(1)} power`);
|
||||||
if (mm.type === 'fan') { t2.push(mm.on ? 'on' : 'off · press F'); if (mm.hot > 760) { t2.push(`${mm.hot}°C · melts at 1085°C`); warn = 'too hot'; } }
|
if (mm.type === 'fan') { t2.push(mm.on ? 'on' : 'off · press F'); if (mm.hot > 760) { t2.push(`${mm.hot}°C · melts at 1085°C`); warn = 'too hot'; } }
|
||||||
if (mm.type === 'silo') t2.push(mm.sn ? `holds ${mm.sn.toLocaleString('en-US')} / ${SILO_CAP.toLocaleString('en-US')} ${MD[mm.sm].name.toLowerCase()}` : `empty (holds ${SILO_CAP.toLocaleString('en-US')} grains of one material)`, mm.open ? 'gate open · F shuts it' : 'gate shut · F opens it');
|
if (mm.type === 'silo') {
|
||||||
|
contents = mm.sn ? `${MD[mm.sm].name.toLowerCase()} ${mm.sn.toLocaleString('en-US')} / ${SILO_CAP.toLocaleString('en-US')}` : 'empty';
|
||||||
|
t2.push(mm.sn ? `holds ${mm.sn.toLocaleString('en-US')} / ${SILO_CAP.toLocaleString('en-US')} ${MD[mm.sm].name.toLowerCase()}` : `empty (holds ${SILO_CAP.toLocaleString('en-US')} grains of one material)`, mm.open ? 'gate open · F shuts it' : 'gate shut · F opens it');
|
||||||
|
}
|
||||||
} else if (b === BP_RACK) {
|
} else if (b === BP_RACK) {
|
||||||
const top = rackTop((ay >> 2) * BW + (ax >> 2)), counts = new Map();
|
const top = rackTop((ay >> 2) * BW + (ax >> 2)), counts = new Map();
|
||||||
let n = 0, capacity = 0;
|
let n = 0, capacity = 0;
|
||||||
@@ -109,14 +115,15 @@ function hoverInfo(ax, ay) {
|
|||||||
t1 = press ? 'Brick press' : saw ? 'Saw' : 'Grinder';
|
t1 = press ? 'Brick press' : saw ? 'Saw' : 'Grinder';
|
||||||
t2.push(s && s.blocked ? saw || press ? 'stopped: no room underneath; a belt there carries them away' : 'stopped: clear the space under it' : s && tick - s.busyT < 30 ? press ? 'pressing' : saw ? 'cutting' : 'grinding' : press ? 'idle: drop clay lumps on top' : saw ? 'idle: drop stone rubble or wood chunks on top' : 'idle: drop rubble, gravel, sand or dirt on top', press ? `${stats.pressed || 0} green bricks pressed` : saw ? `${(stats.bricks || 0).toLocaleString('en-US')} stone blocks, ${(stats.planks || 0).toLocaleString('en-US')} planks cut` : `${(stats.refined || 0).toLocaleString('en-US')} ground, ${(stats.refinedOre || 0).toLocaleString('en-US')} ore found`);
|
t2.push(s && s.blocked ? saw || press ? 'stopped: no room underneath; a belt there carries them away' : 'stopped: clear the space under it' : s && tick - s.busyT < 30 ? press ? 'pressing' : saw ? 'cutting' : 'grinding' : press ? 'idle: drop clay lumps on top' : saw ? 'idle: drop stone rubble or wood chunks on top' : 'idle: drop rubble, gravel, sand or dirt on top', press ? `${stats.pressed || 0} green bricks pressed` : saw ? `${(stats.bricks || 0).toLocaleString('en-US')} stone blocks, ${(stats.planks || 0).toLocaleString('en-US')} planks cut` : `${(stats.refined || 0).toLocaleString('en-US')} ground, ${(stats.refinedOre || 0).toLocaleString('en-US')} ore found`);
|
||||||
} else if (p === P_FILTER) {
|
} else if (p === P_FILTER) {
|
||||||
const fb = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pr = filterParts(filterInfo(fb)), ar = blockV[fb] & 128 ? '→' : '←';
|
const owner = slopeSealOwner(i), fb = owner >= 0 ? owner : ((ay / BS) | 0) * BW + ((ax / BS) | 0), pr = filterParts(filterInfo(fb)), ar = beltDirection(blockP[fb], blockV[fb]);
|
||||||
t1 = 'Filter ' + ar; t2.push(`drops ${pr.drops}`, `carries ${pr.rides} ${ar}`, 'F to change · I shows all');
|
t1 = 'Filter ' + ar; t2.push(`drops ${pr.drops}`, `carries ${pr.rides} ${ar}`, 'F to change · I shows all');
|
||||||
} else if (p === P_PIPE) {
|
} else if (p === P_PIPE) {
|
||||||
const pb = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pn = pipeNetOf(pb);
|
const pb = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pn = pipeNetOf(pb);
|
||||||
t1 = 'Pipe' + (m !== EMPTY ? ` · ${MD[m].name} flowing past` : '');
|
t1 = 'Pipe' + (m !== EMPTY ? ` · ${MD[m].name} flowing past` : '');
|
||||||
t2.push(pipeContentsText(pn));
|
t2.push(pipeContentsText(pn));
|
||||||
} else if (p) t1 = ({ 1: 'Screw tube ↑', 2: 'Screw tube ↓', 3: 'Screw tube ←', 4: 'Screw tube →', 5: 'Grate', 6: 'Gate (open)', 7: 'Water wheel', 8: 'Engine intake' })[p] + (m !== EMPTY ? ` · ${MD[m].name}` : '');
|
} else if (IS_SCREW[p]) t1 = 'Screw conveyor ' + PIECE_BY_ID.screw.dirs[SCREW_PARTS.indexOf(p)] + (m !== EMPTY ? ` · ${MD[m].name}` : '');
|
||||||
else if (m === BELTL || m === BELTR) t1 = 'Belt ' + (m === BELTL ? '←' : '→');
|
else if (p) t1 = ({ 5: 'Grate', 6: 'Gate (open)', 7: 'Water wheel', 8: 'Engine intake' })[p] + (m !== EMPTY ? ` · ${MD[m].name}` : '');
|
||||||
|
else if (m === BELTL || m === BELTR) { const owner = slopeSealOwner(i), fb = owner >= 0 ? owner : (ay >> 2) * BW + (ax >> 2); t1 = 'Belt ' + beltDirection(blockP[fb], blockV[fb]); }
|
||||||
else if (m === PIPE && isPipeCode(b)) {
|
else if (m === PIPE && isPipeCode(b)) {
|
||||||
const pb = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pn = pipeNetOf(pb), v = blockV[pb], arrows = ['↑', '→', '↓', '←'];
|
const pb = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pn = pipeNetOf(pb), v = blockV[pb], arrows = ['↑', '→', '↓', '←'];
|
||||||
t1 = b === BP_PUMP ? `Pump ${arrows[v & 3]}` : b === BP_OUTLET ? `Outlet ${['↓', '←', '↑', '→'][v & 3]}` : 'Pipe';
|
t1 = b === BP_PUMP ? `Pump ${arrows[v & 3]}` : b === BP_OUTLET ? `Outlet ${['↓', '←', '↑', '→'][v & 3]}` : 'Pipe';
|
||||||
@@ -124,7 +131,13 @@ function hoverInfo(ax, ay) {
|
|||||||
if (b === BP_PUMP) t2.push(v & 4 ? 'off · press F' : 'on · F turns it off');
|
if (b === BP_PUMP) t2.push(v & 4 ? 'off · press F' : 'on · F turns it off');
|
||||||
if (b === BP_OUTLET) t2.push(v & 4 ? 'closed · press F' : 'open · F closes it');
|
if (b === BP_OUTLET) t2.push(v & 4 ? 'closed · press F' : 'open · F closes it');
|
||||||
}
|
}
|
||||||
else if (b === BP_WALL && PH[m] === PH_SOLID && !(flags[i] & F_NAT)) t1 = `${MD[m].name} block`;
|
else if (isWall(b) && PH[m] === PH_SOLID && !(flags[i] & F_NAT)) t1 = `${MD[m].name} ${isSlope(b) ? 'sloped wall' : 'block'}`;
|
||||||
|
const jb = ((ay / BS) | 0) * BW + ((ax / BS) | 0);
|
||||||
|
if (GAME.infoOverlay && !id && !bar && pieceEnds(blockP[jb], blockV[jb]).length) t2.push(`moving ${(flowRate[jb] || 0).toFixed(1)} grains/s`);
|
||||||
|
for (let end = 0; end < 2 && !id && !bar; end++) {
|
||||||
|
const miss = nearMiss(jb % BW, (jb / BW) | 0, end);
|
||||||
|
if (miss) { t2.push(`doesn't connect: place it one block ${miss < 0 ? 'higher' : 'lower'}`); warn = warn || "doesn't connect"; break; }
|
||||||
|
}
|
||||||
const n = nets[netId[i]];
|
const n = nets[netId[i]];
|
||||||
if (n && isPow[i] && !POWER_FREE) t2.push(`network ${n.supply.toFixed(1)} supply / ${n.demand.toFixed(1)} demand`);
|
if (n && isPow[i] && !POWER_FREE) t2.push(`network ${n.supply.toFixed(1)} supply / ${n.demand.toFixed(1)} demand`);
|
||||||
if (player.lens && !bar) t2.push(fmtT(temp[i]));
|
if (player.lens && !bar) t2.push(fmtT(temp[i]));
|
||||||
@@ -134,9 +147,9 @@ function hoverInfo(ax, ay) {
|
|||||||
if (!ok && toolId() === 'drill') warn = HARD[m] > 6 ? 'cannot be drilled' : 'too hard';
|
if (!ok && toolId() === 'drill') warn = HARD[m] > 6 ? 'cannot be drilled' : 'too hard';
|
||||||
}
|
}
|
||||||
if (!bar && PH[m] === PH_POWDER) t2.push('loose');
|
if (!bar && PH[m] === PH_POWDER) t2.push('loose');
|
||||||
if (!bar && m === EMPTY && bgm[i]) { t1 = bgm[i] === WOOD ? 'Tree trunk' : 'Leaves'; t2.unshift(bgm[i] === WOOD ? 'drill it for wood chunks' : 'water and sand pass through'); }
|
if (!id && !bar && m === EMPTY && bgm[i]) { t1 = bgm[i] === WOOD ? 'Tree trunk' : 'Leaves'; t2.unshift(bgm[i] === WOOD ? 'drill it for wood chunks' : 'water and sand pass through'); }
|
||||||
if (flags[i] & F_BURN) t2.push('burning');
|
if (flags[i] & F_BURN) t2.push('burning');
|
||||||
const mo = moldAt(ax, ay);
|
const mo = silo ? null : moldAt(ax, ay);
|
||||||
if (mo) {
|
if (mo) {
|
||||||
t1 = MOLDS[mo.kind].name;
|
t1 = MOLDS[mo.kind].name;
|
||||||
t2 = [mo.state === 'ready' ? (mo.kind === 'ingot' ? `${mo.ready.length} ${mo.ready.length === 1 ? 'bar' : 'bars'} set` : '100% set · press F') : mo.state === 'empty' ? 'empty' : `${mo.state === 'short' ? 'not full' : mo.state} ${Math.round(mo.fill * 100)}%`];
|
t2 = [mo.state === 'ready' ? (mo.kind === 'ingot' ? `${mo.ready.length} ${mo.ready.length === 1 ? 'bar' : 'bars'} set` : '100% set · press F') : mo.state === 'empty' ? 'empty' : `${mo.state === 'short' ? 'not full' : mo.state} ${Math.round(mo.fill * 100)}%`];
|
||||||
@@ -145,12 +158,12 @@ function hoverInfo(ax, ay) {
|
|||||||
const hint = furnaceJunkHint(mo).trim(); if (hint) t2.push(hint);
|
const hint = furnaceJunkHint(mo).trim(); if (hint) t2.push(hint);
|
||||||
if (player.lens) t2.push(fmtT(temp[i]));
|
if (player.lens) t2.push(fmtT(temp[i]));
|
||||||
}
|
}
|
||||||
const reachOk = toolId() === 'drill' ? inDrillReach(ax, ay, GAME.drillW) : inReach(ax, ay);
|
const reachOk = toolId() === 'drill' || (toolId() === 'build' ? inBuildRange(ax, ay) : inReach(ax, ay));
|
||||||
if (!reachOk) { t2.push('out of reach'); warn = 'out of reach'; }
|
if (!reachOk) { t2.push('out of reach'); warn = 'out of reach'; }
|
||||||
// what it does, unless the cursor is on a machine or mold, which have their own details
|
// what it does, unless the cursor is on a machine or mold, which have their own details
|
||||||
const what = bar ? bar.m : !mo && !id && !(p && p !== P_GRATE && p !== P_PIPE) && m !== BELTL && m !== BELTR && m !== PIPE ? (m === EMPTY && bgm[i] ? bgm[i] : m) : 0;
|
const what = bar ? bar.m : !mo && !id && !(p && p !== P_GRATE && p !== P_PIPE) && m !== BELTL && m !== BELTR && m !== PIPE ? (m === EMPTY && bgm[i] ? bgm[i] : m) : 0;
|
||||||
const t3 = what ? reactionsOf(what) : [];
|
const t3 = what ? reactionsOf(what) : [];
|
||||||
return { t1, t2, t3, warn, air: m === EMPTY && !bgm[i] && !bar && !mo && !id && !p && !b };
|
return { t1, t2, t3, warn, contents, air: m === EMPTY && !bgm[i] && !bar && !mo && !id && !p && !b };
|
||||||
}
|
}
|
||||||
function renderInfo() {
|
function renderInfo() {
|
||||||
const r = hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y));
|
const r = hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y));
|
||||||
@@ -166,8 +179,8 @@ function drawCursorLabel(px, py, r) {
|
|||||||
const ctx = R.ctx, dpr = R.canvas.width / window.innerWidth;
|
const ctx = R.ctx, dpr = R.canvas.width / window.innerWidth;
|
||||||
const fs = Math.round(12 * dpr * SET.uiScale);
|
const fs = Math.round(12 * dpr * SET.uiScale);
|
||||||
if (!R.uiFont) R.uiFont = getComputedStyle(document.documentElement).getPropertyValue('--font-ui').trim() || 'sans-serif';
|
if (!R.uiFont) R.uiFont = getComputedStyle(document.documentElement).getPropertyValue('--font-ui').trim() || 'sans-serif';
|
||||||
const main = info.t1 + (player.lens && R.lens ? '' : '');
|
const main = info.t1 + (info.contents && info.warn ? ` ${info.contents}` : '');
|
||||||
const sub = info.warn || (R.lens ? info.t2.find(t => /°C$/.test(t)) || '' : '');
|
const sub = info.warn || info.contents || (R.lens ? info.t2.find(t => /°C$/.test(t)) || '' : '');
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.font = `600 ${fs}px ${R.uiFont}`;
|
ctx.font = `600 ${fs}px ${R.uiFont}`;
|
||||||
const w1 = ctx.measureText(main).width;
|
const w1 = ctx.measureText(main).width;
|
||||||
@@ -181,7 +194,7 @@ function drawCursorLabel(px, py, r) {
|
|||||||
ctx.beginPath(); ctx.roundRect(x, y, w1 + w2 + padX * 2, h, 4 * dpr); ctx.fill();
|
ctx.beginPath(); ctx.roundRect(x, y, w1 + w2 + padX * 2, h, 4 * dpr); ctx.fill();
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
ctx.font = `600 ${fs}px ${R.uiFont}`; ctx.fillStyle = '#efe6d8'; ctx.fillText(main, x + padX, y + h / 2);
|
ctx.font = `600 ${fs}px ${R.uiFont}`; ctx.fillStyle = '#efe6d8'; ctx.fillText(main, x + padX, y + h / 2);
|
||||||
if (sub) { ctx.font = `500 ${Math.round(fs * 0.9)}px ${R.uiFont}`; ctx.fillStyle = info.warn ? '#ef6a55' : '#ffd27a'; ctx.fillText(sub, x + padX + w1 + fs * 0.6, y + h / 2); }
|
if (sub) { ctx.font = `500 ${Math.round(fs * 0.9)}px ${R.uiFont}`; ctx.fillStyle = info.warn ? '#ef6a55' : info.contents ? '#efe6d8' : '#ffd27a'; ctx.fillText(sub, x + padX + w1 + fs * 0.6, y + h / 2); }
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -24,17 +24,18 @@ window.addEventListener('keydown', e => {
|
|||||||
if (e.repeat) return;
|
if (e.repeat) return;
|
||||||
GAME.usingPad = false;
|
GAME.usingPad = false;
|
||||||
switch (k) {
|
switch (k) {
|
||||||
|
case '`': e.preventDefault(); toggleDevConsole(); break;
|
||||||
case '1': case '2': case '3': case '4': selectTool(+k - 1); break;
|
case '1': case '2': case '3': case '4': selectTool(+k - 1); break;
|
||||||
case 'c': case 'x': hudDirty = true; break;
|
case 'c': case 'x': hudDirty = true; break;
|
||||||
case 'v': startPaste(); break;
|
case 'v': startPaste(); break;
|
||||||
case 'q': if (e.shiftKey && toolId() === 'build') cycleBuildCat(-1); else cycleItem(-1); break;
|
case 'q': if (e.shiftKey && toolId() === 'build') cycleBuildCat(-1); else cycleItem(-1); break;
|
||||||
case 'e': if (e.shiftKey && toolId() === 'build') cycleBuildCat(1); else cycleItem(1); break;
|
case 'e': if (e.shiftKey && toolId() === 'build') cycleBuildCat(1); else cycleItem(1); break;
|
||||||
case 'r': rotate(); break;
|
case 'r': rotate(); break;
|
||||||
case 't': if (toolId() === 'build' && piece().filter) pickFilterUnderCursor(); break;
|
case 't': if (toolId() === 'build' && piece().code === BP_WALL) turnSlope(); else if (toolId() === 'build' && piece().filter) pickFilterUnderCursor(); break;
|
||||||
case 'f': doInteract(); break;
|
case 'f': doInteract(); break;
|
||||||
case 'g': GAME.replace = true; hudDirty = true; break;
|
case 'g': GAME.replace = true; hudDirty = true; break;
|
||||||
case 'h': toggleLens(); break;
|
case 'h': toggleLens(); break;
|
||||||
case 'i': GAME.infoOverlay = !GAME.infoOverlay; playSfx('ui'); toast(GAME.infoOverlay ? 'Info overlay on: what every filter does, and which way belts and tubes run. I hides it.' : 'Info overlay off.'); break;
|
case 'i': GAME.infoOverlay = !GAME.infoOverlay; setFlowCounting(GAME.infoOverlay); playSfx('ui'); toast(GAME.infoOverlay ? 'Info overlay on: what every filter does, which way belts and screw conveyors run, and how many grains a second each line moves (orange: backing up). I hides it.' : 'Info overlay off.'); break;
|
||||||
case 'l': bot.lamp = !bot.lamp; playSfx('ui'); break;
|
case 'l': bot.lamp = !bot.lamp; playSfx('ui'); break;
|
||||||
case 'n': openNotebook(); break;
|
case 'n': openNotebook(); break;
|
||||||
case 'b': openNotebook('Blueprints'); break;
|
case 'b': openNotebook('Blueprints'); break;
|
||||||
@@ -42,7 +43,7 @@ window.addEventListener('keydown', e => {
|
|||||||
case '[': setBrush(-1); break;
|
case '[': setBrush(-1); break;
|
||||||
case ']': setBrush(1); break;
|
case ']': setBrush(1); break;
|
||||||
case 'Delete': if (ghostBlocks.size || ghostMachines.length || ghostMolds.length) { clearAllGhosts(); toast('Blueprint ghosts cleared.'); } break;
|
case 'Delete': if (ghostBlocks.size || ghostMachines.length || ghostMolds.length) { clearAllGhosts(); toast('Blueprint ghosts cleared.'); } break;
|
||||||
case 'Escape': if (GAME.selDrag || GAME.paste) stopSelectAndPaste(); else openPause(); break;
|
case 'Escape': if (!$('dev-console').hidden) closeDevConsole(); else if (GAME.selDrag || GAME.paste) stopSelectAndPaste(); else openPause(); break;
|
||||||
case 'Tab': $('bagpanel').hidden = !$('bagpanel').hidden; break;
|
case 'Tab': $('bagpanel').hidden = !$('bagpanel').hidden; break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+10
-5
@@ -22,11 +22,11 @@ function frame(now) {
|
|||||||
const pad = pollPad();
|
const pad = pollPad();
|
||||||
if (GAME.usingPad) { GAME.aim.x = bot.x + bot.w / 2 + GAME.padAim.x; GAME.aim.y = bot.y + bot.h / 2 + GAME.padAim.y; }
|
if (GAME.usingPad) { GAME.aim.x = bot.x + bot.w / 2 + GAME.padAim.x; GAME.aim.y = bot.y + bot.h / 2 + GAME.padAim.y; }
|
||||||
else { const w = screenToWorld(GAME.mouse.x, GAME.mouse.y); GAME.aim.x = w.x; GAME.aim.y = w.y; }
|
else { const w = screenToWorld(GAME.mouse.x, GAME.mouse.y); GAME.aim.x = w.x; GAME.aim.y = w.y; }
|
||||||
if (!GAME.paused) {
|
if (!GAME.paused && (!devPaused || devSteps > 0)) {
|
||||||
acc += dtReal * SET.simSpeed;
|
acc += devPaused ? 0 : dtReal * SET.simSpeed * devSpeed;
|
||||||
let steps = 0;
|
let steps = 0;
|
||||||
const step = 1 / 60;
|
const step = 1 / 60;
|
||||||
while (acc >= step && steps < SET.maxSteps) {
|
while ((devPaused ? devSteps > 0 : acc >= step) && steps < SET.maxSteps) {
|
||||||
let ix = 0, iy = 0;
|
let ix = 0, iy = 0;
|
||||||
if (keys.has('a') || keys.has('ArrowLeft')) ix -= 1; if (keys.has('d') || keys.has('ArrowRight')) ix += 1;
|
if (keys.has('a') || keys.has('ArrowLeft')) ix -= 1; if (keys.has('d') || keys.has('ArrowRight')) ix += 1;
|
||||||
if (keys.has('w') || keys.has('ArrowUp') || keys.has(' ')) iy -= 1; if (keys.has('s') || keys.has('ArrowDown')) iy += 1;
|
if (keys.has('w') || keys.has('ArrowUp') || keys.has(' ')) iy -= 1; if (keys.has('s') || keys.has('ArrowDown')) iy += 1;
|
||||||
@@ -38,7 +38,8 @@ function frame(now) {
|
|||||||
botStep(ix, iy, keys.has('Shift') || GAME.padRun > 24);
|
botStep(ix, iy, keys.has('Shift') || GAME.padRun > 24);
|
||||||
toolTick();
|
toolTick();
|
||||||
const s0 = performance.now(); simTick(); simMs = simMs * 0.9 + (performance.now() - s0) * 0.1;
|
const s0 = performance.now(); simTick(); simMs = simMs * 0.9 + (performance.now() - s0) * 0.1;
|
||||||
acc -= step; steps++;
|
if (devPaused) devSteps--; else acc -= step;
|
||||||
|
steps++;
|
||||||
GAME.playTime += step;
|
GAME.playTime += step;
|
||||||
}
|
}
|
||||||
if (steps >= SET.maxSteps) acc = 0;
|
if (steps >= SET.maxSteps) acc = 0;
|
||||||
@@ -53,6 +54,7 @@ function frame(now) {
|
|||||||
// grid under the cursor does not slide around mid-line.
|
// grid under the cursor does not slide around mid-line.
|
||||||
if (GAME.drag) { lookX = GAME.lookHold.x; lookY = GAME.lookHold.y; } else { GAME.lookHold.x = lookX; GAME.lookHold.y = lookY; }
|
if (GAME.drag) { lookX = GAME.lookHold.x; lookY = GAME.lookHold.y; } else { GAME.lookHold.x = lookX; GAME.lookHold.y = lookY; }
|
||||||
if (!GAME.drag || Math.hypot(bot.vx, bot.vy) > 0.08) updateCamera(dt, lookX, lookY);
|
if (!GAME.drag || Math.hypot(bot.vx, bot.vy) > 0.08) updateCamera(dt, lookX, lookY);
|
||||||
|
{ const v = viewCells(); setBuildRange(R.camX + v.w / 2, R.camY + v.h / 2, v.w, v.h); }
|
||||||
R.frame++;
|
R.frame++;
|
||||||
drawWorld();
|
drawWorld();
|
||||||
drawCracks();
|
drawCracks();
|
||||||
@@ -69,7 +71,10 @@ function frame(now) {
|
|||||||
drawParticles();
|
drawParticles();
|
||||||
const hb = GAME.mouse.in || GAME.usingPad ? filterBlockAt(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)) : -1;
|
const hb = GAME.mouse.in || GAME.usingPad ? filterBlockAt(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)) : -1;
|
||||||
if (GAME.editRun) drawRunOutline(GAME.editRun, true); else if (hb >= 0) drawRunOutline(filterRun(hb), false);
|
if (GAME.editRun) drawRunOutline(GAME.editRun, true); else if (hb >= 0) drawRunOutline(filterRun(hb), false);
|
||||||
if (GAME.infoOverlay) drawFlowArrows();
|
if (GAME.infoOverlay) { drawFlowArrows(); drawFlowRates(); }
|
||||||
|
drawJoinMarks();
|
||||||
|
drawPing();
|
||||||
|
drawPingRecharge();
|
||||||
drawFilterTags(!!GAME.infoOverlay, GAME.editRun ? GAME.editRun[0] : hb);
|
drawFilterTags(!!GAME.infoOverlay, GAME.editRun ? GAME.editRun[0] : hb);
|
||||||
drawFloaters();
|
drawFloaters();
|
||||||
drawRain();
|
drawRain();
|
||||||
|
|||||||
+8
-8
@@ -43,8 +43,8 @@ function renderNotebook() {
|
|||||||
let h = '<p class="note">Everything you build snaps to a grid of 4×4-grain blocks. Pieces only move, hold, blow or sense material; every change to the material itself comes from heat. Select pieces with the Build tool (3). Hover a tile for its cost, full description and placement help. Your bot flies straight through anything you build.</p><div class="cards">';
|
let h = '<p class="note">Everything you build snaps to a grid of 4×4-grain blocks. Pieces only move, hold, blow or sense material; every change to the material itself comes from heat. Select pieces with the Build tool (3). Hover a tile for its cost, full description and placement help. Your bot flies straight through anything you build.</p><div class="cards">';
|
||||||
for (const P of PIECES) {
|
for (const P of PIECES) {
|
||||||
if (P.hidden) continue;
|
if (P.hidden) continue;
|
||||||
const cost = P.kind === 'mold' ? `${MOLDS[P.mold].walls} grains of wall material (R cycles, F opens the menu)` : P.code === BP_WALL ? '16 grains of rubble, lumps or chunks, or 2 bars' : costText(P.cost, 1);
|
const cost = P.kind === 'mold' ? `${MOLDS[P.mold].walls} grains of wall material (R cycles, F opens the menu)` : isWall(P.code) ? 'Square: 16 grains or 2 bars; slope: 8 grains or 1 bar' : costText(P.cost, 1);
|
||||||
const afford = P.kind === 'mold' || P.code === BP_WALL ? true : canAfford(P.cost, 1);
|
const afford = P.kind === 'mold' || isWall(P.code) ? true : canAfford(P.cost, 1);
|
||||||
const size = P.kind === 'machine' ? `${P.bw}×${P.bh}` : P.kind === 'mold' ? `${MOLDS[P.mold].bw}×${MOLDS[P.mold].bh}` : '1×1';
|
const size = P.kind === 'machine' ? `${P.bw}×${P.bh}` : P.kind === 'mold' ? `${MOLDS[P.mold].bw}×${MOLDS[P.mold].bh}` : '1×1';
|
||||||
h += `<div class="card"><div class="h"><span>${esc(P.name)}</span><span class="chip ${afford ? 'ok' : ''}">${P.power ? esc(P.power + ' power') : P.id === 'wheel' || P.id === 'engine' ? 'makes power' : size + (size === '1×1' ? ' block' : ' blocks')}</span></div><div class="p">${esc(P.desc)}</div><div class="p">Cost: ${esc(cost)}</div></div>`;
|
h += `<div class="card"><div class="h"><span>${esc(P.name)}</span><span class="chip ${afford ? 'ok' : ''}">${P.power ? esc(P.power + ' power') : P.id === 'wheel' || P.id === 'engine' ? 'makes power' : size + (size === '1×1' ? ' block' : ' blocks')}</span></div><div class="p">${esc(P.desc)}</div><div class="p">Cost: ${esc(cost)}</div></div>`;
|
||||||
}
|
}
|
||||||
@@ -57,10 +57,10 @@ function renderNotebook() {
|
|||||||
}
|
}
|
||||||
function controlsHtml() {
|
function controlsHtml() {
|
||||||
const kb = [
|
const kb = [
|
||||||
['Fly', 'W A S D'], ['Boost while flying', 'Shift'], ['Tool modifier (while not flying)', 'Shift'], ['Use tool', 'Left mouse'], ['Second action', 'Right mouse'],
|
['Dev console (marks the world)', 'Backtick (`)'], ['Fly', 'W A S D'], ['Boost while flying', 'Shift'], ['Tool modifier (while not flying)', 'Shift'], ['Use tool', 'Left mouse'], ['Second action', 'Right mouse'], ['Resonance ping: light up nearby veins (Drill)', 'Right mouse'],
|
||||||
['Tools', '1 – 4'], ['Select one to blow · piece', 'Q / E'], ['Switch build tab', 'Shift + Q / E'], ['Variant: material · direction · flip', 'R'], ['Wall or mold material menu (Build)', 'F'],
|
['Tools', '1 – 4'], ['Select one to blow · piece', 'Q / E'], ['Switch build tab', 'Shift + Q / E'], ['Variant: material · direction · flip', 'R'], ['Wall: cycle shape · flip diagonal drag', 'T'], ['Wall or mold material menu (Build)', 'F'],
|
||||||
['Pick up selection or what you click (Pneumatic nozzle)', 'Left drag · Shift: everything'], ['Select (Pneumatic nozzle)', 'Click a bag name · Q/E'], ['Add to selection (Pneumatic nozzle)', 'Ctrl+click a bag name'], ['Show everything found (Pneumatic nozzle)', 'Hold Ctrl in the bag'], ['Clear selection (Pneumatic nozzle)', 'Clear in the bag'], ['Blow everything loose in the bag', 'Shift + right click'], ['Nozzle or brush size', '[ ] or Shift + wheel'],
|
['Pick up selection or what you click (Pneumatic nozzle)', 'Left drag · Shift: everything'], ['Select (Pneumatic nozzle)', 'Click a bag name · Q/E'], ['Add to selection (Pneumatic nozzle)', 'Ctrl+click a bag name'], ['Show everything found (Pneumatic nozzle)', 'Hold Ctrl in the bag'], ['Clear selection (Pneumatic nozzle)', 'Clear in the bag'], ['Blow everything loose in the bag', 'Shift + right click'], ['Nozzle or brush size', '[ ] or Shift + wheel'],
|
||||||
['Replace what is built (Build)', 'Hold G'], ['Copy a box of what you built', 'Hold C, drag'], ['Cut a box (refunded, paste it back)', 'Hold X, drag'], ['Paste again', 'V'], ['Undo building', 'Ctrl + Z'], ['Blueprint library', 'B'], ['Zoom', 'Mouse wheel'], ['Interact (gates, molds, fans, filters)', 'F'], ['Thermal lens', 'H'], ['Info overlay: filters, belt and tube directions', 'I'], ['Bot lamp', 'L'],
|
['Replace what is built (Build)', 'Hold G'], ['Copy a box of what you built', 'Hold C, drag'], ['Cut a box (refunded, paste it back)', 'Hold X, drag'], ['Paste again', 'V'], ['Undo building', 'Ctrl + Z'], ['Blueprint library', 'B'], ['Zoom', 'Mouse wheel'], ['Interact (gates, molds, fans, filters)', 'F'], ['Thermal lens', 'H'], ['Info overlay: filters, belt and screw conveyor directions, grains per second', 'I'], ['Bot lamp', 'L'],
|
||||||
['Pick the piece under the cursor (with its direction and settings)', 'Middle mouse'], ['Notebook', 'N'], ['Clear all blueprint ghosts', 'Delete'], ['Save', 'Ctrl + S'], ['Pause · menu', 'Esc'], ['Mute', 'Shift + M'], ['Full map', 'M'], ['Minimap zoom', '+ / − on minimap'],
|
['Pick the piece under the cursor (with its direction and settings)', 'Middle mouse'], ['Notebook', 'N'], ['Clear all blueprint ghosts', 'Delete'], ['Save', 'Ctrl + S'], ['Pause · menu', 'Esc'], ['Mute', 'Shift + M'], ['Full map', 'M'], ['Minimap zoom', '+ / − on minimap'],
|
||||||
];
|
];
|
||||||
const pad = [
|
const pad = [
|
||||||
@@ -71,7 +71,7 @@ function controlsHtml() {
|
|||||||
const tools = [
|
const tools = [
|
||||||
['1 Resonance drill', 'Hold left click to send pulses from the bot toward the cursor: rock they shake hard enough shatters and falls as rubble, ore, coal or scrap, and trees come apart into wood chunks and acorns. Q and E set the pulse width: narrow bores a tunnel fast, wide clears a big cone slowly, medium is in between. R over a material tunes the drill to it, so only that shatters (great for pulling a vein out of the rock); R again untunes. Built blocks are never harmed. Better bits dig harder rock, faster and further.'],
|
['1 Resonance drill', 'Hold left click to send pulses from the bot toward the cursor: rock they shake hard enough shatters and falls as rubble, ore, coal or scrap, and trees come apart into wood chunks and acorns. Q and E set the pulse width: narrow bores a tunnel fast, wide clears a big cone slowly, medium is in between. R over a material tunes the drill to it, so only that shatters (great for pulling a vein out of the rock); R again untunes. Built blocks are never harmed. Better bits dig harder rock, faster and further.'],
|
||||||
['2 Pneumatic nozzle', "Left drag picks up the selection, or what you click if nothing is selected. Right click blows the selection. Click a bag name or Q/E selects one; Ctrl+click mixes. Hold Ctrl to see everything found. Clear removes the selection. Bars blow alone. Blowing out the last of a material deselects it. Shift while not flying takes everything; Shift + right click blows everything loose in the bag."],
|
['2 Pneumatic nozzle', "Left drag picks up the selection, or what you click if nothing is selected. Right click blows the selection. Click a bag name or Q/E selects one; Ctrl+click mixes. Hold Ctrl to see everything found. Clear removes the selection. Bars blow alone. Blowing out the last of a material deselects it. Shift while not flying takes everything; Shift + right click blows everything loose in the bag."],
|
||||||
['3 Build', 'Places pieces on a grid of 4×4 blocks: wall blocks, belts, tubes, filters, grates, gates, pipes, pumps, outlets, Grinders, Saws, Brick presses and Bar racks, machines (fans and silos) and molds. Drag for a line, hold Shift while dragging for a filled rectangle. Right click takes a piece back. Your bot flies through anything you build.'],
|
['3 Build', 'Places pieces on a grid of 4×4 blocks: wall blocks, belts, screw conveyors, filters, grates, gates, pipes, pumps, outlets, Grinders, Saws, Brick presses and Bar racks, machines (fans and silos) and molds. Drag for a line, hold Shift while dragging for a filled rectangle (belts, screw conveyors, filters, shafts and pipes always draw a line). A diagonal wall drag makes a sealed sloped face, solid below; T flips it. For one wall block, T cycles square and four slope corners. Uphill belts use cleats to carry pockets up; downhill they carry like a flat belt. Extra grains roll back. Diagonal filters always screen downhill, whichever way you drag. R leaves a picked sloped filter downhill. Right click takes a piece back. Your bot flies through anything you build.'],
|
||||||
['4 Igniter', 'Heats a small spot to 640°C, and its flame reaches a few grains into a pile. Enough to light coal and wood that touch air.'],
|
['4 Igniter', 'Heats a small spot to 640°C, and its flame reaches a few grains into a pile. Enough to light coal and wood that touch air.'],
|
||||||
['Copy, cut and paste', 'Hold C (copy) or X (cut) and drag a box over what you built; let go and it is picked up, ready to paste. Cut takes it down and refunds it to your bag. Click to place ghosts: your bot builds them from your bag when it is close. R flips, V pastes again, right click or Esc stops. Ctrl+Z undoes building. The notebook (B) keeps saved blueprints and a few starter designs.'],
|
['Copy, cut and paste', 'Hold C (copy) or X (cut) and drag a box over what you built; let go and it is picked up, ready to paste. Cut takes it down and refunds it to your bag. Click to place ghosts: your bot builds them from your bag when it is close. R flips, V pastes again, right click or Esc stops. Ctrl+Z undoes building. The notebook (B) keeps saved blueprints and a few starter designs.'],
|
||||||
];
|
];
|
||||||
@@ -84,8 +84,8 @@ function controlsHtml() {
|
|||||||
3. Build (3) a chamber of stone wall blocks with a one-block vent in the roof, a floor of clay Grates, and an Ingot mold right under the grates. The notebook's Blueprints tab has this as a starter design.<br>
|
3. Build (3) a chamber of stone wall blocks with a one-block vent in the roof, a floor of clay Grates, and an Ingot mold right under the grates. The notebook's Blueprints tab has this as a starter design.<br>
|
||||||
4. Ctrl+click coal and copper ore in the bag to blow a mix. Fly inside (the bot passes through what you build) and right click to blow an even mix onto the grates: about 50 of each. (Ore first and coal on top works too; coal buried under ore will not light.) Light a coal grain on top with the Igniter.<br>
|
4. Ctrl+click coal and copper ore in the bag to blow a mix. Fly inside (the bot passes through what you build) and right click to blow an even mix onto the grates: about 50 of each. (Ore first and coal on top works too; coal buried under ore will not light.) Light a coal grain on top with the Igniter.<br>
|
||||||
5. When the ingot sets, bars drop out of the bottom of the mold onto whatever is under it. Leave one open block under it: a belt below that carries them off, or pick them up with the nozzle. Leftover metal stays for the next firing. Ingot molds empty themselves: bars, junk and scrap drop below, slag is thrown away and the grate above is raked. Removing any mold returns its set metal to your bag: whole bars as bars, the rest as scrap.<br>
|
5. When the ingot sets, bars drop out of the bottom of the mold onto whatever is under it. Leave one open block under it: a belt below that carries them off, or pick them up with the nozzle. Leftover metal stays for the next firing. Ingot molds empty themselves: bars, junk and scrap drop below, slag is thrown away and the grate above is raked. Removing any mold returns its set metal to your bag: whole bars as bars, the rest as scrap.<br>
|
||||||
Screw tubes carry grains and whole bars: feed bars in from a belt, turn corners and unload at the mouth.<br>
|
Screw conveyors carry grains and whole bars: feed bars in from a belt, turn corners and unload at the mouth. Drag at about 45° for a diagonal run; R cycles eight directions.<br>
|
||||||
6. Blow the bars into the trade dock: they sell the moment they drop through its grate, and bars pay far more than ore. Watch the Market page for orders that pay double. Money buys belts, screw tubes and fans; pay down the claim there too.<br>
|
6. Blow the bars into the trade dock: they sell the moment they drop through its grate, and bars pay far more than ore. Watch the Market page for orders that pay double. Money buys belts, screw conveyors and fans; pay down the claim there too.<br>
|
||||||
7. From there: deeper ores (tin, zinc, silver, gold), alloys, fans for hotter fires, iron and steel.</p>
|
7. From there: deeper ores (tin, zinc, silver, gold), alloys, fans for hotter fires, iron and steel.</p>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ function drawDrillAim(ax, ay) {
|
|||||||
const o = drillOrigin(), c = worldToScreenPx(o.x, o.y), p = worldToScreenPx(ax + 0.5, ay + 0.5);
|
const o = drillOrigin(), c = worldToScreenPx(o.x, o.y), p = worldToScreenPx(ax + 0.5, ay + 0.5);
|
||||||
const d = Math.hypot(ax + 0.5 - o.x, ay + 0.5 - o.y), ang = Math.atan2(ay + 0.5 - o.y, ax + 0.5 - o.x);
|
const d = Math.hypot(ax + 0.5 - o.x, ay + 0.5 - o.y), ang = Math.atan2(ay + 0.5 - o.y, ax + 0.5 - o.x);
|
||||||
const Wd = PULSE_W[GAME.drillW], range = Math.max(3, Math.min(drillReach(GAME.drillW), d + 3));
|
const Wd = PULSE_W[GAME.drillW], range = Math.max(3, Math.min(drillReach(GAME.drillW), d + 3));
|
||||||
const ok = inDrillReach(ax, ay, GAME.drillW), col = ok ? pulseRGB(GAME.drillTune) : '239,106,85';
|
const col = pulseRGB(GAME.drillTune);
|
||||||
const mouth = fanPoints(o.x, o.y, ang, Wd, 2.5, 1), far = fanPoints(o.x, o.y, ang, Wd, range, 10);
|
const mouth = fanPoints(o.x, o.y, ang, Wd, 2.5, 1), far = fanPoints(o.x, o.y, ang, Wd, range, 10);
|
||||||
ctx.save();
|
ctx.save();
|
||||||
// the outline gets a dark under-stroke so it reads on bright sky as well as dark rock
|
// the outline gets a dark under-stroke so it reads on bright sky as well as dark rock
|
||||||
@@ -84,6 +84,9 @@ function drawDrillAim(ax, ay) {
|
|||||||
ctx.strokeStyle = 'rgba(10,14,20,0.28)'; ctx.lineWidth = Math.max(2, zp * 0.3); ctx.stroke();
|
ctx.strokeStyle = 'rgba(10,14,20,0.28)'; ctx.lineWidth = Math.max(2, zp * 0.3); ctx.stroke();
|
||||||
ctx.strokeStyle = `rgba(${col},0.6)`; ctx.lineWidth = Math.max(1, zp * 0.14); ctx.stroke();
|
ctx.strokeStyle = `rgba(${col},0.6)`; ctx.lineWidth = Math.max(1, zp * 0.14); ctx.stroke();
|
||||||
ctx.setLineDash([]); ctx.lineDashOffset = 0;
|
ctx.setLineDash([]); ctx.lineDashOffset = 0;
|
||||||
|
// A solid far edge marks where the pulse stops, even with a distant cursor.
|
||||||
|
ctx.beginPath(); far.forEach((pt, k) => k ? ctx.lineTo(pt.x, pt.y) : ctx.moveTo(pt.x, pt.y));
|
||||||
|
ctx.strokeStyle = `rgba(${col},0.85)`; ctx.lineWidth = Math.max(1, zp * 0.22); ctx.stroke();
|
||||||
// the emitter glows across the bot's face while it fires
|
// the emitter glows across the bot's face while it fires
|
||||||
const hot = Math.max(0, 1 - (tick - lastPulseTick) / PULSE_EVERY);
|
const hot = Math.max(0, 1 - (tick - lastPulseTick) / PULSE_EVERY);
|
||||||
if (GAME.lmb && hot > 0) {
|
if (GAME.lmb && hot > 0) {
|
||||||
@@ -95,7 +98,7 @@ function drawDrillAim(ax, ay) {
|
|||||||
ctx.fillStyle = g; ctx.fillRect(c.x - zp * 3.5, c.y - zp * 3.5, zp * 7, zp * 7);
|
ctx.fillStyle = g; ctx.fillRect(c.x - zp * 3.5, c.y - zp * 3.5, zp * 7, zp * 7);
|
||||||
ctx.globalCompositeOperation = 'source-over';
|
ctx.globalCompositeOperation = 'source-over';
|
||||||
}
|
}
|
||||||
ctx.strokeStyle = ok ? 'rgba(239,230,216,0.85)' : 'rgba(239,106,85,0.85)'; ctx.lineWidth = Math.max(1, zp * 0.2);
|
ctx.strokeStyle = 'rgba(239,230,216,0.85)'; ctx.lineWidth = Math.max(1, zp * 0.2);
|
||||||
const k = zp * 0.9;
|
const k = zp * 0.9;
|
||||||
ctx.beginPath(); ctx.moveTo(p.x - k * 1.6, p.y); ctx.lineTo(p.x - k * 0.5, p.y); ctx.moveTo(p.x + k * 0.5, p.y); ctx.lineTo(p.x + k * 1.6, p.y);
|
ctx.beginPath(); ctx.moveTo(p.x - k * 1.6, p.y); ctx.lineTo(p.x - k * 0.5, p.y); ctx.moveTo(p.x + k * 0.5, p.y); ctx.lineTo(p.x + k * 1.6, p.y);
|
||||||
ctx.moveTo(p.x, p.y - k * 1.6); ctx.lineTo(p.x, p.y - k * 0.5); ctx.moveTo(p.x, p.y + k * 0.5); ctx.lineTo(p.x, p.y + k * 1.6); ctx.stroke();
|
ctx.moveTo(p.x, p.y - k * 1.6); ctx.lineTo(p.x, p.y - k * 0.5); ctx.moveTo(p.x, p.y + k * 0.5); ctx.lineTo(p.x, p.y + k * 1.6); ctx.stroke();
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ function drawPastePreview() {
|
|||||||
const [x, y, w, h] = blockRectPx(ox, oy, bp.bw, bp.bh);
|
const [x, y, w, h] = blockRectPx(ox, oy, bp.bw, bp.bh);
|
||||||
ctx.strokeStyle = 'rgba(92,186,166,0.85)'; ctx.setLineDash([zp, zp]); ctx.lineWidth = Math.max(1, zp * 0.3);
|
ctx.strokeStyle = 'rgba(92,186,166,0.85)'; ctx.setLineDash([zp, zp]); ctx.lineWidth = Math.max(1, zp * 0.3);
|
||||||
ctx.strokeRect(x, y, w, h); ctx.setLineDash([]);
|
ctx.strokeRect(x, y, w, h); ctx.setLineDash([]);
|
||||||
for (const [dx, dy, code, v] of bp.blocks) { const [bx2, by2, bw2, bh2] = blockRectPx(ox + dx, oy + dy, 1, 1); ctx.fillStyle = `rgba(${previewColor(code, v)},0.5)`; ctx.fillRect(bx2, by2, bw2, bh2); }
|
const planned = new Map(bp.blocks.map(([dx, dy, code, v]) => [(oy + dy) * BW + ox + dx, [code, v]]));
|
||||||
|
for (const [dx, dy, code, v] of bp.blocks) { const [bx2, by2, bw2, bh2] = blockRectPx(ox + dx, oy + dy, 1, 1); ctx.fillStyle = `rgba(${previewColor(code, v)},0.5)`; previewBlock(code, bx2, by2, bw2, bh2, false, code === BP_SCREW || isBelt(code) ? v : -1, ox + dx, oy + dy, planned); }
|
||||||
for (const [type, dx, dy] of bp.machines) { const P = PIECE_BY_ID[type]; const [mx, my, mw, mh] = blockRectPx(ox + dx, oy + dy, P.bw, P.bh); ctx.strokeStyle = 'rgba(255,210,122,0.8)'; ctx.strokeRect(mx, my, mw, mh); }
|
for (const [type, dx, dy] of bp.machines) { const P = PIECE_BY_ID[type]; const [mx, my, mw, mh] = blockRectPx(ox + dx, oy + dy, P.bw, P.bh); ctx.strokeStyle = 'rgba(255,210,122,0.8)'; ctx.strokeRect(mx, my, mw, mh); }
|
||||||
for (const [k, dx, dy] of bp.molds) { const md = MOLDS[k]; const [mx, my, mw, mh] = blockRectPx(ox + dx, oy + dy, md.bw, md.bh); ctx.strokeStyle = 'rgba(255,190,90,0.8)'; ctx.strokeRect(mx, my, mw, mh); }
|
for (const [k, dx, dy] of bp.molds) { const md = MOLDS[k]; const [mx, my, mw, mh] = blockRectPx(ox + dx, oy + dy, md.bw, md.bh); ctx.strokeStyle = 'rgba(255,190,90,0.8)'; ctx.strokeRect(mx, my, mw, mh); }
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|||||||
+85
-34
@@ -14,9 +14,10 @@ function drawGrid(cbx, cby, radius) {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
function previewColor(code, v) {
|
function previewColor(code, v) {
|
||||||
if (code === BP_WALL) { const m = WALL_OF[v]; return `${COLR[m]},${COLG[m]},${COLB[m]}`; }
|
if (isWall(code)) { const m = WALL_OF[v]; return `${COLR[m]},${COLG[m]},${COLB[m]}`; }
|
||||||
if (code === BP_FILTER) return '96,100,110';
|
if (isFilter(code)) return '96,100,110';
|
||||||
return { [BP_BELT]: '110,114,124', [BP_TUBE]: '120,132,150', [BP_SHAFT]: '160,118,70', [BP_GRATE]: '170,90,66', [BP_GATE]: '160,84,64', [BP_PIPE]: '176,112,72', [BP_PUMP]: '104,122,142', [BP_OUTLET]: '170,106,66' }[code] || '120,200,190';
|
if (isBelt(code)) return '110,114,124';
|
||||||
|
return { [BP_BELT]: '110,114,124', [BP_SCREW]: '120,132,150', [BP_SHAFT]: '160,118,70', [BP_GRATE]: '170,90,66', [BP_GATE]: '160,84,64', [BP_PIPE]: '176,112,72', [BP_PUMP]: '104,122,142', [BP_OUTLET]: '170,106,66' }[code] || '120,200,190';
|
||||||
}
|
}
|
||||||
// Air swirls into the pickup ring or spreads from the nozzle toward the cursor.
|
// Air swirls into the pickup ring or spreads from the nozzle toward the cursor.
|
||||||
function drawAirStreaks(ax, ay, p, r, blowing) {
|
function drawAirStreaks(ax, ay, p, r, blowing) {
|
||||||
@@ -66,12 +67,9 @@ function drawPreviews() {
|
|||||||
ctx.save();
|
ctx.save();
|
||||||
const reachOk = inReach(ax, ay);
|
const reachOk = inReach(ax, ay);
|
||||||
const sel = GAME.selDrag ? GAME.selDrag.mode : selModeHeld(), pasting = !sel && GAME.paste && GAME.clip;
|
const sel = GAME.selDrag ? GAME.selDrag.mode : selModeHeld(), pasting = !sel && GAME.paste && GAME.clip;
|
||||||
if (!reachOk && t !== 'drill' && !sel) {
|
if (!reachOk && t !== 'drill' && t !== 'build' && !sel) {
|
||||||
drawReachCircle(REACH, 0.35);
|
drawReachCircle(REACH, 0.35);
|
||||||
}
|
}
|
||||||
if (t === 'drill' && !sel && !pasting && !inDrillReach(ax, ay, GAME.drillW)) {
|
|
||||||
drawReachCircle(drillReach(GAME.drillW), 0.45);
|
|
||||||
}
|
|
||||||
drawDrillPulses();
|
drawDrillPulses();
|
||||||
if (t === 'drill' && !sel && !pasting) drawDrillAim(ax, ay);
|
if (t === 'drill' && !sel && !pasting) drawDrillAim(ax, ay);
|
||||||
if (sel) drawSelection(sel, ax, ay);
|
if (sel) drawSelection(sel, ax, ay);
|
||||||
@@ -80,7 +78,9 @@ function drawPreviews() {
|
|||||||
const r = t === 'ignite' ? 1 : GAME.brush[GAME.tool];
|
const r = t === 'ignite' ? 1 : GAME.brush[GAME.tool];
|
||||||
const reachOk = inReach(ax, ay);
|
const reachOk = inReach(ax, ay);
|
||||||
const blowing = t === 'vacuum' && GAME.rmb && !GAME.rmbConsumed;
|
const blowing = t === 'vacuum' && GAME.rmb && !GAME.rmbConsumed;
|
||||||
ctx.strokeStyle = !reachOk ? 'rgba(239,106,85,0.8)' : t === 'drill' ? 'rgba(239,230,216,0.8)' : blowing ? 'rgba(255,210,122,0.9)' : t === 'vacuum' ? 'rgba(92,186,166,0.95)' : 'rgba(255,154,60,0.9)';
|
const nz = blowing && !reachOk ? nozzle(ax + 0.5, ay + 0.5) : null;
|
||||||
|
const p = nz ? worldToScreenPx(nz.ox + nz.dx * nz.dist - 0.5, nz.oy + nz.dy * nz.dist - 0.5) : worldToScreenPx(ax, ay);
|
||||||
|
ctx.strokeStyle = !reachOk && !nz ? 'rgba(239,106,85,0.8)' : t === 'drill' ? 'rgba(239,230,216,0.8)' : blowing ? 'rgba(255,210,122,0.9)' : t === 'vacuum' ? 'rgba(92,186,166,0.95)' : 'rgba(255,154,60,0.9)';
|
||||||
ctx.lineWidth = Math.max(1, zp * 0.25);
|
ctx.lineWidth = Math.max(1, zp * 0.25);
|
||||||
if (t === 'vacuum') ctx.setLineDash([zp, zp * 0.6]);
|
if (t === 'vacuum') ctx.setLineDash([zp, zp * 0.6]);
|
||||||
ctx.beginPath(); ctx.arc(p.x + zp / 2, p.y + zp / 2, (r + 0.5) * zp, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]);
|
ctx.beginPath(); ctx.arc(p.x + zp / 2, p.y + zp / 2, (r + 0.5) * zp, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]);
|
||||||
@@ -98,50 +98,40 @@ function drawPreviews() {
|
|||||||
if (reachOk && (blowing || GAME.lmb)) drawAirStreaks(ax, ay, p, r, blowing);
|
if (reachOk && (blowing || GAME.lmb)) drawAirStreaks(ax, ay, p, r, blowing);
|
||||||
}
|
}
|
||||||
} else if (t === 'build') {
|
} else if (t === 'build') {
|
||||||
const P = piece(), b = aimBlock();
|
const P = piece(), b = aimBlock(), plan = P.kind === 'block' ? dragPlan(P, GAME.drag, b, modShift()) : null;
|
||||||
|
const code = plan ? plan.code : pieceCode(P, b);
|
||||||
|
if (GAME.previewCode !== code) { GAME.previewCode = code; hudDirty = true; }
|
||||||
drawGrid(b.bx, b.by, 9);
|
drawGrid(b.bx, b.by, 9);
|
||||||
let cells = [], col = '120,200,190';
|
let cells = [], col = '120,200,190';
|
||||||
if (P.kind === 'block') {
|
if (P.kind === 'block') {
|
||||||
const v = pieceV(P);
|
const { v, dd, dir: dIdx, planned } = plan;
|
||||||
col = previewColor(P.code, v);
|
col = previewColor(code, v);
|
||||||
cells = GAME.drag ? (modShift() ? blockRect(GAME.drag.bx, GAME.drag.by, b.bx, b.by) : blockLine(GAME.drag.bx, GAME.drag.by, b.bx, b.by)) : [[b.bx, b.by]];
|
cells = plan.cells;
|
||||||
const dd = GAME.drag ? dragDirFor(P, GAME.drag, b) : -1;
|
|
||||||
if (dd !== GAME.dragDir) { GAME.dragDir = dd; hudDirty = true; }
|
if (dd !== GAME.dragDir) { GAME.dragDir = dd; hudDirty = true; }
|
||||||
const dIdx = dd >= 0 ? dd : (GAME.pieceDir[P.id] || 0);
|
|
||||||
const afford = canAfford(pieceCostNow(P), cells.length);
|
const afford = canAfford(pieceCostNow(P), cells.length);
|
||||||
for (const [bx, by] of cells) {
|
for (const [bx, by] of cells) {
|
||||||
const err = canPlaceBlock(bx, by), rb = by * BW + bx;
|
const err = canPlaceBlock(bx, by, code), rb = by * BW + bx;
|
||||||
const swap = GAME.replace && !!blockP[rb] && /already built/.test(err) && !blockIs(rb, P.code, v, pieceExtra(P));
|
const swap = GAME.replace && !!blockP[rb] && /already built/.test(err) && !blockIs(rb, code, v, pieceExtra(P));
|
||||||
const ghost = !blockInReach(bx, by) && (!err || /bar/.test(err)); // out of reach: it will be a ghost
|
const ghost = !blockInReach(bx, by) && (!err || /bar/.test(err)); // out of reach: it will be a ghost
|
||||||
const ok = (!err || swap) && !ghost && afford;
|
const ok = (!err || swap) && !ghost && afford;
|
||||||
const [x, y, w, h] = blockRectPx(bx, by, 1, 1);
|
const [x, y, w, h] = blockRectPx(bx, by, 1, 1);
|
||||||
ctx.fillStyle = ghost ? 'rgba(90,190,230,0.22)' : ok ? (swap ? 'rgba(255,190,90,0.5)' : `rgba(${col},0.55)`) : 'rgba(239,106,85,0.35)';
|
ctx.fillStyle = ghost ? 'rgba(90,190,230,0.22)' : ok ? (swap ? 'rgba(255,190,90,0.5)' : `rgba(${col},0.55)`) : 'rgba(239,106,85,0.35)';
|
||||||
ctx.fillRect(x, y, w, h);
|
previewBlock(code, x, y, w, h, false, P.id === 'screw' || P.id === 'belt' ? dIdx : -1, bx, by, planned);
|
||||||
ctx.strokeStyle = ghost ? 'rgba(90,190,230,0.8)' : ok ? 'rgba(236,226,210,0.8)' : 'rgba(239,106,85,0.9)'; ctx.lineWidth = Math.max(1, zp * 0.25);
|
ctx.strokeStyle = ghost ? 'rgba(90,190,230,0.8)' : ok ? 'rgba(236,226,210,0.8)' : 'rgba(239,106,85,0.9)'; ctx.lineWidth = Math.max(1, zp * 0.25);
|
||||||
if (ghost) ctx.setLineDash([zp, zp * 0.7]);
|
if (ghost) ctx.setLineDash([zp, zp * 0.7]);
|
||||||
ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1); ctx.setLineDash([]);
|
previewBlock(code, x, y, w, h, true, P.id === 'screw' || P.id === 'belt' ? dIdx : -1, bx, by, planned); ctx.setLineDash([]);
|
||||||
if (P.dirs && (P.id === 'belt' || P.id === 'tube' || P.filter || P.id === 'pump' || P.id === 'outlet')) {
|
if (P.dirs && (P.id === 'belt' || P.id === 'screw' || P.filter || P.id === 'pump' || P.id === 'outlet')) {
|
||||||
// a chevron on every block, pointing the way it will carry (or pump, or pour)
|
// a chevron on every block, pointing the way it will carry (or pump, or pour)
|
||||||
const [dx, dy] = P.id === 'tube' ? [[0, -1], [1, 0], [0, 1], [-1, 0]][dIdx] : P.id === 'pump' ? PUMP_V[dIdx] : P.id === 'outlet' ? OUTLET_V[dIdx] : [[-1, 0], [1, 0]][dIdx];
|
const [dx, dy] = isSlopeBelt(code) ? [dIdx ? 1 : -1, beltGrade(code) * (dIdx ? 1 : -1)] : P.id === 'screw' ? SCREW_DIRS[dIdx] : P.id === 'pump' ? PUMP_V[dIdx] : P.id === 'outlet' ? OUTLET_V[dIdx] : [[-1, 0], [1, 0]][dIdx];
|
||||||
const cx = x + w / 2, cy = y + h / 2, k = w * 0.22, px_ = -dy, py_ = dx;
|
const cx = x + w / 2, cy = y + h / 2, k = w * 0.22, px_ = -dy, py_ = dx;
|
||||||
ctx.strokeStyle = 'rgba(20,16,12,0.55)'; ctx.lineWidth = Math.max(2, zp * 0.9); ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
ctx.strokeStyle = 'rgba(20,16,12,0.55)'; ctx.lineWidth = Math.max(2, zp * 0.9); ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
||||||
const chev = () => { ctx.beginPath(); ctx.moveTo(cx - dx * k * 0.6 + px_ * k, cy - dy * k * 0.6 + py_ * k); ctx.lineTo(cx + dx * k * 0.7, cy + dy * k * 0.7); ctx.lineTo(cx - dx * k * 0.6 - px_ * k, cy - dy * k * 0.6 - py_ * k); ctx.stroke(); };
|
const chev = () => { ctx.beginPath(); ctx.moveTo(cx - dx * k * 0.6 + px_ * k, cy - dy * k * 0.6 + py_ * k); ctx.lineTo(cx + dx * k * 0.7, cy + dy * k * 0.7); ctx.lineTo(cx - dx * k * 0.6 - px_ * k, cy - dy * k * 0.6 - py_ * k); ctx.stroke(); };
|
||||||
chev(); ctx.strokeStyle = 'rgba(255,224,160,0.98)'; ctx.lineWidth = Math.max(1.2, zp * 0.45); chev();
|
chev(); ctx.strokeStyle = 'rgba(255,224,160,0.98)'; ctx.lineWidth = Math.max(1.2, zp * 0.45); chev();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// dragging a line of belts, filters or tubes: one long arrow over it, the way it will run
|
// ends of the line that would join one block up or down get the "doesn't connect" mark
|
||||||
if (GAME.drag && dd >= 0 && cells.length > 1 && !modShift()) {
|
for (const [bx, by] of cells.length > 1 ? [cells[0], cells[cells.length - 1]] : cells)
|
||||||
const [x0, y0, w0, h0] = blockRectPx(cells[0][0], cells[0][1], 1, 1), [x1, y1, w1, h1] = blockRectPx(cells[cells.length - 1][0], cells[cells.length - 1][1], 1, 1);
|
for (let end = 0; end < 2; end++) if (nearMiss(bx, by, end, planned)) drawJoinMark(bx, by, end, code, v);
|
||||||
let sx = x0 + w0 / 2, sy = y0 + h0 / 2, ex = x1 + w1 / 2, ey = y1 + h1 / 2;
|
|
||||||
const [dx, dy] = P.id === 'tube' ? [[0, -1], [1, 0], [0, 1], [-1, 0]][dd] : [[-1, 0], [1, 0]][dd];
|
|
||||||
if ((ex - sx) * dx + (ey - sy) * dy < 0) { const tx = sx, ty = sy; sx = ex; sy = ey; ex = tx; ey = ty; }
|
|
||||||
const off = BS * zp * 1.0, hx = ex + dx * BS * zp * 0.55, hy = ey + dy * BS * zp * 0.55, ah = Math.max(8, BS * zp * 0.5);
|
|
||||||
const ox = dy ? off : 0, oy = dx ? off : 0;
|
|
||||||
const arrow = () => { ctx.beginPath(); ctx.moveTo(sx - dx * BS * zp * 0.3 + ox, sy - dy * BS * zp * 0.3 + oy); ctx.lineTo(hx + ox, hy + oy); ctx.moveTo(hx - dx * ah - dy * ah * 0.6 + ox, hy - dy * ah - dx * ah * 0.6 + oy); ctx.lineTo(hx + ox, hy + oy); ctx.lineTo(hx - dx * ah + dy * ah * 0.6 + ox, hy - dy * ah + dx * ah * 0.6 + oy); ctx.stroke(); };
|
|
||||||
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
|
||||||
ctx.strokeStyle = 'rgba(20,16,12,0.6)'; ctx.lineWidth = Math.max(4, zp * 1.1); arrow();
|
|
||||||
ctx.strokeStyle = 'rgba(255,224,160,0.98)'; ctx.lineWidth = Math.max(2, zp * 0.55); arrow();
|
|
||||||
}
|
|
||||||
if (GAME.drag && cells.length > 1) {
|
if (GAME.drag && cells.length > 1) {
|
||||||
const label = `${cells.length} × ${costText(pieceCostNow(P), 1)}`;
|
const label = `${cells.length} × ${costText(pieceCostNow(P), 1)}`;
|
||||||
ctx.font = `600 12px ui-monospace, monospace`; ctx.fillStyle = 'rgba(14,12,11,0.85)';
|
ctx.font = `600 12px ui-monospace, monospace`; ctx.fillStyle = 'rgba(14,12,11,0.85)';
|
||||||
@@ -153,7 +143,7 @@ function drawPreviews() {
|
|||||||
const { bx, by } = stampOrigin(bw, bh);
|
const { bx, by } = stampOrigin(bw, bh);
|
||||||
const err = P.kind === 'mold' ? canPlaceMold(P.mold, bx, by) : canPlaceMachine(P.id, bx, by);
|
const err = P.kind === 'mold' ? canPlaceMold(P.mold, bx, by) : canPlaceMachine(P.id, bx, by);
|
||||||
const swap = !!err && GAME.replace && /already/.test(err) && !footprintBlocker(bx, by, bw, bh);
|
const swap = !!err && GAME.replace && /already/.test(err) && !footprintBlocker(bx, by, bw, bh);
|
||||||
const ok = (!err || swap) && inReach(bx * BS + bw * BS / 2, by * BS + bh * BS / 2) && canAfford(pieceCostNow(P), 1);
|
const ok = (!err || swap) && inBuildRange(bx * BS + bw * BS / 2, by * BS + bh * BS / 2) && canAfford(pieceCostNow(P), 1);
|
||||||
const [x, y, w, h] = blockRectPx(bx, by, bw, bh);
|
const [x, y, w, h] = blockRectPx(bx, by, bw, bh);
|
||||||
if (swap && ok) { ctx.fillStyle = 'rgba(255,190,90,0.35)'; ctx.fillRect(x, y, w, h); }
|
if (swap && ok) { ctx.fillStyle = 'rgba(255,190,90,0.35)'; ctx.fillRect(x, y, w, h); }
|
||||||
if (P.kind === 'mold') {
|
if (P.kind === 'mold') {
|
||||||
@@ -188,3 +178,64 @@ function drawPreviews() {
|
|||||||
if (t === 'build' && GAME.replace && !sel && !pasting) drawTag('Replace', '255,190,90', p.x + BS * zp * 1.2, p.y + BS * zp * 1.4);
|
if (t === 'build' && GAME.replace && !sel && !pasting) drawTag('Replace', '255,190,90', p.x + BS * zp * 1.2, p.y + BS * zp * 1.4);
|
||||||
drawCursorLabel(p.x, p.y, t === 'vacuum' ? GAME.brush[GAME.tool] : t === 'ignite' || t === 'drill' ? 1 : 2);
|
drawCursorLabel(p.x, p.y, t === 'vacuum' ? GAME.brush[GAME.tool] : t === 'ignite' || t === 'drill' ? 1 : 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function previewBlock(code, x, y, w, h, stroke = false, direction = -1, bx = -1, by = -1, planned = null) {
|
||||||
|
const ctx = R.ctx;
|
||||||
|
if (code === BP_SCREW && (direction >= 4 || (bx >= 0 && straightScrewJoins(bx, by, direction, planned)))) {
|
||||||
|
const joins = bx >= 0 ? (direction >= 4 ? diagonalScrewJoins(bx, by, direction, planned) : straightScrewJoins(bx, by, direction, planned)) : 0;
|
||||||
|
const scale = w / BS, step = SET.look === "smooth" ? 0.25 : 1;
|
||||||
|
const b = bx >= 0 ? by * BW + bx : 0, ox = (b % BW) * BS, oy = ((b / BW) | 0) * BS;
|
||||||
|
ctx.beginPath();
|
||||||
|
for (let yy = -4; yy < 8; yy += step) for (let xx = -4; xx < 8; xx += step) {
|
||||||
|
if (direction < 4 && ((direction & 1) ? xx < 0 || xx >= BS : yy < 0 || yy >= BS)) continue;
|
||||||
|
const sd = diagonalScrewShape(ox + xx + step / 2, oy + yy + step / 2, b, direction, joins);
|
||||||
|
if (sd <= 0 && (!stroke || sd > -step * 1.5)) ctx.rect(x + xx * scale, y + yy * scale, step * scale, step * scale);
|
||||||
|
}
|
||||||
|
if (stroke) { const fill = ctx.fillStyle; ctx.fillStyle = ctx.strokeStyle; ctx.fill(); ctx.fillStyle = fill; }
|
||||||
|
else ctx.fill();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isSlopeBelt(code)) {
|
||||||
|
const b = by * BW + bx, joins = bx >= 0 ? slopeBeltConnections(bx, by, code, planned) : 0;
|
||||||
|
const scale = w / BS, step = SET.look === 'smooth' ? 0.25 : 1;
|
||||||
|
const ox = bx * BS, oy = by * BS;
|
||||||
|
ctx.beginPath();
|
||||||
|
for (let yy = -4; yy < 8; yy += step) for (let xx = -4; xx < 8; xx += step) {
|
||||||
|
const wx = ox + xx, wy = oy + yy, ix = Math.floor(wx), iy = Math.floor(wy);
|
||||||
|
if (bx >= 0 && (ix < ox || ix >= ox + BS || iy < oy || iy >= oy + BS)) {
|
||||||
|
if (ix < 0 || ix >= W || iy < 0 || iy >= H) continue;
|
||||||
|
const i = iy * W + ix, n = (iy >> 2) * BW + (ix >> 2), next = planned && planned.get(n);
|
||||||
|
if (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || (blockP[n] && !isSlopeBelt(blockP[n])) || (next && !isSlopeBelt(next[0]))) continue;
|
||||||
|
}
|
||||||
|
const sd = slopeBeltShape(wx + step / 2, wy + step / 2, b, joins, code);
|
||||||
|
if (sd <= 0 && (!stroke || sd > -step * 1.5)) ctx.rect(x + xx * scale, y + yy * scale, step * scale, step * scale);
|
||||||
|
}
|
||||||
|
if (stroke) { const fill = ctx.fillStyle; ctx.fillStyle = ctx.strokeStyle; ctx.fill(); ctx.fillStyle = fill; }
|
||||||
|
else ctx.fill();
|
||||||
|
// Use the same filled wall profile as placed slopes; ribs stay on the physics feet.
|
||||||
|
if (!isFilter(code) && (direction < 0 || slopeUphill(code, direction))) { // downhill: a plain belt
|
||||||
|
ctx.beginPath();
|
||||||
|
for (let yy = -9; yy < 8; yy += step) for (let xx = -5; xx < 9; xx += step)
|
||||||
|
if (slopeWallShape(ox + xx + step / 2, oy + yy + step / 2, b, code) <= 0)
|
||||||
|
ctx.rect(x + xx * scale, y + yy * scale, step * scale, step * scale);
|
||||||
|
const alpha = ctx.globalAlpha; ctx.globalAlpha *= 0.45; ctx.fill(); ctx.globalAlpha = alpha;
|
||||||
|
ctx.beginPath();
|
||||||
|
for (let k = 0; k < BS; k++) if (((ox + k) % CLEAT_SPACING + CLEAT_SPACING) % CLEAT_SPACING === 0) {
|
||||||
|
const fy = beltFaceY(code, 0, 0, k);
|
||||||
|
for (let h = 0; h <= CLEAT_REACH; h++)
|
||||||
|
ctx.rect(x + (k + beltGrade(code) * h) * scale, y + (fy - h - 1) * scale, scale, scale);
|
||||||
|
}
|
||||||
|
if (stroke) { const fill = ctx.fillStyle; ctx.fillStyle = ctx.strokeStyle; ctx.fill(); ctx.fillStyle = fill; }
|
||||||
|
else ctx.fill();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
code = slopeShape(code);
|
||||||
|
if (!isSlope(code)) { if (stroke) ctx.strokeRect(x, y, w, h); else ctx.fillRect(x, y, w, h); return; }
|
||||||
|
const right = code === BP_SLOPE_BR || code === BP_SLOPE_TR, above = code >= BP_SLOPE_TL;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x + (right ? w : 0), y + (above ? 0 : h));
|
||||||
|
ctx.lineTo(x + (right ? w : 0), y + (above ? h : 0));
|
||||||
|
ctx.lineTo(x + (right ? 0 : w), y + (above ? 0 : h)); ctx.closePath();
|
||||||
|
if (stroke) ctx.stroke(); else ctx.fill();
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -112,7 +112,7 @@ async function saveWorld(slot, quiet) {
|
|||||||
const list = await listWorlds();
|
const list = await listWorlds();
|
||||||
let entry = list.find(w => w.id === id);
|
let entry = list.find(w => w.id === id);
|
||||||
if (!entry) { entry = { id, name, createdAt: savedAt }; list.push(entry); }
|
if (!entry) { entry = { id, name, createdAt: savedAt }; list.push(entry); }
|
||||||
Object.assign(entry, { savedAt, playTime, seed: s.meta.seed, w: s.meta.W, h: s.meta.H, debt: s.meta.economy.debt, ...(c ? { career: c.id, claimNo } : {}) });
|
Object.assign(entry, { savedAt, playTime, seed: s.meta.seed, w: s.meta.W, h: s.meta.H, debt: s.meta.economy.debt, dev: s.meta.dev, ...(c ? { career: c.id, claimNo } : {}) });
|
||||||
await DB.op('readwrite', st => { st.put({ meta: s.meta, buffers, savedAt, playTime }, `${id}:${slot}`); st.put(list, 'worlds'); if (c) st.put(c, 'career:' + c.id); });
|
await DB.op('readwrite', st => { st.put({ meta: s.meta, buffers, savedAt, playTime }, `${id}:${slot}`); st.put(list, 'worlds'); if (c) st.put(c, 'career:' + c.id); });
|
||||||
});
|
});
|
||||||
if (!quiet) toast(slot === 'auto' ? 'Autosaved.' : 'World saved.');
|
if (!quiet) toast(slot === 'auto' ? 'Autosaved.' : 'World saved.');
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ function renderSelCard() {
|
|||||||
const b = BIT_TIERS[player.bitTier];
|
const b = BIT_TIERS[player.bitTier];
|
||||||
const pw = PULSE_W[GAME.drillW], tn = GAME.drillTune;
|
const pw = PULSE_W[GAME.drillW], tn = GAME.drillTune;
|
||||||
h = `<div class="row cycle"><kbd>Q</kbd><span class="name grow">Resonance drill</span><span class="chip hot">${pw.name} pulse</span><kbd>E</kbd></div>
|
h = `<div class="row cycle"><kbd>Q</kbd><span class="name grow">Resonance drill</span><span class="chip hot">${pw.name} pulse</span><kbd>E</kbd></div>
|
||||||
<div class="row" style="flex-wrap:wrap;gap:4px"><span class="chip">${esc(b.name.toLowerCase())}</span><span class="chip">hardness ≤ ${b.hard}</span><span class="chip">reach ${drillReach(GAME.drillW)}</span>${tn ? `<span class="chip ok"><span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:4px;background:${rgbOf(tn)}"></span>tuned: ${esc(MD[tn].name.toLowerCase())} · R</span>` : '<span class="chip">untuned · R tunes</span>'}</div>
|
<div class="row" style="flex-wrap:wrap;gap:4px"><span class="chip">${esc(b.name.toLowerCase())}</span><span class="chip">hardness ≤ ${b.hard}</span><span class="chip">reach ${drillReach(GAME.drillW)}</span>${pingWait() ? `<span class="chip">ping ${pingWait()}s</span>` : '<span class="chip ok">ping ready</span>'}${tn ? `<span class="chip ok"><span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:4px;background:${rgbOf(tn)}"></span>tuned: ${esc(MD[tn].name.toLowerCase())} · R</span>` : '<span class="chip">untuned · R tunes</span>'}</div>
|
||||||
<div class="sub">Hold to send pulses at the cursor: rock they shake hard enough shatters and falls. <kbd>Q</kbd>/<kbd>E</kbd> width: narrow bores a long tunnel fast, wide clears a big face up close. <kbd>R</kbd> over a material tunes to it, so only that shatters. Built blocks are never harmed.</div>`;
|
<div class="sub">Hold to send pulses at the cursor: rock they shake hard enough shatters and falls. <kbd>Q</kbd>/<kbd>E</kbd> width: narrow bores a long tunnel fast, wide clears a big face up close. <kbd>R</kbd> over a material tunes to it, so only that shatters. Right click pings: veins nearby light up, even unexplored. Built blocks are never harmed.</div>`;
|
||||||
} else if (t === 'build') {
|
} else if (t === 'build') {
|
||||||
// the build tool in general; the piece itself is described in the menu's hover popup
|
// the build tool in general; the piece itself is described in the menu's hover popup
|
||||||
const P = piece();
|
const P = piece();
|
||||||
|
|||||||
@@ -13,18 +13,20 @@ function filterParts(f) {
|
|||||||
function filterSentence(f) { const p = filterParts(f); return `drop ${p.drops} and carry ${p.rides}`; }
|
function filterSentence(f) { const p = filterParts(f); return `drop ${p.drops} and carry ${p.rides}`; }
|
||||||
// Change the whole run of matching filters at once
|
// Change the whole run of matching filters at once
|
||||||
function applyFilterRun(run, f) {
|
function applyFilterRun(run, f) {
|
||||||
for (const b of run) { blockV[b] = f.dir ? 128 : 0; setFilter(b, f); }
|
undoRecord('filter settings', () => {
|
||||||
|
for (const b of run) { undoNote({ t: 'filter', bx: b % BW, by: (b / BW) | 0, v: blockV[b], extra: filterInfo(b) }); blockV[b] = filterVariant(blockP[b], f.dir ? 128 : 0); setFilter(b, f); }
|
||||||
|
});
|
||||||
filtersDirty = true;
|
filtersDirty = true;
|
||||||
}
|
}
|
||||||
function openFilterPicker(cur, onDone, title) {
|
function openFilterPicker(cur, onDone, title) {
|
||||||
const sel = new Set(cur.m);
|
const sel = new Set(cur.m);
|
||||||
let inv = cur.inv ? 1 : 0, dir = cur.dir ? 1 : 0;
|
let inv = cur.inv ? 1 : 0, dir = beltGrade(cur.code) ? (beltGrade(cur.code)>0 ? 1 : 0) : cur.dir ? 1 : 0;
|
||||||
const list = filterChoices(cur.m);
|
const list = filterChoices(cur.m);
|
||||||
const body = `<div class="fmode" role="radiogroup" aria-label="Mode">
|
const body = `<div class="fmode" role="radiogroup" aria-label="Mode">
|
||||||
<button class="fm" role="radio" data-inv="0"><b>↓ Drop these</b><span>They fall through; the rest rides along</span></button>
|
<button class="fm" role="radio" data-inv="0"><b>↓ Drop these</b><span>They fall through; the rest rides along</span></button>
|
||||||
<button class="fm" role="radio" data-inv="1"><b>→ Carry these</b><span>They ride along; the rest falls through</span></button>
|
<button class="fm" role="radio" data-inv="1"><b>→ Carry these</b><span>They ride along; the rest falls through</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="fdir" role="radiogroup" aria-label="Rollers turn"><span class="label">Rollers turn</span><button class="fd" role="radio" data-dir="0">← Left</button><button class="fd" role="radio" data-dir="1">Right →</button></div>`
|
<div class="fdir" role="radiogroup" aria-label="Screen travel"><span class="label">Screen travel</span>${beltGrade(cur.code) ? `<span>${esc(beltDirection(cur.code, dir << 7))}</span>` : '<button class="fd" role="radio" data-dir="0">← Left</button><button class="fd" role="radio" data-dir="1">Right →</button>'}</div>`
|
||||||
+ (list.length ? `<div class="fpick">${list.map(m => `<button class="fp" role="checkbox" data-m="${m}"><span class="ck" aria-hidden="true"></span><span class="sw" style="background:${rgbOf(m)}"></span><span>${esc(MD[m].name)}</span></button>`).join('')}</div>` : '<p class="note">Find something loose first.</p>')
|
+ (list.length ? `<div class="fpick">${list.map(m => `<button class="fp" role="checkbox" data-m="${m}"><span class="ck" aria-hidden="true"></span><span class="sw" style="background:${rgbOf(m)}"></span><span>${esc(MD[m].name)}</span></button>`).join('')}</div>` : '<p class="note">Find something loose first.</p>')
|
||||||
+ `<p class="note fp-sum" id="fp-sum"></p>`;
|
+ `<p class="note fp-sum" id="fp-sum"></p>`;
|
||||||
dialog(title || 'Filter', body, [{ label: 'Clear', cls: 'ghost', fn: () => { sel.clear(); sync(); return true; } }, { label: 'Cancel' }, { label: 'Set filter', cls: 'primary', fn: () => { onDone({ m: [...sel].sort((a, b) => a - b), inv, dir }); hudDirty = true; } }]);
|
dialog(title || 'Filter', body, [{ label: 'Clear', cls: 'ghost', fn: () => { sel.clear(); sync(); return true; } }, { label: 'Cancel' }, { label: 'Set filter', cls: 'primary', fn: () => { onDone({ m: [...sel].sort((a, b) => a - b), inv, dir }); hudDirty = true; } }]);
|
||||||
@@ -34,7 +36,7 @@ function openFilterPicker(cur, onDone, title) {
|
|||||||
box.querySelectorAll('[data-dir]').forEach(b => { const on = +b.dataset.dir === dir; b.classList.toggle('on', on); b.setAttribute('aria-checked', on); });
|
box.querySelectorAll('[data-dir]').forEach(b => { const on = +b.dataset.dir === dir; b.classList.toggle('on', on); b.setAttribute('aria-checked', on); });
|
||||||
box.querySelectorAll('[data-m]').forEach(b => { const on = sel.has(+b.dataset.m); b.classList.toggle('on', on); b.setAttribute('aria-checked', on); });
|
box.querySelectorAll('[data-m]').forEach(b => { const on = sel.has(+b.dataset.m); b.classList.toggle('on', on); b.setAttribute('aria-checked', on); });
|
||||||
const f = { m: [...sel].sort((a, b) => a - b), inv };
|
const f = { m: [...sel].sort((a, b) => a - b), inv };
|
||||||
box.querySelector('#fp-sum').textContent = `${title && /in this row/.test(title) ? 'These filters' : 'This filter'} will ${filterSentence(f)}, ${dir ? 'right' : 'left'}.`;
|
box.querySelector('#fp-sum').textContent = `${title && /in this row/.test(title) ? 'These filters' : 'This filter'} will ${filterSentence(f)}, ${beltGrade(cur.code) ? beltDirection(cur.code, dir << 7) : dir ? 'right' : 'left'}.`;
|
||||||
}
|
}
|
||||||
box.querySelectorAll('[data-dir]').forEach(b => b.onclick = () => { dir = +b.dataset.dir; playSfx('ui'); sync(); });
|
box.querySelectorAll('[data-dir]').forEach(b => b.onclick = () => { dir = +b.dataset.dir; playSfx('ui'); sync(); });
|
||||||
box.querySelectorAll('[data-inv]').forEach(b => b.onclick = () => { inv = +b.dataset.inv; playSfx('ui'); sync(); });
|
box.querySelectorAll('[data-inv]').forEach(b => b.onclick = () => { inv = +b.dataset.inv; playSfx('ui'); sync(); });
|
||||||
|
|||||||
+41
-4
@@ -47,6 +47,7 @@ function rotate() {
|
|||||||
else if (t === 'build') {
|
else if (t === 'build') {
|
||||||
const P = piece();
|
const P = piece();
|
||||||
if (pieceUsesWall(P)) cycleWall(1);
|
if (pieceUsesWall(P)) cycleWall(1);
|
||||||
|
else if (P.filter && beltGrade(pieceCode(P))) return;
|
||||||
else if (P.dirs) GAME.pieceDir[P.id] = ((GAME.pieceDir[P.id] || 0) + 1) % P.dirs.length;
|
else if (P.dirs) GAME.pieceDir[P.id] = ((GAME.pieceDir[P.id] || 0) + 1) % P.dirs.length;
|
||||||
else return;
|
else return;
|
||||||
} else if (t === 'drill') { tuneUnderCursor(); return; }
|
} else if (t === 'drill') { tuneUnderCursor(); return; }
|
||||||
@@ -82,15 +83,51 @@ function setBrush(d) {
|
|||||||
// The block variant a piece is placed with: wall material, or direction (a filter's
|
// The block variant a piece is placed with: wall material, or direction (a filter's
|
||||||
// materials and mode travel separately, as GAME.filterCfg)
|
// materials and mode travel separately, as GAME.filterCfg)
|
||||||
function pieceV(P) {
|
function pieceV(P) {
|
||||||
if (P.code === BP_WALL) return GAME.wallMat;
|
if (isWall(P.code)) return GAME.wallMat;
|
||||||
if (P.filter) return (GAME.pieceDir[P.id] || 0) << 7;
|
if (P.filter) return filterVariant(pieceCode(P), (GAME.pieceDir[P.id] || 0) << 7);
|
||||||
return GAME.pieceDir[P.id] || 0;
|
return GAME.pieceDir[P.id] || 0;
|
||||||
}
|
}
|
||||||
|
// One drag for the build tool and its preview: code, direction, variant, blocks, and the planned
|
||||||
|
// blocks the join rule sees. A line that joins one block up or down snaps there (joinSnap).
|
||||||
|
function dragPlan(P, a, b, shift) {
|
||||||
|
const code = pieceCode(P, b), dd = a ? dragDirFor(P, a, b) : -1, dir = dd >= 0 ? dd : (GAME.pieceDir[P.id] || 0);
|
||||||
|
const v = isWall(P.code) ? GAME.wallMat : P.filter ? filterVariant(code, dir << 7) : dir;
|
||||||
|
let cells = !a ? [[b.bx, b.by]] : shift && !lineOnly(code) ? blockRect(a.bx, a.by, b.bx, b.by)
|
||||||
|
: blockLine(a.bx, a.by, b.bx, b.by, isWall(code) || isSlopeBelt(code) || code === BP_SCREW);
|
||||||
|
const snap = lineOnly(code) ? joinSnap(code, v, cells) : 0;
|
||||||
|
if (snap) cells = cells.map(([x, y]) => [x, y + snap]);
|
||||||
|
return { code, dd, dir, v, cells, snap, planned: new Map(cells.map(([x, y]) => [y * BW + x, [code, v]])) };
|
||||||
|
}
|
||||||
function pieceExtra(P) { return P.filter ? { m: GAME.filterCfg.m.slice(), inv: GAME.filterCfg.inv } : undefined; }
|
function pieceExtra(P) { return P.filter ? { m: GAME.filterCfg.m.slice(), inv: GAME.filterCfg.inv } : undefined; }
|
||||||
function filterBlockAt(x, y) { if (x < 0 || y < 0 || x >= W || y >= H) return -1; const b = ((y / BS) | 0) * BW + ((x / BS) | 0); return blockP[b] === BP_FILTER ? b : -1; }
|
function filterBlockAt(x, y) { if (x < 0 || y < 0 || x >= W || y >= H) return -1; const b = ((y / BS) | 0) * BW + ((x / BS) | 0); return isFilter(blockP[b]) ? b : -1; }
|
||||||
function chipCost(cost) { const ok = canAfford(cost, 1); return `<span class="chip ${ok ? 'ok' : 'bad'}">${esc(costText(cost, 1))}</span>`; }
|
function chipCost(cost) { const ok = canAfford(cost, 1); return `<span class="chip ${ok ? 'ok' : 'bad'}">${esc(costText(cost, 1))}</span>`; }
|
||||||
function pieceCostNow(P, m = GAME.wallMat) {
|
function pieceCostNow(P, m = GAME.wallMat) {
|
||||||
|
if (devFree) return [[CASH, 0]];
|
||||||
if (P.kind === 'mold') return wallCost(m, MOLDS[P.mold].walls);
|
if (P.kind === 'mold') return wallCost(m, MOLDS[P.mold].walls);
|
||||||
if (P.kind === 'block' && P.code === BP_WALL) return wallCost(m, BS * BS);
|
if (P.kind === 'block' && isWall(P.code)) return pieceCost(pieceCode(P), m);
|
||||||
return P.cost;
|
return P.cost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pieceCode(P, end = aimBlock()) {
|
||||||
|
if (P.id === 'belt' || P.filter) {
|
||||||
|
const a = GAME.drag || end;
|
||||||
|
// Shift changes nothing here: belts and filters only ever draw a line
|
||||||
|
if (GAME.drag && diagonalDrag(end.bx - a.bx, end.by - a.by))
|
||||||
|
return (P.filter ? BP_FILTER_UP : BP_BELT_UP) + ((end.bx - a.bx) * (end.by - a.by) > 0 ? 1 : 0);
|
||||||
|
if (end.bx === a.bx && end.by === a.by) return GAME.pieceShape && GAME.pieceShape[P.id] || P.code;
|
||||||
|
return P.code;
|
||||||
|
}
|
||||||
|
if (P.code !== BP_WALL) return P.code;
|
||||||
|
const a = GAME.drag || end, dx = end.bx - a.bx, dy = end.by - a.by;
|
||||||
|
if (GAME.drag && (dx || dy)) {
|
||||||
|
return diagonalDrag(dx, dy) && !(typeof modShift === 'function' && modShift()) ? slopeCode(dx, dy, a.flip ? 2 : 0) : BP_WALL;
|
||||||
|
}
|
||||||
|
const shape = GAME.pieceDir.wall || 0;
|
||||||
|
return shape ? BP_SLOPE_BL + shape - 1 : BP_WALL;
|
||||||
|
}
|
||||||
|
function turnSlope() {
|
||||||
|
const a = GAME.drag, b = aimBlock();
|
||||||
|
if (a && diagonalDrag(b.bx - a.bx, b.by - a.by)) a.flip = !a.flip;
|
||||||
|
else GAME.pieceDir.wall = ((GAME.pieceDir.wall || 0) + 1) % 5;
|
||||||
|
hudDirty = true;
|
||||||
|
}
|
||||||
|
|||||||
+3
-2
@@ -11,6 +11,7 @@ function showLoading(on, p, label, title) {
|
|||||||
if (on) { $('loadbar').style.width = Math.round((p || 0) * 100) + '%'; $('loadtext').textContent = Math.round((p || 0) * 100) + '%'; }
|
if (on) { $('loadbar').style.width = Math.round((p || 0) * 100) + '%'; $('loadtext').textContent = Math.round((p || 0) * 100) + '%'; }
|
||||||
}
|
}
|
||||||
function enterGame() {
|
function enterGame() {
|
||||||
|
closeDevConsole(); $('dev-output').textContent = ''; $('dev-input').value = '';
|
||||||
$('cards').innerHTML = '';
|
$('cards').innerHTML = '';
|
||||||
if (R.floaters) R.floaters.length = 0; R.particles.length = 0;
|
if (R.floaters) R.floaters.length = 0; R.particles.length = 0;
|
||||||
GAME.status = ''; GAME.statusT = 0;
|
GAME.status = ''; GAME.statusT = 0;
|
||||||
@@ -80,7 +81,7 @@ async function loadSavedWorld(id) {
|
|||||||
}
|
}
|
||||||
async function quitToTitle() {
|
async function quitToTitle() {
|
||||||
await saveWorld('main', true);
|
await saveWorld('main', true);
|
||||||
GAME.running = false; closeAllModals();
|
GAME.running = false; closeDevConsole(); closeAllModals();
|
||||||
$('hud').hidden = true; $('title').hidden = false;
|
$('hud').hidden = true; $('title').hidden = false;
|
||||||
refreshContinue();
|
refreshContinue();
|
||||||
if (!TITLE.raf) drawTitle();
|
if (!TITLE.raf) drawTitle();
|
||||||
@@ -135,7 +136,7 @@ async function renderWorlds(editId) {
|
|||||||
for (const w of list) {
|
for (const w of list) {
|
||||||
const row = el('div', { class: 'world-row', 'data-world': w.id });
|
const row = el('div', { class: 'world-row', 'data-world': w.id });
|
||||||
const mins = Math.floor((w.playTime || 0) / 60);
|
const mins = Math.floor((w.playTime || 0) / 60);
|
||||||
row.innerHTML = `<div class="world-info"><strong>${esc(w.name)}</strong><div class="note">seed ${esc(w.seed)} · ${esc(w.w)}×${esc(w.h)} grains · ${Math.floor(mins / 60)}:${String(mins % 60).padStart(2, '0')} played</div><div class="note">${w.savedAt ? esc(new Date(w.savedAt).toLocaleString()) : 'Not saved yet'}</div></div>`;
|
row.innerHTML = `<div class="world-info"><strong>${esc(w.name)}</strong>${w.dev ? ' <span class="dev-badge">Dev console</span>' : ''}<div class="note">seed ${esc(w.seed)} · ${esc(w.w)}×${esc(w.h)} grains · ${Math.floor(mins / 60)}:${String(mins % 60).padStart(2, '0')} played</div><div class="note">${w.savedAt ? esc(new Date(w.savedAt).toLocaleString()) : 'Not saved yet'}</div></div>`;
|
||||||
const actions = el('div', { class: 'world-actions' });
|
const actions = el('div', { class: 'world-actions' });
|
||||||
const button = (label, fn, cls = '') => { const b = el('button', { class: 'btn ' + cls }, label); b.onclick = async () => { try { await fn(); } catch (e) { toast('Could not reach browser storage.'); } }; actions.appendChild(b); };
|
const button = (label, fn, cls = '') => { const b = el('button', { class: 'btn ' + cls }, label); b.onclick = async () => { try { await fn(); } catch (e) { toast('Could not reach browser storage.'); } }; actions.appendChild(b); };
|
||||||
if (editId === w.id) {
|
if (editId === w.id) {
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+168
-4
@@ -434,7 +434,23 @@ with sync_playwright() as p:
|
|||||||
}
|
}
|
||||||
})()"""))
|
})()"""))
|
||||||
|
|
||||||
# 3. right button blows the selection; exhausted materials stay selected
|
check('out-of-reach blow preview sits at the capped nozzle target', ev("""(() => {
|
||||||
|
const aim = GAME.aim, consumed = GAME.rmbConsumed, arc = R.ctx.arc;
|
||||||
|
const arcs = [];
|
||||||
|
try {
|
||||||
|
GAME.aim = { x: bot.x + bot.w / 2 + 3 * REACH, y: bot.y + bot.h / 2 };
|
||||||
|
GAME.rmb = true; GAME.rmbConsumed = false;
|
||||||
|
R.ctx.arc = function (x, y, r, ...rest) { arcs.push({ x, y, r }); return arc.call(this, x, y, r, ...rest); };
|
||||||
|
drawPreviews();
|
||||||
|
const nz = nozzle(Math.floor(GAME.aim.x) + 0.5, Math.floor(GAME.aim.y) + 0.5);
|
||||||
|
const target = worldToScreenPx(nz.ox + nz.dx * nz.dist, nz.oy + nz.dy * nz.dist);
|
||||||
|
return arcs.some(p => Math.abs(p.x - target.x) < 0.01 && Math.abs(p.y - target.y) < 0.01 && Math.abs(p.r - (GAME.brush[1] + 0.5) * R.zp) < 0.01);
|
||||||
|
} finally {
|
||||||
|
R.ctx.arc = arc; GAME.aim = aim; GAME.rmb = false; GAME.rmbConsumed = consumed;
|
||||||
|
}
|
||||||
|
})()"""))
|
||||||
|
|
||||||
|
# 3. right button blows the selection; exhausted materials are deselected
|
||||||
refill('bag[COAL] = 120; bag[RSTONE] = 600; bag[CUORE] = 60; bag[LCLAY] = 200; bag[LWOOD] = 120')
|
refill('bag[COAL] = 120; bag[RSTONE] = 600; bag[CUORE] = 60; bag[LCLAY] = 200; bag[LWOOD] = 120')
|
||||||
ok = select('2', 'GAME.blow.size === 1 && GAME.blow.has(COAL)')
|
ok = select('2', 'GAME.blow.size === 1 && GAME.blow.has(COAL)')
|
||||||
check('Q/E chooses coal to blow', ok, ev('[...GAME.blow]'))
|
check('Q/E chooses coal to blow', ok, ev('[...GAME.blow]'))
|
||||||
@@ -647,6 +663,12 @@ with sync_playwright() as p:
|
|||||||
drag(lambda: blk(bbx - 10, bby - 8), lambda: blk(bbx - 8, bby - 6), 4, True)
|
drag(lambda: blk(bbx - 10, bby - 8), lambda: blk(bbx - 8, bby - 6), 4, True)
|
||||||
nb2 = ev('blockP.reduce((a, v) => a + (v === BP_WALL), 0)')
|
nb2 = ev('blockP.reduce((a, v) => a + (v === BP_WALL), 0)')
|
||||||
check('Shift-drag fills a 3×3 rectangle', nb2 - nb1 == 9, nb2 - nb1)
|
check('Shift-drag fills a 3×3 rectangle', nb2 - nb1 == 9, nb2 - nb1)
|
||||||
|
ev("GAME.pieceIdx = PIECE_BY_ID.belt.index; for (const [m, n] of PIECE_BY_ID.belt.cost) bag[m] += n * 10; hudDirty = true")
|
||||||
|
drag(lambda: blk(bbx + 3, bby - 10), lambda: blk(bbx + 5, bby - 8), 4, True)
|
||||||
|
belts = ev('[blockP.reduce((a, v) => a + isBelt(v), 0), blockP.reduce((a, v) => a + isSlopeBelt(v), 0), ghostBlocks.size]')
|
||||||
|
check('Shift-drag with a belt still lays a sloped line, not a rectangle', belts == [3, 3, 0], f'{belts}, {ev("GAME.status")}')
|
||||||
|
ev('doUndo()'); settle(30)
|
||||||
|
ev("GAME.pieceIdx = PIECE_BY_ID.wall.index; hudDirty = true")
|
||||||
r0 = ev('bag[RSTONE]')
|
r0 = ev('bag[RSTONE]')
|
||||||
pg.mouse.move(*blk(bbx + 3, bby - 4)); pg.mouse.down(button='right'); settle(120); pg.mouse.up(button='right'); settle(80)
|
pg.mouse.move(*blk(bbx + 3, bby - 4)); pg.mouse.down(button='right'); settle(120); pg.mouse.up(button='right'); settle(80)
|
||||||
check('right click takes a block back with a refund', ev('bag[RSTONE]') - r0 == 16 and ev('blockP.reduce((a, v) => a + (v === BP_WALL), 0)') == nb2 - 1, ev('bag[RSTONE]') - r0)
|
check('right click takes a block back with a refund', ev('bag[RSTONE]') - r0 == 16 and ev('blockP.reduce((a, v) => a + (v === BP_WALL), 0)') == nb2 - 1, ev('bag[RSTONE]') - r0)
|
||||||
@@ -660,6 +682,66 @@ with sync_playwright() as p:
|
|||||||
check('bot can sit inside a built wall', not ev(f'botOverlapSolid({wx_ - 2}, {wy_ - 2})'))
|
check('bot can sit inside a built wall', not ev(f'botOverlapSolid({wx_ - 2}, {wy_ - 2})'))
|
||||||
check('bot cannot sit inside natural rock', ev(f'botOverlapSolid({bx}, {gy + 12})'))
|
check('bot cannot sit inside natural rock', ev(f'botOverlapSolid({bx}, {gy + 12})'))
|
||||||
|
|
||||||
|
# Slopes use the same mouse drag path as walls, with T for their corner.
|
||||||
|
pg.click('[data-piece="wall"]'); settle(60)
|
||||||
|
check('Wall replaces the Sloped wall menu piece', ev("piece().id === 'wall' && !BUILD_ORDER.includes('slope') && !document.querySelector('[data-piece=\"slope\"]')"))
|
||||||
|
ev('GAME.wallMat = RSTONE; GAME.pieceDir.wall = 0')
|
||||||
|
drag(lambda: blk(bbx - 5, bby - 12), lambda: blk(bbx - 2, bby - 9))
|
||||||
|
check('diagonal slope drag places four oriented blocks', ev(f'Array.from({{length:4}}, (_,k)=>blockP[({bby}-12+k)*BW+{bbx}-5+k]).every(c=>c===BP_SLOPE_BL)'))
|
||||||
|
pg.keyboard.press('Control+z'); settle(60)
|
||||||
|
pg.keyboard.press('t'); settle(60)
|
||||||
|
check('T cycles the single slope corner', ev('GAME.pieceDir.wall === 1 && pieceCode(piece()) === BP_SLOPE_BL'))
|
||||||
|
for shape in [2, 3, 4, 0]:
|
||||||
|
pg.keyboard.press('t'); settle(40)
|
||||||
|
check(f'T cycles Wall shape {shape}', ev(f'GAME.pieceDir.wall === {shape} && pieceCode(piece()) === ' + ('BP_WALL' if shape == 0 else f'BP_SLOPE_BL+{shape}-1')))
|
||||||
|
pg.mouse.move(*blk(bbx - 5, bby - 12)); settle(60); pg.mouse.down()
|
||||||
|
pg.mouse.move(*blk(bbx - 2, bby - 9)); settle(60)
|
||||||
|
pg.keyboard.press('t'); settle(60); pg.mouse.up(); settle(60)
|
||||||
|
check('T flips a diagonal slope above its face', ev(f'Array.from({{length:4}}, (_,k)=>blockP[({bby}-12+k)*BW+{bbx}-5+k]).every(c=>c===BP_SLOPE_TR)'))
|
||||||
|
pg.keyboard.press('Control+z'); settle(60)
|
||||||
|
pg.click('[data-piece="wall"]'); settle(60)
|
||||||
|
drag(lambda: blk(bbx - 5, bby - 12), lambda: blk(bbx - 2, bby - 9))
|
||||||
|
check('Wall diagonal defaults back to solid below', ev(f'Array.from({{length:4}}, (_,k)=>blockP[({bby}-12+k)*BW+{bbx}-5+k]).every(c=>c===BP_SLOPE_BL)'))
|
||||||
|
pg.keyboard.press('Control+z'); settle(60)
|
||||||
|
|
||||||
|
# Cleated belts and downhill screens use real drags, previews, picking and R.
|
||||||
|
for piece_id in ['belt', 'filter']:
|
||||||
|
check('select ' + piece_id + ' for a diagonal drag', select('3', "piece().id === '" + piece_id + "'"))
|
||||||
|
ev('money=1e6')
|
||||||
|
drag(lambda: blk(bbx - 5, bby - 9), lambda: blk(bbx - 2, bby - 12))
|
||||||
|
code = 'BP_BELT_UP' if piece_id == 'belt' else 'BP_FILTER_UP'
|
||||||
|
check(piece_id + ' uphill drag lays the expected shape', ev(f'Array.from({{length:4}},(_,k)=>blockP[({bby}-9-k)*BW+{bbx}-5+k]).every(c=>c==={code})'))
|
||||||
|
if piece_id == 'filter':
|
||||||
|
check('uphill filter drag runs downhill left', ev(f'Array.from({{length:4}},(_,k)=>blockV[({bby}-9-k)*BW+{bbx}-5+k]).every(v=>v===0)'))
|
||||||
|
pg.mouse.move(*blk(bbx - 3, bby - 11)); settle(60)
|
||||||
|
check('screen hover says down-left', ev(f"hoverInfo(({bbx}-3)*4+1,({bby}-11)*4+2).t1.includes('down-left')"))
|
||||||
|
pg.mouse.click(*blk(bbx - 3, bby - 11), button='middle'); settle(60)
|
||||||
|
pg.keyboard.press('r'); settle(60)
|
||||||
|
check('R leaves a picked screen downhill', ev('pieceCode(piece())===BP_FILTER_UP && pieceV(piece())===0'))
|
||||||
|
pg.keyboard.press('f'); settle(100)
|
||||||
|
check('screen picker offers only downhill travel', pg.locator('#dlg-body [data-dir]').count() == 0 and 'down-left' in pg.locator('#dlg-body').inner_text())
|
||||||
|
pg.locator('#dlg-foot').get_by_role('button', name='Cancel', exact=True).click()
|
||||||
|
pg.keyboard.press('Control+z'); settle(60)
|
||||||
|
|
||||||
|
ev('GAME.pieceShape = {}; GAME.pieceDir.filter = 1; GAME.editRun = null')
|
||||||
|
|
||||||
|
# Diagonal screws use real mouse placement, preview arrows, R and picking.
|
||||||
|
check('select screw conveyor', select('3', "piece().id === 'screw'"))
|
||||||
|
ev('GAME.pieceDir.screw = 0')
|
||||||
|
directions = []
|
||||||
|
for _ in range(8):
|
||||||
|
directions.append(ev('GAME.pieceDir.screw'))
|
||||||
|
pg.keyboard.press('r'); settle(40)
|
||||||
|
check('R cycles eight screw directions', directions == list(range(8)) and ev('GAME.pieceDir.screw') == 0)
|
||||||
|
drag(lambda: blk(bbx - 5, bby - 12), lambda: blk(bbx - 2, bby - 9))
|
||||||
|
check('diagonal screw drag places four down-right blocks', ev(f'Array.from({{length:4}}, (_,k)=>[blockP[({bby}-12+k)*BW+{bbx}-5+k],blockV[({bby}-12+k)*BW+{bbx}-5+k]]).every(([c,v])=>c===BP_SCREW && v===5)'))
|
||||||
|
pg.mouse.move(*blk(bbx - 4, bby - 11)); settle(60)
|
||||||
|
check('diagonal hover names its direction', ev(f"hoverInfo(({bbx}-4)*4+1,({bby}-11)*4+1).t1.includes('down-right')"))
|
||||||
|
pg.mouse.click(*blk(bbx - 4, bby - 11), button='middle'); settle(60)
|
||||||
|
check('picking a diagonal screw keeps its direction', ev("piece().id === 'screw' && GAME.pieceDir.screw === 5"))
|
||||||
|
pg.screenshot(path=str(out / '04-diagonal-screws.png'))
|
||||||
|
pg.keyboard.press('Control+z'); settle(60)
|
||||||
|
|
||||||
# 6. ingot mold: click to place, pour, bars drop below, vacuum them
|
# 6. ingot mold: click to place, pour, bars drop below, vacuum them
|
||||||
ok = select('3', "piece().id === 'ingotmold'")
|
ok = select('3', "piece().id === 'ingotmold'")
|
||||||
check('E cycles to the ingot mold', ok)
|
check('E cycles to the ingot mold', ok)
|
||||||
@@ -679,9 +761,18 @@ with sync_playwright() as p:
|
|||||||
pg.screenshot(path=str(out / '05-mold-ready.png'))
|
pg.screenshot(path=str(out / '05-mold-ready.png'))
|
||||||
check('three bars drop below the mold without F', ev('ents.every(e => e.m === BARCU && e.iy >= molds[0].y + molds[0].h)'), ev('ents.map(e => [e.ix, e.iy])'))
|
check('three bars drop below the mold without F', ev('ents.every(e => e.m === BARCU && e.iy >= molds[0].y + molds[0].h)'), ev('ents.map(e => [e.ix, e.iy])'))
|
||||||
pg.keyboard.press('2'); settle(60)
|
pg.keyboard.press('2'); settle(60)
|
||||||
ev("GAME.status = ''; GAME.statusT = 0"); n_toasts = ev("$('toasts').children.length")
|
ev("GAME.status = ''; GAME.statusT = 0")
|
||||||
pg.mouse.move(*cell_to_screen(ev('molds[0].x') + 3, ev('molds[0].y') + 3)); settle(150); pg.keyboard.press('f'); settle(150)
|
# Keep the camera still while aiming at a mold only four grains tall.
|
||||||
check('F on an automatic ingot mold stays quiet without a toast', ev("$('m-dialog').hidden") and ev("$('toasts').children.length") == n_toasts)
|
lead = ev('SET.cameraLead')
|
||||||
|
ev('SET.cameraLead = false; snapCamera()'); settle(100)
|
||||||
|
pg.mouse.move(*cell_to_screen(ev('molds[0].x') + 8, ev('molds[0].y') + 2)); settle(150)
|
||||||
|
check('mouse aims at the automatic ingot mold', ev('moldAt(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)) === molds[0]'))
|
||||||
|
# count toasts made by the key press itself: counting the ones on screen also caught an older toast expiring
|
||||||
|
ev("window._toastsMade = []; window._toastFn = toast; toast = function (t, o) { _toastsMade.push(t); return _toastFn(t, o); }; 0")
|
||||||
|
pg.keyboard.press('f'); settle(150)
|
||||||
|
made = ev("(() => { toast = _toastFn; return _toastsMade; })()")
|
||||||
|
check('F on an automatic ingot mold stays quiet without a toast', ev("$('m-dialog').hidden") and made == [], made)
|
||||||
|
ev(f'SET.cameraLead = {json.dumps(lead)}')
|
||||||
ev("closeModal('m-dialog')")
|
ev("closeModal('m-dialog')")
|
||||||
check('F with vacuum still uses what is in reach', ev("""(() => {
|
check('F with vacuum still uses what is in reach', ev("""(() => {
|
||||||
$('toasts').innerHTML = '';
|
$('toasts').innerHTML = '';
|
||||||
@@ -1070,6 +1161,7 @@ with sync_playwright() as p:
|
|||||||
settle(1500)
|
settle(1500)
|
||||||
ev("__pad.buttons[7].pressed = false; __pad.buttons[7].value = 0;")
|
ev("__pad.buttons[7].pressed = false; __pad.buttons[7].value = 0;")
|
||||||
settle(200)
|
settle(200)
|
||||||
|
check('gamepad drill aim stays within its reach', ev('Math.hypot(GAME.padAim.x, GAME.padAim.y) <= drillReach(GAME.drillW)'))
|
||||||
check('gamepad RT digs where the right stick aims', ev('stats.dug') > d0, f"{d0}->{ev('stats.dug')}")
|
check('gamepad RT digs where the right stick aims', ev('stats.dug') > d0, f"{d0}->{ev('stats.dug')}")
|
||||||
ev("__pad.buttons[5].pressed = true;"); settle(100); ev("__pad.buttons[5].pressed = false;"); settle(100)
|
ev("__pad.buttons[5].pressed = true;"); settle(100); ev("__pad.buttons[5].pressed = false;"); settle(100)
|
||||||
check('gamepad RB switches to the vacuum', ev('GAME.tool') == 1)
|
check('gamepad RB switches to the vacuum', ev('GAME.tool') == 1)
|
||||||
@@ -1121,6 +1213,78 @@ with sync_playwright() as p:
|
|||||||
fp.goto(url)
|
fp.goto(url)
|
||||||
fp.evaluate('startNewWorld(4242, 512, 384)')
|
fp.evaluate('startNewWorld(4242, 512, 384)')
|
||||||
fp.wait_for_function('GAME.running')
|
fp.wait_for_function('GAME.running')
|
||||||
|
# Silo feedback uses real pointer positions; drawing checks sample an isolated canvas.
|
||||||
|
fp.evaluate("""() => {
|
||||||
|
GAME.paused = true; SET.fog = false; SET.look = 'pixel'; SET.zoom = 8; SET.cursorLabel = true;
|
||||||
|
SET.cameraLead = false; SET.cameraSmooth = 0; resizeRender();
|
||||||
|
for (let y = 80; y < 140; y++) for (let x = 80; x < 160; x++) { setCell(y * W + x, EMPTY, 20, 0); bgm[y * W + x] = 0; }
|
||||||
|
window.testSilo = addMachine('silo', 28, 25, 0);
|
||||||
|
bot.x = 104; bot.y = 106; snapCamera(); $('cards').replaceChildren();
|
||||||
|
}""")
|
||||||
|
for amount in [0, 2000]:
|
||||||
|
fp.evaluate(f'testSilo.sm = SAND; testSilo.sn = {amount}')
|
||||||
|
for name, dx, dy in [('drum', 5, 8), ('hatch with snow', 5, 0), ('cone gap', 1, 22), ('leg', 0, 23), ('gate', 5, 23)]:
|
||||||
|
fp.evaluate('setCell((testSilo.y) * W + testSilo.x + 5, SNOW, -10, 0)')
|
||||||
|
point = fp.evaluate(f'(() => {{ const p = worldToScreenPx(testSilo.x + {dx} + 0.5, testSilo.y + {dy} + 0.5); return [p.x * innerWidth / R.canvas.width, p.y * innerWidth / R.canvas.width]; }})()')
|
||||||
|
fp.mouse.move(*point); fp.wait_for_timeout(200)
|
||||||
|
want = 'sand 2,000 / 4,000' if amount else 'empty'
|
||||||
|
fp.wait_for_function(f"$('info').textContent.includes('{('holds 2,000 / 4,000 sand' if amount else 'empty')}')")
|
||||||
|
check(f'silo {name} hover shows {want}', fp.evaluate(f"hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)).t1 === 'Silo' && hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)).contents === '{want}' && $('info').textContent.includes('{('holds 2,000 / 4,000 sand' if amount else 'empty')}')"))
|
||||||
|
check(f'silo {name} cursor label draws {want} in normal colour', fp.evaluate("""() => {
|
||||||
|
const calls = [], ctx = R.ctx, text = ctx.fillText;
|
||||||
|
ctx.fillText = function(t) { calls.push([t, this.fillStyle]); };
|
||||||
|
try { drawCursorLabel(500, 300, 1); } finally { ctx.fillText = text; }
|
||||||
|
const info = hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y));
|
||||||
|
return calls.some(([t, c]) => t === info.contents && c === '#efe6d8');
|
||||||
|
}"""))
|
||||||
|
check('snow in silo is named snow', fp.evaluate("testSilo.sm = SNOW; hoverInfo(testSilo.x + 5, testSilo.y + 8).contents === 'snow 2,000 / 4,000'"))
|
||||||
|
for look in ['pixel', 'smooth']:
|
||||||
|
check(f'{look} silo fill pixels show empty, near empty, half and full at zooms 2 through 12', fp.evaluate("""pixel => {
|
||||||
|
const canvas = document.createElement('canvas'); canvas.width = 160; canvas.height = 300;
|
||||||
|
const ctx = canvas.getContext('2d'), mm = { x: 112, y: 100, sm: SAND };
|
||||||
|
const at = (x, y, z) => Array.from(ctx.getImageData(Math.floor(x * z), Math.floor(y * z), 1, 1).data).slice(0, 3).join(',');
|
||||||
|
for (let z = 2; z <= 12; z++) for (const [n, h] of [[0, 0], [1, 1], [2000, 8], [4000, 16]]) {
|
||||||
|
mm.sn = n; drawSiloWindow(ctx, mm, 0, 0, z, pixel);
|
||||||
|
for (let y = 3; y < 19; y++) if ((at(9.5, y + 0.5, z) !== '20,24,28') !== (y >= 19 - h)) return false;
|
||||||
|
}
|
||||||
|
// Every powder siloTick accepts must stand apart from dark glass, including coal.
|
||||||
|
for (let m = 1; m < NMAT; m++) if (PH[m] === PH_POWDER) {
|
||||||
|
mm.sm = m; mm.sn = 2000; drawSiloWindow(ctx, mm, 0, 0, 2, pixel);
|
||||||
|
const filled = at(9.5, 18.5, 2).split(',').map(Number);
|
||||||
|
if (Math.max(...filled.map((c, k) => Math.abs(c - [20, 24, 28][k]))) < 25) return false;
|
||||||
|
const image = ctx.getImageData(0, 0, 24, 48).data.slice().join(',');
|
||||||
|
drawSiloWindow(ctx, mm, 0, 0, 2, pixel);
|
||||||
|
if (ctx.getImageData(0, 0, 24, 48).data.join(',') !== image) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}""", look == 'pixel'))
|
||||||
|
fp.set_viewport_size({'width': 700, 'height': 800})
|
||||||
|
check('silo contents remain available when the narrow HUD hides the info plate', fp.evaluate("getComputedStyle($('info')).display === 'none' && hoverInfo(testSilo.x + 5, testSilo.y + 8).contents === 'snow 2,000 / 4,000'"))
|
||||||
|
fp.set_viewport_size({'width': 1280, 'height': 800})
|
||||||
|
check('other tools still warn beyond reach', fp.evaluate("GAME.tool = 1; hoverInfo(testSilo.x + 80, testSilo.y).warn === 'out of reach'"))
|
||||||
|
# Holding the drill with a distant pointer is ordinary aim, but hard rock still warns.
|
||||||
|
fp.evaluate('bot.y = 130; snapCamera()')
|
||||||
|
fp.keyboard.press('1')
|
||||||
|
fp.mouse.move(1000, 400); fp.wait_for_timeout(100)
|
||||||
|
fp.evaluate("$('toasts').replaceChildren(); lastPulseTick = -99; player.bitTier = 0; GAME.drillTune = 0")
|
||||||
|
fp.evaluate('GAME.paused = false')
|
||||||
|
fp.mouse.down()
|
||||||
|
fp.evaluate('GAME.paused = true; for (let k = 0; k < 24; k++) { tick++; toolTick(); }')
|
||||||
|
check('held drill beyond reach makes no toast or distance label', fp.evaluate("GAME.lmb && !inDrillReach(GAME.aim.x, GAME.aim.y, GAME.drillW) && !$('toasts').children.length && !hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)).warn && !hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)).t2.includes('out of reach')"))
|
||||||
|
check('distant drill fan and crosshair stay normal with no red reach circle', fp.evaluate("""() => {
|
||||||
|
const ctx = R.ctx, stroke = ctx.stroke, circle = drawReachCircle, colours = []; let circles = 0;
|
||||||
|
ctx.stroke = function() { colours.push(this.strokeStyle); };
|
||||||
|
drawReachCircle = () => circles++;
|
||||||
|
try { drawPreviews(); } finally { ctx.stroke = stroke; drawReachCircle = circle; }
|
||||||
|
return circles === 0 && !colours.some(c => c.includes('239, 106, 85'));
|
||||||
|
}"""))
|
||||||
|
fp.evaluate("""() => {
|
||||||
|
const o = drillOrigin(), dx = GAME.aim.x + 0.5 - o.x, dy = GAME.aim.y + 0.5 - o.y, d = Math.hypot(dx, dy);
|
||||||
|
for (let k = 6; k < 10; k++) setCell(Math.floor(o.y + dy / d * k) * W + Math.floor(o.x + dx / d * k), GRANITE, 20, F_NAT);
|
||||||
|
tick += 20; toolTick();
|
||||||
|
}""")
|
||||||
|
check('held drill still toasts too hard with a distant pointer', fp.evaluate("$('toasts').textContent.includes('Granite is too hard; needs an iron bit.')"))
|
||||||
|
fp.mouse.up()
|
||||||
fp.evaluate("""() => {
|
fp.evaluate("""() => {
|
||||||
GAME.paused = true; SET.fog = true; SET.look = 'pixel'; SET.zoom = 6; SET.lighting = 'off';
|
GAME.paused = true; SET.fog = true; SET.look = 'pixel'; SET.zoom = 6; SET.lighting = 'off';
|
||||||
SET.cameraLead = false; SET.cameraSmooth = 0; resizeRender();
|
SET.cameraLead = false; SET.cameraSmooth = 0; resizeRender();
|
||||||
|
|||||||
+2
-2
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
python3 test/run_all.py every headless suite (test/t_*.js), in parallel
|
python3 test/run_all.py every headless suite (test/t_*.js), in parallel
|
||||||
python3 test/run_all.py pipes stone only suites whose name contains one of these words
|
python3 test/run_all.py pipes stone only suites whose name contains one of these words
|
||||||
python3 test/run_all.py --browser also the browser tests (play.py, then t_smooth.py),
|
python3 test/run_all.py --browser also browser tests (play.py, t_smooth.py, t_dev_console.py),
|
||||||
after rebuilding dist/; they run one at a time
|
after rebuilding dist/; they run one at a time
|
||||||
python3 test/run_all.py --browser-only
|
python3 test/run_all.py --browser-only
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ def main():
|
|||||||
if browser:
|
if browser:
|
||||||
subprocess.run([sys.executable, str(ROOT / 'build.py')], cwd=ROOT, check=True)
|
subprocess.run([sys.executable, str(ROOT / 'build.py')], cwd=ROOT, check=True)
|
||||||
# one at a time: two headless browsers at once can starve each other and flake
|
# one at a time: two headless browsers at once can starve each other and flake
|
||||||
for name in ['play.py', 't_smooth.py']: results.append(run_py(name))
|
for name in ['play.py', 't_smooth.py', 't_dev_console.py']: results.append(run_py(name))
|
||||||
bad = 0
|
bad = 0
|
||||||
for name, code, out, secs in results:
|
for name, code, out, secs in results:
|
||||||
fails = [l for l in out.splitlines() if l.startswith('FAIL') or 'Error' in l or l.startswith('FAILS: [') and l != 'FAILS: []']
|
fails = [l for l in out.splitlines() if l.startswith('FAIL') or 'Error' in l or l.startswith('FAILS: [') and l != 'FAILS: []']
|
||||||
|
|||||||
@@ -96,4 +96,65 @@ const before = run('JSON.stringify({bag: bagTotal, ents: ents.length, molds: mol
|
|||||||
run('globalThis.__sv = serializeWorld(); deserializeWorld(__sv.meta, __sv.buffers);');
|
run('globalThis.__sv = serializeWorld(); deserializeWorld(__sv.meta, __sv.buffers);');
|
||||||
const after = run('JSON.stringify({bag: bagTotal, ents: ents.length, molds: molds.length, mach: machines.filter(m => m && m.type !== "dock").length, blocks: blockP.reduce((a,b)=>a+(b>0),0)})');
|
const after = run('JSON.stringify({bag: bagTotal, ents: ents.length, molds: molds.length, mach: machines.filter(m => m && m.type !== "dock").length, blocks: blockP.reduce((a,b)=>a+(b>0),0)})');
|
||||||
check('save/load keeps bag, bars, molds, machines and blocks', before === after, before + ' vs ' + after);
|
check('save/load keeps bag, bars, molds, machines and blocks', before === after, before + ' vs ' + after);
|
||||||
|
// 12. vacuum catches grains in flight, before loose cells, with the same limits.
|
||||||
|
flatWorld(run, 240, 160, 140);
|
||||||
|
run('bot.x = 98; bot.y = 58; player.flask = false; EV.length = 0;');
|
||||||
|
run('flyers.push({x: 102.9, y: 62.9, m: SAND});');
|
||||||
|
const airborne = run('toolVacuum(100, 60, 3, () => true)');
|
||||||
|
check('vacuum takes airborne sand using its current cell', airborne.got === 1 && airborne.m === run('SAND') && run('bag[SAND] === 1 && flyers.length === 0'));
|
||||||
|
check('airborne pickup emits a suck at the grain cell', run('EV.some(e => e.t === "suck" && e.x === 102 && e.y === 62 && e.m === SAND)'));
|
||||||
|
run('flyers.push({x: 103, y: 63, m: SAND}, {x: 180, y: 60, m: SAND});');
|
||||||
|
check('vacuum leaves flyers outside the circular brush', run('toolVacuum(100, 60, 3, () => true).got === 0 && flyers.length === 2'));
|
||||||
|
check('vacuum leaves flyers out of reach', run('toolVacuum(180, 60, 3, () => true).got === 0 && flyers.length === 2 && vacuumTargetAt(180, 60, 3) === 0'));
|
||||||
|
run('flyers = [{x: 100, y: 60, m: WATER}];');
|
||||||
|
check('liquid flyers need the flask for pickup and targeting', run('toolVacuum(100, 60, 3, () => true).got === 0 && vacuumTargetAt(100, 60, 3) === 0 && flyers.length === 1'));
|
||||||
|
run('player.flask = true;');
|
||||||
|
check('flask allows liquid flyer targeting', run('vacuumTargetAt(100, 60, 3) === WATER'));
|
||||||
|
check('flask allows liquid flyer pickup', run('toolVacuum(100, 60, 3, () => true).got === 1 && bag[WATER] === 1 && flyers.length === 0'));
|
||||||
|
run('flyers = [{x: 100, y: 60, m: WATER, rain: true}];');
|
||||||
|
const moisture = run('weather.moist.reduce((a, b) => a + b, 0)');
|
||||||
|
check('rain is neither targeted nor collected', run('vacuumTargetAt(100, 60, 3) === 0 && toolVacuum(100, 60, 3, () => true).got === 0 && flyers.length === 1 && bag[WATER] === 1') && moisture === run('weather.moist.reduce((a, b) => a + b, 0)'));
|
||||||
|
run('flyers = [{x: 100, y: 60, m: COAL}, {x: 101, y: 60, m: SAND, bag: true}];');
|
||||||
|
check('pickup filter also applies to bag-blown flyers', run('toolVacuum(100, 60, 3, m => m === SAND).got === 1 && flyers.length === 1 && flyers[0].m === COAL && bag[COAL] === 0'));
|
||||||
|
check('targeting finds a material only in flight', run('vacuumTargetAt(100, 60, 3) === COAL'));
|
||||||
|
run('setCell(60*W+102, SAND, 20, 0);');
|
||||||
|
check('nearer flyer wins over a loose cell', run('vacuumTargetAt(100, 60, 3) === COAL'));
|
||||||
|
run('flyers[0].x = 103;');
|
||||||
|
check('nearer loose cell wins over a flyer', run('vacuumTargetAt(100, 60, 3) === SAND'));
|
||||||
|
run('spawnBar(BARCU, 100, 60, 0, 0);');
|
||||||
|
check('bars still take targeting priority', run('vacuumTargetAt(100, 60, 3) === BARCU'));
|
||||||
|
run('ents = []; bag.fill(0); bag[SAND] = bagTotal = bagCap();');
|
||||||
|
const fullPickup = run('toolVacuum(100, 60, 3, () => true)');
|
||||||
|
check('full bag leaves both flyers and cells untouched', fullPickup.full && fullPickup.got === 0 && run('flyers.length === 1 && mat[60*W+102] === SAND && bagTotal === bagCap()'));
|
||||||
|
run('bagTake(SAND, 1);');
|
||||||
|
const lastRoom = run('toolVacuum(100, 60, 3, () => true)');
|
||||||
|
check('last bag space goes to the flyer before the cell', lastRoom.full && lastRoom.got === 1 && lastRoom.m === run('COAL') && run('flyers.length === 0 && mat[60*W+102] === SAND && bagTotal === bagCap()'));
|
||||||
|
run('bag.fill(0); bagTotal = 0; EV.length = 0; for (let k = 0; k < 50; k++) flyers.push({x: 100, y: 60, m: COAL});');
|
||||||
|
check('flyers share the 48-grain budget and leave cells for later', run('toolVacuum(100, 60, 3, () => true).got === 48 && flyers.length === 2 && bag[COAL] === 48 && mat[60*W+102] === SAND'));
|
||||||
|
check('flyers emit suck on every other collected grain', run('EV.filter(e => e.t === "suck").length === 24'));
|
||||||
|
check('remaining budget collects both flyers and cells', run('toolVacuum(100, 60, 3, () => true).got === 3 && flyers.length === 0 && bag[COAL] === 50 && bag[SAND] === 1'));
|
||||||
|
// A real screw launches sand; hold the brush below its airborne arc, away from its mouth.
|
||||||
|
flatWorld(run, 240, 160, 140);
|
||||||
|
run('RS = 7; nextPowerBuild = 0; bot.x = 106; bot.y = 70; placeBlock(BP_SCREW, 1, 24, 15, true);');
|
||||||
|
rect(run, 96, 60, 99, 63, run('SAND'));
|
||||||
|
const screwCatch = run(`(() => {
|
||||||
|
let airborne = 0, caught = 0;
|
||||||
|
for (let k = 0; k < 120; k++) {
|
||||||
|
simTick();
|
||||||
|
const n = flyers.length;
|
||||||
|
const result = toolVacuum(106, 70, 3, m => m === SAND);
|
||||||
|
airborne += n - flyers.length; caught += result.got;
|
||||||
|
}
|
||||||
|
return {airborne, caught, bag: bag[SAND]};
|
||||||
|
})()`);
|
||||||
|
check('vacuum held under a screw arc catches its launched sand', screwCatch.airborne === 16 && screwCatch.caught === 16 && screwCatch.bag === 16, JSON.stringify(screwCatch));
|
||||||
|
// Rock in the bag: solids go in as what drilling them gives, and older saves holding solids load
|
||||||
|
// that way too, so the bag never holds something its list can't show.
|
||||||
|
flatWorld(run, 120, 80, 60);
|
||||||
|
check('putting rock in the bag stores its rubble', run('(() => { bag.fill(0); bagTotal = 0; bagAdd(BASALT, 10, true); bagAdd(CLAY, 5, true); return bag[BASALT] === 0 && bag[RBAS] === 10 && bag[CLAY] === 0 && bag[LCLAY] === 5 && bagTotal === 15; })()'));
|
||||||
|
check('a save holding solid rock loads it as rubble with the same total', run(`(() => {
|
||||||
|
bag.fill(0); bagTotal = 0; const s = serializeWorld(); s.meta.bag[BASALT] = 1000; s.meta.bag[CLAY] = 200; s.meta.bag[MSLAG] = 7;
|
||||||
|
deserializeWorld(s.meta, s.buffers);
|
||||||
|
return bag[BASALT] === 0 && bag[RBAS] === 1000 && bag[CLAY] === 0 && bag[LCLAY] === 200 && bag[MSLAG] === 7 && bagTotal === 1207;
|
||||||
|
})()`));
|
||||||
done();
|
done();
|
||||||
|
|||||||
@@ -79,4 +79,29 @@ run('for (let y = 120; y < 122; y++) for (let x = 60; x < 62; x++) setCell(y * W
|
|||||||
const n = run('clearHardSpecks(true)');
|
const n = run('clearHardSpecks(true)');
|
||||||
check('a 4-grain speck of iron ore in stone becomes stone; a 100-grain vein stays', n === 1 && count(run, run('FEV')) === 100 && run('mat[120 * W + 60]') === run('STONE'), `${n} specks, ${count(run, run('FEV'))} vein`);
|
check('a 4-grain speck of iron ore in stone becomes stone; a 100-grain vein stays', n === 1 && count(run, run('FEV')) === 100 && run('mat[120 * W + 60]') === run('STONE'), `${n} specks, ${count(run, run('FEV'))} vein`);
|
||||||
|
|
||||||
|
// t64: building reaches about as far as the screen shows, and a replace placed out of range
|
||||||
|
// becomes a ghost that replaces the old piece when the bot comes in range.
|
||||||
|
fresh();
|
||||||
|
check('with no view the build range is a REACH box around the bot', run('buildBox = null; inBuildRange(bot.x + bot.w / 2 + REACH, bot.y) && !inBuildRange(bot.x + bot.w / 2 + REACH + 2, bot.y)'));
|
||||||
|
check('the build range follows the view box', run('(() => { setBuildRange(200, 100, 300, 160); const r = inBuildRange(340, 100) && !inBuildRange(352, 100) && inBuildRange(200, 180) && !inBuildRange(200, 190); buildBox = null; return r; })()'));
|
||||||
|
check('a tiny view still reaches REACH and a huge one stops at BUILD_MAX', run('(() => { setBuildRange(500, 500, 20, 20000); const r = buildBox.hw === REACH && buildBox.hh === BUILD_MAX; buildBox = null; return r; })()'));
|
||||||
|
fresh();
|
||||||
|
const swap = run(`(() => {
|
||||||
|
placeBlock(BP_BELT, 1, 50, 20, true); const b = 20 * BW + 50, m0 = money;
|
||||||
|
ghostBlocks.set(b, [BP_SCREW, 1, undefined, 1]);
|
||||||
|
bot.x = 20; bot.y = 20; for (let k = 0; k < 30; k++) ghostTick();
|
||||||
|
const before = blockP[b];
|
||||||
|
bot.x = 50 * BS - 4; bot.y = 20 * BS - 10; for (let k = 0; k < 30; k++) ghostTick();
|
||||||
|
return { before, after: blockP[b], v: blockV[b], spent: m0 - money, ghosts: ghostBlocks.size };
|
||||||
|
})()`);
|
||||||
|
check('an out-of-range replace ghost leaves the old piece until the bot comes near', swap.before === run('BP_BELT'), JSON.stringify(swap));
|
||||||
|
check('then replaces it: the new piece is built and the ghost is gone', swap.after === run('BP_SCREW') && swap.v === 1 && swap.ghosts === 0, JSON.stringify(swap));
|
||||||
|
check('the replace charges the new piece and refunds the old one', swap.spent === 25 - 12, JSON.stringify(swap));
|
||||||
|
fresh();
|
||||||
|
check('a plain ghost over a different piece still leaves it alone', run(`(() => {
|
||||||
|
placeBlock(BP_BELT, 1, 30, 20, true); const b = 20 * BW + 30;
|
||||||
|
ghostBlocks.set(b, [BP_SCREW, 1]); bot.x = 30 * BS - 4; bot.y = 20 * BS - 10;
|
||||||
|
for (let k = 0; k < 30; k++) ghostTick();
|
||||||
|
return blockP[b] === BP_BELT && ghostBlocks.has(b);
|
||||||
|
})()`));
|
||||||
done();
|
done();
|
||||||
|
|||||||
+48
-48
@@ -1,9 +1,9 @@
|
|||||||
// Screw tubes that never carry liquid (it flows through), tube and belt throughput, filters.
|
// Screw conveyors that never carry liquid (it flows through), screw and belt throughput, filters.
|
||||||
const { load, suite, flatWorld, rect, ticks, count } = require('./harness');
|
const { load, suite, flatWorld, rect, ticks, count } = require('./harness');
|
||||||
const { run } = load();
|
const { run } = load();
|
||||||
const { check, done } = suite();
|
const { check, done } = suite();
|
||||||
function fresh() { flatWorld(run, 320, 200, 120); run('bot.x = 20; bot.y = 40; money = 1e6; globalThis.__got = 0;'); }
|
function fresh() { flatWorld(run, 320, 200, 120); run('bot.x = 20; bot.y = 40; money = 1e6; globalThis.__got = 0;'); }
|
||||||
const tubeLiquid = () => run('(() => { let n = 0; for (let i = 0; i < N; i++) if (part[i] >= P_TU && part[i] <= P_TR && PH[mat[i]] === PH_LIQUID) n++; return n; })()');
|
const screwLiquid = () => run('(() => { let n = 0; for (let i = 0; i < N; i++) if (part[i] >= P_SCREW_U && part[i] <= P_SCREW_R && PH[mat[i]] === PH_LIQUID) n++; return n; })()');
|
||||||
// grains that land in the sink region are counted and taken away, so outlets never back up
|
// grains that land in the sink region are counted and taken away, so outlets never back up
|
||||||
function rate(x0, x1, y0, y1, warm, meas) {
|
function rate(x0, x1, y0, y1, warm, meas) {
|
||||||
const sink = `for (let y=${y0};y<=${y1};y++) for (let x=${x0};x<=${x1};x++) { const i=y*W+x; if (mat[i]===SAND) { mat[i]=EMPTY; __got++; } }`;
|
const sink = `for (let y=${y0};y<=${y1};y++) for (let x=${x0};x<=${x1};x++) { const i=y*W+x; if (mat[i]===SAND) { mat[i]=EMPTY; __got++; } }`;
|
||||||
@@ -13,42 +13,42 @@ function rate(x0, x1, y0, y1, warm, meas) {
|
|||||||
return run('__got') / (meas / 60);
|
return run('__got') / (meas / 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- tubes are sealed: no water in, no water carried
|
// --- screws are sealed: no water in, no water carried
|
||||||
fresh();
|
fresh();
|
||||||
run('for (let y = 120; y < 170; y++) for (let x = 100; x < 124; x++) setCell(y*W+x, EMPTY, AMB[y], 0)');
|
run('for (let y = 120; y < 170; y++) for (let x = 100; x < 124; x++) setCell(y*W+x, EMPTY, AMB[y], 0)');
|
||||||
rect(run, 100, 150, 123, 169, run('SAND'));
|
rect(run, 100, 150, 123, 169, run('SAND'));
|
||||||
rect(run, 100, 130, 123, 149, run('WATER'));
|
rect(run, 100, 130, 123, 149, run('WATER'));
|
||||||
run('for (let by = 22; by <= 41; by++) placeBlock(BP_TUBE, 0, 27, by, true); for (let bx = 27; bx <= 34; bx++) placeBlock(BP_TUBE, 1, bx, 21, true);');
|
run('for (let by = 22; by <= 41; by++) placeBlock(BP_SCREW, 0, 27, by, true); for (let bx = 27; bx <= 34; bx++) placeBlock(BP_SCREW, 1, bx, 21, true);');
|
||||||
const water0 = count(run, run('WATER'));
|
const water0 = count(run, run('WATER'));
|
||||||
ticks(run, 900);
|
ticks(run, 900);
|
||||||
const wOut = count(run, run('WATER'), 124, 0, 319, 119), sOut = count(run, run('SAND'), 124, 0, 319, 119);
|
const wOut = count(run, run('WATER'), 124, 0, 319, 119), sOut = count(run, run('SAND'), 124, 0, 319, 119);
|
||||||
check('a lift standing in a flooded pit brings up the sand but not the water', sOut > 150 && wOut === 0, `sand out ${sOut}, water out ${wOut}, water standing in the tubes ${tubeLiquid()}`);
|
check('a lift standing in a flooded pit brings up the sand but not the water', sOut > 150 && wOut === 0, `sand out ${sOut}, water out ${wOut}, water standing in the screws ${screwLiquid()}`);
|
||||||
const pitClouds = run('weather.moist.reduce((a, v) => a + v, 0)');
|
const pitClouds = run('weather.moist.reduce((a, v) => a + v, 0)');
|
||||||
const pitSteam = count(run, run('STEAM')); // the pit's floor is hot: some is steam on its way up at any moment
|
const pitSteam = count(run, run('STEAM')); // the pit's floor is hot: some is steam on its way up at any moment
|
||||||
check('the pit keeps its water (bar what steams off into the clouds)', water0 - count(run, run('WATER')) - pitSteam - pitClouds <= 6, `${water0} -> ${count(run, run('WATER'))}, ${pitSteam} steam, ${pitClouds.toFixed(0)} in the clouds`);
|
check('the pit keeps its water (bar what steams off into the clouds)', water0 - count(run, run('WATER')) - pitSteam - pitClouds <= 6, `${water0} -> ${count(run, run('WATER'))}, ${pitSteam} steam, ${pitClouds.toFixed(0)} in the clouds`);
|
||||||
fresh();
|
fresh();
|
||||||
run('for (let bx = 20; bx < 26; bx++) placeBlock(BP_TUBE, 1, bx, 25, true);');
|
run('for (let bx = 20; bx < 26; bx++) placeBlock(BP_SCREW, 1, bx, 25, true);');
|
||||||
rect(run, 84, 60, 95, 79, run('WATER'));
|
rect(run, 84, 60, 95, 79, run('WATER'));
|
||||||
ticks(run, 300);
|
ticks(run, 300);
|
||||||
check('water poured on a tube runs straight through it', count(run, run('WATER'), 0, 0, 319, 99) === 0 && count(run, run('WATER'), 0, 104, 319, 119) > 200 && tubeLiquid() < 10, `${count(run, run('WATER'), 0, 104, 319, 119)} below, ${tubeLiquid()} still inside`);
|
check('water poured on a screw runs straight through it', count(run, run('WATER'), 0, 0, 319, 99) === 0 && count(run, run('WATER'), 0, 104, 319, 119) > 200 && screwLiquid() < 10, `${count(run, run('WATER'), 0, 104, 319, 119)} below, ${screwLiquid()} still inside`);
|
||||||
fresh();
|
fresh();
|
||||||
// a vertical tube pointing up, its top open to a pool: the water drains down through it, against the screw
|
// a vertical screw pointing up, its top open to a pool: the water drains down through it, against the screw
|
||||||
run('for (let by = 20; by <= 26; by++) placeBlock(BP_TUBE, 0, 25, by, true); for (let y = 60; y < 80; y++) { setCell(y*W+99, STONE, AMB[y], 0); setCell(y*W+104, STONE, AMB[y], 0); }');
|
run('for (let by = 20; by <= 26; by++) placeBlock(BP_SCREW, 0, 25, by, true); for (let y = 60; y < 80; y++) { setCell(y*W+99, STONE, AMB[y], 0); setCell(y*W+104, STONE, AMB[y], 0); }');
|
||||||
rect(run, 100, 60, 103, 79, run('WATER'));
|
rect(run, 100, 60, 103, 79, run('WATER'));
|
||||||
ticks(run, 400);
|
ticks(run, 400);
|
||||||
check('water runs down through an upward screw, which never pushes it back', count(run, run('WATER'), 0, 0, 319, 79) === 0, count(run, run('WATER'), 0, 0, 319, 79));
|
check('water runs down through an upward screw, which never pushes it back', count(run, run('WATER'), 0, 0, 319, 79) === 0, count(run, run('WATER'), 0, 0, 319, 79));
|
||||||
|
|
||||||
// Intake itself fills the far lanes first, for either tube axis.
|
// Intake itself fills the far lanes first, for either screw axis.
|
||||||
for (const horizontal of [false, true]) {
|
for (const horizontal of [false, true]) {
|
||||||
fresh();
|
fresh();
|
||||||
run(`placeBlock(BP_TUBE, ${horizontal ? 1 : 0}, 25, 20, true);`);
|
run(`placeBlock(BP_SCREW, ${horizontal ? 1 : 0}, 25, 20, true);`);
|
||||||
const filled = run(`(() => {
|
const filled = run(`(() => {
|
||||||
const lanes = [];
|
const lanes = [];
|
||||||
for (let k = 0; k < BS; k++) {
|
for (let k = 0; k < BS; k++) {
|
||||||
tickStamp++;
|
tickStamp++;
|
||||||
const x = ${horizontal ? 100 : 99}, y = ${horizontal ? 79 : 80};
|
const x = ${horizontal ? 100 : 99}, y = ${horizontal ? 79 : 80};
|
||||||
setCell(y*W+x, SAND, AMB[y], 0);
|
setCell(y*W+x, SAND, AMB[y], 0);
|
||||||
tubeIntake(80*W+100, ${horizontal ? 'P_TR, 1, 0' : 'P_TU, 0, -1'});
|
screwIntake(80*W+100, ${horizontal ? 'P_SCREW_R, 1, 0' : 'P_SCREW_U, 0, -1'});
|
||||||
for (let lane = 0; lane < BS; lane++) if (mat[(80+${horizontal ? 'lane' : '0'})*W+100+${horizontal ? '0' : 'lane'}] === SAND && !lanes.includes(lane)) lanes.push(lane);
|
for (let lane = 0; lane < BS; lane++) if (mat[(80+${horizontal ? 'lane' : '0'})*W+100+${horizontal ? '0' : 'lane'}] === SAND && !lanes.includes(lane)) lanes.push(lane);
|
||||||
}
|
}
|
||||||
return lanes;
|
return lanes;
|
||||||
@@ -59,7 +59,7 @@ for (const horizontal of [false, true]) {
|
|||||||
// A finite heap rides into the side of a six-block lift; remove each mouth flyer once.
|
// A finite heap rides into the side of a six-block lift; remove each mouth flyer once.
|
||||||
const sideSeed = run('RS');
|
const sideSeed = run('RS');
|
||||||
fresh();
|
fresh();
|
||||||
run('RS = 7; for (let bx = 16; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true); for (let by = 19; by <= 24; by++) placeBlock(BP_TUBE, 0, 32, by, true);');
|
run('RS = 7; for (let bx = 16; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true); for (let by = 19; by <= 24; by++) placeBlock(BP_SCREW, 0, 32, by, true);');
|
||||||
rect(run, 64, 40, 119, 99, run('SAND'));
|
rect(run, 64, 40, 119, 99, run('SAND'));
|
||||||
const sideLift = run(`(() => {
|
const sideLift = run(`(() => {
|
||||||
let out = 0; const lanes = [0, 0, 0, 0];
|
let out = 0; const lanes = [0, 0, 0, 0];
|
||||||
@@ -79,7 +79,7 @@ check('the side-fed lift conserves its sand, including mouth flyers', sideLift.b
|
|||||||
// The same belt and heap deliver 2243 into an open drop, the most the lift can be given.
|
// The same belt and heap deliver 2243 into an open drop, the most the lift can be given.
|
||||||
check('a side-fed lift carries at least 90% of what its belt brings (it was 65%)', sideLift.out >= Math.floor(0.9 * 2243), sideLift.out);
|
check('a side-fed lift carries at least 90% of what its belt brings (it was 65%)', sideLift.out >= Math.floor(0.9 * 2243), sideLift.out);
|
||||||
|
|
||||||
// Same belt and finite heap, with a non-blocking sink at the tube side.
|
// Same belt and finite heap, with a non-blocking sink at the screw side.
|
||||||
run(`RS = ${sideSeed}`);
|
run(`RS = ${sideSeed}`);
|
||||||
fresh();
|
fresh();
|
||||||
run('RS = 7; for (let bx = 16; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true);');
|
run('RS = 7; for (let bx = 16; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true);');
|
||||||
@@ -100,24 +100,24 @@ console.log('Belt delivery to a non-blocking sink, 3000 ticks: ' + beltDelivery)
|
|||||||
|
|
||||||
// --- throughput
|
// --- throughput
|
||||||
fresh();
|
fresh();
|
||||||
run('for (let bx = 20; bx < 30; bx++) placeBlock(BP_TUBE, 1, bx, 20, true);');
|
run('for (let bx = 20; bx < 30; bx++) placeBlock(BP_SCREW, 1, bx, 20, true);');
|
||||||
run('for (let y = 0; y < 80; y++) { setCell(y*W+79, STONE, AMB[y], 0); setCell(y*W+96, STONE, AMB[y], 0); }');
|
run('for (let y = 0; y < 80; y++) { setCell(y*W+79, STONE, AMB[y], 0); setCell(y*W+96, STONE, AMB[y], 0); }');
|
||||||
rect(run, 80, 0, 95, 79, run('SAND'));
|
rect(run, 80, 0, 95, 79, run('SAND'));
|
||||||
ticks(run, 60);
|
ticks(run, 60);
|
||||||
const lanes = run('(() => { const s = new Set(); for (let y = 80; y < 84; y++) for (let x = 100; x < 120; x++) if (mat[y*W+x] === SAND) s.add(y); return s.size; })()');
|
const lanes = run('(() => { const s = new Set(); for (let y = 80; y < 84; y++) for (let x = 100; x < 120; x++) if (mat[y*W+x] === SAND) s.add(y); return s.size; })()');
|
||||||
check('a sideways tube fed from above (its side) fills more than one row', lanes > 1, lanes);
|
check('a sideways screw fed from above (its side) fills more than one row', lanes > 1, lanes);
|
||||||
check('a sideways tube fed from a hopper runs full, all four grains wide', lanes === 4, lanes);
|
check('a sideways screw fed from a hopper runs full, all four grains wide', lanes === 4, lanes);
|
||||||
fresh();
|
fresh();
|
||||||
run('for (let bx = 20; bx < 30; bx++) placeBlock(BP_TUBE, 1, bx, 20, true);');
|
run('for (let bx = 20; bx < 30; bx++) placeBlock(BP_SCREW, 1, bx, 20, true);');
|
||||||
run('for (let y = 0; y < 80; y++) { setCell(y*W+79, STONE, AMB[y], 0); setCell(y*W+96, STONE, AMB[y], 0); }');
|
run('for (let y = 0; y < 80; y++) { setCell(y*W+79, STONE, AMB[y], 0); setCell(y*W+96, STONE, AMB[y], 0); }');
|
||||||
rect(run, 80, 0, 95, 79, run('SAND'));
|
rect(run, 80, 0, 95, 79, run('SAND'));
|
||||||
const tr = rate(120, 319, 84, 119, 90, 300);
|
const tr = rate(120, 319, 84, 119, 90, 300);
|
||||||
// tubes run at belt pace (every other tick), so a belt under a tube's mouth keeps up with it
|
// screws run at belt pace (every other tick), so a belt under a screw's mouth keeps up with it
|
||||||
check('a sideways tube moves around a hundred grains a second', tr > 80 && tr < 150, `${tr.toFixed(0)}/s`);
|
check('a sideways screw moves around a hundred grains a second', tr > 80 && tr < 150, `${tr.toFixed(0)}/s`);
|
||||||
fresh();
|
fresh();
|
||||||
run('for (let y = 120; y < 196; y++) for (let x = 90; x < 130; x++) setCell(y*W+x, EMPTY, AMB[y], 0)');
|
run('for (let y = 120; y < 196; y++) for (let x = 90; x < 130; x++) setCell(y*W+x, EMPTY, AMB[y], 0)');
|
||||||
rect(run, 90, 130, 129, 195, run('SAND'));
|
rect(run, 90, 130, 129, 195, run('SAND'));
|
||||||
run('for (let by = 20; by <= 37; by++) placeBlock(BP_TUBE, 0, 26, by, true); for (let bx = 26; bx <= 40; bx++) placeBlock(BP_TUBE, 1, bx, 19, true);');
|
run('for (let by = 20; by <= 37; by++) placeBlock(BP_SCREW, 0, 26, by, true); for (let bx = 26; bx <= 40; bx++) placeBlock(BP_SCREW, 1, bx, 19, true);');
|
||||||
const lr = rate(164, 319, 80, 119, 400, 300);
|
const lr = rate(164, 319, 80, 119, 400, 300);
|
||||||
const liftLanes = run('(() => { const s = new Set(); for (let y = 76; y < 80; y++) for (let x = 120; x < 160; x++) if (mat[y*W+x] === SAND) s.add(y); return s.size; })()');
|
const liftLanes = run('(() => { const s = new Set(); for (let y = 76; y < 80; y++) for (let x = 120; x < 160; x++) if (mat[y*W+x] === SAND) s.add(y); return s.size; })()');
|
||||||
check('a lift turns its corner without squeezing down to one lane', liftLanes === 4 && lr > 30, `${lr.toFixed(0)}/s, ${liftLanes} lanes at the top`);
|
check('a lift turns its corner without squeezing down to one lane', liftLanes === 4 && lr > 30, `${lr.toFixed(0)}/s, ${liftLanes} lanes at the top`);
|
||||||
@@ -151,7 +151,7 @@ check('the load rides a belt up to a block deep', depth >= 3, depth);
|
|||||||
// --- filters
|
// --- filters
|
||||||
fresh();
|
fresh();
|
||||||
// a filter for copper ore under a mixed pile; everything else rides right and off the end
|
// a filter for copper ore under a mixed pile; everything else rides right and off the end
|
||||||
// fed from a walled hopper over its left end, the way a tube or belt would drop a mixed load on it
|
// fed from a walled hopper over its left end, the way a screw or belt would drop a mixed load on it
|
||||||
run('for (let bx = 20; bx < 30; bx++) placeBlock(BP_FILTER, 128, bx, 25, true, { m: [CUORE] });');
|
run('for (let bx = 20; bx < 30; bx++) placeBlock(BP_FILTER, 128, bx, 25, true, { m: [CUORE] });');
|
||||||
run('for (let y = 30; y < 120; y++) setCell(y*W+79, STONE, AMB[y], 0); for (let y = 30; y < 96; y++) setCell(y*W+88, STONE, AMB[y], 0);');
|
run('for (let y = 30; y < 120; y++) setCell(y*W+79, STONE, AMB[y], 0); for (let y = 30; y < 96; y++) setCell(y*W+88, STONE, AMB[y], 0);');
|
||||||
run('for (let y = 40; y < 96; y++) for (let x = 80; x < 88; x++) setCell(y*W+x, (x + y) % 3 ? SAND : CUORE, AMB[y], 0)');
|
run('for (let y = 40; y < 96; y++) for (let x = 80; x < 88; x++) setCell(y*W+x, (x + y) % 3 ? SAND : CUORE, AMB[y], 0)');
|
||||||
@@ -236,7 +236,7 @@ run(`globalThis.__carry = carryColumn;
|
|||||||
for (const lift of [true, false]) {
|
for (const lift of [true, false]) {
|
||||||
fresh();
|
fresh();
|
||||||
run('RS = 7; __shear = 0; for (let bx = 20; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true);');
|
run('RS = 7; __shear = 0; for (let bx = 20; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true);');
|
||||||
if (lift) run('for (let by = 15; by <= 24; by++) placeBlock(BP_TUBE, 0, 32, by, true);');
|
if (lift) run('for (let by = 15; by <= 24; by++) placeBlock(BP_SCREW, 0, 32, by, true);');
|
||||||
else run('for (let by = 10; by <= 25; by++) placeBlock(BP_WALL, RSTONE, 32, by, true);');
|
else run('for (let by = 10; by <= 25; by++) placeBlock(BP_WALL, RSTONE, 32, by, true);');
|
||||||
rect(run, 88, 94, 119, 99, run('SAND'));
|
rect(run, 88, 94, 119, 99, run('SAND'));
|
||||||
rect(run, 88, 70, 119, 93, run('CUORE'));
|
rect(run, 88, 70, 119, 93, run('CUORE'));
|
||||||
@@ -251,60 +251,60 @@ for (const lift of [true, false]) {
|
|||||||
}
|
}
|
||||||
run('carryColumn = __carry;');
|
run('carryColumn = __carry;');
|
||||||
|
|
||||||
// --- whole bars in screw tubes; position an existing bar inside for mid-ride cases
|
// --- whole bars in screw conveyors; position an existing bar inside for mid-ride cases
|
||||||
function putBar(x, y) { run(`{ spawnBar(BARCU, 20, 40, 0, 0); const e=ents[0]; entMark(e,0); e.x=e.ix=${x}; e.y=e.iy=${y}; entMark(e,1); }`); }
|
function putBar(x, y) { run(`{ spawnBar(BARCU, 20, 40, 0, 0); const e=ents[0]; entMark(e,0); e.x=e.ix=${x}; e.y=e.iy=${y}; entMark(e,1); }`); }
|
||||||
function barTube(corner = false) {
|
function barScrew(corner = false) {
|
||||||
fresh();
|
fresh();
|
||||||
run(`for (let by = 20; by < 24; by++) placeBlock(BP_TUBE, 0, 25, by, true);
|
run(`for (let by = 20; by < 24; by++) placeBlock(BP_SCREW, 0, 25, by, true);
|
||||||
${corner ? 'for (let bx = 25; bx < 29; bx++) placeBlock(BP_TUBE, 1, bx, 19, true);' : ''}`);
|
${corner ? 'for (let bx = 25; bx < 29; bx++) placeBlock(BP_SCREW, 1, bx, 19, true);' : ''}`);
|
||||||
putBar(100,94);
|
putBar(100,94);
|
||||||
}
|
}
|
||||||
function watchBar(n, condition) { return run(`(() => { for (let k=0;k<${n};k++) { simTick(); if (${condition}) return true; } return false; })()`); }
|
function watchBar(n, condition) { return run(`(() => { for (let k=0;k<${n};k++) { simTick(); if (${condition}) return true; } return false; })()`); }
|
||||||
fresh();
|
fresh();
|
||||||
run('for (let bx=20;bx<25;bx++) placeBlock(BP_BELT,1,bx,25,true); for(let by=21;by<25;by++) placeBlock(BP_TUBE,0,25,by,true); spawnBar(BARCU,84,98,0,0);');
|
run('for (let bx=20;bx<25;bx++) placeBlock(BP_BELT,1,bx,25,true); for(let by=21;by<25;by++) placeBlock(BP_SCREW,0,25,by,true); spawnBar(BARCU,84,98,0,0);');
|
||||||
check('a belt feeds a whole copper bar into a lift and out its top', watchBar(200, 'ents.length===1 && ents[0].iy+BAR_H<=84'), run('JSON.stringify(ents)'));
|
check('a belt feeds a whole copper bar into a lift and out its top', watchBar(200, 'ents.length===1 && ents[0].iy+BAR_H<=84'), run('JSON.stringify(ents)'));
|
||||||
fresh();
|
fresh();
|
||||||
run('for(let bx=20;bx<24;bx++) placeBlock(BP_TUBE,1,bx,20,true);'); putBar(80,82);
|
run('for(let bx=20;bx<24;bx++) placeBlock(BP_SCREW,1,bx,20,true);'); putBar(80,82);
|
||||||
check('a whole bar leaves a right tube', watchBar(100,'ents.length===1 && ents[0].ix>=96'));
|
check('a whole bar leaves a right screw', watchBar(100,'ents.length===1 && ents[0].ix>=96'));
|
||||||
// steam and smoke drift through tubes near furnaces: a bar passes them as it does in the open
|
// steam and smoke drift through screws near furnaces: a bar passes them as it does in the open
|
||||||
barTube();
|
barScrew();
|
||||||
check('steam and smoke in a tube do not stop a bar', run(`(() => { for (let k = 0; k < 120; k++) {
|
check('steam and smoke in a screw do not stop a bar', run(`(() => { for (let k = 0; k < 120; k++) {
|
||||||
for (let y = 80; y < 94; y++) for (let x = 100; x < 104; x++) if (mat[y*W+x] === EMPTY && !occ[y*W+x]) setCell(y*W+x, (x + y) & 1 ? STEAM : SMOKE, 120, 0);
|
for (let y = 80; y < 94; y++) for (let x = 100; x < 104; x++) if (mat[y*W+x] === EMPTY && !occ[y*W+x]) setCell(y*W+x, (x + y) & 1 ? STEAM : SMOKE, 120, 0);
|
||||||
simTick(); if (ents.length === 1 && ents[0].iy + BAR_H <= 80) return true; } return false; })()`), run('JSON.stringify(ents)'));
|
simTick(); if (ents.length === 1 && ents[0].iy + BAR_H <= 80) return true; } return false; })()`), run('JSON.stringify(ents)'));
|
||||||
barTube(true);
|
barScrew(true);
|
||||||
check('a bar turns from an up tube into a right tube and leaves', watchBar(200,'ents.length===1 && ents[0].ix>=116'));
|
check('a bar turns from an up screw into a right screw and leaves', watchBar(200,'ents.length===1 && ents[0].ix>=116'));
|
||||||
fresh();
|
fresh();
|
||||||
run('for(let bx=20;bx<24;bx++) placeBlock(BP_TUBE,1,bx,20,true);'); putBar(80,82);
|
run('for(let bx=20;bx<24;bx++) placeBlock(BP_SCREW,1,bx,20,true);'); putBar(80,82);
|
||||||
rect(run,96,80,99,83,run('STONE'));
|
rect(run,96,80,99,83,run('STONE'));
|
||||||
ticks(run,100);
|
ticks(run,100);
|
||||||
check('a blocked mouth holds the whole bar inside without wall overlap',run('ents.length===1 && ents[0].ix===92 && !mat.some((m,i)=>m===STONE && occ[i])'),run('JSON.stringify(ents)'));
|
check('a blocked mouth holds the whole bar inside without wall overlap',run('ents.length===1 && ents[0].ix===92 && !mat.some((m,i)=>m===STONE && occ[i])'),run('JSON.stringify(ents)'));
|
||||||
rect(run,96,80,99,83,run('EMPTY'));
|
rect(run,96,80,99,83,run('EMPTY'));
|
||||||
check('the bar leaves when the mouth wall is removed',watchBar(80,'ents[0].ix>=96'));
|
check('the bar leaves when the mouth wall is removed',watchBar(80,'ents[0].ix>=96'));
|
||||||
barTube();
|
barScrew();
|
||||||
rect(run,100,88,103,89,run('SAND'));
|
rect(run,100,88,103,89,run('SAND'));
|
||||||
const shared = run(`(() => { let overlap=false, rose=false; const n=mat.reduce((n,m)=>n+(m===SAND),0); for(let k=0;k<160;k++) { simTick(); if(mat.some((m,i)=>m===SAND && occ[i])) overlap=true; if(ents[0].iy<90) rose=true; } return { overlap, rose, n, after:mat.reduce((n,m)=>n+(m===SAND),0)+flyers.filter(f=>f.m===SAND).length }; })()`);
|
const shared = run(`(() => { let overlap=false, rose=false; const n=mat.reduce((n,m)=>n+(m===SAND),0); for(let k=0;k<160;k++) { simTick(); if(mat.some((m,i)=>m===SAND && occ[i])) overlap=true; if(ents[0].iy<90) rose=true; } return { overlap, rose, n, after:mat.reduce((n,m)=>n+(m===SAND),0)+flyers.filter(f=>f.m===SAND).length }; })()`);
|
||||||
check('grains and a moving bar share a lift without overlap or lost sand',!shared.overlap && shared.rose && shared.n===shared.after,JSON.stringify(shared));
|
check('grains and a moving bar share a lift without overlap or lost sand',!shared.overlap && shared.rose && shared.n===shared.after,JSON.stringify(shared));
|
||||||
barTube(); ticks(run,8);
|
barScrew(); ticks(run,8);
|
||||||
run('globalThis.__barSave=serializeWorld(); deserializeWorld(__barSave.meta,__barSave.buffers);');
|
run('globalThis.__barSave=serializeWorld(); deserializeWorld(__barSave.meta,__barSave.buffers);');
|
||||||
check('a bar saved mid-tube continues out of its mouth',watchBar(100,'ents.length===1 && ents[0].iy+BAR_H<=80'));
|
check('a bar saved mid-screw continues out of its mouth',watchBar(100,'ents.length===1 && ents[0].iy+BAR_H<=80'));
|
||||||
fresh();
|
fresh();
|
||||||
run('for(let bx=20;bx<24;bx++) placeBlock(BP_BELT,1,bx,25,true); placeBlock(BP_WALL,RSTONE,24,25,true); placeBlock(BP_WALL,RSTONE,24,24,true); spawnBar(BARCU,80,98,0,0);');
|
run('for(let bx=20;bx<24;bx++) placeBlock(BP_BELT,1,bx,25,true); placeBlock(BP_WALL,RSTONE,24,25,true); placeBlock(BP_WALL,RSTONE,24,24,true); spawnBar(BARCU,80,98,0,0);');
|
||||||
ticks(run,100);
|
ticks(run,100);
|
||||||
check('a normal belt still carries a bar and stops at its blocked end',run('ents.length===1 && ents[0].ix===92 && ents[0].iy===98'));
|
check('a normal belt still carries a bar and stops at its blocked end',run('ents.length===1 && ents[0].ix===92 && ents[0].iy===98'));
|
||||||
fresh(); run('placeBlock(BP_TUBE,1,25,25,true); spawnBar(BARCU,100,98,0,0);'); ticks(run,100);
|
fresh(); run('placeBlock(BP_SCREW,1,25,25,true); spawnBar(BARCU,100,98,0,0);'); ticks(run,100);
|
||||||
check('a bar resting on a horizontal tube stays on top',run('ents[0].ix===100 && ents[0].iy===98'));
|
check('a bar resting on a horizontal screw stays on top',run('ents[0].ix===100 && ents[0].iy===98'));
|
||||||
|
|
||||||
for (const dir of [0, 2]) {
|
for (const dir of [0, 2]) {
|
||||||
fresh(); run(`placeBlock(BP_TUBE,${dir},25,20,true); spawnBar(BARCU,100,${dir ? 78 : 84},0,${dir ? 0 : -0.4});`);
|
fresh(); run(`placeBlock(BP_SCREW,${dir},25,20,true); spawnBar(BARCU,100,${dir ? 78 : 84},0,${dir ? 0 : -0.4});`);
|
||||||
check(dir ? 'a falling bar enters a down tube from above' : 'an upward moving bar enters the bottom of an up tube',watchBar(20,'entTubeAt(ents[0].ix,ents[0].iy)!==0'));
|
check(dir ? 'a falling bar enters a down screw from above' : 'an upward moving bar enters the bottom of an up screw',watchBar(20,'entScrewAt(ents[0].ix,ents[0].iy)!==0'));
|
||||||
}
|
}
|
||||||
fresh(); run('for(let bx=20;bx<24;bx++) placeBlock(BP_TUBE,1,bx,20,true); for(let by=21;by<25;by++) placeBlock(BP_TUBE,2,23,by,true);'); putBar(80,82);
|
fresh(); run('for(let bx=20;bx<24;bx++) placeBlock(BP_SCREW,1,bx,20,true); for(let by=21;by<25;by++) placeBlock(BP_SCREW,2,23,by,true);'); putBar(80,82);
|
||||||
check('a bar turns from a right tube down into a vertical lane',watchBar(160,'ents[0].iy>=100'));
|
check('a bar turns from a right screw down into a vertical lane',watchBar(160,'ents[0].iy>=100'));
|
||||||
barTube(true); rect(run,100,76,103,79,run('STONE')); ticks(run,100);
|
barScrew(true); rect(run,100,76,103,79,run('STONE')); ticks(run,100);
|
||||||
check('a blocked corner waits without overlapping stone',run('ents[0].iy===80 && !mat.some((m,i)=>m===STONE && occ[i])'));
|
check('a blocked corner waits without overlapping stone',run('ents[0].iy===80 && !mat.some((m,i)=>m===STONE && occ[i])'));
|
||||||
rect(run,100,76,103,79,run('EMPTY'));
|
rect(run,100,76,103,79,run('EMPTY'));
|
||||||
check('a blocked corner resumes when cleared',watchBar(120,'ents[0].ix>=116'));
|
check('a blocked corner resumes when cleared',watchBar(120,'ents[0].ix>=116'));
|
||||||
barTube(); run('ents[0].T=1200; for(let y=94;y<96;y++) for(let x=100;x<104;x++) temp[y*W+x]=1200; entTick();');
|
barScrew(); run('ents[0].T=1200; for(let y=94;y<96;y++) for(let x=100;x<104;x++) temp[y*W+x]=1200; entTick();');
|
||||||
check('a bar melting inside a tube becomes eight molten grains',run('ents.length===0') && count(run,run('MCU'))===8);
|
check('a bar melting inside a screw becomes eight molten grains',run('ents.length===0') && count(run,run('MCU'))===8);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// Corner-touching square walls seal loose grains without changing free piles or worldgen.
|
||||||
|
const { load, suite, flatWorld, rect, count, ticks } = require('./harness');
|
||||||
|
const { createHash } = require('node:crypto');
|
||||||
|
const { run } = load();
|
||||||
|
const { check, done } = suite();
|
||||||
|
|
||||||
|
for (const [seed, expected] of [
|
||||||
|
[777, '040644fb50c891911383ef0206ea988677d7ec447a3e6637cefa99364e765861'],
|
||||||
|
[77, '2e6af816af54c8de4fe9c7188607f7af503ef844b2087245779bcf39e8330bdb'],
|
||||||
|
[4242, 'f09270f4ac5da9347bc277c9959e31ee797313b4a994598b4647dcf852532782'],
|
||||||
|
]) {
|
||||||
|
run(`generateWorld(${seed}, 512, 384)`);
|
||||||
|
check(`seed ${seed} restores corner sealing`,run('CORNER_SEAL')===true);
|
||||||
|
check(`seed ${seed} keeps its pre-seal material hash`, createHash('sha256').update(run('mat')).digest('hex') === expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const material of ['SAND', 'WATER', 'STEAM']) for (const mirror of [false, true]) {
|
||||||
|
flatWorld(run, 160, 256, 224);
|
||||||
|
// Span the whole world so escaping an end cannot be confused with crossing a corner.
|
||||||
|
run(`RS=12345; AMB.fill(20); temp.fill(20); weather.skyY=-1000;
|
||||||
|
for(let bx=0;bx<40;bx++) placeBlock(BP_WALL,RSTONE,bx,${mirror ? '47-bx' : '8+bx'},true);`);
|
||||||
|
const gas = material === 'STEAM';
|
||||||
|
rect(run, 70, gas ? 166 : 20, 89, gas ? 170 : 24, run(material));
|
||||||
|
if (gas) {
|
||||||
|
// Isolate gas movement from cooling/condensation, including chunk sleeping.
|
||||||
|
run(`temp.fill(120); var gasLeaks=0; for(let k=0;k<500;k++){tick++;tickStamp=tickStamp%255+1;simMove();
|
||||||
|
for(let y=0;y<H;y++)for(let x=0;x<W;x++)if(mat[y*W+x]===STEAM && y<(${mirror ? '47-(x>>2)' : '8+(x>>2)'})*4) gasLeaks++;}`);
|
||||||
|
} else ticks(run, 500);
|
||||||
|
const result = run(`(()=>{
|
||||||
|
let leaked=0,held=0,x0=W,x1=0,y0=H,y1=0;
|
||||||
|
for(let y=0;y<H;y++)for(let x=0;x<W;x++)if(mat[y*W+x]===${material}) {
|
||||||
|
const top=(${mirror ? '47-(x>>2)' : '8+(x>>2)'})*4;
|
||||||
|
if(${gas ? 'y<top' : 'y>=top+4'}) leaked++; else held++;
|
||||||
|
x0=Math.min(x0,x);x1=Math.max(x1,x);y0=Math.min(y0,y);y1=Math.max(y1,y);
|
||||||
|
}
|
||||||
|
return {leaked,held,width:x1-x0+1,height:y1-y0+1};
|
||||||
|
})()`);
|
||||||
|
check(`${material} stays ${gas ? 'under' : 'above'} ${mirror ? 'rising' : 'descending'} square stairs`, result.leaked === 0 && result.held === 100 && (!gas || run('gasLeaks')===0), JSON.stringify(result)+(gas ? ` crossings ${run('gasLeaks')}` : ''));
|
||||||
|
if (!gas) check(`${material} piles and slides along the steps`, result.height > 1 && count(run,run(material),70,0,89,223)<100, JSON.stringify(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A screw mouth must not draw sand through two external wall cells either.
|
||||||
|
for (const direction of [4,5,6,7]) {
|
||||||
|
flatWorld(run,160,160,140);
|
||||||
|
run(`placeBlock(BP_SCREW,${direction},20,20,true);
|
||||||
|
var dx=SCREW_DX[SCREW_PARTS[${direction}]],dy=SCREW_DY[SCREW_PARTS[${direction}]];
|
||||||
|
var x=dx>0?80:83,y=dy>0?80:83;
|
||||||
|
setCell((y-dy)*W+x-dx,SAND,20,0);
|
||||||
|
setCell((y-dy)*W+x,STONE,20,0);setCell(y*W+x-dx,STONE,20,0);
|
||||||
|
screwIntake(y*W+x,part[y*W+x],dx,dy);`);
|
||||||
|
check(`diagonal screw ${direction} cannot pull through a solid corner`,run('mat[(y-dy)*W+x-dx]===SAND'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A free flyer can take the same diagonal shortcut between wall cells.
|
||||||
|
flatWorld(run, 160, 160, 140);
|
||||||
|
run(`setCell(79*W+80,STONE,20,0);setCell(80*W+79,STONE,20,0);
|
||||||
|
flyers=[{x:79.5,y:79.5,vx:1,vy:1,age:0,m:SAND,T:20,free:true,run:0}];flyTick();`);
|
||||||
|
check('a flying grain cannot cross a sealed corner', run('flyers.length===0 && mat[79*W+79]===SAND'));
|
||||||
|
|
||||||
|
flatWorld(run, 240, 160, 120);
|
||||||
|
run('RS=12345;');
|
||||||
|
rect(run, 110, 40, 129, 59, run('SAND'));
|
||||||
|
ticks(run, 600);
|
||||||
|
const pile = run(`(()=>{let x0=W,x1=0,y0=H,y1=0;
|
||||||
|
for(let y=0;y<H;y++)for(let x=0;x<W;x++)if(mat[y*W+x]===SAND){x0=Math.min(x0,x);x1=Math.max(x1,x);y0=Math.min(y0,y);y1=Math.max(y1,y);}
|
||||||
|
return [x1-x0+1,y1-y0+1];})()`);
|
||||||
|
// Before the fix: 57 wide, 17 high. Small bounds allow random-state differences from prior scenes.
|
||||||
|
check('open-ground sand still forms the same size pile', pile[0]>=55 && pile[0]<=59 && pile[1]>=16 && pile[1]<=18 && count(run,run('SAND'))===400, pile);
|
||||||
|
const sealedPile = createHash('sha256').update(run('mat')).digest('hex'), sealedRS=run('RS');
|
||||||
|
flatWorld(run,240,160,120);
|
||||||
|
run('RS=12345; CORNER_SEAL=false;');
|
||||||
|
rect(run,110,40,129,59,run('SAND'));ticks(run,600);
|
||||||
|
check('open pile and random sequence exactly match unsealed movement', createHash('sha256').update(run('mat')).digest('hex')===sealedPile && run('RS')===sealedRS);
|
||||||
|
run('CORNER_SEAL=true;');
|
||||||
|
done();
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// Dev command validation, discovery, build costs, runtime controls and save metadata.
|
||||||
|
const { load, suite, flatWorld } = require('./harness');
|
||||||
|
const { run } = load();
|
||||||
|
const { check, done } = suite();
|
||||||
|
flatWorld(run, 240, 160, 110);
|
||||||
|
const command = text => run(`devCommand(${JSON.stringify(text)})`);
|
||||||
|
check('commands cannot change an unmarked world', command('money 123')[0].includes('warning'));
|
||||||
|
run('enableDevConsole(); money = 100;');
|
||||||
|
command('MoNeY -25'); command('money set 1234.5');
|
||||||
|
check('money adds signed values and sets amounts, ignoring case', run('money === 1234.5'));
|
||||||
|
command('money -34.5');
|
||||||
|
check('negative money amount subtracts', run('money === 1200'));
|
||||||
|
command('debt 500');
|
||||||
|
check('debt sets claim amount', run('debt === 500'));
|
||||||
|
command('debt clear');
|
||||||
|
check('debt clear pays off claim', run('debt === 0'));
|
||||||
|
run('seen.fill(0); EV.length = 0;');
|
||||||
|
command('give cUoRe 7'); command('give Copper Bar 3'); command('give charc'); command('give sand 30000');
|
||||||
|
check('give matches internal names, display names and unique prefixes', run('bag[CUORE] === 7 && bag[BARCU] === 3 && bag[CHARCOAL] === 1000'));
|
||||||
|
check('give exceeds bag capacity, counts totals and discovers like a pickup', run("bag[SAND] === 30000 && bagTotal === 31010 && seen[SAND] && EV.some(e => e.t === 'discover' && e.m === SAND && e.how === 'pickup')"));
|
||||||
|
check('ambiguous prefix explains candidates', command('give cop')[0].startsWith('Ambiguous material:'));
|
||||||
|
check('unknown material is explained', command('give nonexistent')[0].startsWith('Unknown material'));
|
||||||
|
check('all internal aliases resolve exactly', run('Object.keys(DEV_MATERIALS).length === NMAT && Object.entries(DEV_MATERIALS).every(([k, v]) => devMaterial(k) === v)'));
|
||||||
|
command('give sand 2147483647');
|
||||||
|
check('give rejects typed-array overflow', run('bag[SAND] === 30000'));
|
||||||
|
command('bit 1e100'); command('bag -5');
|
||||||
|
check('tier setters clamp to valid range', run('player.bitTier === BIT_TIERS.length - 1 && player.tankTier === 0'));
|
||||||
|
check('tier queries list every tier', command('bit').length === run('BIT_TIERS.length') && command('bag').length === run('TANK_TIERS.length'));
|
||||||
|
run('player.lens = player.flask = false;'); command('unlock');
|
||||||
|
check('unlock learns materials, rewards and milestones', run('seen.every(Boolean) && player.lens && player.flask && MILESTONES.every(m => ledger[m.id])'));
|
||||||
|
command('tp -500 9999');
|
||||||
|
check('teleport clamps the entire bot inside the world', run('bot.x === 0 && bot.y === H - bot.h && bot.vx === 0 && bot.vy === 0'));
|
||||||
|
run('devCommand("tp", {x: 70, y: 45})');
|
||||||
|
check('teleport can use the cursor', run('bot.x === 70 && bot.y === 45'));
|
||||||
|
command('speed 1e100'); check('speed clamps high', run('devSpeed === 8'));
|
||||||
|
command('speed -1'); check('speed clamps low', run('devSpeed === 0.1'));
|
||||||
|
command('step'); check('step requires dev pause', run('devSteps === 0'));
|
||||||
|
command('pause'); command('step 5');
|
||||||
|
check('paused steps are queued for the frame loop', run('devPaused && devSteps === 5'));
|
||||||
|
command('pause'); check('resuming discards queued steps', run('!devPaused && devSteps === 0'));
|
||||||
|
const beforeBad = run('JSON.stringify([money, debt, bagTotal, player, devLog, devSpeed, devSteps])');
|
||||||
|
for (const text of ['money', 'money set', 'money Infinity', 'money 1e309', 'money 1 2', 'debt -1', 'debt nope', 'give sand -1', 'give sand 1.5', 'bit 1.5', 'bag nope', 'unlock extra', 'tp 1', 'tp NaN 4', 'free maybe', 'speed NaN', 'step 0', 'step 10001', 'pause extra', 'weather snow', 'nonsense']) {
|
||||||
|
const output = command(text);
|
||||||
|
check('bad arguments produce a helpful error: ' + text, output.length > 0 && !output[0].startsWith('>'));
|
||||||
|
}
|
||||||
|
check('invalid commands do not mutate or log', beforeBad === run('JSON.stringify([money, debt, bagTotal, player, devLog, devSpeed, devSteps])'));
|
||||||
|
check('help lists every command', command('help').length === 13);
|
||||||
|
run('money = 17; bag.fill(0); bagTotal = 0; bagAdd(RSTONE, 32, true); bagAdd(LCLAY, 100, true);');
|
||||||
|
check('normal build costs are checked', run('!canAfford([[CASH, 100]], 1)'));
|
||||||
|
command('free on');
|
||||||
|
const buildBag = run('JSON.stringify(Array.from(bag))');
|
||||||
|
check('free bypasses affordability', run('canAfford([[CASH, 100000], [BARCU, 99]], 1)'));
|
||||||
|
check('free places a wall without taking existing rubble', run("placeBlock(BP_WALL, RSTONE, 10, 10, false) === '' && bag[RSTONE] === 32"));
|
||||||
|
check('free places a machine without charging money', run("placeMachineAt('fan', 0, 24, 10) === '' && money === 17"));
|
||||||
|
check('free never charges, including blueprint ghosts built later', run('(() => { const m = money; payCost([[CASH, 500]], 3); return money === m; })()'));
|
||||||
|
check('free places a mold without taking clay', run("typeof addMold('ingot', 35, 10, LCLAY, false) !== 'string' && bag[LCLAY] === 100"));
|
||||||
|
run("ghostBlocks.set(12 * BW + 17, [BP_BELT, 1]); ghostMachines.push({type: 'fan', dir: 0, bx: 20, by: 17}); ghostMolds.push({kind: 'ingot', bx: 9, by: 16, wall: LCLAY}); ghostTick(); ghostTick();");
|
||||||
|
check('ghost blocks, machines and molds build free too', run('!ghostBlocks.size && !ghostMachines.length && !ghostMolds.length && money === 17 && ghostMissing === 0'));
|
||||||
|
check('all free paths preserve bag contents and totals', buildBag === run('JSON.stringify(Array.from(bag))') && run('bagTotal === 132'));
|
||||||
|
command('free off'); run('payCost([[CASH, 2], [RSTONE, 4]], 1)');
|
||||||
|
check('normal payment resumes after free off', run('money === 15 && bag[RSTONE] === 28 && bagTotal === 128'));
|
||||||
|
command('weather rain');
|
||||||
|
check('weather rain starts a shower near the bot', run('weather.shower && rainAt(bot.x) > 0 && weather.shower.until === tick + 3600'));
|
||||||
|
command('weather clear');
|
||||||
|
check('weather clear stops current shower', run('weather.shower === null && weather.raining.every(v => !v)'));
|
||||||
|
run('for (let i = 0; i < 210; i++) devCommand("money 1");');
|
||||||
|
check('dev log keeps the last 200 commands', run('devLog.length === 200 && devLog.every(e => e[0] === tick && e[1] === "money 1")'));
|
||||||
|
run('devCommand("speed 4"); devCommand("pause"); devCommand("free on"); const devSaved = serializeWorld(); const devLogCopy = JSON.stringify(devLog); deserializeWorld(devSaved.meta, devSaved.buffers);');
|
||||||
|
check('dev mark and command log survive save round trip', run('devWorld && devSaved.meta.dev === true && JSON.stringify(devLog) === devLogCopy'));
|
||||||
|
check('load resets speed, free building, pause and pending steps', run('devSpeed === 1 && !devFree && !devPaused && devSteps === 0'));
|
||||||
|
run('alloc(240,160)');
|
||||||
|
check('a new world starts unmarked without a dev log', run('!devWorld && devLog.length === 0'));
|
||||||
|
check('give turns rock into its rubble and says so', run(`(() => { enableDevConsole(); const before = bag[RBAS]; const out = devCommand('give basalt 50').join(' '); return bag[RBAS] === before + 50 && bag[BASALT] === 0 && /Basalt rubble/.test(out); })()`));
|
||||||
|
check('give refuses things that cannot be in a bag', run(`(() => { enableDevConsole(); const out = devCommand('give steam 5').join(' '); return /cannot go in the bag/.test(out) && bag[STEAM] === 0; })()`));
|
||||||
|
done();
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
"""Dev console confirmation, keyboard isolation, HUD, time controls and saved badge."""
|
||||||
|
import pathlib, sys
|
||||||
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / 'tools'))
|
||||||
|
from use_venv import ensure_playwright; ensure_playwright()
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||||
|
SHOTS = ROOT / 'shots'
|
||||||
|
SHOTS.mkdir(exist_ok=True)
|
||||||
|
fails = []
|
||||||
|
def check(name, ok):
|
||||||
|
print(('PASS ' if ok else 'FAIL ') + name)
|
||||||
|
if not ok: fails.append(name)
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch()
|
||||||
|
page = browser.new_page(viewport={'width': 1280, 'height': 760})
|
||||||
|
errors = []
|
||||||
|
page.on('pageerror', lambda e: errors.append(str(e)))
|
||||||
|
page.add_init_script("localStorage.setItem('deepgrain.settings', JSON.stringify({look:'pixel', sv:2}));")
|
||||||
|
page.goto((ROOT / 'dist/index.html').as_uri())
|
||||||
|
page.wait_for_function("!$('title').hidden")
|
||||||
|
page.evaluate("startNewWorld(424242, 512, 384, 'Console test')")
|
||||||
|
page.wait_for_function('GAME.running && !GAME.loading')
|
||||||
|
page.evaluate("document.querySelectorAll('.card, .toast').forEach(e => e.remove())")
|
||||||
|
page.keyboard.press('`')
|
||||||
|
check('backtick opens the first-use warning', page.is_visible('#m-dialog') and not page.is_visible('#dev-console'))
|
||||||
|
page.screenshot(path=str(SHOTS / 't66-warning.png'))
|
||||||
|
check('warning explains permanent marking and future achievements', 'permanently' in page.inner_text('#dlg-body') and 'achievements' in page.inner_text('#dlg-body'))
|
||||||
|
page.get_by_role('button', name='Cancel', exact=True).click()
|
||||||
|
check('cancel leaves the world unmarked and console closed', page.evaluate("!devWorld && $('dev-console').hidden && !GAME.paused"))
|
||||||
|
page.keyboard.press('`')
|
||||||
|
page.get_by_role('button', name='Enable dev console', exact=True).click()
|
||||||
|
page.wait_for_function("!$('dev-console').hidden && document.activeElement === $('dev-input')")
|
||||||
|
check('accepting marks the world and resumes play', page.evaluate('devWorld && !GAME.paused'))
|
||||||
|
check('build material chip stays stable across HUD refreshes', page.evaluate("""() => {
|
||||||
|
selectTool(2); renderSelCard();
|
||||||
|
const chip = $('selcard').querySelector('[data-wall-picker]');
|
||||||
|
renderSelCard();
|
||||||
|
const stable = chip === $('selcard').querySelector('[data-wall-picker]');
|
||||||
|
selectTool(0); return stable;
|
||||||
|
}"""))
|
||||||
|
old_money = page.evaluate('money')
|
||||||
|
page.fill('#dev-input', 'money 1000'); page.keyboard.press('Enter')
|
||||||
|
page.wait_for_function(f"money === {old_money + 1000} && $('wallet').textContent.includes(fmtMoney(money))")
|
||||||
|
check('money command changes the wallet HUD', True)
|
||||||
|
tool = page.evaluate('GAME.tool')
|
||||||
|
page.fill('#dev-input', ''); page.keyboard.type('1234wasd')
|
||||||
|
check('typing game keys does not switch tools or hold movement', page.evaluate(f'GAME.tool === {tool} && keys.size === 0'))
|
||||||
|
page.keyboard.press('Escape')
|
||||||
|
check('Escape closes console without opening pause menu', page.evaluate("$('dev-console').hidden && $('m-pause').hidden && !GAME.paused"))
|
||||||
|
page.keyboard.press('`')
|
||||||
|
check('marked worlds reopen without the warning', page.evaluate("!$('dev-console').hidden && $('m-dialog').hidden"))
|
||||||
|
page.keyboard.press('`')
|
||||||
|
check('backtick closes even with input focused', not page.is_visible('#dev-console'))
|
||||||
|
page.keyboard.press('`')
|
||||||
|
def command(text):
|
||||||
|
page.fill('#dev-input', text); page.keyboard.press('Enter')
|
||||||
|
command('pause')
|
||||||
|
tick = page.evaluate('tick')
|
||||||
|
page.wait_for_timeout(150)
|
||||||
|
check('dev pause freezes simulation ticks', page.evaluate('tick') == tick)
|
||||||
|
command('step 5')
|
||||||
|
page.wait_for_function(f'tick === {tick + 5} && devSteps === 0')
|
||||||
|
page.wait_for_timeout(150)
|
||||||
|
check('step advances exactly five ticks then stays paused', page.evaluate('tick') == tick + 5)
|
||||||
|
# Drive the actual frame accumulator with controlled elapsed time and no scheduled frames.
|
||||||
|
counts = page.evaluate('''() => {
|
||||||
|
requestAnimationFrame = () => 0;
|
||||||
|
GAME.paused = false; devPaused = false; SET.fpsCap = 0; SET.maxSteps = 3;
|
||||||
|
const counts = [];
|
||||||
|
for (const speed of [0.5, 1, 2, 8]) {
|
||||||
|
devSpeed = speed; acc = 0; const before = tick;
|
||||||
|
const t = performance.now(); lastT = t - 34; lastRender = t - 34; frame(t);
|
||||||
|
counts.push(tick - before);
|
||||||
|
}
|
||||||
|
return counts;
|
||||||
|
}''')
|
||||||
|
check('speed scales fixed ticks and respects maxSteps', counts == [1, 2, 3, 3])
|
||||||
|
page.evaluate('devCommand("debt clear"); runDevCommand("money 1"); openNotebook("Claims")')
|
||||||
|
page.wait_for_function("$('nb-body').textContent.includes('paid off')")
|
||||||
|
check('cleared debt is reflected in Claims', True)
|
||||||
|
page.evaluate('closeAllModals(); runDevCommand("debt 123"); openNotebook("Market")')
|
||||||
|
check('new debt is reflected in the Market', '$123' in page.inner_text('#nb-body'))
|
||||||
|
page.evaluate('closeAllModals()')
|
||||||
|
page.evaluate("async () => { await saveWorld('main', true); await renderWorlds(); }")
|
||||||
|
page.evaluate("openModal('m-worlds')")
|
||||||
|
check('world list entry stores and draws the dev badge', page.locator('#worlds-list .dev-badge').is_visible() and page.inner_text('#worlds-list .dev-badge') == 'Dev console')
|
||||||
|
page.screenshot(path=str(SHOTS / 't66-worlds.png'))
|
||||||
|
page.evaluate('closeAllModals()')
|
||||||
|
check('saving and loading preserves mark and log, resets runtime controls', page.evaluate('''async () => {
|
||||||
|
const sv = await worldNewest(GAME.worldId);
|
||||||
|
const count = devLog.length;
|
||||||
|
await startFromSave(sv);
|
||||||
|
return devWorld && devLog.length === count && devSpeed === 1 && !devPaused && !devFree && $('dev-console').hidden;
|
||||||
|
}'''))
|
||||||
|
check('no browser errors', not errors)
|
||||||
|
browser.close()
|
||||||
|
|
||||||
|
sys.exit(1 if fails else 0)
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
// Diagonal screws: sealed four-lane corner transfers, bars, joins and persistence.
|
||||||
|
const { load, suite, flatWorld, rect, ticks, count } = require('./harness');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { run } = load();
|
||||||
|
const { check, done } = suite();
|
||||||
|
run(fs.readFileSync('src/render/diagonal-screws.js', 'utf8'));
|
||||||
|
run(fs.readFileSync('src/render/world.js', 'utf8'));
|
||||||
|
function fresh() { flatWorld(run, 256, 192, 176); run('RS=7; nextPowerBuild=0; money=10000; AMB.fill(20); temp.fill(20); hot.fill(0);'); }
|
||||||
|
function line(v, chain = false) {
|
||||||
|
fresh();
|
||||||
|
const [dx, dy] = run(`SCREW_DIRS[${v}]`), bx = dx > 0 ? 16 : 36, by = dy < 0 ? 32 : 12;
|
||||||
|
run(`for(let k=0;k<10;k++) placeBlock(BP_SCREW,${v},${bx}+k*${dx},${by}+k*${dy},true);`);
|
||||||
|
let ty = by, tail = bx, head = bx + 9 * dx;
|
||||||
|
if (chain) {
|
||||||
|
run(`for(let k=1;k<=3;k++) { placeBlock(BP_SCREW,${dx > 0 ? 1 : 3},${bx}-k*${dx},${by},true); placeBlock(BP_SCREW,${dx > 0 ? 1 : 3},${head}+k*${dx},${by + 10 * dy},true); }`);
|
||||||
|
tail -= 3 * dx; head += 3 * dx;
|
||||||
|
}
|
||||||
|
return { dx, dy, bx, by, ty, tail, head, hy: by + 9 * dy };
|
||||||
|
}
|
||||||
|
function drain(s, steps, feed = false) {
|
||||||
|
return run(`(() => {
|
||||||
|
let out=0, leaks=0, added=0;
|
||||||
|
for(let k=0;k<${steps};k++) {
|
||||||
|
${feed ? `for(let l=0;l<4;l++) { const i=(${s.ty ?? s.by}*4+l)*W+${s.tail}*4+${s.dx > 0 ? 0 : 3}; if(mat[i]===EMPTY) {setCell(i,SAND,20,0);added++;} }` : ''}
|
||||||
|
simTick();
|
||||||
|
for(let i=0;i<N;i++) if(mat[i]===SAND && !IS_SCREW[part[i]]) {
|
||||||
|
const x=i%W; if(${s.dx}>0 ? x>=${(s.head + 1) * 4} : x<${s.head * 4}) out++; else leaks++;
|
||||||
|
setCell(i,EMPTY,20,0);
|
||||||
|
}
|
||||||
|
for(const f of flyers) if(f.m===SAND) {
|
||||||
|
if(${s.dx}>0 ? f.x>=${(s.head + 1) * 4} : f.x<${s.head * 4}) out++; else leaks++;
|
||||||
|
}
|
||||||
|
flyers=flyers.filter(f=>f.m!==SAND);
|
||||||
|
}
|
||||||
|
return {out,leaks,added,inside:mat.reduce((n,m)=>n+(m===SAND),0)};
|
||||||
|
})()`);
|
||||||
|
}
|
||||||
|
for (const v of [4, 5, 6, 7]) {
|
||||||
|
const s = line(v);
|
||||||
|
check(`direction ${v} uses appended part ids and marks power`,run(`part[${s.by * 4}*W+${s.bx * 4}]===${v + 7} && blockV[${s.by}*BW+${s.bx}]===${v} && pcells.size===160`));
|
||||||
|
// Fill every lane, in every block: a corner or side leak is counted before it can fall away.
|
||||||
|
run('for(let i=0;i<N;i++) if(IS_SCREW[part[i]]) setCell(i,SAND,20,0);');
|
||||||
|
const r = drain(s, 300);
|
||||||
|
check(`direction ${v} carries sand through ten blocks and out the head`, r.out === 160, JSON.stringify(r));
|
||||||
|
check(`direction ${v} has no corner or side spills and conserves grains`, r.leaks === 0 && r.out+r.inside === 160,JSON.stringify(r));
|
||||||
|
run('rebuildPower();');
|
||||||
|
check(`direction ${v} joins corner-touching power networks`,run('nets.length===2 && nets[1].nt===160'));
|
||||||
|
const c = line(v, true);
|
||||||
|
rect(run,c.tail*4,c.ty*4,c.tail*4+3,c.ty*4+3,run('SAND'));
|
||||||
|
const joined = drain(c,500);
|
||||||
|
check(`direction ${v}: straight to diagonal to straight`,joined.out===16 && joined.leaks===0,JSON.stringify(joined));
|
||||||
|
}
|
||||||
|
// Every straight direction and neighbour offset, with actual transfers in both source orders.
|
||||||
|
for(const v of [4,5,6,7])for(const sv of [0,1,2,3])for(const [ox,oy] of [[-1,-1],[0,-1],[1,-1],[-1,0],[1,0],[-1,1],[0,1],[1,1]]) {
|
||||||
|
const [dx,dy]=run(`SCREW_DIRS[${v}]`),[sxv,syv]=run(`SCREW_DIRS[${sv}]`),vertical=!sxv;
|
||||||
|
const out=ox===dx&&oy===dy&&dx*sxv+dy*syv>0;
|
||||||
|
const incoming=ox===-sxv&&oy===-syv&&dx*sxv+dy*syv>0;
|
||||||
|
const joined=out||incoming,end=out?(dx>0?1:0):(dx>0?0:1),nx=20+ox,ny=20+oy;
|
||||||
|
const label=`diagonal ${v} straight ${sv} offset ${ox},${oy}`;
|
||||||
|
fresh();run(`placeBlock(BP_SCREW,${v},20,20,true);placeBlock(BP_SCREW,${sv},${nx},${ny},true);`);
|
||||||
|
const kind=joined?(oy===0?(ox>0?2:4):ox===0?(oy<0?1:3):vertical?6:5):0;
|
||||||
|
check(`${label}: drawing agrees with travel joins`,run(`(()=>{const j=diagonalScrewJoins(20,20,${v});return pieceJoins(20,20)===${joined?1<<end:0}&&${joined?`(j>>${end*3}&7)===${kind}`:'(j&192)===192'};})()`));
|
||||||
|
check(`${label}: complete ghost plan matches placed joins`,run(`(()=>{
|
||||||
|
const j=diagonalScrewJoins(20,20,${v}),sj=straightScrewJoins(${nx},${ny},${sv});removeBlock(20,20);removeBlock(${nx},${ny});
|
||||||
|
const p=new Map([[20*BW+20,[BP_SCREW,${v}]],[${ny}*BW+${nx},[BP_SCREW,${sv}]]]);
|
||||||
|
return diagonalScrewJoins(20,20,${v},p)===j&&straightScrewJoins(${nx},${ny},${sv},p)===sj;
|
||||||
|
})()`));
|
||||||
|
for(const forward of [true,false]) {
|
||||||
|
fresh();run(`placeBlock(BP_SCREW,${v},20,20,true);placeBlock(BP_SCREW,${sv},${nx},${ny},true);rebuildPower();`);
|
||||||
|
const sx=forward?20:nx,sy=forward?20:ny,tx=forward?nx:20,ty=forward?ny:20,passes=forward?out:incoming;
|
||||||
|
rect(run,sx*4,sy*4,sx*4+3,sy*4+3,run('SAND'));
|
||||||
|
const crossed=run(`(()=>{let n=0;for(let k=0;k<40;k++){tick+=2;tickStamp++;machinesTick();for(let i=0;i<N;i++)if(mat[i]===SAND){const x=i%W>>2,y=(i/W|0)>>2;if(x===${tx}&&y===${ty})n++;if(x!==${sx}||y!==${sy})setCell(i,EMPTY,20,0);}flyers=[];}return n;})()`);
|
||||||
|
check(`${label}: grains ${forward?'out':'in'} ${passes?'cross':'stay separate'}`,passes?crossed===16:crossed===0,crossed);
|
||||||
|
run('for(let i=0;i<N;i++)if(mat[i]===SAND)setCell(i,EMPTY,20,0);');putBar(sx*4,sy*4+1);
|
||||||
|
const bar=run(`(()=>{const e=ents[0];for(let k=0;k<24;k++){tick+=8;entMark(e,0);entScrewTick(e,entScrewAt(e.ix,e.iy));entMark(e,1);if((e.ix>>2)===${tx}&&(e.iy>>2)===${ty})return true;if(!entScrewAt(e.ix,e.iy))break;}return false;})()`);
|
||||||
|
check(`${label}: bar ${forward?'out':'in'} ${passes?'crosses':'stays separate'}`,bar===passes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fresh();
|
||||||
|
run('placeBlock(BP_SCREW,1,20,20,true);placeBlock(BP_SCREW,0,21,20,true);rebuildPower();');
|
||||||
|
rect(run,80,80,83,83,run('SAND'));
|
||||||
|
check('a horizontal screw still feeds the side of a vertical lift',run(`(()=>{for(let k=0;k<24;k++){tick+=2;tickStamp++;machinesTick();for(let y=80;y<84;y++)for(let x=84;x<88;x++)if(mat[y*W+x]===SAND)return true;}return false;})()`));
|
||||||
|
// The shared opposite-direction check preserves all eight direction combinations.
|
||||||
|
check('screw entry rejects exactly the opposite direction',run('SCREW_PARTS.every(p=>SCREW_DIRS.every(([dx,dy])=>screwAccepts(p,dx,dy)===(SCREW_DX[p]!==-dx||SCREW_DY[p]!==-dy)))'));
|
||||||
|
// Flat belts and filters receive diagonal outlets in E, on their carrying face.
|
||||||
|
for (const v of [4,5,6,7]) for (const side of [-1,1]) for (const code of ['BP_BELT','BP_FILTER']) {
|
||||||
|
fresh();
|
||||||
|
const slope=v===4||v===6?-1:1,dy=side*slope,nx=20+side,ny=20+dy;
|
||||||
|
const dv=(side>0)===(v<6)?v:(v<6?v+2:v-2),end=side>0?1:0,kind=5;
|
||||||
|
run(`placeBlock(BP_SCREW,${dv},20,20,true);placeBlock(${code},${code==='BP_FILTER'?(side>0?128:0):(side>0?1:0)},${nx},${ny},true);rebuildPower();`);
|
||||||
|
check(`${code} at diagonal ${v} end ${end} draws the shared join`,run(`(diagonalScrewJoins(20,20,${dv})>>${end*3}&7)===${kind}`));
|
||||||
|
check(`${code} at diagonal ${v} end ${end} receives every grain lane above its face`,run(`(()=>{for(let l=0;l<4;l++){const i=(80+l)*W+${side>0?83:80};if(diagonalScrewNext(i,SCREW_PARTS[${dv}])!==${ny*4-1}*W+${side>0?84:79})return false;}return true;})()`));
|
||||||
|
putBar(80,81);
|
||||||
|
check(`${code} at diagonal ${v} end ${end} receives a bar on its face`,run(`(()=>{const e=ents[0];tick=8;entMark(e,0);entDiagonalScrewTick(e,SCREW_PARTS[${dv}]);entMark(e,1);return e.ix===${nx*4}&&e.iy===${ny*4-2};})()`));
|
||||||
|
}
|
||||||
|
// Flat carriers enter every diagonal at its same-row inlet; blocked mouths must wait safely.
|
||||||
|
for(const v of [4,5,6,7])for(const filter of [false,true]) {
|
||||||
|
fresh();const dx=run(`SCREW_DIRS[${v}][0]`),nx=20+dx;
|
||||||
|
run(`placeBlock(${filter?'BP_FILTER':'BP_BELT'},${filter?(dx>0?128:0):(dx>0?1:0)},20,20,true);placeBlock(BP_SCREW,${v},${nx},20,true);rebuildPower();`);
|
||||||
|
putBar(80,78);
|
||||||
|
run(`setCell(80*W+${nx*4},SAND,20,0);`);
|
||||||
|
check(`${filter?'filter':'belt'} → diagonal ${v}: a blocked joined inlet holds the bar`,run(`(()=>{
|
||||||
|
const e=ents[0];entMark(e,0);tick=8;entCurM=e.m;const held=entBeltScrewEnter(e);entMark(e,1);
|
||||||
|
return held&&e.ix===80&&e.iy===78;
|
||||||
|
})()`));
|
||||||
|
run(`setCell(80*W+${nx*4},EMPTY,20,0);`);
|
||||||
|
check(`${filter?'filter':'belt'} → diagonal ${v}: clearing the inlet resumes the drop`,run(`(()=>{
|
||||||
|
const e=ents[0];entMark(e,0);const moved=entBeltScrewEnter(e);entMark(e,1);
|
||||||
|
return moved&&e.ix===${nx*4}&&e.iy===80&&!!entScrewAt(e.ix,e.iy);
|
||||||
|
})()`));
|
||||||
|
fresh();run(`placeBlock(${filter?'BP_FILTER':'BP_BELT'},${filter?(dx>0?128:0):(dx>0?1:0)},20,20,true);placeBlock(BP_SCREW,${v<6?v+2:v-2},${nx},20,true);rebuildPower();`);
|
||||||
|
putBar(80,78);
|
||||||
|
check(`${filter?'filter':'belt'} → diagonal ${v}: incompatible travel never snaps a bar`,run('(()=>{const e=ents[0];entMark(e,0);const entered=entBeltScrewEnter(e);entMark(e,1);return !entered&&e.ix===80&&e.iy===78;})()'));
|
||||||
|
}
|
||||||
|
// The removed straight-to-diagonal corner path stays open.
|
||||||
|
fresh();
|
||||||
|
run('placeBlock(BP_SCREW,1,20,20,true);placeBlock(BP_SCREW,6,21,19,true);');
|
||||||
|
check('a straight mouth never takes the removed corner shortcut',run('straightScrewNext(82*W+83,P_SCREW_R)===82*W+84'));
|
||||||
|
// A saturated ten-block line: 80 warmup ticks, then 600 measured ticks at 60 Hz.
|
||||||
|
const rates = [];
|
||||||
|
for (const v of [0, 4, 5]) {
|
||||||
|
let s;
|
||||||
|
if (v) s=line(v);
|
||||||
|
else {
|
||||||
|
fresh();run('for(let by=23;by<=32;by++) placeBlock(BP_SCREW,0,16,by,true);');
|
||||||
|
s={dx:0,tail:16,by:32,head:16};
|
||||||
|
}
|
||||||
|
const result = v ? (drain(s,80,true),drain(s,600,true)) : run(`(() => {
|
||||||
|
let out=0;
|
||||||
|
for(let k=0;k<680;k++) {
|
||||||
|
for(let l=0;l<4;l++) {const i=131*W+64+l;if(mat[i]===EMPTY)setCell(i,SAND,20,0);}
|
||||||
|
simTick();
|
||||||
|
if(k>=80) out+=flyers.filter(f=>f.m===SAND).length;
|
||||||
|
flyers=[];
|
||||||
|
}return {out};
|
||||||
|
})()`);
|
||||||
|
rates.push(result.out/10);
|
||||||
|
}
|
||||||
|
console.log('Saturated throughput, ten blocks, grains/s: straight/up-diagonal/down-diagonal '+rates.join('/'));
|
||||||
|
check('diagonals match straight throughput per block',rates.every(n=>n===120),rates);
|
||||||
|
// A belt's last carrying cell feeds the tail side of an upward diagonal screw.
|
||||||
|
const belt=line(4);
|
||||||
|
run('for(let bx=10;bx<16;bx++)placeBlock(BP_BELT,1,bx,32,true);');
|
||||||
|
rect(run,39,100,39,127,run('STONE'));
|
||||||
|
rect(run,40,116,55,127,run('SAND'));
|
||||||
|
const beltOut=run(`(()=>{let out=0;for(let k=0;k<1000;k++){simTick();for(let f=flyers.length-1;f>=0;f--)if(flyers[f].m===SAND && flyers[f].x>=104){out++;flyers.splice(f,1);}}return out;})()`);
|
||||||
|
check('a belt feeds sand into a diagonal tail and out its head',beltOut>150,beltOut);
|
||||||
|
for(const v of [4,5,6,7]) {
|
||||||
|
const s=line(v);
|
||||||
|
rect(run,s.bx*4,s.by*4-8,s.bx*4+3,s.by*4-1,run('WATER'));
|
||||||
|
ticks(run,400);
|
||||||
|
check(`water flows freely through diagonal ${v}`,count(run,run('WATER'),0,0,255,172)===0 && count(run,run('WATER'))===32,count(run,run('WATER')));
|
||||||
|
}
|
||||||
|
// Bars keep their horizontal shape, with no saved ride state.
|
||||||
|
function putBar(x,y) {
|
||||||
|
run(`{ const e=spawnBar(BARCU,20,20,0,0); entMark(e,0); e.x=e.ix=${x}; e.y=e.iy=${y}; entMark(e,1); }`);
|
||||||
|
}
|
||||||
|
function watchBars(s, n=600) {
|
||||||
|
return run(`(()=>{let out=0,overlap=false;for(let k=0;k<${n};k++){
|
||||||
|
simTick();
|
||||||
|
for(let a=0;a<ents.length;a++)for(let b=0;b<a;b++) {
|
||||||
|
const e=ents[a],f=ents[b];if(e.ix<f.ix+4 && f.ix<e.ix+4 && e.iy<f.iy+2 && f.iy<e.iy+2)overlap=true;
|
||||||
|
}
|
||||||
|
if(mat.some((m,i)=>occ[i] && (PH[m]===PH_SOLID || PH[m]===PH_POWDER)))overlap=true;
|
||||||
|
for(let a=ents.length-1;a>=0;a--) {const e=ents[a];if(${s.dx}>0 ? e.ix>=${(s.head + 1) * 4} : e.ix+4<=${s.head * 4}) {out++;removeEnt(e);} }
|
||||||
|
}return {out,overlap,left:ents.length};})()`);
|
||||||
|
}
|
||||||
|
for(const v of [4,5,6,7]) for(const chain of [false,true]) {
|
||||||
|
const s=line(v,chain);
|
||||||
|
putBar(s.tail*4,s.ty*4);
|
||||||
|
putBar(s.tail*4,s.ty*4+2); // a queued bar must wait for the first one
|
||||||
|
const r=watchBars(s);
|
||||||
|
check(`bars travel diagonal ${v}${chain?' and both straight joins':''} without overlap`,r.out===2 && !r.overlap,JSON.stringify(r));
|
||||||
|
}
|
||||||
|
// A lift feeds a diagonal directly ahead; the diagonal feeds the next lift in E.
|
||||||
|
for (const v of [4,5,6,7]) {
|
||||||
|
const s=line(v), vv=s.dy<0?0:2, tailX=s.bx, headX=s.head+s.dx;
|
||||||
|
run(`for(let k=1;k<=3;k++){placeBlock(BP_SCREW,${vv},${tailX},${s.by}-k*${s.dy},true);placeBlock(BP_SCREW,${vv},${headX},${s.hy}+k*${s.dy},true);}`);
|
||||||
|
putBar(tailX*4,(s.by-3*s.dy)*4+1);
|
||||||
|
const r=run(`(()=>{let out=false;for(let k=0;k<500;k++){simTick();if(${s.dy}<0?ents[0].iy+2<=${(s.hy-3)*4}:ents[0].iy>=${(s.hy+4)*4})out=true;}return {out,overlap:mat.some((m,i)=>occ[i] && PH[m]===PH_SOLID)};})()`);
|
||||||
|
check(`a bar crosses vertical joins at diagonal ${v}`,r.out && !r.overlap,JSON.stringify(r));
|
||||||
|
run('for(const e of ents.slice())removeEnt(e);');
|
||||||
|
rect(run,tailX*4,(s.by-3*s.dy)*4,tailX*4+3,(s.by-3*s.dy)*4+3,run('SAND'));
|
||||||
|
const grain=run(`(()=>{let out=0,leak=0;for(let k=0;k<500;k++){simTick();for(const f of flyers)if(f.m===SAND){if(${s.dy}<0?f.y<${(s.hy-3)*4}:f.y>=${(s.hy+4)*4})out++;else leak++;}flyers=[];}return {out,leak};})()`);
|
||||||
|
check(`sand crosses vertical joins at diagonal ${v}`,grain.out===16 && !grain.leak,JSON.stringify(grain));
|
||||||
|
}
|
||||||
|
for(const v of [4,5]) {
|
||||||
|
const s=line(v);
|
||||||
|
run('for(let bx=10;bx<16;bx++)placeBlock(BP_BELT,1,bx,'+s.by+',true);spawnBar(BARCU,44,'+(s.by*4-2)+',0,0);');
|
||||||
|
check(`a belt feeds a bar into diagonal ${v}`,watchBars(s).out===1);
|
||||||
|
// Both diagonal outlet grades discharge onto the joined belt in E.
|
||||||
|
const b=line(v), beltY=b.hy+b.dy;
|
||||||
|
run(`for(let k=1;k<=8;k++)placeBlock(BP_BELT,1,${b.head}+k,${beltY},true);`);
|
||||||
|
rect(run,b.bx*4,b.by*4,b.bx*4+3,b.by*4+3,run('SAND'));
|
||||||
|
const onBelt=run(`(()=>{let seen=false;for(let k=0;k<300;k++){simTick();for(let x=${(b.head+2)*4};x<${(b.head+8)*4};x++)if(mat[${beltY*4-1}*W+x]===SAND)seen=true;}return seen;})()`);
|
||||||
|
check(`diagonal ${v} unloads sand onto a belt`,onBelt);
|
||||||
|
run('for(let i=0;i<N;i++)if(mat[i]===SAND)setCell(i,EMPTY,20,0);flyers=[];');
|
||||||
|
putBar(b.bx*4,b.by*4+1);
|
||||||
|
check(`diagonal ${v} unloads a bar onto a belt`,run(`(()=>{for(let k=0;k<400;k++){simTick();if(ents[0].ix>=${(b.head+2)*4} && ents[0].iy===${beltY*4-2})return true;}return false;})()`));
|
||||||
|
}
|
||||||
|
const savedRide=line(4);
|
||||||
|
putBar(savedRide.bx*4,savedRide.by*4+1);ticks(run,24);
|
||||||
|
run('globalThis.rideSave=serializeWorld();deserializeWorld(rideSave.meta,rideSave.buffers);');
|
||||||
|
check('a bar saved mid-diagonal resumes its ride',watchBars(savedRide).out===1);
|
||||||
|
// Mouth obstruction must hold the bar, and clearing it must resume transport.
|
||||||
|
const blocked=line(4);
|
||||||
|
putBar(blocked.bx*4,blocked.by*4+1);
|
||||||
|
rect(run,(blocked.head+1)*4,(blocked.hy-1)*4,(blocked.head+2)*4-1,blocked.hy*4-1,run('STONE'));
|
||||||
|
ticks(run,300);
|
||||||
|
check('a blocked diagonal mouth holds its bar without wall overlap',run('ents.length===1 && entScrewAt(ents[0].ix,ents[0].iy) && !mat.some((m,i)=>m===STONE && occ[i])'));
|
||||||
|
rect(run,(blocked.head+1)*4,(blocked.hy-1)*4,(blocked.head+2)*4-1,blocked.hy*4-1,run('EMPTY'));
|
||||||
|
check('the diagonal bar resumes after the mouth clears',watchBars(blocked).out===1);
|
||||||
|
for(const v of [4,5,6,7]) {
|
||||||
|
fresh(); run(`placeBlock(BP_SCREW,${v},20,20,true);`);
|
||||||
|
run('globalThis.saved=serializeWorld();deserializeWorld(saved.meta,saved.buffers);');
|
||||||
|
check(`save/load keeps direction ${v} and power`,run(`blockV[20*BW+20]===${v} && part[80*W+80]===${v+7} && isPow[80*W+80]`));
|
||||||
|
run('globalThis.clip=copyRegion(80,80,83,83);bot.x=100;bot.y=80;undoRecord(\'paste\',()=>pasteGhost(clip,24,20));ghostTick();');
|
||||||
|
check(`copy/paste keeps direction ${v}`,run(`blockV[20*BW+24]===${v} && blockIntegrity(20*BW+24)===16`));
|
||||||
|
run('undoLast();');
|
||||||
|
check(`undo removes pasted diagonal ${v}`,run('blockP[20*BW+24]===0'));
|
||||||
|
run('globalThis.flipped=flipBlueprint(clip);');
|
||||||
|
check(`flip maps direction ${v}`,run('flipped.blocks[0][3]')===[7,6,5,4][v-4]);
|
||||||
|
run('undoRecord(\'cut\',()=>cutRegion(80,80,83,83));undoLast();');
|
||||||
|
check(`cut and undo restore direction ${v}`,run(`blockV[20*BW+20]===${v} && blockIntegrity(20*BW+20)===16`));
|
||||||
|
run('money=100;removeBlock(20,20);');
|
||||||
|
check(`diagonal ${v} refunds the straight screw price`,run('money')===125);
|
||||||
|
}
|
||||||
|
// Exercise the actual UI functions without starting a browser or simulating a replacement.
|
||||||
|
run(fs.readFileSync('src/ui/selection.js','utf8'));
|
||||||
|
run(fs.readFileSync('src/ui/actions.js','utf8'));
|
||||||
|
run('globalThis.GAME={pieceDir:{screw:0}};globalThis.hudDirty=false;globalThis.toolId=()=>\'build\';globalThis.piece=()=>PIECE_BY_ID.screw;globalThis.playSfx=()=>{};');
|
||||||
|
const cycle=run('(()=>{const ds=[];for(let k=0;k<8;k++){ds.push(GAME.pieceDir.screw);rotate();}return ds;})()');
|
||||||
|
check('R cycles all eight screw directions then wraps',cycle.join(',')==='0,1,2,3,4,5,6,7' && run('GAME.pieceDir.screw')===0,cycle);
|
||||||
|
for(const [dx,dy,v] of [[5,-4,4],[4,5,5],[-5,4,6],[-4,-5,7]]) {
|
||||||
|
check(`a diagonal drag picks direction ${v}`,run(`dragDirFor(PIECE_BY_ID.screw,{bx:20,by:20},{bx:${20+dx},by:${20+dy}})`)===v);
|
||||||
|
check(`direction ${v} lays a corner-touching diagonal run`,run(`blockLine(20,20,${20+dx},${20+dy},true).every(([x,y],k)=>x===20+k*${Math.sign(dx)} && y===20+k*${Math.sign(dy)})`));
|
||||||
|
}
|
||||||
|
// The drawing can bridge a joint only in unoccupied air; transport parts stay square.
|
||||||
|
for (const v of [4,5,6,7]) {
|
||||||
|
flatWorld(run,240,192,176);
|
||||||
|
const rising = v===4 || v===6;
|
||||||
|
run(`placeBlock(BP_SCREW,${v},20,20,true);placeBlock(BP_SCREW,${v},21,${rising ? 19 : 21},true);`);
|
||||||
|
const x=84, y=rising ? 80 : 83;
|
||||||
|
check(`diagonal ${v} draws over the empty corner joint`,run(`diagonalScrewBlock(${x},${y},false)>=0 && part[${y}*W+${x}]===0`));
|
||||||
|
for(const obstacle of ['mat[j]=STONE','mat[j]=SAND','part[j]=P_PIPE','mach[j]=1','occ[j]=1','blockP[(j/W>>2)*BW+(j%W>>2)]=BP_WALL']) {
|
||||||
|
check(`diagonal ${v} leaves ${obstacle} visible`,run(`(()=>{const j=${y}*W+${x};${obstacle};const hidden=diagonalScrewBlock(${x},${y},true);mat[j]=EMPTY;part[j]=0;mach[j]=0;occ[j]=0;blockP[(${y}>>2)*BW+(${x}>>2)]=0;return hidden<0;})()`));
|
||||||
|
}
|
||||||
|
check(`diagonal ${v} keeps its continuation uncapped`,run(`(diagonalScrewJoins(20,20,${v}) & 128)===0 && (diagonalScrewJoins(21,${rising ? 19 : 21},${v}) & 64)===0`));
|
||||||
|
run(`removeBlock(21,${rising ? 19 : 21});`);
|
||||||
|
check(`diagonal ${v} has no stray corner after its end`,run(`diagonalScrewBlock(${x+1},${rising ? y-1 : y+1},false)===-1`));
|
||||||
|
}
|
||||||
|
fresh();
|
||||||
|
run('placeBlock(BP_SCREW,5,20,20,true);placeBlock(BP_SCREW,1,21,21,true);');
|
||||||
|
check('shared rule: downhill low end draws a horizontal elbow in E',run('(diagonalScrewJoins(20,20,5)>>3&7)===5'));
|
||||||
|
check('shared rule: downhill low end sends its last lane into E',run('diagonalScrewNext(83*W+83,P_SCREW_DR)===87*W+84'));
|
||||||
|
done();
|
||||||
+56
-2
@@ -16,6 +16,11 @@ const med = { d: depth(), n: opened() };
|
|||||||
check('held on a stone wall, medium pulses dig a hole and drop rubble', med.d >= 6 && run('stats.dug') > 30 && count(run, run('RSTONE')) > 30, `${med.d} deep, ${med.n} opened, ${count(run, run('RSTONE'))} rubble`);
|
check('held on a stone wall, medium pulses dig a hole and drop rubble', med.d >= 6 && run('stats.dug') > 30 && count(run, run('RSTONE')) > 30, `${med.d} deep, ${med.n} opened, ${count(run, run('RSTONE'))} rubble`);
|
||||||
wall(); hold(150, 78, 1, 0, 0.3);
|
wall(); hold(150, 78, 1, 0, 0.3);
|
||||||
check('rock that is shaking but has not broken yet shows it', run('(() => { let n = 0; for (let i = 0; i < N; i++) if (digp[i] > 0 && PH[mat[i]] === PH_SOLID) n++; return n; })()') > 5);
|
check('rock that is shaking but has not broken yet shows it', run('(() => { let n = 0; for (let i = 0; i < N; i++) if (digp[i] > 0 && PH[mat[i]] === PH_SOLID) n++; return n; })()') > 5);
|
||||||
|
// A distant cursor chooses direction; the pulse still ends at its reach.
|
||||||
|
flatWorld(run, 240, 160, 100);
|
||||||
|
run('bot.x = 40; bot.y = 40; player.bitTier = 0; lastPulseTick = -99;');
|
||||||
|
check('aiming past reach fires without a distance warning', run("toolPulse(220, 42, 1, 0).status") === '');
|
||||||
|
check('a distant aim still caps the pulse at its reach', run('drillPulses[drillPulses.length - 1].range === drillReach(1)'));
|
||||||
// the widths, with a copper bit so the reach is not what stops them
|
// the widths, with a copper bit so the reach is not what stops them
|
||||||
const wall2 = () => { wall(); run('player.bitTier = 1;'); };
|
const wall2 = () => { wall(); run('player.bitTier = 1;'); };
|
||||||
wall2(); hold(150, 78, 1, 0, 2);
|
wall2(); hold(150, 78, 1, 0, 2);
|
||||||
@@ -81,10 +86,10 @@ check('and the granite stays whole', count(run, run('GRANITE')) === 116 * 40, co
|
|||||||
wall();
|
wall();
|
||||||
run('drillPulses = []; tick += 20;');
|
run('drillPulses = []; tick += 20;');
|
||||||
const far = JSON.parse(run('JSON.stringify(toolPulse(200, 78, 1, 0))'));
|
const far = JSON.parse(run('JSON.stringify(toolPulse(200, 78, 1, 0))'));
|
||||||
check('aiming past the reach says how far the bit reaches', /Out of reach/.test(far.status), far.status);
|
check('aiming past the reach fires without a warning', far.fired && !far.status, far.status);
|
||||||
run('drillPulses = []; tick += 20;');
|
run('drillPulses = []; tick += 20;');
|
||||||
const farW = JSON.parse(run('JSON.stringify(toolPulse(125, 78, 2, 0))')), nearM = JSON.parse(run('tick += 20; JSON.stringify(toolPulse(125, 78, 1, 0))'));
|
const farW = JSON.parse(run('JSON.stringify(toolPulse(125, 78, 2, 0))')), nearM = JSON.parse(run('tick += 20; JSON.stringify(toolPulse(125, 78, 1, 0))'));
|
||||||
check('a spot a medium pulse reaches is out of reach for a wide one, and it says so', /Out of reach/.test(farW.status) && /wide/.test(farW.status) && !nearM.status, `${farW.status} | ${nearM.status}`);
|
check('aiming beyond a wide pulse also fires without a warning', farW.fired && !farW.status && nearM.fired && !nearM.status);
|
||||||
|
|
||||||
// stray bits: a lump of rock left hanging when the rock round it breaks falls apart
|
// stray bits: a lump of rock left hanging when the rock round it breaks falls apart
|
||||||
const hanging = (m) => {
|
const hanging = (m) => {
|
||||||
@@ -134,4 +139,53 @@ run('for (let y = 80; y < 100; y++) for (let x = 120; x < 122; x++) bgm[y * W +
|
|||||||
hold(121, 92, 1, 0, 3);
|
hold(121, 92, 1, 0, 3);
|
||||||
check('a tree trunk in the path is cut into wood chunks', count(run, run('LWOOD')) > 0, count(run, run('LWOOD')));
|
check('a tree trunk in the path is cut into wood chunks', count(run, run('LWOOD')) > 0, count(run, run('LWOOD')));
|
||||||
|
|
||||||
|
// Melts that cool loose in the world (magma from the bag, metal spilled outside a mold) are
|
||||||
|
// ordinary solids the drill can break; built walls, slope corner cells and molds stay protected.
|
||||||
|
flatWorld(run, 160, 128, 120);
|
||||||
|
const melt = run(`(() => {
|
||||||
|
player.bitTier = BIT_TIERS.length - 1; bot.x = 60; bot.y = 60;
|
||||||
|
for (let y = 80; y < 88; y++) for (let x = 60; x < 72; x++) setCell(y * W + x, MAGMA, 1250, 0);
|
||||||
|
for (let k = 0; k < 2400; k++) simTick();
|
||||||
|
for (let x = 90; x < 96; x++) setCell(110 * W + x, COPPER, 20, F_CAST); // metal that set outside a mold
|
||||||
|
let bas = 0, nat = 0, cu = 0, drillable = 0, cuDrill = 0;
|
||||||
|
for (let i = 0; i < N; i++) {
|
||||||
|
if (mat[i] === BASALT) { bas++; if (flags[i] & F_NAT) nat++; if (crumbly(i)) drillable++; }
|
||||||
|
if (mat[i] === COPPER) { cu++; if (crumbly(i)) cuDrill++; }
|
||||||
|
}
|
||||||
|
return { bas, nat, drillable, cu, cuDrill };
|
||||||
|
})()`);
|
||||||
|
check('magma from the bag cools into natural basalt', melt.bas > 0 && melt.nat === melt.bas, JSON.stringify(melt));
|
||||||
|
check('that basalt can be drilled', melt.drillable === melt.bas, JSON.stringify(melt));
|
||||||
|
check('copper that set outside a mold can be drilled for scrap', melt.cu > 0 && melt.cuDrill === melt.cu, JSON.stringify(melt));
|
||||||
|
check('basalt already stuck without the natural mark can be drilled too',
|
||||||
|
run('(() => { setCell(20 * W + 20, BASALT, 20, 0); player.bitTier = BIT_TIERS.length - 1; return crumbly(20 * W + 20); })()'));
|
||||||
|
check('built walls and slope corner cells stay safe from the drill', run(`(() => {
|
||||||
|
placeBlock(BP_WALL, RSTONE, 5, 5, true); placeBlock(BP_SLOPE_BL, RSTONE, 8, 5, true);
|
||||||
|
const wallCell = 5 * BS * W + 5 * BS + 1; let seal = -1;
|
||||||
|
for (let i = 0; i < N; i++) if (slopeSealOwner(i) >= 0 && PH[mat[i]] === PH_SOLID) { seal = i; break; }
|
||||||
|
return !crumbly(wallCell) && (seal < 0 || !crumbly(seal));
|
||||||
|
})()`));
|
||||||
|
check('a mold and what sets inside it stay safe from the drill', run(`(() => {
|
||||||
|
bag[RSTONE] = 400; bagTotal = 400; const err = placeMoldAt('ingot', 30, 10, RSTONE);
|
||||||
|
const mo = molds[molds.length - 1]; if (err || !mo) return false;
|
||||||
|
for (const i of moldCellsAt(mo.kind, mo.x, mo.y).walls) if (crumbly(i)) return false;
|
||||||
|
return true;
|
||||||
|
})()`));
|
||||||
|
// Resonance ping (t50): finds natural veins within reach of the bit, under fog too, and
|
||||||
|
// nothing else.
|
||||||
|
flatWorld(run, 240, 160, 150);
|
||||||
|
const ping = run(`(() => {
|
||||||
|
player.bitTier = 0; const cx = 100, cy = 100, r = pingRadius();
|
||||||
|
setCell(cy * W + cx + 10, CUV, 20, F_NAT); setCell(cy * W + cx - 20, FEV, 20, F_NAT);
|
||||||
|
setCell(cy * W + cx + r + 5, AUV, 20, F_NAT); // out of reach
|
||||||
|
setCell((cy + 5) * W + cx, CUV, 20, 0); // not natural (set loose)
|
||||||
|
setCell((cy - 5) * W + cx, STONE, 20, F_NAT); // not a vein
|
||||||
|
const p = resonancePing(cx, cy);
|
||||||
|
player.bitTier = BIT_TIERS.length - 1; const far = resonancePing(cx, cy);
|
||||||
|
return { n: p.cells.length, found: p.found.map(m => MD[m].name), r, farFound: far.found.map(m => MD[m].name), farR: far.r };
|
||||||
|
})()`);
|
||||||
|
check('a ping finds natural veins in reach', ping.n === 2 && ping.found.includes('Copper ore vein') && ping.found.includes('Iron ore vein'), JSON.stringify(ping));
|
||||||
|
check('it skips rock, loose ore and veins out of reach', !ping.found.includes('Gold ore vein') && ping.n === 2, JSON.stringify(ping));
|
||||||
|
check('a better bit pings farther', ping.farR > ping.r && ping.farFound.includes('Gold ore vein'), JSON.stringify(ping));
|
||||||
|
check('a ping never changes the world', run('(() => { const h0 = mat.reduce((a, m, i) => (a * 31 + m + i) | 0, 0); resonancePing(100, 100); return h0 === mat.reduce((a, m, i) => (a * 31 + m + i) | 0, 0); })()'));
|
||||||
done();
|
done();
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Throughput overlay (t49): grains per second per transport block, counted only while the
|
||||||
|
// overlay is on, never changing what the simulation does.
|
||||||
|
const { load, suite, flatWorld, rect } = require('./harness');
|
||||||
|
const { run } = load();
|
||||||
|
const { check, done } = suite();
|
||||||
|
// A flat belt line fed from a sand heap, a screw line going right, and an uphill belt.
|
||||||
|
function scene() {
|
||||||
|
flatWorld(run, 240, 160, 140);
|
||||||
|
run('RS = 7; nextPowerBuild = 0; bot.x = 120; bot.y = 60;');
|
||||||
|
run('for (let bx = 10; bx < 20; bx++) placeBlock(BP_BELT, 1, bx, 30, true);');
|
||||||
|
rect(run, 40, 100, 47, 119, run('SAND'));
|
||||||
|
run('for (let bx = 30; bx < 38; bx++) placeBlock(BP_SCREW, 1, bx, 30, true);');
|
||||||
|
rect(run, 120, 110, 123, 119, run('SAND'));
|
||||||
|
run('for (let k = 0; k < 6; k++) placeBlock(BP_BELT_UP, 1, 44 + k, 30 - k, true); placeBlock(BP_BELT, 1, 43, 31, true);');
|
||||||
|
rect(run, 172, 112, 175, 123, run('SAND'));
|
||||||
|
}
|
||||||
|
const hash = () => run('(() => { let h = 0; for (let i = 0; i < N; i++) h = (h * 31 + mat[i]) | 0; return h; })()');
|
||||||
|
scene(); run('setFlowCounting(false); for (let k = 0; k < 300; k++) simTick();');
|
||||||
|
const off = hash();
|
||||||
|
scene(); run('setFlowCounting(true); for (let k = 0; k < 300; k++) simTick();');
|
||||||
|
check('counting changes nothing in the simulation', hash() === off);
|
||||||
|
const r = run('({ belt: flowRate[30 * BW + 15], beltEnd: flowRate[30 * BW + 19], screw: flowRate[30 * BW + 34], uphill: Math.max(...[0,1,2,3,4,5].map(k => flowRate[(30 - k) * BW + 44 + k])) })');
|
||||||
|
check('a loaded flat belt reports grains per second', r.belt > 0, JSON.stringify(r));
|
||||||
|
check('a loaded screw conveyor reports grains per second', r.screw > 0, JSON.stringify(r));
|
||||||
|
check('an uphill belt carrying sand reports grains per second', r.uphill > 0, JSON.stringify(r));
|
||||||
|
check('the rate is grains per second, not per tick (a flat belt moves well under 600)', r.belt < 600, JSON.stringify(r));
|
||||||
|
scene(); run('setFlowCounting(false); for (let k = 0; k < 120; k++) simTick();');
|
||||||
|
check('with the overlay off nothing is counted', run('flowCount.every(n => n === 0) && flowRate.every(n => n === 0)'));
|
||||||
|
done();
|
||||||
+206
@@ -0,0 +1,206 @@
|
|||||||
|
// The oracle states the geometric rule independently of the precomputed neighbour tables.
|
||||||
|
const { load, suite, flatWorld } = require('./harness');
|
||||||
|
const { run } = load();
|
||||||
|
const { check, done } = suite();
|
||||||
|
flatWorld(run, 96, 96, 92);
|
||||||
|
run(`
|
||||||
|
var joinCases = [];
|
||||||
|
for (let v = 0; v < 8; v++) joinCases.push([BP_SCREW, v]);
|
||||||
|
for (const c of [BP_BELT, BP_FILTER, BP_BELT_UP, BP_BELT_DOWN, BP_FILTER_UP, BP_FILTER_DOWN])
|
||||||
|
for (const v of c === BP_FILTER || isFilter(c) ? [0, 127, 128, 255] : [0, 1]) joinCases.push([c, v]);
|
||||||
|
`);
|
||||||
|
const cases = run('joinCases');
|
||||||
|
const screw = run('BP_SCREW');
|
||||||
|
const dirs = run('SCREW_DIRS');
|
||||||
|
function describe([code, v]) {
|
||||||
|
if(code===screw)return {family:'screw',x:dirs[v][0],y:dirs[v][1],diagonal:!!(dirs[v][0]&&dirs[v][1])};
|
||||||
|
const x=run(`isFilter(${code})`)?(v&128?1:-1):(v&1?1:-1),y=run(`beltGrade(${code})`)*x;
|
||||||
|
return {family:'belt',x,y,diagonal:!!y};
|
||||||
|
}
|
||||||
|
function directed(a,b,dx,dy) {
|
||||||
|
if(a.x*b.x+a.y*b.y<=0)return false;
|
||||||
|
if(a.family!==b.family&&((!a.x||!b.x)||(a.diagonal&&b.diagonal)))return false;
|
||||||
|
if(a.diagonal&&b.diagonal&&(a.x!==b.x||a.y!==b.y))return false;
|
||||||
|
return dx===a.x&&(dy===(a.family==='belt'&&a.y<0?0:a.y)||
|
||||||
|
(a.family==='belt'&&b.family==='belt'&&!a.y&&b.y<0&&dy===-1));
|
||||||
|
}
|
||||||
|
function oracle(a,b,dx,dy,ae,be) {
|
||||||
|
const ao=(a.x||a.y)>0?1:0,bo=(b.x||b.y)>0?1:0;
|
||||||
|
return ae===ao&&be!==bo?directed(a,b,dx,dy):ae!==ao&&be===bo&&directed(b,a,-dx,-dy);
|
||||||
|
}
|
||||||
|
let tableErrors = [], symmetryErrors = [], pairCount = 0;
|
||||||
|
for (const ac of cases) for (const bc of cases) {
|
||||||
|
const a = describe(ac), b = describe(bc);
|
||||||
|
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
|
||||||
|
if (!dx && !dy) continue;
|
||||||
|
const result = run(`(() => {
|
||||||
|
blockP.fill(0); blockV.fill(0);
|
||||||
|
const a = 10 * BW + 10, b = (10 + ${dy}) * BW + 10 + ${dx};
|
||||||
|
blockP[a] = ${ac[0]}; blockV[a] = ${ac[1]}; blockP[b] = ${bc[0]}; blockV[b] = ${bc[1]};
|
||||||
|
return [a,b,endJoin(10,10,0),endJoin(10,10,1),endJoin(10+${dx},10+${dy},0),endJoin(10+${dx},10+${dy},1),pieceJoins(10,10)];
|
||||||
|
})()`);
|
||||||
|
let mask = 0;
|
||||||
|
for (let ae = 0; ae < 2; ae++) {
|
||||||
|
let expected = -1;
|
||||||
|
for (let be = 0; be < 2; be++) if (oracle(a, b, dx, dy, ae, be)) expected = result[1] * 2 + be;
|
||||||
|
if (expected >= 0) mask |= 1 << ae;
|
||||||
|
if (result[2 + ae] !== expected) tableErrors.push([ac,bc,dx,dy,ae,expected,result[2+ae]]);
|
||||||
|
for (let be = 0; be < 2; be++)
|
||||||
|
if ((result[2+ae] === result[1]*2+be) !== (result[4+be] === result[0]*2+ae)) symmetryErrors.push([ac,bc,dx,dy,ae,be]);
|
||||||
|
}
|
||||||
|
if (mask !== result[6]) tableErrors.push(['mask', ac,bc,dx,dy]);
|
||||||
|
pairCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check('every kind and travel direction matches the rule throughout the 3 by 3 neighbourhood', !tableErrors.length, `${pairCount} pairs; ${JSON.stringify(tableErrors.slice(0,3))}`);
|
||||||
|
check('every joined pair names the reciprocal end', !symmetryErrors.length, JSON.stringify(symmetryErrors.slice(0,3)));
|
||||||
|
function pair(label, a, b, dx, dy, expected) {
|
||||||
|
check(label, run(`(() => {
|
||||||
|
blockP.fill(0); blockV.fill(0);
|
||||||
|
blockP[10*BW+10]=${a[0]}; blockV[10*BW+10]=${a[1]};
|
||||||
|
blockP[(10+${dy})*BW+10+${dx}]=${b[0]}; blockV[(10+${dy})*BW+10+${dx}]=${b[1]};
|
||||||
|
return pieceJoins(10,10)!==0;
|
||||||
|
})()`) === expected);
|
||||||
|
}
|
||||||
|
const ids = run('[BP_SCREW,BP_BELT,BP_FILTER,BP_BELT_UP,BP_BELT_DOWN]');
|
||||||
|
for(const [label,av,bv,dx,dy,yes] of [
|
||||||
|
['H(right) → DR next same row',1,5,1,0,true],['DR → H(right) diagonally past',5,1,1,1,true],
|
||||||
|
['H(right) → UR next same row',1,4,1,0,true],['UR → H(right) diagonally past',4,1,1,-1,true],
|
||||||
|
['V(up) → UR directly above',0,4,0,-1,true],['V(up) → UL directly above',0,7,0,-1,true],
|
||||||
|
['UR → V(up) diagonally past',4,0,1,-1,true],['DR → V(down) diagonally past',5,2,1,1,true],
|
||||||
|
['DR → H same row stays free',5,1,1,0,false],['H → DR one right and lower stays free',1,5,1,1,false],
|
||||||
|
['V(up) → UR diagonally above-right stays free',0,4,1,-1,false]
|
||||||
|
])pair(label,[screw,av],[screw,bv],dx,dy,yes);
|
||||||
|
pair('uphill belt outlet stays on its row',[ids[3],1],[ids[1],1],1,0,true);
|
||||||
|
pair('uphill slope outlet also targets a slope on the same row',[ids[3],1],[ids[3],1],1,0,true);
|
||||||
|
pair('uphill slope leaves a diagonally adjacent slope free',[ids[3],1],[ids[3],1],1,-1,false);
|
||||||
|
pair('flat belt feeds uphill low end on same row',[ids[1],1],[ids[3],1],1,0,true);
|
||||||
|
pair('flat belt also feeds uphill low end from below',[ids[1],1],[ids[3],1],1,-1,true);
|
||||||
|
check('planned source, neighbour, direction and empty entries override the world', run(`(() => {
|
||||||
|
blockP.fill(0); blockV.fill(0);
|
||||||
|
const a=10*BW+10,b=a+1, p=new Map([[a,[BP_BELT,1]],[b,[BP_SCREW,1,'extra']]]);
|
||||||
|
blockP[a]=BP_WALL; blockP[b]=BP_SCREW; blockV[b]=0;
|
||||||
|
if(endJoin(10,10,1,p)!==b*2 || endJoin(11,10,0,p)!==a*2+1) return false;
|
||||||
|
p.set(b,[0,0]);
|
||||||
|
return endJoin(10,10,1,p)===-1 && endJoin(10,10,1)===-1;
|
||||||
|
})()`));
|
||||||
|
check('non-transport pieces, invalid ends and world edges are open without row wrapping', run(`(() => {
|
||||||
|
blockP.fill(BP_BELT); blockV.fill(0);
|
||||||
|
return pieceEnds(BP_WALL,0).length===0 && endJoin(-1,0,0)===-1 && endJoin(BW,0,0)===-1 &&
|
||||||
|
endJoin(0,-1,0)===-1 && endJoin(0,BH,0)===-1 && endJoin(0,0,0)===-1 &&
|
||||||
|
endJoin(BW-1,BH-1,1)===-1 && endJoin(1,1,2)===-1;
|
||||||
|
})()`));
|
||||||
|
check('same-shape inlet continuation beats an elbow',run(`(()=>{
|
||||||
|
blockP.fill(0);blockV.fill(0);const b=10*BW+10;
|
||||||
|
for(const [n,v] of [[b,4],[b+BW-1,4],[b-1,1]]){blockP[n]=BP_SCREW;blockV[n]=v;}
|
||||||
|
return endJoin(10,10,0)===(b+BW-1)*2+1&&endJoin(9,10,1)===-1;
|
||||||
|
})()`));
|
||||||
|
check('equal-shape inlet ties use top to bottom then left to right',run(`(()=>{
|
||||||
|
blockP.fill(0);blockV.fill(0);const b=10*BW+10;
|
||||||
|
for(const [n,v] of [[b,1],[b-BW-1,5],[b+BW-1,4]]){blockP[n]=BP_SCREW;blockV[n]=v;}
|
||||||
|
return endJoin(10,10,0)===(b-BW-1)*2+1&&endJoin(9,11,1)===-1;
|
||||||
|
})()`));
|
||||||
|
const fs=require('fs');
|
||||||
|
run(fs.readFileSync('src/render/slope-belts.js','utf8'));
|
||||||
|
const setup=fs.readFileSync('src/render/setup.js','utf8');
|
||||||
|
run(setup.slice(setup.indexOf('function beltLike('),setup.indexOf('\n',setup.indexOf('function beltLike('))));
|
||||||
|
check('filter frames open at an uphill low-end feed and compatible screws',run(`(()=>{
|
||||||
|
blockP.fill(0);blockV.fill(0);const b=10*BW+10;
|
||||||
|
blockP[b]=BP_FILTER;blockV[b]=255;blockP[b+1]=BP_BELT_UP;blockV[b+1]=1;
|
||||||
|
if(!beltLike(b,1))return false;
|
||||||
|
blockP[b+1]=BP_SCREW;blockV[b+1]=1;if(!beltLike(b,1))return false;
|
||||||
|
blockV[b+1]=3;return !beltLike(b,1);
|
||||||
|
})()`));
|
||||||
|
check('planned flat caps and slope continuations use the shared ends',run(`(()=>{
|
||||||
|
blockP.fill(0);blockV.fill(0);const b=10*BW+10;
|
||||||
|
const p=new Map([[b,[BP_BELT_DOWN,1]],[b-1,[BP_FILTER,255]],[b+BW+1,[BP_FILTER_DOWN,128]]]);
|
||||||
|
const before=JSON.stringify([Array.from(blockP),Array.from(blockV)]);
|
||||||
|
if(slopeBeltConnections(10,10,BP_BELT_DOWN,p)!==9||flatBeltCaps(b-1,p)!==1)return false;
|
||||||
|
p.set(b,[0,0]);return flatBeltCaps(b-1,p)===3&&before===JSON.stringify([Array.from(blockP),Array.from(blockV)]);
|
||||||
|
})()`));
|
||||||
|
check('a flat belt picks the same-row uphill inlet before the below-low-end exception',run(`(()=>{
|
||||||
|
blockP.fill(0);blockV.fill(0);const b=10*BW+10;
|
||||||
|
for(const n of [b,b+1,b-BW+1]){blockP[n]=n===b?BP_BELT:BP_BELT_UP;blockV[n]=1;}
|
||||||
|
return endJoin(10,10,1)===(b+1)*2&&endJoin(11,9,0)===-1;
|
||||||
|
})()`));
|
||||||
|
check('same-row uphill low end draws as two capped ends in placed and planned geometry',run(`(()=>{
|
||||||
|
blockP.fill(0);blockV.fill(0);const b=10*BW+10;
|
||||||
|
const p=new Map([[b,[BP_BELT_UP,1]],[b-1,[BP_BELT,1]]]);
|
||||||
|
const j=slopeBeltConnections(10,10,BP_BELT_UP,p);
|
||||||
|
return j===0&&endJoin(10,10,0,p)>=0&&flatBeltCaps(b-1,p)===3;
|
||||||
|
})()`));
|
||||||
|
check('uphill slopes side by side on one row are joined but drawn as two capped ends',run(`(()=>{
|
||||||
|
blockP.fill(0);blockV.fill(0);const b=10*BW+10,p=new Map([[b,[BP_BELT_UP,1]],[b+1,[BP_BELT_UP,1]]]);
|
||||||
|
return endJoin(10,10,1,p)>=0&&slopeBeltConnections(10,10,BP_BELT_UP,p)===0&&slopeBeltConnections(11,10,BP_BELT_UP,p)===0;
|
||||||
|
})()`));
|
||||||
|
// Downhill face-to-face transfers (screens always travel downhill when placed).
|
||||||
|
for(const grade of [-1,1])for(const filter of [false,true])for(const outgoing of [false,true])for(const joined of [false,true]) {
|
||||||
|
flatWorld(run,96,96,92);
|
||||||
|
const d=grade,c=filter?(grade<0?'BP_FILTER_UP':'BP_FILTER_DOWN'):(grade<0?'BP_BELT_UP':'BP_BELT_DOWN');
|
||||||
|
const r=run(`(()=>{
|
||||||
|
const d=${d},c=${c},out=${outgoing},bx=10,by=10;
|
||||||
|
placeBlock(c,${filter?(d>0?128:0):(d>0?1:0)},bx,by,true);
|
||||||
|
const fx=bx+(out?d:-d),fy=by+(out?1:0)+(${joined}?0:out?-1:1);
|
||||||
|
placeBlock(BP_BELT,d>0?1:0,fx,fy,true);
|
||||||
|
const sx=bx*BS+(d>0?3:0),ix=bx*BS+(d>0?0:3),x=out?sx:ix-d;
|
||||||
|
const y=out?beltFaceY(c,bx,by,sx):fy*BS;
|
||||||
|
const targetY=out?fy*BS:beltFaceY(c,bx,by,ix);
|
||||||
|
setCell((y-1)*W+x,SAND,20,0);tickStamp++;
|
||||||
|
carryColumn((y-1)*W+x,d,null);
|
||||||
|
return [endJoin(bx,by,out?(d>0?1:0):(d>0?0:1))>=0,mat[(targetY-1)*W+x+d]===SAND];
|
||||||
|
})()`);
|
||||||
|
check(`grade ${grade} ${filter?'screen':'belt'} ${outgoing?'outlet':'inlet'} ${joined?'joined':'free'} sand handoff agrees`,r[0]===joined&&r[1]===joined,r);
|
||||||
|
}
|
||||||
|
// The same-row low-end feed drops onto the face before the uphill cleats carry it.
|
||||||
|
for(const grade of [-1,1])for(const below of [false,true]) {
|
||||||
|
flatWorld(run,160,128,120);
|
||||||
|
const d=-grade,code=grade<0?'BP_BELT_UP':'BP_BELT_DOWN';
|
||||||
|
const r=run(`(()=>{
|
||||||
|
const d=${d},c=${code},v=${d>0?1:0},bx=20,by=20,fy=by+${below?1:0};
|
||||||
|
placeBlock(BP_BELT,d>0?1:0,bx-d,fy,true);
|
||||||
|
for(let k=0;k<4;k++)placeBlock(c,v,bx+k*d,by-k,true);
|
||||||
|
rebuildPower();const source=fy*BS-1,x=(bx-d)*BS+(d>0?3:0);
|
||||||
|
setCell(source*W+x,SAND,20,0);
|
||||||
|
let seen=false;
|
||||||
|
for(let k=0;k<320;k++) {simTick();for(let i=0;i<N;i++)if(mat[i]===SAND&&((i/W|0)<by*BS-4))seen=true;}
|
||||||
|
return [endJoin(bx-d,fy,d>0?1:0)>=0,seen];
|
||||||
|
})()`);
|
||||||
|
check(`grade ${grade} belt low end accepts sand from ${below?'below':'same row'} and carries it up`,r[0]&&r[1],r);
|
||||||
|
}
|
||||||
|
// Snapping (t67 phase 4): a drag moves one block up or down only to make a seamless join
|
||||||
|
// involving a diagonal piece, and only when every block there is free.
|
||||||
|
const snap = (setup, code, v, cells) => { flatWorld(run, 96, 96, 92); return run(`(()=>{${setup};return joinSnap(${code},${v},${JSON.stringify(cells)});})()`); };
|
||||||
|
check('an uphill belt started beside a flat belt on the same level snaps up one to join seamlessly',
|
||||||
|
snap('placeBlock(BP_BELT,1,10,10,true)', 'BP_BELT_UP', 1, [[11,10],[12,9],[13,8]]) === -1);
|
||||||
|
check('the snapped uphill belt then joins the flat belt',
|
||||||
|
run('(()=>{placeBlocks(BP_BELT_UP,1,[[11,9],[12,8],[13,7]]);return endJoin(10,10,1)===(9*BW+11)*2+0||endJoin(11,9,0)>=0;})()'));
|
||||||
|
check('an uphill belt already one block up stays put',
|
||||||
|
snap('placeBlock(BP_BELT,1,10,10,true)', 'BP_BELT_UP', 1, [[11,9],[12,8]]) === 0);
|
||||||
|
check('no snap when a block above is taken; the same-row drop join stays',
|
||||||
|
snap('placeBlock(BP_BELT,1,10,10,true);placeBlock(BP_WALL,RSTONE,12,8,true)', 'BP_BELT_UP', 1, [[11,10],[12,9]]) === 0);
|
||||||
|
check('a flat belt beside another flat belt one row off never jumps',
|
||||||
|
snap('placeBlock(BP_BELT,1,10,10,true)', 'BP_BELT', 1, [[11,11],[12,11],[13,11]]) === 0);
|
||||||
|
check('a flat belt started level with a downhill belt low end snaps down one (t58)',
|
||||||
|
snap('placeBlock(BP_BELT_DOWN,1,10,10,true);placeBlock(BP_BELT_DOWN,1,11,11,true)', 'BP_BELT', 1, [[12,11],[13,11]]) === 1);
|
||||||
|
check('a downhill screw started one block right and one lower than a screw end snaps up to the same row',
|
||||||
|
snap('placeBlock(BP_SCREW,1,10,10,true)', 'BP_SCREW', 5, [[11,11],[12,12]]) === -1);
|
||||||
|
check('a screw that already joins stays put',
|
||||||
|
snap('placeBlock(BP_SCREW,1,10,10,true)', 'BP_SCREW', 5, [[11,10],[12,11]]) === 0);
|
||||||
|
check('pieces with no join ends never snap', snap('', 'BP_PIPE', 0, [[11,10]]) === 0);
|
||||||
|
// "Doesn't connect" marks (t67 phase 4): nearMiss says which way one block would make the join.
|
||||||
|
const miss = (setup, expr) => { flatWorld(run, 96, 96, 92); return run(`(()=>{${setup};return ${expr};})()`); };
|
||||||
|
check('a flat belt level with a downhill belt low end is marked: place it one lower (t58)',
|
||||||
|
miss('placeBlock(BP_BELT_DOWN,1,10,10,true);placeBlock(BP_BELT_DOWN,1,11,11,true);placeBlock(BP_BELT,1,12,11,true)', 'nearMiss(12,11,0)') === 1);
|
||||||
|
check('a flat belt one lower at that low end is joined and unmarked',
|
||||||
|
miss('placeBlock(BP_BELT_DOWN,1,10,10,true);placeBlock(BP_BELT_DOWN,1,11,11,true);placeBlock(BP_BELT,1,12,12,true)', 'endJoin(12,12,0)>=0 && nearMiss(12,12,0)===0 && nearMiss(11,11,1)===0'));
|
||||||
|
check('a screw end diagonally off a downhill screw start is marked on both pieces',
|
||||||
|
miss('placeBlock(BP_SCREW,1,10,10,true);placeBlock(BP_SCREW,5,11,11,true)', '[nearMiss(10,10,1),nearMiss(11,11,0)].join()') === '1,-1');
|
||||||
|
check('a drop join (flat feeding an uphill low end on its row) counts as connected',
|
||||||
|
miss('placeBlock(BP_BELT,1,10,10,true);placeBlock(BP_BELT_UP,1,11,10,true)', 'nearMiss(10,10,1)===0 && nearMiss(11,10,0)===0'));
|
||||||
|
check('straight belts one row apart are never marked',
|
||||||
|
miss('placeBlock(BP_BELT,1,10,10,true);placeBlock(BP_BELT,1,11,11,true)', 'nearMiss(10,10,1)===0 && nearMiss(11,11,0)===0'));
|
||||||
|
check('no mark when the block that would fix it is taken',
|
||||||
|
miss('placeBlock(BP_BELT_DOWN,1,10,10,true);placeBlock(BP_BELT_DOWN,1,11,11,true);placeBlock(BP_BELT,1,12,11,true);placeBlock(BP_WALL,RSTONE,12,12,true)', 'nearMiss(12,11,0)') === 0);
|
||||||
|
check('ghosts are checked through their planned map without copying it',
|
||||||
|
miss('ghostBlocks.set(10*BW+10,[BP_SCREW,1]);ghostBlocks.set(11*BW+11,[BP_SCREW,5])', 'nearMiss(10,10,1,ghostBlocks)===1 && ghostBlocks.size===2'));
|
||||||
|
done();
|
||||||
+146
@@ -0,0 +1,146 @@
|
|||||||
|
// Run the renderer's lighting against real simulation data, with only canvas storage stubbed.
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { load, suite, flatWorld } = require('./harness');
|
||||||
|
const { run } = load();
|
||||||
|
const { check, done } = suite();
|
||||||
|
for (const f of ['setup.js', 'light.js']) run(fs.readFileSync(path.join(__dirname, '../src/render', f), 'utf8'));
|
||||||
|
run(`
|
||||||
|
const SET = { lighting: 'full', daylight: 1, ambient: 0.12 };
|
||||||
|
R.lightC = {};
|
||||||
|
R.lightX = { createImageData: (w, h) => ({ data: new Uint8ClampedArray(w * h * 4) }), putImageData() {} };
|
||||||
|
R.vx0 = 0; R.vy0 = 100; R.bw = 320; R.bh = 80;
|
||||||
|
function lightAt(a, x, y) { return a[(Math.floor(y / R.lres) - R.ly0) * R.lw + Math.floor(x / R.lres) - R.lx0]; }
|
||||||
|
`);
|
||||||
|
flatWorld(run, 320, 200, 160);
|
||||||
|
run(`
|
||||||
|
bot.lamp = false;
|
||||||
|
for (let bx = 8; bx < 30; bx++) placeBlock(BP_WALL, RSTONE, bx, 20, true);
|
||||||
|
for (let by = 21; by < 39; by++) {
|
||||||
|
placeBlock(BP_WALL, RSTONE, 8, by, true);
|
||||||
|
placeBlock(BP_WALL, RSTONE, 29, by, true);
|
||||||
|
}
|
||||||
|
placeBlock(BP_BELT, 1, 12, 30, true);
|
||||||
|
placeBlock(BP_SCREW, 0, 14, 30, true);
|
||||||
|
addMachine('silo', 18, 30, 0);
|
||||||
|
addMold('bit', 24, 30, RSTONE, true);
|
||||||
|
for (let y = 80; y < 88; y++) for (let x = 180; x < 280; x++) setCell(y * W + x, STONE, AMB[y], F_NAT);
|
||||||
|
updateRoof();
|
||||||
|
const simRoof = Array.from(roofY);
|
||||||
|
`);
|
||||||
|
for (const mode of ['full', 'fast']) {
|
||||||
|
run(`SET.lighting = '${mode}'; computeLight();`);
|
||||||
|
for (const channel of ['lr', 'lg']) {
|
||||||
|
const [inside, open, rock, ground, openGround] = run(`[lightAt(R.${channel}, 80, 144), lightAt(R.${channel}, 152, 144), lightAt(R.${channel}, 232, 144), lightAt(R.${channel}, 80, 160), lightAt(R.${channel}, 152, 160)]`);
|
||||||
|
check(`${mode} ${channel}: under a built roof matches open daylight`, Math.abs(inside / open - 1) < 0.03, `${inside} / ${open}`);
|
||||||
|
check(`${mode} ${channel}: ground under the base matches outside`, Math.abs(ground / openGround - 1) < 0.03, `${ground} / ${openGround}`);
|
||||||
|
check(`${mode} ${channel}: natural overhang still shades`, rock < open * 0.95, `${rock} / ${open}`);
|
||||||
|
}
|
||||||
|
// Directly check transmission as daylight can hide an opaque piece in the open.
|
||||||
|
for (const [name, x, y] of [['wall', 32, 120], ['belt', 48, 120], ['screw', 56, 120], ['machine', 72, 120], ['mold', 96, 120]]) {
|
||||||
|
const a = run(`lightAt(R.latt, ${x}, ${y})`);
|
||||||
|
check(`${mode}: ${name} transmits like air`, Math.abs(a - Math.pow(0.978, mode === 'fast' ? 4 : 2)) < 1e-6, a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check('lighting leaves the simulation roof unchanged', run('simRoof.every((y, x) => roofY[x] === y) && roofY[80] === 80'));
|
||||||
|
for (const material of ['STONE', 'SAND', 'WATER']) {
|
||||||
|
run(`setCell(96 * W + 80, ${material}, AMB[96], F_NAT); setCell(97 * W + 80, STONE, AMB[97], F_NAT); updateRoof(); computeLight();`);
|
||||||
|
check(`${material} above the view still blocks daylight`, run('lightAt(R.lr, 80, 144) < 0.95'));
|
||||||
|
run('setCell(96 * W + 80, EMPTY, AMB[96], 0); setCell(97 * W + 80, EMPTY, AMB[97], 0);');
|
||||||
|
}
|
||||||
|
run('updateRoof(); computeLight();');
|
||||||
|
check('removing an obstruction restores daylight', run('lightAt(R.lr, 80, 144) === 1'));
|
||||||
|
for (const field of ['blockP[24 * BW + 20]', 'mach[96 * W + 80]', 'part[96 * W + 80]']) {
|
||||||
|
run(`setCell(96 * W + 80, STONE, AMB[96], F_NAT); ${field} = 1; updateRoof(); computeLight();`);
|
||||||
|
check(`${field}: built contents do not shade or absorb light`, run('lightAt(R.lr, 80, 144) === 1 && Math.abs(lightAt(R.latt, 80, 96) - Math.pow(0.978, 4)) < 1e-6'));
|
||||||
|
run(`${field} = 0; setCell(96 * W + 80, EMPTY, AMB[96], 0);`);
|
||||||
|
}
|
||||||
|
run('updateRoof();');
|
||||||
|
run(`SET.daylight = 0; temp[140 * W + 80] = 1500; computeLight();`);
|
||||||
|
check('hot material inside the base lights its surroundings', run('lightAt(R.lr, 88, 140) > 0.2'));
|
||||||
|
run(`temp[140 * W + 80] = AMB[140]; bot.x = 72; bot.y = 130; bot.lamp = true; computeLight();`);
|
||||||
|
check('the bot lamp still lights the base', run('lightAt(R.lr, 80, 140) > 0.2'));
|
||||||
|
|
||||||
|
flatWorld(run, 320, 200, 160);
|
||||||
|
run(`
|
||||||
|
SET.daylight = 1; bot.lamp = false;
|
||||||
|
for (let bx = 8; bx < 30; bx++) placeBlock(BP_BELT, 1, bx, 20, true);
|
||||||
|
for (let x = 32; x < 120; x++) for (let y = 77; y < 80; y++) setCell(y * W + x, SAND, AMB[y], 0);
|
||||||
|
for (let x = 180; x < 280; x++) {
|
||||||
|
setCell(80 * W + x, STONE, AMB[80], F_NAT);
|
||||||
|
for (let y = 77; y < 80; y++) setCell(y * W + x, SAND, AMB[y], 0);
|
||||||
|
}
|
||||||
|
for (let y = 40; y <= 76; y += 2) setCell(y * W + 152, SAND, AMB[y], 0);
|
||||||
|
updateRoof();
|
||||||
|
`);
|
||||||
|
for (const mode of ['full', 'fast']) {
|
||||||
|
run(`SET.lighting = '${mode}'; computeLight();`);
|
||||||
|
for (const channel of ['lr', 'lg', 'lb']) {
|
||||||
|
const [belt, open, stone] = run(`[lightAt(R.${channel}, 80, 84), lightAt(R.${channel}, 160, 84), lightAt(R.${channel}, 232, 84)]`);
|
||||||
|
check(`${mode} ${channel}: a loaded belt leaves daylight below it`, Math.abs(belt - open) < 1e-6, `${belt} / ${open}`);
|
||||||
|
check(`${mode} ${channel}: sand on natural stone still shades below`, stone < open * 0.95, `${stone} / ${open}`);
|
||||||
|
const [streamGround, openGround] = run(`[lightAt(R.${channel}, 152, 160), lightAt(R.${channel}, 160, 160)]`);
|
||||||
|
check(`${mode} ${channel}: a falling stream leaves ground as bright as open sky`, Math.abs(streamGround - openGround) < 1e-6, `${streamGround} / ${openGround}`);
|
||||||
|
}
|
||||||
|
run('setCell(76 * W + 80, WATER, AMB[76], 0); updateRoof(); computeLight();');
|
||||||
|
const a = run('lightAt(R.latt, 80, 76)');
|
||||||
|
check(`${mode}: a wet belt load transmits like air`, Math.abs(a - Math.pow(0.978, mode === 'fast' ? 4 : 2)) < 1e-6, a);
|
||||||
|
run('setCell(76 * W + 80, EMPTY, AMB[76], 0); updateRoof();');
|
||||||
|
}
|
||||||
|
for (const [name, support, depth, gap, expected] of [
|
||||||
|
['left belt', 'mat[i] = BELTL', 12, '', true],
|
||||||
|
['right belt', 'mat[i] = BELTR', 12, '', true],
|
||||||
|
['filter', 'part[i] = P_FILTER', 12, '', true],
|
||||||
|
['deep heap', 'mat[i] = BELTR', 13, '', false],
|
||||||
|
['air gap', 'mat[i] = BELTR', 3, 'EMPTY', true],
|
||||||
|
['gas gap', 'mat[i] = BELTR', 3, 'STEAM', true],
|
||||||
|
['12 falling grains', '', 12, '', true],
|
||||||
|
['13 falling grains', '', 13, '', false],
|
||||||
|
['natural ledge', 'mat[i] = STONE; flags[i] = F_NAT', 3, '', false],
|
||||||
|
['deep natural heap', 'mat[i] = SAND; flags[i] = F_NAT', 13, '', false],
|
||||||
|
['wall floor', 'mat[i] = STONE', 3, '', true],
|
||||||
|
['screw', 'part[i] = P_SCREW_R', 3, '', true],
|
||||||
|
['loaded screw', 'mat[i] = SAND; part[i] = P_SCREW_R', 3, '', true],
|
||||||
|
]) {
|
||||||
|
run(`
|
||||||
|
for (let y = 40; y <= 53; y++) { const i = y * W + 300; mat[i] = SAND; part[i] = 0; flags[i] = 0; }
|
||||||
|
{ const i = (40 + ${depth}) * W + 300; mat[i] = EMPTY; mat[i + W] = BELTR; ${support}; }
|
||||||
|
${gap ? `mat[41 * W + 300] = ${gap};` : ''}
|
||||||
|
`);
|
||||||
|
check(`${name}: loose cell detection`, run('looseForLight(40 * W + 300)') === expected);
|
||||||
|
}
|
||||||
|
run('mat[40 * W + 300] = SAND; mat[41 * W + 300] = EMPTY;');
|
||||||
|
check('a single grain in mid-air is see-through', run('looseForLight(40 * W + 300)'));
|
||||||
|
run('mat[(H - 1) * W + 300] = SAND;');
|
||||||
|
check('loose cell detection stops at the world bottom', run('!looseForLight((H - 1) * W + 300)'));
|
||||||
|
|
||||||
|
flatWorld(run, 320, 200, 160);
|
||||||
|
run(`
|
||||||
|
bot.lamp = false; stats.refined = 0;
|
||||||
|
placeBlock(BP_GRIND, 0, 20, 20, true);
|
||||||
|
for (let bx = 8; bx < 30; bx++) placeBlock(BP_BELT, 1, bx, 36, true);
|
||||||
|
setCell(79 * W + 80, SAND, AMB[79], 0);
|
||||||
|
`);
|
||||||
|
check('sand resting on grinder housing is see-through', run('looseForLight(79 * W + 80)'));
|
||||||
|
for (const mode of ['full', 'fast']) {
|
||||||
|
const result = run(`(() => {
|
||||||
|
SET.lighting = '${mode}';
|
||||||
|
let mismatches = 0, first = '';
|
||||||
|
for (let t = 0; t < 180; t++) {
|
||||||
|
if (t % 4 === 0) setCell(48 * W + 80, SAND, AMB[48], 0);
|
||||||
|
simTick();
|
||||||
|
computeLight();
|
||||||
|
for (const a of [R.lr, R.lg, R.lb]) for (let x = 76; x < 88; x += 2) {
|
||||||
|
const below = lightAt(a, x, 160), open = lightAt(a, 152, 160);
|
||||||
|
if (Math.abs(below - open) > 1e-6) {
|
||||||
|
mismatches++;
|
||||||
|
if (!first) first = 'tick ' + tick + ', x ' + x + ': ' + below + ' / ' + open;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { mismatches, first };
|
||||||
|
})()`);
|
||||||
|
check(`${mode}: streaming grinder leaves ground in daylight for 180 consecutive ticks`, result.mismatches === 0, result.first || 'no shadow');
|
||||||
|
}
|
||||||
|
check('the streaming grinder actually processes its input', run('stats.refined > 0'), run('stats.refined'));
|
||||||
|
done();
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Blowing aims within the nozzle's reach, with room for the brush and settling.
|
||||||
|
const { load, suite, flatWorld, ticks, count } = require('./harness');
|
||||||
|
const { run } = load();
|
||||||
|
const { check, done } = suite();
|
||||||
|
|
||||||
|
function blowSand(distance, radius) {
|
||||||
|
flatWorld(run, 640, 160, 100);
|
||||||
|
run('RS = 12345; bot.x = 300 - bot.w / 2; bot.y = 80 - bot.h / 2; bag[SAND] = 40; bagTotal = 40;');
|
||||||
|
run(`for (let k = 0; k < 40 && bag[SAND]; k++) { toolBlow(300 + ${distance}, 99, ${radius}, SAND); simTick(); }`);
|
||||||
|
ticks(run, 400);
|
||||||
|
check('all blown sand lands and stays out of the bag', count(run, run('SAND')) === 40 && run('flyers.length === 0 && bag[SAND] === 0'));
|
||||||
|
return run(`(() => {
|
||||||
|
let farthest = 0, cursorDistance = 0;
|
||||||
|
for (let i = 0; i < N; i++) if (mat[i] === SAND) {
|
||||||
|
const x = i % W + 0.5, y = Math.floor(i / W) + 0.5;
|
||||||
|
farthest = Math.max(farthest, Math.hypot(x - 300, y - 80));
|
||||||
|
cursorDistance = Math.max(cursorDistance, Math.hypot(x - (300 + ${distance} + 0.5), y - 99.5));
|
||||||
|
}
|
||||||
|
return { farthest, cursorDistance };
|
||||||
|
})()`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Widest brush, 40 grains on flat ground: measured 14.95 grains past REACH.
|
||||||
|
const BRUSH_AND_SETTLING_MARGIN = 16;
|
||||||
|
for (const distance of [run('3 * REACH'), run('5 * REACH')]) {
|
||||||
|
const landing = blowSand(distance, 6);
|
||||||
|
check('aiming far away lands sand within reach plus brush and settling', landing.farthest <= run('REACH') + BRUSH_AND_SETTLING_MARGIN, landing.farthest.toFixed(2));
|
||||||
|
}
|
||||||
|
const nearby = blowSand(30, 0);
|
||||||
|
check('aiming inside reach still lands sand around the cursor', nearby.cursorDistance <= BRUSH_AND_SETTLING_MARGIN, nearby.cursorDistance.toFixed(2));
|
||||||
|
|
||||||
|
done();
|
||||||
+2
-2
@@ -64,10 +64,10 @@ check('missing rack metadata defaults to empty and closed', run('rackState.get(2
|
|||||||
fresh(3);
|
fresh(3);
|
||||||
run('rackPush(rackState.get(20 * BW + 10), BARFE, 3); rackPush(rackState.get(22 * BW + 10), BARCU, 1); interactAt(40, 84); for (let t = 0; t < 20; t++) rackTick();');
|
run('rackPush(rackState.get(20 * BW + 10), BARFE, 3); rackPush(rackState.get(22 * BW + 10), BARCU, 1); interactAt(40, 84); for (let t = 0; t < 20; t++) rackTick();');
|
||||||
check('refilling crosses an empty middle block without losing FIFO order', run('ents[0].m === BARCU && rackState.get(22 * BW + 10).n === 3 && rackState.get(20 * BW + 10).n === 0'));
|
check('refilling crosses an empty middle block without losing FIFO order', run('ents[0].m === BARCU && rackState.get(22 * BW + 10).n === 3 && rackState.get(20 * BW + 10).n === 0'));
|
||||||
for (const delivery of ['belt', 'tube', 'thrown', 'mold']) {
|
for (const delivery of ['belt', 'screw', 'thrown', 'mold']) {
|
||||||
fresh();
|
fresh();
|
||||||
if (delivery === 'belt') run('for (let bx = 6; bx <= 9; bx++) placeBlock(BP_BELT, 1, bx, 20, true); spawnBar(BARCU, 30, 78);');
|
if (delivery === 'belt') run('for (let bx = 6; bx <= 9; bx++) placeBlock(BP_BELT, 1, bx, 20, true); spawnBar(BARCU, 30, 78);');
|
||||||
if (delivery === 'tube') run('placeBlock(BP_TUBE, 2, 10, 17, true); placeBlock(BP_TUBE, 2, 10, 18, true); spawnBar(BARCU, 40, 68);');
|
if (delivery === 'screw') run('placeBlock(BP_SCREW, 2, 10, 17, true); placeBlock(BP_SCREW, 2, 10, 18, true); spawnBar(BARCU, 40, 68);');
|
||||||
if (delivery === 'thrown') run('spawnBar(BARCU, 34, 66, 0.35, -0.3);');
|
if (delivery === 'thrown') run('spawnBar(BARCU, 34, 66, 0.35, -0.3);');
|
||||||
if (delivery === 'mold') run("addMold('ingot', 10, 17, RSTONE, true); for (const i of molds[0].cavs[0].slice(0, 8)) setCell(i, COPPER, 20, F_CAST); checkMolds();");
|
if (delivery === 'mold') run("addMold('ingot', 10, 17, RSTONE, true); for (const i of molds[0].cavs[0].slice(0, 8)) setCell(i, COPPER, 20, F_CAST); checkMolds();");
|
||||||
run('for (let t = 0; t < 300; t++) simTick();');
|
run('for (let t = 0; t < 300; t++) simTick();');
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// Stored snow keeps its identity and remains in the world's water budget.
|
||||||
|
const { load, suite, flatWorld, ticks } = require('./harness');
|
||||||
|
const { run } = load();
|
||||||
|
const { check, done } = suite();
|
||||||
|
flatWorld(run, 240, 160, 120);
|
||||||
|
run("globalThis.snowSilo = addMachine('silo', 20, 20, 0); setCell(80 * W + 85, SNOW, -10, 0); stamp[80 * W + 85] = 0; tickStamp = 1;");
|
||||||
|
const before = run('worldWater()');
|
||||||
|
run('siloTick(snowSilo)');
|
||||||
|
check('silo accepts snow and keeps its name', run('snowSilo.sm === SNOW && snowSilo.sn === 1'));
|
||||||
|
check('snow inside still counts as water', run('worldWater()') === before);
|
||||||
|
run('for (let y = 80; y < 104; y++) for (let x = 80; x < 92; x++) temp[y * W + x] = 100;');
|
||||||
|
ticks(run, 180);
|
||||||
|
check('stored snow does not melt in a warm silo', run('snowSilo.sm === SNOW && snowSilo.sn === 1'));
|
||||||
|
run('snowSilo.open = true; siloTick(snowSilo)');
|
||||||
|
check('opening the gate releases snow, then the silo is empty', run('snowSilo.sn === 0 && snowSilo.sm === EMPTY && mat[103 * W + 84 + (tick & 3)] === SNOW'));
|
||||||
|
check('releasing snow preserves the water count', run('worldWater()') === before);
|
||||||
|
done();
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
// Drawing geometry against real parts, without changing transport.
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { load, suite, flatWorld } = require('./harness');
|
||||||
|
const { run } = load();
|
||||||
|
const { check, done } = suite();
|
||||||
|
run(fs.readFileSync(path.join(__dirname, '../src/render/slope-belts.js'), 'utf8'));
|
||||||
|
flatWorld(run, 160, 120, 112);
|
||||||
|
for (const code of ['BP_BELT_UP', 'BP_BELT_DOWN', 'BP_FILTER_UP', 'BP_FILTER_DOWN']) {
|
||||||
|
run(`
|
||||||
|
mat.fill(EMPTY); part.fill(0); blockP.fill(0); blockV.fill(0);
|
||||||
|
var artCode = ${code}, artGrade = beltGrade(artCode), artB = 12 * BW + 12;
|
||||||
|
for (let k = -2; k <= 2; k++) placeBlock(artCode, artGrade > 0 ? 1 : 0, 12 + k, 12 + artGrade * k, true);
|
||||||
|
prepareSlopeBelts(0, 0, W, H);
|
||||||
|
`);
|
||||||
|
check(`${code}: downhill diagonal neighbours continue the band`, run('(slopeBeltJoins[artB] & 12) === 12'));
|
||||||
|
check(`${code}: corner seals use the same band`, run(`(() => {
|
||||||
|
const i = slopeSeal(artCode, 12, 12);
|
||||||
|
return i < 0 || slopeBeltAt(i, i % W, (i / W) | 0) >= 0;
|
||||||
|
})()`));
|
||||||
|
check(`${code}: band is as thick as a flat belt`, run(`(() => {
|
||||||
|
const g = artGrade, x = 50 - g * 1.85 * Math.SQRT1_2, y = 50 + 1.85 * Math.SQRT1_2;
|
||||||
|
return slopeBeltShape(x, y, artB) < -1.8 &&
|
||||||
|
slopeBeltShape(x - g * 1.7 * Math.SQRT1_2, y + 1.7 * Math.SQRT1_2, artB) < 0 &&
|
||||||
|
slopeBeltShape(x - g * 2.1 * Math.SQRT1_2, y + 2.1 * Math.SQRT1_2, artB) > 0;
|
||||||
|
})()`));
|
||||||
|
check(`${code}: air below the triangle gets the continuous band`, run(`(() => {
|
||||||
|
for (let y = 48; y < 57; y++) for (let x = 48; x < 56; x++) {
|
||||||
|
const i = y * W + x, own = (y >> 2) * BW + (x >> 2);
|
||||||
|
if (!blockP[own] && mat[i] === EMPTY && slopeBeltAt(i, x, y) >= 0) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})()`));
|
||||||
|
run(`
|
||||||
|
for (let end = 0; end < 2; end++) {
|
||||||
|
const side = end ? 1 : -1;
|
||||||
|
removeBlock(12 + side, 12 + side * artGrade, true);
|
||||||
|
placeBlock(BP_BELT, artGrade > 0 ? 1 : 0, 12 + side, 12 + (end ? artGrade > 0 : artGrade < 0), true);
|
||||||
|
}
|
||||||
|
prepareSlopeBelts(0, 0, W, H);
|
||||||
|
`);
|
||||||
|
check(`${code}: both flat joints form elbows`, run('(slopeBeltJoins[artB] & 3) === 3'));
|
||||||
|
check(`${code}: adjoining flats do not round off at the joint`, run(`(() => {
|
||||||
|
const l = (12 + (artGrade < 0)) * BW + 11, r = (12 + (artGrade > 0)) * BW + 13;
|
||||||
|
return !(flatBeltCaps(l) & 2) && !(flatBeltCaps(r) & 1);
|
||||||
|
})()`));
|
||||||
|
check(`${code}: overhang never covers terrain, parts, machines or bars`, run(`(() => {
|
||||||
|
for (let y = 48; y < 57; y++) for (let x = 48; x < 56; x++) {
|
||||||
|
const i = y * W + x, own = (y >> 2) * BW + (x >> 2);
|
||||||
|
if (blockP[own] || mat[i] !== EMPTY || slopeBeltAt(i, x, y) < 0) continue;
|
||||||
|
mat[i] = STONE; const ground = slopeBeltAt(i, x, y); mat[i] = EMPTY;
|
||||||
|
part[i] = P_GRATE; const built = slopeBeltAt(i, x, y); part[i] = 0;
|
||||||
|
mach[i] = 1; const machine = slopeBeltAt(i, x, y); mach[i] = 0;
|
||||||
|
occ[i] = 1; const bar = slopeBeltAt(i, x, y); occ[i] = 0;
|
||||||
|
return ground < 0 && built < 0 && machine < 0 && bar < 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})()`));
|
||||||
|
}
|
||||||
|
check('free end rounds its corners and stops at the end', run(`(() => {
|
||||||
|
const b = artB, x = 48 - Math.SQRT2, y = 48 + Math.SQRT2;
|
||||||
|
blockP[b] = BP_BELT_DOWN;
|
||||||
|
return slopeBeltShape(x + 0.8, y + 0.8, b, 0) < 0 && slopeBeltShape(x - 1, y - 1, b, 0) > 0;
|
||||||
|
})()`));
|
||||||
|
check('flat belts still meet diagonal screw elbows without an end cap', run(`(() => {
|
||||||
|
blockP[artB - 1] = BP_BELT;
|
||||||
|
blockP[artB] = BP_SCREW; blockV[artB] = 5; // same-row flat at the high end
|
||||||
|
const joined = !(flatBeltCaps(artB - 1) & 2);
|
||||||
|
blockP[artB] = BP_BELT_DOWN;
|
||||||
|
return joined;
|
||||||
|
})()`));
|
||||||
|
check('unpowered rollers and studs stand still', run(`(() => {
|
||||||
|
slopeBeltJoins[artB] &= ~16;
|
||||||
|
for (let y = 48; y < 56; y++) for (let x = 48; x < 56; x++)
|
||||||
|
if (slopeBeltColor(artB, x, y, 0, 150, 170, 190) !== slopeBeltColor(artB, x, y, 20, 150, 170, 190)) return false;
|
||||||
|
return true;
|
||||||
|
})()`));
|
||||||
|
check('powered rollers and studs reverse with the belt', run(`(() => {
|
||||||
|
slopeBeltJoins[artB] |= 16;
|
||||||
|
let moves = false;
|
||||||
|
for (let y = 48; y < 56; y++) for (let x = 48; x < 56; x++) {
|
||||||
|
blockV[artB] = 1;
|
||||||
|
const a = slopeBeltColor(artB, x, y, 12, 150, 170, 190), stopped = slopeBeltColor(artB, x, y, 0, 150, 170, 190);
|
||||||
|
blockV[artB] = 0;
|
||||||
|
if (a !== slopeBeltColor(artB, x, y, -12, 150, 170, 190)) return false;
|
||||||
|
if (a !== stopped) moves = true;
|
||||||
|
}
|
||||||
|
return moves;
|
||||||
|
})()`));
|
||||||
|
check('drawing leaves the seed, cells and saved block data unchanged', run(`(() => {
|
||||||
|
const snapshot = () => JSON.stringify([RS, Array.from(mat), Array.from(part), Array.from(blockP), Array.from(blockV)]);
|
||||||
|
const before = snapshot();
|
||||||
|
prepareSlopeBelts(0, 0, W, H);
|
||||||
|
for (let y = 40; y < 64; y++) for (let x = 40; x < 72; x++) {
|
||||||
|
const b = slopeBeltAt(y * W + x, x, y);
|
||||||
|
if (b >= 0) slopeBeltColor(b, x, y, 12, 150, 170, 190);
|
||||||
|
}
|
||||||
|
return snapshot() === before;
|
||||||
|
})()`));
|
||||||
|
for (const grade of [-1,1]) {
|
||||||
|
flatWorld(run,160,120,112);
|
||||||
|
run(`for(let k=0;k<6;k++)placeBlock(${grade<0?'BP_BELT_UP':'BP_BELT_DOWN'},${grade<0?1:0},12+k,${grade<0?20:12}+k*${grade},true);`); // uphill, the cleated way
|
||||||
|
for(let phase=0;phase<8;phase++) {
|
||||||
|
run(`rebuildPower();slopePhase.fill(${phase});slopeCleatsTick(false);prepareSlopeBelts(0,0,W,H);`);
|
||||||
|
check(`grade ${grade} phase ${phase} draws exactly the physical cleat raster`,run(`(()=>{
|
||||||
|
let drawn=0;
|
||||||
|
for(let i=0;i<N;i++){const x=i%W,y=(i/W)|0,detail=slopeDetailAt(i,x,y);if(detail===1)drawn++;
|
||||||
|
if((detail===1)!==Boolean(cleatMask[i] && !part[i] && !occ[i] && !mach[i] && PH[mat[i]]!==PH_SOLID))return false;
|
||||||
|
}return drawn>0;
|
||||||
|
})()`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flatWorld(run,160,120,112);
|
||||||
|
run('placeBlock(BP_FILTER_DOWN,128,12,12,true);tick=0;prepareSlopeBelts(0,0,W,H);var idleJoins=slopeBeltJoins[12*BW+12];tick=2;prepareSlopeBelts(0,0,W,H);');
|
||||||
|
check('empty screen has a stable frame',run('slopeBeltJoins[12*BW+12]===idleJoins'));
|
||||||
|
run('setCell(47*W+48,SAND,20,0);prepareSlopeBelts(0,0,W,H);');
|
||||||
|
check('loaded screen shakes the frame with the simulation tick',run('(slopeBeltJoins[12*BW+12]&64)!==0'));
|
||||||
|
run('tick=4;prepareSlopeBelts(0,0,W,H);');
|
||||||
|
check('loaded frame returns on the next shake beat',run('(slopeBeltJoins[12*BW+12]&64)===0'));
|
||||||
|
flatWorld(run,160,120,112);
|
||||||
|
run('for(let k=0;k<6;k++)placeBlock(BP_BELT_UP,1,12+k,20-k,true);rebuildPower();slopeCleatsTick(false);');
|
||||||
|
check('wall reaches the belt and cleat height, and clips its free ends',run(`(()=>{
|
||||||
|
const b=20*BW+12,g=-1,sy=84;
|
||||||
|
for(const depth of [0.5,3,5.5])
|
||||||
|
if(slopeWallShape(50+g*depth*Math.SQRT1_2,sy-2-depth*Math.SQRT1_2,b)>0)return false;
|
||||||
|
return slopeWallShape(46,sy+2,b)>0 && slopeWallShape(54,sy-6,b)>0;
|
||||||
|
})()`));
|
||||||
|
check('far wall is filled down to the belt and grains cover it',run(`(()=>{
|
||||||
|
prepareSlopeBelts(0,0,W,H);
|
||||||
|
let wall=0;
|
||||||
|
for(let y=8;y<100;y++)for(let x=8;x<140;x++) {
|
||||||
|
const i=y*W+x;
|
||||||
|
if(slopeDetailAt(i,x,y)!==2)continue;
|
||||||
|
wall++;
|
||||||
|
mat[i]=SAND;const covered=slopeDetailAt(i,x,y)===0;mat[i]=EMPTY;
|
||||||
|
if(!covered)return false;
|
||||||
|
}
|
||||||
|
return wall>20;
|
||||||
|
})()`));
|
||||||
|
flatWorld(run,160,120,112);
|
||||||
|
run('for(let k=0;k<6;k++)placeBlock(BP_BELT_UP,0,12+k,20-k,true);rebuildPower();slopeCleatsTick(false);prepareSlopeBelts(0,0,W,H);');
|
||||||
|
check('a downhill belt draws no wall and no ribs',run('(()=>{let n=0;for(let i=0;i<N;i++)if(slopeDetailAt(i,i%W,(i/W)|0))n++;return n===0 && cleatCells.length===0;})()'));
|
||||||
|
flatWorld(run,320,240,232);
|
||||||
|
run('for(let k=0;k<8;k++)placeBlock(BP_BELT_UP,1,12+k,30-k,true);for(let t=0;t<120;t++)simTick();');
|
||||||
|
check('idle empty slope lets every moving area sleep',run('awake.every(v=>v===0)'),run('awake.reduce((n,v)=>n+(v>0),0)'));
|
||||||
|
check('cleat chunk flags include neighbours across chunk borders',run(`(()=>{
|
||||||
|
for(const i of cleatCells) {
|
||||||
|
const x=i%W,y=(i/W)|0;
|
||||||
|
for(let yy=y-1;yy<=y+1;yy++)for(let xx=x-1;xx<=x+1;xx++)
|
||||||
|
if(xx>=0 && xx<W && yy>=0 && yy<H && !cleatChunks[(yy>>5)*CW+(xx>>5)])return false;
|
||||||
|
}
|
||||||
|
return cleatCells.length>0 && cleatChunks.some(v=>v===0);
|
||||||
|
})()`));
|
||||||
|
check('a load next to a cleat wakes after the empty line slept',run(`(()=>{
|
||||||
|
const i=cleatCells[0];setCell(i,SAND,20,0);awake.fill(0);
|
||||||
|
slopeCleatsTick(false);
|
||||||
|
return awake.some(v=>v>0) && awake[0]===0;
|
||||||
|
})()`));
|
||||||
|
done();
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
// Sloped belts: transport, joins, sorting, persistence and the actual drag controls.
|
||||||
|
const { load, suite, flatWorld, rect, ticks, count } = require('./harness');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { run } = load(), { check, done } = suite();
|
||||||
|
function fresh() {
|
||||||
|
flatWorld(run, 256, 224, 208);
|
||||||
|
run('RS=7;nextPowerBuild=0;money=10000;AMB.fill(20);temp.fill(20);hot.fill(0);');
|
||||||
|
}
|
||||||
|
fresh();
|
||||||
|
check('slope belts use new codes, without renumbering old slopes', run("typeof BP_BELT_UP !== 'undefined' && BP_BELT_UP > BP_SLOPE_TR"));
|
||||||
|
function line(grade, filter = false, dir = 1) {
|
||||||
|
fresh();
|
||||||
|
if (filter) dir = grade > 0 ? 1 : 0;
|
||||||
|
const by = grade < 0 ? 32 : 16, inlet = by + (grade < 0 ? 1 : 0), outlet = by + 9 * grade + (grade > 0 ? 1 : 0);
|
||||||
|
run(`for(let k=0;k<10;k++) placeBlock(${filter ? 'BP_FILTER_UP' : 'BP_BELT_UP'}+${grade>0?1:0},${filter?dir*128:dir},16+k,${by}+k*${grade},true,{m:[SAND],inv:0});
|
||||||
|
for(let x=10;x<16;x++)placeBlock(BP_BELT,${dir},x,${inlet},true);
|
||||||
|
for(let x=26;x<32;x++)placeBlock(BP_BELT,${dir},x,${outlet},true);`);
|
||||||
|
return { by, inlet: filter && grade < 0 ? outlet : inlet, outlet, grade, left: filter && grade < 0, feedX: filter && grade < 0 ? 120 : 44 };
|
||||||
|
}
|
||||||
|
function collect(s, steps, feed = false, sink = 128) {
|
||||||
|
return run(`(()=>{let out=0,added=0;for(let k=0;k<${steps};k++){
|
||||||
|
${feed ? `for(let l=1;l<=4;l++){const i=(${s.inlet}*4-l)*W+${s.feedX || 44};if(mat[i]===EMPTY){setCell(i,SAND,20,0);added++;}}` : ''}
|
||||||
|
simTick();
|
||||||
|
for(let i=0;i<N;i++)if(mat[i]===SAND && ${s.left ? "i%W<40" : "i%W>="+sink}){out++;setCell(i,EMPTY,20,0);}
|
||||||
|
for(let j=flyers.length-1;j>=0;j--)if(flyers[j].m===SAND && ${s.left ? "flyers[j].x<40" : "flyers[j].x>="+sink}){out++;flyers.splice(j,1);}
|
||||||
|
}return {out,added,left:mat.reduce((n,m)=>n+(m===SAND),0)+flyers.filter(f=>f.m===SAND).length};})()`);
|
||||||
|
}
|
||||||
|
const rates=[];
|
||||||
|
for(const grade of [0,-1,1]) {
|
||||||
|
let s;
|
||||||
|
if(grade) s=line(grade);
|
||||||
|
else {fresh();run('for(let bx=10;bx<32;bx++)placeBlock(BP_BELT,1,bx,24,true);');s={inlet:24};}
|
||||||
|
collect(s,220,true);
|
||||||
|
const r=collect(s,600,true);rates.push(r.out/10);
|
||||||
|
// uphill pockets carry about as much as a flat belt, and downhill runs like one
|
||||||
|
const floor = 60;
|
||||||
|
check(`grade ${grade} carries a generous feed at least ${floor} grains/s`,r.out>0 && r.out/10>=floor,JSON.stringify(r));
|
||||||
|
const end=collect(s,600);
|
||||||
|
check(`grade ${grade} leaves no load stuck`,end.left===0,JSON.stringify(end));
|
||||||
|
}
|
||||||
|
console.log('Throughput grains/s flat/up/down: '+rates.join('/'));
|
||||||
|
for(const grade of [-1,1]) {
|
||||||
|
const s=line(grade);
|
||||||
|
rect(run,40,s.inlet*4-8,55,s.inlet*4-1,run('SAND'));
|
||||||
|
rect(run,39,20,39,s.inlet*4-1,run('STONE'));
|
||||||
|
const r=collect(s,1000);
|
||||||
|
check(`grade ${grade} conserves and delivers every grain across both flat joins`,r.out===128 && r.left===0,JSON.stringify(r));
|
||||||
|
line(grade);
|
||||||
|
run(`spawnBar(BARCU,${s.feedX || 44},${s.inlet*4-2},0,0);`);
|
||||||
|
ticks(run,500);
|
||||||
|
check(`grade ${grade} carries a bar through both flat joins`,run('ents.length===1 && ents[0].ix>=128'),run('ents.map(e=>[e.ix,e.iy])'));
|
||||||
|
}
|
||||||
|
for(const grade of [-1,1]) {
|
||||||
|
const s=line(grade);
|
||||||
|
rect(run,68,s.by*4-12,71,s.by*4-5,run('WATER'));
|
||||||
|
const before=count(run,run('WATER'));
|
||||||
|
ticks(run,600);
|
||||||
|
check(`water drains through grade ${grade} and is conserved`,count(run,run('WATER'))===before && count(run,run('WATER'),0,200,255,207)===before);
|
||||||
|
// A completely full face held at its head must not unload backwards or through corners.
|
||||||
|
line(grade);
|
||||||
|
run(`for(let x=64;x<104;x++){const by=${s.by}+((x>>2)-16)*${grade},c=blockP[by*BW+(x>>2)],y=beltFaceY(c,x>>2,by,x);for(let l=1;l<=4;l++)setCell((y-l)*W+x,SAND,20,0);}`);
|
||||||
|
rect(run,104,0,104,s.outlet*4-1,run('STONE'));
|
||||||
|
ticks(run,300);
|
||||||
|
const held=run('mat.reduce((n,m)=>n+(m===SAND),0)+flyers.filter(f=>f.m===SAND).length');
|
||||||
|
check(`blocked grade ${grade} conserves its load and lets overflow roll back`,held===160 && (grade>0 || count(run,run('SAND'),0,0,63,207)>0),held);
|
||||||
|
rect(run,104,0,104,s.outlet*4-1,run('EMPTY'));
|
||||||
|
const released=collect(s,800);
|
||||||
|
check(`grade ${grade} resumes after the head clears`,released.out===160 && released.left===0,JSON.stringify(released));
|
||||||
|
line(grade);
|
||||||
|
run(`for(let bx=26;bx<32;bx++)removeBlock(bx,${s.outlet});setCell((${s.by+9*grade}*4+${grade<0?0:3}-1)*W+103,SAND,20,0);tick=2;tickStamp++;machinesTick();`);
|
||||||
|
check(`grade ${grade} launches off an open end along its face`,run(`flyers.length===1 && flyers[0].vx>0 && Math.sign(flyers[0].vy)===${grade}`));
|
||||||
|
run(`spawnBar(BARCU,${s.feedX || 44},${s.inlet*4-2},0,0);`);
|
||||||
|
const barLaunch=run('(()=>{for(let k=0;k<400;k++){simTick();if(ents[0].ix>=104)return ents[0].vx>0;}return false;})()');
|
||||||
|
check(`grade ${grade} keeps a bar moving forward off an open end`,barLaunch);
|
||||||
|
for(const diagonal of [false,true]) {
|
||||||
|
line(grade);
|
||||||
|
run(`for(let bx=26;bx<32;bx++)removeBlock(bx,${s.outlet});for(let k=0;k<3;k++)placeBlock(BP_SCREW,${diagonal?(grade<0?4:5):1},26+k,${s.by+9*grade+(grade<0?-1:0)}+k*${diagonal?grade:0},true);`);
|
||||||
|
rect(run,40,s.inlet*4-8,55,s.inlet*4-1,run('SAND'));
|
||||||
|
rect(run,39,20,39,s.inlet*4-1,run('STONE'));
|
||||||
|
const r=collect(s,1000,false,116);
|
||||||
|
check(`grade ${grade} unloads grains into a ${diagonal?'diagonal':'straight'} screw`,r.out===128 && r.left===0,JSON.stringify(r));
|
||||||
|
run(`spawnBar(BARCU,${s.feedX || 44},${s.inlet*4-2},0,0);`);
|
||||||
|
ticks(run,600);
|
||||||
|
check(`grade ${grade} unloads a bar into a ${diagonal?'diagonal':'straight'} screw`,run('ents.length===1 && ents[0].ix>=116 && !mat.some((m,i)=>occ[i] && PH[m]===PH_SOLID)'),run('ents.map(e=>[e.ix,e.iy])'));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(const grade of [-1,1]) {
|
||||||
|
const s=line(grade,true);
|
||||||
|
run('for(const b of filterCfg.keys())setFilter(b,{m:[SAND],inv:1});');
|
||||||
|
collect(s,220,true);
|
||||||
|
const r=collect(s,600,true);
|
||||||
|
console.log(`Filter throughput grade ${grade}, grains/s: ${r.out/10}`);
|
||||||
|
check(`downhill screen grade ${grade} discharges its generous feed`,r.out/10>=45 && r.out/10<=90,JSON.stringify(r));
|
||||||
|
}
|
||||||
|
for(const grade of [-1,1]) for(const inv of [0,1]) {
|
||||||
|
const s=line(grade,true);
|
||||||
|
run(`for(const b of filterCfg.keys())setFilter(b,{m:[SAND],inv:${inv}});`);
|
||||||
|
rect(run,s.left?128:39,20,s.left?128:39,s.inlet*4-1,run('STONE'));
|
||||||
|
run(`for(let y=${s.inlet*4-8};y<${s.inlet*4};y++)for(let x=${s.left?112:40};x<${s.left?128:56};x++)setCell(y*W+x,(x+y)&1?SAND:GRAVEL,20,0);`);
|
||||||
|
const result=run(`(()=>{const out=[0,0],below=[0,0];for(let k=0;k<1200;k++){
|
||||||
|
simTick();
|
||||||
|
for(let i=0;i<N;i++)if(mat[i]===SAND || mat[i]===GRAVEL){const m=mat[i]===SAND?0:1,x=i%W,y=(i/W)|0;
|
||||||
|
if(${s.left?"x<40":"x>=128"}){out[m]++;setCell(i,EMPTY,20,0);}else if(y>=180){below[m]++;setCell(i,EMPTY,20,0);}}
|
||||||
|
for(let f=flyers.length-1;f>=0;f--){const p=flyers[f];if((p.m===SAND || p.m===GRAVEL) && ${s.left?"p.x<40":"p.x>=128"}){out[p.m===SAND?0:1]++;flyers.splice(f,1);}}
|
||||||
|
}return {out,below,left:mat.reduce((n,m)=>n+(m===SAND || m===GRAVEL),0)+flyers.filter(f=>f.m===SAND || f.m===GRAVEL).length};})()`);
|
||||||
|
console.log(`Sorting grade ${grade} inverse ${inv}: `+JSON.stringify(result));
|
||||||
|
check(`grade ${grade} ${inv?'carries selected':'drops selected'} and conserves the full mix`,result.out[inv?0:1]===64 && result.below[inv?1:0]===64 && result.left===0 && result.out[inv?1:0]===0 && result.below[inv?0:1]===0,JSON.stringify(result));
|
||||||
|
}
|
||||||
|
for(const c of ['BP_BELT_UP','BP_BELT_DOWN','BP_FILTER_UP','BP_FILTER_DOWN'])for(const d of [0,1]) {
|
||||||
|
fresh();
|
||||||
|
run(`placeBlock(${c},${c.includes('FILTER')?d*128:d},20,20,true,{m:[SAND,BARCU],inv:1});globalThis.saved=serializeWorld();deserializeWorld(saved.meta,saved.buffers);`);
|
||||||
|
check(`${c} ${d} saves its shape, direction and settings`,run(`blockP[20*BW+20]===${c} && blockV[20*BW+20]===${c.includes('FILTER')?'filterVariant('+c+','+d+'*128)':d} && blockIntegrity(20*BW+20)===10 ${c.includes('FILTER')?'&& sameFilter(filterInfo(20*BW+20),{m:[SAND,BARCU].sort((a,b)=>a-b),inv:1})':''}`));
|
||||||
|
if(c.includes('BELT')) {
|
||||||
|
run(`slopePhase[20*BW+20]=${d?1:7};rebuildPower();slopeCleatsTick(false);globalThis.savedPhase=serializeWorld();deserializeWorld(savedPhase.meta,savedPhase.buffers);`);
|
||||||
|
// only an uphill run has cleats
|
||||||
|
const up=(c==='BP_BELT_UP')===(d===1);
|
||||||
|
check(`${c} saves the moving cleat phase and immediately restores its barriers`,run(`slopePhase[20*BW+20]===${d?1:7} && cleatCells.length${up?'>0':'===0'}`));
|
||||||
|
}
|
||||||
|
run(`globalThis.clip=copyRegion(80,80,83,83);bot.x=96;bot.y=80;undoRecord('paste',()=>pasteGhost(clip,24,20));ghostTick();`);
|
||||||
|
check(`${c} ${d} copy/paste keeps all settings`,run('blockP[20*BW+24]===blockP[20*BW+20] && blockV[20*BW+24]===blockV[20*BW+20] && sameFilter(filterInfo(20*BW+20),filterInfo(20*BW+24))'));
|
||||||
|
run('undoLast();');
|
||||||
|
check(`${c} ${d} undo clears the paste including its seal`,run(`blockP[20*BW+24]===0 && blockCellList(24,20).every(i=>mat[i]===EMPTY && !part[i]) && mat[slopeSeal(${c},24,20)]===EMPTY && !part[slopeSeal(${c},24,20)]`));
|
||||||
|
run("undoRecord('cut',()=>cutRegion(80,80,83,83));undoLast();");
|
||||||
|
check(`${c} ${d} undo restores a cut`,run(`blockP[20*BW+20]===${c} && blockIntegrity(20*BW+20)===10`));
|
||||||
|
check(`${c} ${d} mirrors its shape and reverses horizontally`,run(`(()=>{const b=flipBlueprint(clip).blocks[0];return b[2]===${c}+${c.endsWith('UP')?1:-1} && b[3]===${c.includes('FILTER')?'filterVariant('+c+'+'+(c.endsWith('UP')?1:-1)+',0)':1-d};})()`));
|
||||||
|
}
|
||||||
|
for(const grade of [-1,1]) {
|
||||||
|
const s=line(grade,true);
|
||||||
|
run(`spawnBar(BARCU,${s.feedX || 44},${s.inlet*4-2},0,0);`);ticks(run,500);
|
||||||
|
check(`sloped filter grade ${grade} slides a bar it does not pass downhill`,run(`ents.length===1 && ${s.left?'ents[0].ix<40':'ents[0].ix>=128'}`));
|
||||||
|
line(grade,true);
|
||||||
|
run(`for(const b of filterCfg.keys())setFilter(b,{m:[BARCU],inv:0});spawnBar(BARCU,${s.feedX || 44},${s.inlet*4-2},0,0);`);ticks(run,500);
|
||||||
|
check(`sloped filter grade ${grade} drops a selected bar through its open frame`,run('ents.length===1 && ents[0].ix<104 && ents[0].iy===206 && !mat.some((m,i)=>occ[i] && PH[m]===PH_SOLID)'));
|
||||||
|
}
|
||||||
|
// Reverse the motor without flipping the upward-facing triangle.
|
||||||
|
for(const grade of [-1,1]) {
|
||||||
|
const s=line(grade,false,0);
|
||||||
|
rect(run,112,s.outlet*4-8,127,s.outlet*4-1,run('SAND'));
|
||||||
|
rect(run,128,20,128,s.outlet*4-1,run('STONE'));
|
||||||
|
const r=run(`(()=>{let out=0;for(let k=0;k<1000;k++){simTick();for(let i=0;i<N;i++)if(mat[i]===SAND && i%W<40){out++;setCell(i,EMPTY,20,0);}for(let f=flyers.length-1;f>=0;f--)if(flyers[f].m===SAND && flyers[f].x<40){out++;flyers.splice(f,1);}}return {out,left:mat.reduce((n,m)=>n+(m===SAND),0)};})()`);
|
||||||
|
check(`reversed grade ${grade} delivers every grain to the left`,r.out===128 && r.left===0,JSON.stringify(r));
|
||||||
|
line(grade,false,0);run(`spawnBar(BARCU,120,${s.outlet*4-2},0,0);`);ticks(run,500);
|
||||||
|
check(`reversed grade ${grade} carries bars left across both joins`,run('ents.length===1 && ents[0].ix<40'),run('ents.map(e=>[e.ix,e.iy])'));
|
||||||
|
}
|
||||||
|
fresh();
|
||||||
|
run('placeBlock(BP_BELT_UP,1,20,32,true);');
|
||||||
|
// The ribs hold the load: under a heavy feed no grain is ever inside a rib, and a grain on the bare
|
||||||
|
// face slides down until a rib stops it, then rides up with that rib.
|
||||||
|
{
|
||||||
|
const s=line(-1);
|
||||||
|
const inRib=run(`(()=>{let worst=0;for(let k=0;k<600;k++){for(let l=1;l<=12;l++){const i=(${s.inlet}*4-l)*W+44;if(mat[i]===EMPTY)setCell(i,SAND,20,0);}
|
||||||
|
simTick();let n=0;for(const c of cleatCells)if(PH[mat[c]]===PH_POWDER)n++;if(n>worst)worst=n;}return worst;})()`);
|
||||||
|
check('under a heavy feed no grain is ever inside a rib',inRib===0,inRib);
|
||||||
|
line(-1);
|
||||||
|
// a free face cell high on the run, halfway between two ribs
|
||||||
|
const g=run(`(()=>{for(let x=90;x<100;x++){const b=((32-((x>>2)-16))*BW)+(x>>2),y=beltFaceY(blockP[b],x>>2,32-((x>>2)-16),x);
|
||||||
|
let near=99;for(let h=0;h<CLEAT_SPACING;h++){if(cleatFoot(x-h,b)||cleatFoot(x+h,b)){near=h;break;}}
|
||||||
|
if(near>=3){setCell((y-1)*W+x,SAND,20,0);return x;}}return -1;})()`);
|
||||||
|
const path=run(`(()=>{const xs=[];for(let k=0;k<40;k++){simTick();for(let i=0;i<N;i++)if(mat[i]===SAND){xs.push(i%W);break;}}return xs;})()`);
|
||||||
|
check('a grain on the bare face slides back down to a rib',g>0 && Math.min(...path)<g,JSON.stringify([g,path.slice(0,12)]));
|
||||||
|
check('then rides up with its rib',path[path.length-1]>Math.min(...path),JSON.stringify(path.slice(-8)));
|
||||||
|
}
|
||||||
|
// A piece added to a running belt takes up its cleat spacing, so no rib starts at the seam and
|
||||||
|
// pushes its pocket into the one ahead (that stalled the whole run).
|
||||||
|
for(const wait of [3,7,11,13]){
|
||||||
|
fresh();
|
||||||
|
run(`for(let k=0;k<9;k++)placeBlock(BP_BELT_UP,1,16+k,40-k,true);for(let k=1;k<=6;k++)placeBlock(BP_BELT,1,16-k,41,true);
|
||||||
|
for(let t=0;t<${wait};t++)simTick();
|
||||||
|
for(let k=9;k<12;k++)placeBlock(BP_BELT_UP,1,16+k,40-k,true);for(let k=1;k<=6;k++)placeBlock(BP_BELT,1,27+k,29,true);`);
|
||||||
|
const r=run(`(()=>{let added=0,past=0;for(let t=0;t<1500;t++){if(t<300&&t%10===0){const i=(41*4-1)*W+50;if(mat[i]===EMPTY){setCell(i,SAND,20,0);added++;}}simTick();}
|
||||||
|
for(let i=0;i<N;i++)if(mat[i]===SAND&&i%W>=112)past++;return {added,past,same:slopePhase[32*BW+24]===slopePhase[31*BW+25]};})()`);
|
||||||
|
check(`a piece added ${wait} ticks later shares the run's phase and the run keeps carrying`,r.same && r.added>0 && r.past===r.added,JSON.stringify(r));
|
||||||
|
}
|
||||||
|
// A stationary cleat blocks downhill crossings; the bare face still lets grains slide.
|
||||||
|
for (const grade of [-1,1]) {
|
||||||
|
const s=line(grade);
|
||||||
|
run('rebuildPower();slopeCleatsTick(false);');
|
||||||
|
if(grade>0) check('a downhill run has no cleats',run('cleatCells.length===0'));
|
||||||
|
else check(`grade ${grade} cleats block every downhill crossing`,run(`cleatCells.length>0 && cleatCells.every(i=>!enter(SAND,PH_POWDER,i,${grade},1) && !enter(SAND,PH_POWDER,i+W+${grade},${grade},1))`));
|
||||||
|
check(`grade ${grade} bare face slides between steps without stamping the load`,run(`(()=>{
|
||||||
|
for(let x=72;x<96;x++){
|
||||||
|
const by=${s.by}+((x>>2)-16)*${grade}, y=beltFaceY(blockP[by*BW+(x>>2)],x>>2,by,x)-1,i=y*W+x;
|
||||||
|
if(cleatMask[i] || cleatMask[i+W+${grade}])continue;
|
||||||
|
setCell(i,SAND,20,0);tick=3;tickStamp++;machinesTick();
|
||||||
|
const unstamped=stamp[i]!==tickStamp;
|
||||||
|
for(let k=0;k<200 && mat[i]===SAND;k++)powder(i,x,y,SAND);
|
||||||
|
return unstamped && mat[i]===EMPTY;
|
||||||
|
} return false;
|
||||||
|
})()`));
|
||||||
|
}
|
||||||
|
fresh();
|
||||||
|
run('placeBlock(BP_FILTER_DOWN,128,20,20,true,{m:[],inv:0});setCell(79*W+80,GRAVEL,20,0);tickStamp++;tick=SCREEN_NUDGE_TICKS-1;screenNudge(80*W+80,1,null);');
|
||||||
|
check('screen waits between its configured nudges',run('mat[79*W+80]===GRAVEL && stamp[79*W+80]!==tickStamp'));
|
||||||
|
run('tick++;screenNudge(80*W+80,1,null);');
|
||||||
|
check('screen nudges just its mesh contact downhill at the configured rate',run('mat[80*W+81]===GRAVEL && mat[79*W+80]===EMPTY'));
|
||||||
|
fresh();run('placeBlock(BP_BELT_UP,1,20,32,true);');
|
||||||
|
run(fs.readFileSync('src/render/slope-belts.js','utf8'));
|
||||||
|
// The renderer prepares joins and power first; stripe pitch is measured along the rotated band.
|
||||||
|
run('prepareSlopeBelts(0,0,W,H);');
|
||||||
|
check('sloped stripes vary along the face and travel with the motor',run('(()=>{const b=32*BW+20,c=(x,y,t)=>slopeBeltColor(b,x,y,t,0,0,0);return c(81,130,0)!==c(82,129,0) && c(81,130,0)===c(82,129,4);})()'));
|
||||||
|
run(fs.readFileSync('src/ui/selection.js','utf8'));
|
||||||
|
run(fs.readFileSync('src/ui/actions.js','utf8'));
|
||||||
|
run("globalThis.GAME={pieceDir:{belt:1,filter:1},filterCfg:{m:[SAND],inv:0},wallMat:RSTONE};globalThis.hudDirty=false;globalThis.aimBlock=()=>({bx:25,by:15});globalThis.toolId=()=>'build';globalThis.playSfx=()=>{};");
|
||||||
|
for(const id of ['belt','filter'])for(const [dx,dy] of [[5,-4],[4,5],[-5,4],[-4,-5]]) {
|
||||||
|
run('GAME.drag={bx:20,by:20};');
|
||||||
|
const c=(id==='belt'?19:21)+(dx*dy>0?1:0);
|
||||||
|
check(`${id} drag ${dx},${dy} picks the slope shape and permitted travel direction`,run(`pieceCode(PIECE_BY_ID.${id},{bx:${20+dx},by:${20+dy}})===${c} && dragDirFor(PIECE_BY_ID.${id},GAME.drag,{bx:${20+dx},by:${20+dy}})===${id==='filter'?(dx*dy>0?1:0):(dx>0?1:0)}`));
|
||||||
|
}
|
||||||
|
run("globalThis.setStatus=()=>{};globalThis.catOf=()=>0;globalThis.selectTool=()=>{};globalThis.aimBlock=()=>blockOf(GAME.aim.x,GAME.aim.y);");
|
||||||
|
for(const id of ['belt','filter'])for(const [dx,dy] of [[3,-3],[3,3],[-3,3],[-3,-3]]) {
|
||||||
|
fresh();
|
||||||
|
run(`globalThis.piece=()=>PIECE_BY_ID.${id};GAME.drag={bx:20,by:20};GAME.aim={x:${(20+dx)*4},y:${(20+dy)*4}};bot.x=80;bot.y=80;primaryRelease(false);`);
|
||||||
|
const c=(id==='belt'?19:21)+(dx*dy>0?1:0), v=id==='filter'?(dx*dy>0?128:0):(dx>0?1:0);
|
||||||
|
check(`${id} release really builds the diagonal in direction ${dx},${dy}`,run(`Array.from({length:4},(_,k)=>{const b=(20+k*${Math.sign(dy)})*BW+20+k*${Math.sign(dx)};return blockP[b]===${c} && blockV[b]===${v};}).every(Boolean)`));
|
||||||
|
run('pickUnderCursor();rotate();');
|
||||||
|
check(`${id} pick and R preserve the slope and use its permitted direction`,run(`pieceCode(PIECE_BY_ID.${id})===${c} && pieceV(PIECE_BY_ID.${id})===${id==='filter'?v:1-v}`));
|
||||||
|
run('GAME.drag=aimBlock();GAME.replace=true;primaryRelease(false);GAME.replace=false;');
|
||||||
|
check(`${id} a single replacement keeps the selected direction`,run(`blockP[${20+dy}*BW+${20+dx}]===${c} && blockV[${20+dy}*BW+${20+dx}]===${id==='filter'?v:1-v}`));
|
||||||
|
|
||||||
|
}
|
||||||
|
// The owner's drag: a flat belt, then an uphill drag started beside its end on the same level,
|
||||||
|
// snaps up one block through the real release so the two join seamlessly.
|
||||||
|
fresh();
|
||||||
|
run("globalThis.piece=()=>PIECE_BY_ID.belt;placeBlock(BP_BELT,1,19,20,true);GAME.drag={bx:20,by:20};GAME.aim={x:23*4,y:17*4};bot.x=80;bot.y=80;primaryRelease(false);");
|
||||||
|
check('uphill drag beside a flat belt end snaps up one and joins it', run('blockP[20*BW+20]===0 && blockP[19*BW+20]===BP_BELT_UP && blockP[16*BW+23]===BP_BELT_UP && endJoin(19,20,1)>=0 && !joinIsDrop(19,20,1,endJoin(19,20,1))'));
|
||||||
|
// Middle click on the belt band you see, even where it is drawn into the empty block next door.
|
||||||
|
fresh();
|
||||||
|
const band=run(`(()=>{globalThis.R=globalThis.R||{smoothOn:false};
|
||||||
|
for(let k=0;k<4;k++)placeBlock(BP_BELT_UP,1,20+k,24-k,true);prepareSlopeBelts(0,0,W,H);
|
||||||
|
for(let y=60;y<110;y++)for(let x=70;x<100;x++){const i=y*W+x,own=(y>>2)*BW+(x>>2);
|
||||||
|
if(!blockP[own] && mat[i]===EMPTY && slopeBeltAt(i,x,y)>=0){GAME.pieceIdx=PIECE_BY_ID.wall.index;GAME.aim={x:x+0.5,y:y+0.5};pickUnderCursor();return GAME.pieceIdx===PIECE_BY_ID.belt.index;}}
|
||||||
|
return 'no band cell outside its block';})()`);
|
||||||
|
check('middle click on a sloped belt band outside its block picks the belt',band===true,band);
|
||||||
|
run(fs.readFileSync('src/ui/selection-pickers.js','utf8'));
|
||||||
|
const filterLine=line(-1,true);
|
||||||
|
run("applyFilterRun(filterRun(32*BW+16),{m:[GRAVEL],inv:1,dir:0});");
|
||||||
|
check('the settings UI updates the matching diagonal filter run',run('filterCfg.size===10 && [...filterCfg.keys()].every(b=>sameFilter(filterInfo(b),{m:[GRAVEL],inv:1}) && blockV[b]===0)'));
|
||||||
|
run('undoLast();');
|
||||||
|
check('undo restores diagonal filter settings and direction',run('filterCfg.size===10 && [...filterCfg.keys()].every(b=>sameFilter(filterInfo(b),{m:[SAND],inv:0}) && blockV[b]===0)'));
|
||||||
|
// An actual v2 fixture, before either diagonal belt or filter existed.
|
||||||
|
const legacy=JSON.parse(require('zlib').gunzipSync(fs.readFileSync(__dirname+'/fixtures/slopes-v2.json.gz')));
|
||||||
|
globalThis.legacyBeltSave=legacy;
|
||||||
|
run('deserializeWorld(legacyBeltSave.meta,legacyBeltSave.buffers)');
|
||||||
|
check('old v2 saves keep all wall slope ids and materials',run('Array.from({length:4},(_,k)=>{const b=6*BW+2+k*3;return blockP[b]===15+k && blockV[b]===RSTONE && blockIntegrity(b)===10;}).every(Boolean)'));
|
||||||
|
// A slope's seal can sit in the open corner of a neighbouring slope or under a grate or screen;
|
||||||
|
// taking both away must leave nothing behind (it used to leave an unremovable belt cell).
|
||||||
|
const strayCells="(()=>{let n=0;for(let i=0;i<N;i++){const m=mat[i];if((m===BELTL||m===BELTR||part[i]===P_FILTER||m===RSTONE)&&!blockP[((i/W|0)>>2)*BW+((i%W)>>2)]&&slopeSealOwner(i)<0)n++;}return n;})()";
|
||||||
|
for(const [first,second] of [['BP_FILTER_DOWN,128,10,12','BP_BELT_DOWN,0,10,11'],['BP_GRATE,0,10,12','BP_BELT_DOWN,0,10,11'],['BP_FILTER,0,10,12','BP_SLOPE_BL,RSTONE,10,11']]) for(const order of [0,1]) for(const built of [0,1]) {
|
||||||
|
fresh();
|
||||||
|
const [a,b]=built?[second,first]:[first,second];
|
||||||
|
run(`placeBlock(${a},true,{m:[],inv:0});placeBlock(${b},true,{m:[],inv:0});`);
|
||||||
|
run(order?'removeBlock(10,12);removeBlock(10,11);':'removeBlock(10,11);removeBlock(10,12);');
|
||||||
|
check(`removing ${first.split(',')[0]} and the ${second.split(',')[0]} seal inside it (built ${built}, removed ${order}) leaves no stray cell`,run(strayCells)===0,run(strayCells));
|
||||||
|
}
|
||||||
|
fresh();
|
||||||
|
run("setCell(40*W+43,BELTL,20,0);globalThis.straySave=serializeWorld();deserializeWorld(straySave.meta,straySave.buffers);");
|
||||||
|
check('loading a world clears a stray belt cell left by the old removal bug',run('mat[40*W+43]===EMPTY'));
|
||||||
|
done();
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
// Sloped walls: flow, sealed joints, building, refunds and persistence.
|
||||||
|
const { load, suite, flatWorld, rect, count, ticks } = require('./harness');
|
||||||
|
const { run } = load();
|
||||||
|
const { check, done } = suite();
|
||||||
|
// Wall selection drives the same saved slope codes and sealed funnel geometry.
|
||||||
|
const fs = require('fs');
|
||||||
|
run(fs.readFileSync('src/ui/selection.js', 'utf8'));
|
||||||
|
run(`const GAME = { pieceDir: {}, drag: null }; let hudDirty = false;
|
||||||
|
function aimBlock() { return { bx: 23, by: 23 }; }`);
|
||||||
|
function fresh() { flatWorld(run, 240, 192, 176); run('AMB.fill(20);temp.fill(20);'); }
|
||||||
|
function funnel(sloped, material) {
|
||||||
|
fresh();
|
||||||
|
run(`for (const [ax,ay,bx,by] of [[12,12,23,23],[36,12,25,23]]) {
|
||||||
|
GAME.drag={bx:ax,by:ay};
|
||||||
|
const code=${sloped ? 'pieceCode(PIECE_BY_ID.wall,{bx,by})' : 'BP_WALL'};
|
||||||
|
for(const [x,y] of blockLine(ax,ay,bx,by,true)) placeBlock(code,RSTONE,x,y,true);
|
||||||
|
} GAME.drag=null;`);
|
||||||
|
rect(run, 80, 20, 119, 29, material);
|
||||||
|
ticks(run, 1800);
|
||||||
|
const out = count(run, material, 0, 96, 239, 175);
|
||||||
|
check(`${sloped ? 'slope' : 'square'} funnel conserves 400 grains`, count(run, material) === 400);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
const sand = run('SAND'), water = run('WATER');
|
||||||
|
const slopeOut = funnel(true, sand), squareOut = funnel(false, sand);
|
||||||
|
check('all sand leaves the sloped funnel', slopeOut === 400, slopeOut);
|
||||||
|
check('square steps retain sand', squareOut < 400, squareOut);
|
||||||
|
check('all water leaves the sloped funnel', funnel(true, water) === 400);
|
||||||
|
for (const material of ['GRAVEL', 'CUORE', 'LDIRT']) check(`${material} slides down`, funnel(true, run(material)) === 400);
|
||||||
|
fresh();
|
||||||
|
run('for (let k=0;k<12;k++) placeBlock(BP_SLOPE_BL,RSTONE,12+k,12+k,true); spawnBar(BARCU,52,35);');
|
||||||
|
ticks(run, 600);
|
||||||
|
check('bars slide off the slope', run('ents.length === 1 && ents[0].iy > 96'), run('ents.map(e=>[e.ix,e.iy])'));
|
||||||
|
for (let c = 15; c <= 18; c++) {
|
||||||
|
fresh();
|
||||||
|
run(`bagAdd(RSTONE,100,true); bagAdd(BARCU,10,true);`);
|
||||||
|
const before = run('bag[RSTONE]');
|
||||||
|
run(`placeBlock(${c},RSTONE,20,20);`);
|
||||||
|
check(`slope ${c} costs half a wall`, before-run('bag[RSTONE]') === 8);
|
||||||
|
check(`slope ${c} has its triangle`, run('blockIntegrity(20*BW+20)') === 10);
|
||||||
|
run(`undoRecord('cut',()=>cutRegion(80,80,83,83)); undoLast();`);
|
||||||
|
check(`undo restores orientation ${c}`, run(`blockP[20*BW+20] === ${c}`));
|
||||||
|
check(`copy and flip preserve material and mirror ${c}`, run(`(()=>{ const bp=flipBlueprint(copyRegion(80,80,83,83)); return bp.blocks[0][2] === ${15+((c-15)^1)} && bp.blocks[0][3]===RSTONE; })()`));
|
||||||
|
run(`const snap${c}=serializeWorld(); deserializeWorld(snap${c}.meta,snap${c}.buffers);`);
|
||||||
|
check(`save restores orientation ${c}`, run(`blockP[20*BW+20] === ${c} && blockIntegrity(20*BW+20)===10`));
|
||||||
|
run(`pasteGhost(copyRegion(80,80,83,83),24,20); bot.x=96;bot.y=80;ghostTick();`);
|
||||||
|
check(`paste builds orientation ${c}`, run(`blockP[20*BW+24]===${c} && blockV[20*BW+24]===RSTONE`));
|
||||||
|
run('removeBlock(24,20);removeBlock(20,20)');
|
||||||
|
check(`slope ${c} refunds half a wall`, run('bag[RSTONE]') === before);
|
||||||
|
run(`placeBlock(${c},BARCU,20,20);removeBlock(20,20);`);
|
||||||
|
check(`metal slope ${c} refunds one bar`, run('bag[BARCU]') === 10);
|
||||||
|
}
|
||||||
|
check('wall diagonal snaps within 30 percent', run('JSON.stringify(blockLine(2,3,7,7,true))') === JSON.stringify([[2,3],[3,4],[4,5],[5,6],[6,7],[7,8]]));
|
||||||
|
check('belts stay straight', run('blockLine(2,3,7,8).every(p=>p[1]===3)'));
|
||||||
|
check('drag chooses below or above and ignores drag reversal', run('slopeCode(5,5,0)===15 && slopeCode(-5,-5,0)===15 && slopeCode(5,-5,0)===16 && slopeCode(5,5,2)===18 && slopeCode(5,-5,2)===17'));
|
||||||
|
// Probe every open cell along both sides, including all eleven joints. A grain may
|
||||||
|
// leave an end, so measure crossing only within the interior of the diagonal run.
|
||||||
|
for (let c = 15; c <= 18; c++) for (const m of ['SAND', 'WATER']) for (const side of [-1, 1]) {
|
||||||
|
fresh();
|
||||||
|
const descending = c === 15 || c === 18;
|
||||||
|
run(`for(let k=0;k<12;k++) placeBlock(${c},RSTONE,12+k,${descending ? '12+k' : '23-k'},true);`);
|
||||||
|
const leaked = run(`(()=>{
|
||||||
|
const sideAt=(x,y)=>${descending ? 'y-x' : 'x+y-143'};
|
||||||
|
let leaks=0, probes=0;
|
||||||
|
for(let y=52;y<92;y++) for(let x=52;x<92;x++) {
|
||||||
|
const i=y*W+x, d=sideAt(x,y);
|
||||||
|
if(mat[i]!==EMPTY || Math.sign(d)!==${side} || Math.abs(d)>2) continue;
|
||||||
|
setCell(i,${m},20,0); probes++;
|
||||||
|
// Exercise the real movement functions repeatedly without letting grains escape an end.
|
||||||
|
for(let k=0;k<20 && mat[i]===${m};k++) { tickStamp++; ${m === 'WATER' ? 'liquid(i,x,y,WATER)' : 'powder(i,x,y,SAND)'}; }
|
||||||
|
for(let yy=y;yy<=y+1;yy++) for(let xx=x-32;xx<=x+32;xx++) {
|
||||||
|
if(xx<0 || xx>=W)continue;
|
||||||
|
const j=yy*W+xx;
|
||||||
|
if(mat[j]===${m}) { if(Math.sign(sideAt(xx,yy))===${-side}) leaks++; setCell(j,EMPTY,20,0); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [leaks,probes];
|
||||||
|
})()`);
|
||||||
|
check(`slope ${c} seals ${m} from side ${side}`, leaked[0] === 0 && leaked[1] > 0, leaked);
|
||||||
|
}
|
||||||
|
for (const c of [15, 16]) {
|
||||||
|
fresh();
|
||||||
|
run(`for(let k=0;k<12;k++) placeBlock(${c},RSTONE,12+k,${c === 15 ? '12+k' : '23-k'},true);spawnBar(BARCU,${c === 15 ? 52 : 136},35);`);
|
||||||
|
ticks(run,600);
|
||||||
|
check(`bars reach the bottom of slope ${c}`,run('ents.length===1 && ents[0].iy>96'));
|
||||||
|
}
|
||||||
|
for(const c of [15,16,17,18]) {
|
||||||
|
fresh();
|
||||||
|
check(`slope ${c} supports every wall material`,run(`WALL_LIST.every((v,k)=>{ const bx=4+k*3;bagAdd(v,16,true);const before=bag[v];if(placeBlock(${c},v,bx,20))return false;removeBlock(bx,20);return bag[v]===before; })`));
|
||||||
|
}
|
||||||
|
fresh();
|
||||||
|
run('placeBlock(BP_SLOPE_BL,RSTONE,20,20,true);placeBlock(BP_WALL,RSTONE,20,21,true);removeBlock(20,21);');
|
||||||
|
check('removing an adjoining square wall restores the slope seal',run('mat[slopeSeal(BP_SLOPE_BL,20,20)]===STONE'));
|
||||||
|
run('removeBlock(20,20)');
|
||||||
|
check('removal leaves no free seal grains behind',count(run,run('STONE'),80,80,83,87)===0);
|
||||||
|
check('old saved slope codes select Wall', run("Array.from({length:4},(_,k)=>PIECE_BY_CODE[15+k].id).every(id=>id==='wall')"));
|
||||||
|
check('single Wall starts square', run('pieceCode(PIECE_BY_ID.wall)===BP_WALL'));
|
||||||
|
const shapes = [];
|
||||||
|
for (let k=0;k<5;k++) { run('turnSlope()'); shapes.push(run('pieceCode(PIECE_BY_ID.wall)')); }
|
||||||
|
check('T cycles square then four corners then square', JSON.stringify(shapes)===JSON.stringify([15,16,17,18,1]), shapes);
|
||||||
|
run('GAME.drag={bx:20,by:20}');
|
||||||
|
check('diagonal Wall defaults below', run('pieceCode(PIECE_BY_ID.wall)===BP_SLOPE_BL'));
|
||||||
|
run('turnSlope()');
|
||||||
|
check('T flips diagonal Wall above', run('pieceCode(PIECE_BY_ID.wall)===BP_SLOPE_TR'));
|
||||||
|
run('GAME.drag=null');
|
||||||
|
check('drag flip preserves single square choice',run('pieceCode(PIECE_BY_ID.wall)===BP_WALL'));
|
||||||
|
run('GAME.pieceDir.wall=3;GAME.drag={bx:20,by:23}');
|
||||||
|
check('straight Wall drag stays square even with a corner selected',run('pieceCode(PIECE_BY_ID.wall)===BP_WALL'));
|
||||||
|
run('GAME.drag=null;GAME.pieceDir.wall=1');
|
||||||
|
check('single Wall slope shows its half-block price',run('pieceCostNow(PIECE_BY_ID.wall,RSTONE)[0][1]===8'));
|
||||||
|
// Fixture saved before Wall absorbed the separate Sloped wall piece (save version 2).
|
||||||
|
const legacy = JSON.parse(require('zlib').gunzipSync(fs.readFileSync(__dirname+'/fixtures/slopes-v2.json.gz')));
|
||||||
|
globalThis.legacySlopeSave = legacy;
|
||||||
|
run('deserializeWorld(legacySlopeSave.meta,legacySlopeSave.buffers)');
|
||||||
|
check('old save loads all four slope corners unchanged',run('Array.from({length:4},(_,k)=>{const b=6*BW+2+k*3;return blockP[b]===15+k && blockV[b]===RSTONE && blockIntegrity(b)===10 && PIECE_BY_CODE[blockP[b]].id===\'wall\';}).every(Boolean)'));
|
||||||
|
delete globalThis.legacySlopeSave;
|
||||||
|
// Stickiness is grain against grain: on a built slope sticky clay lumps run as fast as sand.
|
||||||
|
const slide=m=>{flatWorld(run,256,224,208);run('for(let k=0;k<20;k++)placeBlock(BP_SLOPE_BL,RSTONE,10+k,10+k,true);');
|
||||||
|
return run(`(()=>{setCell(39*W+42,${m},20,0);for(let t=0;t<2000;t++){simTick();for(let i=0;i<N;i++)if(mat[i]===${m}&&i%W>=118)return t;}return -1;})()`);};
|
||||||
|
const sandT=slide('SAND'),clayT=slide('LCLAY');
|
||||||
|
check('clay lumps slide down a built slope as fast as sand',clayT>0 && clayT<=sandT+4,JSON.stringify([sandT,clayT]));
|
||||||
|
done();
|
||||||
@@ -35,6 +35,202 @@ with sync_playwright() as p:
|
|||||||
ev("SET.look = 'smooth'"); pg.wait_for_timeout(1500)
|
ev("SET.look = 'smooth'"); pg.wait_for_timeout(1500)
|
||||||
ev("SM.scale = 0.5"); pg.wait_for_timeout(1500)
|
ev("SM.scale = 0.5"); pg.wait_for_timeout(1500)
|
||||||
st = ev('[R.smoothOn, SM.scale, SM.canvas.width, R.canvas.width, SET.look, SM.ok, SM.why]'); check('half-resolution smooth still draws', (st[0] and st[2] == round(st[3] * st[1])) or st[1] > 0.5, st)
|
st = ev('[R.smoothOn, SM.scale, SM.canvas.width, R.canvas.width, SET.look, SM.ok, SM.why]'); check('half-resolution smooth still draws', (st[0] and st[2] == round(st[3] * st[1])) or st[1] > 0.5, st)
|
||||||
|
# Uniform colours isolate the solid/loose seam from material noise and lighting.
|
||||||
|
ground = ev('''() => {
|
||||||
|
GAME.paused = true; requestAnimationFrame = () => 0;
|
||||||
|
SET.lighting = 'off'; SET.glow = 'off'; SET.grain = 0; SET.zoom = 6;
|
||||||
|
resizeRender(); bot.x = 138; bot.y = 118; snapCamera();
|
||||||
|
for (let y = 60; y < 220; y++) for (let x = 40; x < 240; x++) {
|
||||||
|
const i = y * W + x;
|
||||||
|
setCell(i, STONE, 22, F_NAT); bgm[i] = 0; noiseB[i] = 128;
|
||||||
|
}
|
||||||
|
const fill = m => { for (let y = 100; y < 140; y++) for (let x = 100; x < 180; x++) setCell(y * W + x, m, 22); };
|
||||||
|
const at = (x, y) => { const c = R.px[(y - R.vy0) * R.bw + x - R.vx0]; return (c & 255) + (c >>> 8 & 255) + (c >>> 16 & 255); };
|
||||||
|
const out = {};
|
||||||
|
for (const lens of [false, true]) {
|
||||||
|
R.lens = lens; SET.look = 'pixel'; fill(RSTONE); drawWorld();
|
||||||
|
const edge = at(140, 140), inside = at(140, 142), loose = at(140, 130);
|
||||||
|
fill(STONE); drawWorld();
|
||||||
|
out[lens ? 'thermal' : 'pixel'] = [edge, inside, loose, at(140, 140)];
|
||||||
|
}
|
||||||
|
R.lens = false;
|
||||||
|
const i = 140 * W + 140;
|
||||||
|
flags[i] = 0; fill(RSTONE); drawWorld(); const built = at(140, 140);
|
||||||
|
fill(STONE); drawWorld(); out.built = [built, at(140, 140)]; flags[i] = F_NAT;
|
||||||
|
fill(RSTONE); SET.look = 'smooth'; SM.scale = 1; drawWorld();
|
||||||
|
const sample = (x, y) => {
|
||||||
|
const p = worldToScreenPx(x, y);
|
||||||
|
const c = R.ctx.getImageData(Math.floor(p.x), Math.floor(p.y), 1, 1).data;
|
||||||
|
return c[0] + c[1] + c[2];
|
||||||
|
};
|
||||||
|
out.smooth = [sample(140.5, 140.08), sample(140.5, 142.5)];
|
||||||
|
const packed = [];
|
||||||
|
for (let y = 120; y < 121; y += 1 / 6) for (let x = 140; x < 141; x += 1 / 6) packed.push(sample(x, y));
|
||||||
|
out.texture = Math.max(...packed) - Math.min(...packed);
|
||||||
|
return out;
|
||||||
|
}''')
|
||||||
|
for look in ['pixel', 'thermal']:
|
||||||
|
edge, inside, loose, solid = ground[look]
|
||||||
|
check(f'{look}: ground has a darker edge against loose grains', edge < inside * 0.85 and inside == solid, ground[look])
|
||||||
|
check(f'{look}: loose rubble is lighter than solid stone', loose > inside, ground[look])
|
||||||
|
check('built solids get no loose-grain outline', ground['built'][0] == ground['built'][1], ground['built'])
|
||||||
|
check('smooth: a thin darker seam separates ground and rubble', ground['smooth'][0] < ground['smooth'][1] * 0.9, ground['smooth'])
|
||||||
|
check('smooth: packed rubble keeps pebble shading', ground['texture'] > 30, ground['texture'])
|
||||||
|
wall = pg.evaluate('''() => {
|
||||||
|
GAME.paused = true; R.lens = false; SET.fog = false;
|
||||||
|
SET.lighting = 'off'; SET.glow = 'off'; SET.grain = false;
|
||||||
|
for(let y=20;y<150;y++)for(let x=40;x<240;x++)setCell(y*W+x,EMPTY,22,0);
|
||||||
|
for(let k=0;k<6;k++)placeBlock(BP_BELT_UP,1,25+k,25-k,true);
|
||||||
|
rebuildPower();slopeCleatsTick(false);prepareSlopeBelts(40,20,240,150);
|
||||||
|
let cell=-1;
|
||||||
|
for(let y=40;y<100 && cell<0;y++)for(let x=100;x<124;x++)
|
||||||
|
if(slopeDetailAt(y*W+x,x,y)===2){cell=y*W+x;break;}
|
||||||
|
if(cell<0)return null;
|
||||||
|
const x=cell%W,y=(cell/W)|0,control=80*W+180;
|
||||||
|
bot.x=118;bot.y=78;snapCamera();
|
||||||
|
setCell(cell,SAND,22,0);setCell(control,SAND,22,0);
|
||||||
|
noiseB[cell]=noiseB[control]=128;
|
||||||
|
const at=i=>R.px[(((i/W)|0)-R.vy0)*R.bw+i%W-R.vx0];
|
||||||
|
SET.look='pixel';drawWorld();const colors=[at(cell)&0xffffff,at(control)&0xffffff];
|
||||||
|
SET.look='smooth';drawWorld();const grainClass=at(cell)>>>24;
|
||||||
|
return {colors,grainClass};
|
||||||
|
}''')
|
||||||
|
check('sidewall stays behind the grain colour in pixels and smooth',
|
||||||
|
wall and wall['colors'][0] == wall['colors'][1] and wall['grainClass'] == 2, wall)
|
||||||
|
# Travel joins: edge elbows in diagonals, E elbows in straight receivers, otherwise collars.
|
||||||
|
pg.set_viewport_size({"width": 2100, "height": 1500})
|
||||||
|
ev("part.fill(0); blockP.fill(0); occ.fill(0); mach.fill(0); ents=[]; ghostBlocks.clear();")
|
||||||
|
ev("() => {" + (root / "tools/scenes/joins.js").read_text() + "}")
|
||||||
|
mouths = ev('''() => {
|
||||||
|
SET.zoom=4; SET.lighting="off"; SET.glow="off"; resizeRender();
|
||||||
|
bot.x=CAM[0]-bot.w/2;bot.y=CAM[1]-bot.h/2;snapCamera();
|
||||||
|
const now=performance.now.bind(performance),frozen=now();
|
||||||
|
performance.now=()=>frozen;
|
||||||
|
try {
|
||||||
|
const out={};
|
||||||
|
for(const look of ["pixel","smooth"]) {
|
||||||
|
SET.look=look;SM.scale=1;drawWorld();
|
||||||
|
const bad=[], copper=c=>c[0]>c[1]*1.15 && c[1]>c[2]*1.1;
|
||||||
|
const sample=(x,y)=>{const p=worldToScreenPx(x,y);return Array.from(R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data);};
|
||||||
|
for(const j of JOIN_CASES.filter(j=>j.code===BP_SCREW)) {
|
||||||
|
const slope=j.v===4 || j.v===6?-1:1, side=j.end?1:-1;
|
||||||
|
const joins=diagonalScrewJoins(j.bx,j.by,j.v);
|
||||||
|
if(!j.joined) {
|
||||||
|
if(!(joins & (64<<j.end)) || (joins>>(j.end*3)&7))bad.push(['missing free collar',j]);
|
||||||
|
// The middle of the free cap, safely inside its diagonal block.
|
||||||
|
const cap=look==='pixel' ? [[1.5,2.5],[2.5,1.5]] : [[1.8,1.8]];
|
||||||
|
const colors=cap.map(([x,y])=>sample(j.bx*4+2+side*x,j.by*4+2+side*slope*y));
|
||||||
|
if(!colors.some(copper))bad.push(['free collar colour',j,colors]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const ex=(j.kind>=5?j.nx:j.bx)*4+2,ey=(j.kind>=5?j.ny:j.by)*4+2;
|
||||||
|
const dx=j.vertical?0:side,dy=j.vertical?side*slope:0;
|
||||||
|
// Both walls on the outgoing axis, beyond the rounded meeting point.
|
||||||
|
for(const sign of [-1,1]) {
|
||||||
|
const x=ex+dx*1.5-dy*sign*1.6,y=ey+dy*1.5+dx*sign*1.6,c=sample(x,y);
|
||||||
|
if(!copper(c))bad.push(['open casing',j,x,y,c]);
|
||||||
|
}
|
||||||
|
// Follow both centrelines through the join: the bore must never show sky.
|
||||||
|
const ax=j.bx*4+2-side*1.5,ay=j.by*4+2-side*slope*1.5;
|
||||||
|
for(let t=0;t<=1;t+=0.05)for(const leg of [0,1]) {
|
||||||
|
const x=leg?ex+dx*t*5:ax+(ex-ax)*t,y=leg?ey+dy*t*5:ay+(ey-ay)*t,c=sample(x,y);
|
||||||
|
if(c[2]>c[0]*1.4 && c[2]>110)bad.push(['background inside tube',j,x,y,c]);
|
||||||
|
}
|
||||||
|
if(j.kind>=5) {
|
||||||
|
// The unused corner of the bent straight block must become background.
|
||||||
|
const x=j.nx*4+2+(j.vertical?side:-side)*1.8;
|
||||||
|
const y=j.ny*4+2+(j.vertical?-side*slope:side*slope)*1.8,c=sample(x,y);
|
||||||
|
if(copper(c) || c[2]<c[0]*1.2)bad.push(['square casing outside elbow',j,x,y,c]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out[look]=bad;
|
||||||
|
}return out;
|
||||||
|
} finally {performance.now=now;}
|
||||||
|
}''')
|
||||||
|
for look, bad in mouths.items():
|
||||||
|
check(f"{look}: shared screw joins have closed elbows, free collars and no bore holes", not bad, bad[:2])
|
||||||
|
previews = ev('''() => {
|
||||||
|
const bad=[], now=performance.now.bind(performance),frozen=now();
|
||||||
|
performance.now=()=>frozen;
|
||||||
|
try {
|
||||||
|
for(const look of ['pixel','smooth'])for(const j of JOIN_CASES.filter(j=>j.code===BP_SCREW && j.joined && j.kind>=5 && (j.v===5&&!j.vertical || j.v===4&&j.vertical))) {
|
||||||
|
SET.look=look;
|
||||||
|
const side=j.end?1:-1,slope=j.v===4||j.v===6?-1:1;
|
||||||
|
const ux=2+(j.vertical?side:-side)*1.8,uy=2+(j.vertical?-side*slope:side*slope)*1.8;
|
||||||
|
const b=j.by*BW+j.bx,nb=j.ny*BW+j.nx;
|
||||||
|
const plan=new Map([[b,[BP_SCREW,j.v]],[nb,[BP_SCREW,j.straight]]]);
|
||||||
|
removeBlock(j.bx,j.by);removeBlock(j.nx,j.ny);
|
||||||
|
const sample=(x,y)=>{const p=worldToScreenPx(j.nx*4+x,j.ny*4+y);return Array.from(R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data).join(',');};
|
||||||
|
SM.scale=1;drawWorld();const bg=sample(ux,uy),empty=sample(2,2);
|
||||||
|
for(const [n,piece] of plan)ghostBlocks.set(n,piece);
|
||||||
|
drawWorld();
|
||||||
|
if(sample(ux,uy)!==bg || sample(2,2)===empty)bad.push(['ghost elbow silhouette',look,j]);
|
||||||
|
ghostBlocks.clear();
|
||||||
|
const ctx=R.ctx,canvas=document.createElement('canvas');canvas.width=canvas.height=192;
|
||||||
|
try {
|
||||||
|
R.ctx=canvas.getContext('2d');R.ctx.fillStyle='#ffffff';
|
||||||
|
previewBlock(BP_SCREW,64,64,64,64,false,j.straight,j.nx,j.ny,plan);
|
||||||
|
const alpha=(x,y)=>R.ctx.getImageData(Math.floor(64+x*16),Math.floor(64+y*16),1,1).data[3];
|
||||||
|
if(alpha(ux,uy)!==0 || alpha(2,2)===0)bad.push(['planned preview elbow silhouette',look,j]);
|
||||||
|
} finally {R.ctx=ctx;}
|
||||||
|
placeBlock(BP_SCREW,j.v,j.bx,j.by,true);placeBlock(BP_SCREW,j.straight,j.nx,j.ny,true);
|
||||||
|
}
|
||||||
|
} finally {performance.now=now;ghostBlocks.clear();}
|
||||||
|
return bad;
|
||||||
|
}''')
|
||||||
|
check('both looks: ghosts and planned previews leave the unused elbow corner empty', not previews, previews[:2])
|
||||||
|
tree_tags = ev('''() => {
|
||||||
|
SET.look='smooth';SM.scale=1;
|
||||||
|
const now=performance.now.bind(performance),frozen=now();performance.now=()=>frozen;
|
||||||
|
try {
|
||||||
|
const j=JOIN_CASES.find(j=>j.v===5 && j.end===1 && !j.vertical && j.spot===2 && j.code===BP_SCREW);
|
||||||
|
const bx=j.nx+1,by=j.ny;
|
||||||
|
const sample=()=>{const p=worldToScreenPx(bx*4,by*4);return Array.from(R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),4*R.zp,4*R.zp).data).join(',');};
|
||||||
|
drawWorld();const plain=sample(),bad=[];
|
||||||
|
for(const m of [WOOD,LEAVES]) {
|
||||||
|
for(let y=by*4;y<by*4+4;y++)for(let x=bx*4;x<bx*4+4;x++)bgm[y*W+x]=m;
|
||||||
|
drawWorld();if(sample()!==plain)bad.push(m);
|
||||||
|
}
|
||||||
|
for(let y=by*4;y<by*4+4;y++)for(let x=bx*4;x<bx*4+4;x++)bgm[y*W+x]=0;
|
||||||
|
return bad;
|
||||||
|
} finally {performance.now=now;}
|
||||||
|
}''')
|
||||||
|
check('smooth: trees behind a straight screw cannot become elbow join bits', not tree_tags, tree_tags)
|
||||||
|
drops = ev("""() => {
|
||||||
|
const detail=slopeDetailAt,now=performance.now.bind(performance),frozen=now(),out={};
|
||||||
|
const j=JOIN_CASES.find(j=>j.v===4&&j.end===0&&j.code===BP_BELT&&j.spot===0);
|
||||||
|
try {
|
||||||
|
slopeDetailAt=()=>0;performance.now=()=>frozen;
|
||||||
|
const sample=()=>{const p=worldToScreenPx(j.bx*BS+0.4,j.by*BS+2.5);return Array.from(R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data);};
|
||||||
|
for(const look of ['pixel','smooth']) {
|
||||||
|
SET.look=look;SM.scale=1;drawWorld();const joined=sample();
|
||||||
|
removeBlock(j.nx,j.ny);drawWorld();const free=sample();
|
||||||
|
placeBlock(BP_BELT,j.straight,j.nx,j.ny,true);
|
||||||
|
out[look]=[joined,free];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
} finally {slopeDetailAt=detail;performance.now=now;}
|
||||||
|
}""")
|
||||||
|
for look, (joined, free) in drops.items():
|
||||||
|
check(f'{look}: the same-row uphill inlet is a drop, drawn like a free low end', joined == free, [joined, free])
|
||||||
|
filter_plans = ev("""() => {
|
||||||
|
const saved={...GAME},original=slopeBeltConnections,seen=[];
|
||||||
|
try {
|
||||||
|
GAME.tool=TOOLS.findIndex(t=>t.id==='build');GAME.pieceIdx=PIECES.findIndex(p=>p.id==='filter');
|
||||||
|
GAME.selDrag=null;GAME.paste=false;GAME.filterCfg={m:[],inv:0};GAME.pieceDir={filter:1};
|
||||||
|
slopeBeltConnections=(x,y,c,p)=>{
|
||||||
|
if(p)seen.push([c,p.get(y*BW+x)?.[1]]);
|
||||||
|
return original(x,y,c,p);
|
||||||
|
};
|
||||||
|
for(const grade of [-1,1]) {
|
||||||
|
GAME.drag={bx:50,by:30};GAME.aim={x:52*BS,y:(30+2*grade)*BS};
|
||||||
|
drawPreviews();
|
||||||
|
}
|
||||||
|
return seen;
|
||||||
|
} finally {slopeBeltConnections=original;Object.assign(GAME,saved);}
|
||||||
|
}""")
|
||||||
|
check('filter drag previews pass stored travel bits to the join rule',
|
||||||
|
bool(filter_plans) and all(v == (128 if c == ev('BP_FILTER_DOWN') else 0) for c, v in filter_plans), filter_plans)
|
||||||
# a browser without hardware drawing gets the pixel look and a note
|
# a browser without hardware drawing gets the pixel look and a note
|
||||||
pg2 = b.new_page(viewport={'width': 800, 'height': 500})
|
pg2 = b.new_page(viewport={'width': 800, 'height': 500})
|
||||||
pg2.add_init_script('localStorage.clear();')
|
pg2.add_init_script('localStorage.clear();')
|
||||||
|
|||||||
+3
-3
@@ -216,16 +216,16 @@ check('blown grains never come back into the bag', run('bag[SAND]') === 0 && cou
|
|||||||
|
|
||||||
// --- round 6: lifts, falling trees, machines without power, the bot and walls
|
// --- round 6: lifts, falling trees, machines without power, the bot and walls
|
||||||
fresh();
|
fresh();
|
||||||
// a shaft 16 wide, a column of up-tubes from its floor, turned sideways at the top to unload onto the ground
|
// a shaft 16 wide, a column of up-screws from its floor, turned sideways at the top to unload onto the ground
|
||||||
run('for (let y = 100; y < 132; y++) for (let x = 120; x < 128; x++) setCell(y*W+x, EMPTY, AMB[y], 0)');
|
run('for (let y = 100; y < 132; y++) for (let x = 120; x < 128; x++) setCell(y*W+x, EMPTY, AMB[y], 0)');
|
||||||
run('bot.x = 110; bot.y = 90;');
|
run('bot.x = 110; bot.y = 90;');
|
||||||
// unload well above the ground: a pile that reaches the outlet would block it
|
// unload well above the ground: a pile that reaches the outlet would block it
|
||||||
run('for (let by = 19; by <= 32; by++) placeBlock(BP_TUBE, 0, 30, by, true); for (let bx = 30; bx <= 35; bx++) placeBlock(BP_TUBE, 1, bx, 18, true);');
|
run('for (let by = 19; by <= 32; by++) placeBlock(BP_SCREW, 0, 30, by, true); for (let bx = 30; bx <= 35; bx++) placeBlock(BP_SCREW, 1, bx, 18, true);');
|
||||||
box(120, 116, 127, 131, run('SAND'));
|
box(120, 116, 127, 131, run('SAND'));
|
||||||
const sand0 = count(run, run('SAND'));
|
const sand0 = count(run, run('SAND'));
|
||||||
ticks(run, 1800);
|
ticks(run, 1800);
|
||||||
const lifted = count(run, run('SAND'), 136, 0, 239, 99);
|
const lifted = count(run, run('SAND'), 136, 0, 239, 99);
|
||||||
check('a column of upward screw tubes lifts sand out of a shaft, unpowered', lifted > sand0 * 0.8, `${lifted} of ${sand0} grains brought up and out`);
|
check('a column of upward screw conveyors lifts sand out of a shaft, unpowered', lifted > sand0 * 0.8, `${lifted} of ${sand0} grains brought up and out`);
|
||||||
fresh();
|
fresh();
|
||||||
run('for (let bx = 25; bx <= 30; bx++) placeBlock(BP_BELT, 1, bx, 22, true); rebuildPower();');
|
run('for (let bx = 25; bx <= 30; bx++) placeBlock(BP_BELT, 1, bx, 22, true); rebuildPower();');
|
||||||
box(101, 84, 104, 87, run('SAND'));
|
box(101, 84, 104, 87, run('SAND'));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Throughput of belts and screw tubes: grains delivered per second. Whatever lands
|
// Throughput of belts and screw conveyors: grains delivered per second. Whatever lands
|
||||||
// past the outlet is taken away, so the output pile never backs up into the line.
|
// past the outlet is taken away, so the output pile never backs up into the line.
|
||||||
const { load, flatWorld, rect, ticks, count } = require('../../test/harness');
|
const { load, flatWorld, rect, ticks, count } = require('../../test/harness');
|
||||||
const { run } = load();
|
const { run } = load();
|
||||||
@@ -11,18 +11,18 @@ function rate(x0, x1, y0, y1) {
|
|||||||
for (let k = 0; k < 300; k++) run(`simTick(); ${sink}`);
|
for (let k = 0; k < 300; k++) run(`simTick(); ${sink}`);
|
||||||
return run('__got') / 5;
|
return run('__got') / 5;
|
||||||
}
|
}
|
||||||
// sideways tube fed by a walled hopper sitting on its first two blocks
|
// sideways screw fed by a walled hopper sitting on its first two blocks
|
||||||
fresh();
|
fresh();
|
||||||
run('for (let bx = 20; bx < 30; bx++) placeBlock(BP_TUBE, 1, bx, 20, true);');
|
run('for (let bx = 20; bx < 30; bx++) placeBlock(BP_SCREW, 1, bx, 20, true);');
|
||||||
run('for (let y = 0; y < 80; y++) { setCell(y*W+79, STONE, AMB[y], 0); setCell(y*W+96, STONE, AMB[y], 0); }');
|
run('for (let y = 0; y < 80; y++) { setCell(y*W+79, STONE, AMB[y], 0); setCell(y*W+96, STONE, AMB[y], 0); }');
|
||||||
rect(run, 80, 0, 95, 79, run('SAND'));
|
rect(run, 80, 0, 95, 79, run('SAND'));
|
||||||
res.tubeSideways = rate(120, 319, 84, 119);
|
res.screwSideways = rate(120, 319, 84, 119);
|
||||||
res.tubeLanes = run('(() => { const s = new Set(); for (let y = 80; y < 84; y++) for (let x = 100; x < 120; x++) if (mat[y*W+x] === SAND) s.add(y); return s.size; })()');
|
res.screwLanes = run('(() => { const s = new Set(); for (let y = 80; y < 84; y++) for (let x = 100; x < 120; x++) if (mat[y*W+x] === SAND) s.add(y); return s.size; })()');
|
||||||
// lift: a column of up-tubes in a pit of sand, turned sideways at the top
|
// lift: a column of up-screws in a pit of sand, turned sideways at the top
|
||||||
fresh();
|
fresh();
|
||||||
run('for (let y = 120; y < 196; y++) for (let x = 90; x < 130; x++) setCell(y*W+x, EMPTY, AMB[y], 0)');
|
run('for (let y = 120; y < 196; y++) for (let x = 90; x < 130; x++) setCell(y*W+x, EMPTY, AMB[y], 0)');
|
||||||
rect(run, 90, 130, 129, 195, run('SAND'));
|
rect(run, 90, 130, 129, 195, run('SAND'));
|
||||||
run('for (let by = 20; by <= 37; by++) placeBlock(BP_TUBE, 0, 26, by, true); for (let bx = 26; bx <= 40; bx++) placeBlock(BP_TUBE, 1, bx, 19, true);');
|
run('for (let by = 20; by <= 37; by++) placeBlock(BP_SCREW, 0, 26, by, true); for (let bx = 26; bx <= 40; bx++) placeBlock(BP_SCREW, 1, bx, 19, true);');
|
||||||
res.lift = rate(164, 319, 80, 119);
|
res.lift = rate(164, 319, 80, 119);
|
||||||
res.liftLanes = run('(() => { const s = new Set(); for (let y = 76; y < 80; y++) for (let x = 120; x < 160; x++) if (mat[y*W+x] === SAND) s.add(y); return s.size; })()');
|
res.liftLanes = run('(() => { const s = new Set(); for (let y = 76; y < 80; y++) for (let x = 120; x < 160; x++) if (mat[y*W+x] === SAND) s.add(y); return s.size; })()');
|
||||||
// belt: 12 blocks, a pile dropped on its back end
|
// belt: 12 blocks, a pile dropped on its back end
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// Many loaded slopes, with a fresh deterministic world for each timing sample.
|
||||||
|
const { performance } = require('perf_hooks');
|
||||||
|
const { load, flatWorld } = require(process.env.DG_HARNESS || '../../test/harness');
|
||||||
|
const { run } = load();
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const samples=[];
|
||||||
|
for(let sample=0;sample<6;sample++) {
|
||||||
|
flatWorld(run,1024,512,496);
|
||||||
|
run(`RS=7;money=1e9;AMB.fill(20);temp.fill(20);hot.fill(0);
|
||||||
|
for(let row=0;row<6;row++)for(let col=0;col<8;col++)for(let k=0;k<12;k++){
|
||||||
|
const bx=col*30+4+k,by=row*18+35-k;
|
||||||
|
placeBlock(BP_BELT_UP,1,bx,by,true);
|
||||||
|
for(let x=bx*4;x<bx*4+4;x++){const y=beltFaceY(BP_BELT_UP,bx,by,x);for(let h=1;h<=4;h++)setCell((y-h)*W+x,SAND,20,0);}
|
||||||
|
}
|
||||||
|
for(let k=0;k<60;k++)simTick();`);
|
||||||
|
const start=performance.now();run('for(let k=0;k<240;k++)simTick();');
|
||||||
|
if(sample) samples.push((performance.now()-start)/240);
|
||||||
|
}
|
||||||
|
assert(samples.length===5 && samples.every(n=>Number.isFinite(n) && n>0));
|
||||||
|
samples.sort((a,b)=>a-b);
|
||||||
|
console.log(`576 loaded slope blocks, median ${samples[2].toFixed(3)} ms/tick; samples ${samples.map(v=>v.toFixed(3)).join(', ')}`);
|
||||||
|
|
||||||
|
// No slopes: use the zero flag supplied once per chunk by simMove. Older powder ignores it.
|
||||||
|
flatWorld(run,512,384,376);
|
||||||
|
run('RS=7;for(let y=32;y<350;y+=2)for(let x=32;x<480;x++){mat[y*W+x]=SAND;mat[(y+1)*W+x]=STONE;}');
|
||||||
|
const powderSamples=[];
|
||||||
|
for(let sample=0;sample<6;sample++) {
|
||||||
|
const start=performance.now();
|
||||||
|
run('for(let k=0;k<30;k++)for(let y=32;y<350;y+=2)for(let x=32;x<480;x++)powder(y*W+x,x,y,SAND,0);');
|
||||||
|
if(sample)powderSamples.push(performance.now()-start);
|
||||||
|
}
|
||||||
|
assert(powderSamples.length===5 && powderSamples.every(n=>Number.isFinite(n) && n>0));
|
||||||
|
powderSamples.sort((a,b)=>a-b);
|
||||||
|
console.log(`2.14M powder calls without cleats, median ${powderSamples[2].toFixed(2)} ms`);
|
||||||
|
|
||||||
|
// Generated terrain plus a replenished falling sand curtain, with zero or 50 slope blocks.
|
||||||
|
for (const slopes of [0, 50]) {
|
||||||
|
const times = [];
|
||||||
|
for (let sample = 0; sample < 6; sample++) {
|
||||||
|
run(`generateWorld(12345, 1024, 640); weather.next=1e15; money=1e9;
|
||||||
|
for(let y=16;y<160;y++)for(let x=80;x<480;x++)setCell(y*W+x,EMPTY,AMB[y],0);
|
||||||
|
for(let k=0;k<${slopes};k++) {const bx=25+(k%10),by=32+Math.floor(k/10)*5-(k%10);
|
||||||
|
placeBlock(BP_BELT_UP,1,bx,by,true);}
|
||||||
|
for(let t=0;t<60;t++){for(let x=80;x<480;x++)if(mat[16*W+x]===EMPTY)setCell(16*W+x,SAND,AMB[16],0);simTick();}`);
|
||||||
|
const start = performance.now();
|
||||||
|
run(`for(let t=0;t<180;t++){for(let x=80;x<480;x++)if(mat[16*W+x]===EMPTY)setCell(16*W+x,SAND,AMB[16],0);simTick();}`);
|
||||||
|
if (sample) times.push((performance.now()-start)/180);
|
||||||
|
}
|
||||||
|
assert(times.every(n=>Number.isFinite(n) && n>0)); times.sort((a,b)=>a-b);
|
||||||
|
console.log(`generated falling world, ${slopes} slopes: ${times[2].toFixed(3)} ms/tick; ${times.map(n=>n.toFixed(3)).join(', ')}`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Reproducible generous-feed throughput and settled pile angles for t42.
|
||||||
|
const { load, flatWorld } = require(process.env.DG_HARNESS || '../../test/harness');
|
||||||
|
const { run } = load();
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
for (const material of ['SAND','GRAVEL','CUORE','COAL']) {
|
||||||
|
const rates=[];
|
||||||
|
for (const grade of [0,-1]) {
|
||||||
|
flatWorld(run,256,224,208);
|
||||||
|
run(`RS=7;money=1e6;AMB.fill(20);temp.fill(20);hot.fill(0);
|
||||||
|
for(let k=0;k<10;k++)placeBlock(${grade?'BP_BELT_UP':'BP_BELT'},1,16+k,${grade?32:24}+k*${grade},true);
|
||||||
|
for(let x=10;x<16;x++)placeBlock(BP_BELT,1,x,${grade?33:24},true);
|
||||||
|
for(let x=26;x<32;x++)placeBlock(BP_BELT,1,x,${grade?23:24},true);`);
|
||||||
|
const result=run(`(()=>{let out=0;for(let k=0;k<1420;k++){
|
||||||
|
for(let l=1;l<=4;l++){const i=(${grade?132:96}-l)*W+44;if(mat[i]===EMPTY)setCell(i,${material},20,0);}
|
||||||
|
simTick();
|
||||||
|
for(let i=0;i<N;i++)if(mat[i]===${material} && i%W>=128){if(k>=220)out++;setCell(i,EMPTY,20,0);}
|
||||||
|
for(let j=flyers.length-1;j>=0;j--)if(flyers[j].m===${material} && flyers[j].x>=128){if(k>=220)out++;flyers.splice(j,1);}
|
||||||
|
}return out/20;})()`);
|
||||||
|
rates.push(result);
|
||||||
|
}
|
||||||
|
flatWorld(run,256,224,208);
|
||||||
|
run(`RS=7;AMB.fill(20);temp.fill(20);hot.fill(0);
|
||||||
|
for(let y=120;y<160;y++)for(let x=118;x<138;x++)setCell(y*W+x,${material},20,0);
|
||||||
|
for(let k=0;k<1200;k++)simTick();`);
|
||||||
|
// Apparent settled angle: heap height divided by half its footprint width.
|
||||||
|
const angle=run(`(()=>{let top=H,left=W,right=0;for(let i=0;i<N;i++)if(mat[i]===${material}){top=Math.min(top,(i/W)|0);left=Math.min(left,i%W);right=Math.max(right,i%W);}return Math.atan2(208-top,(right-left+1)/2)*180/Math.PI;})()`);
|
||||||
|
assert(rates[1]>=60 && rates[1]<=rates[0]*1.1, `${material} slope rate outside the target`);
|
||||||
|
console.log(`${material}: flat ${rates[0].toFixed(1)}, uphill ${rates[1].toFixed(1)} grains/s; pile angle ${angle.toFixed(1)} degrees`);
|
||||||
|
}
|
||||||
@@ -8,8 +8,8 @@ for (let x = (bx - 3) * BS; x < (bx + 27) * BS; x++) surfY[x] = (by + 4) * BS;
|
|||||||
const mo = addMold('ingot', bx, by - 4, RSTONE, true);
|
const mo = addMold('ingot', bx, by - 4, RSTONE, true);
|
||||||
const beltY = Math.ceil((mo.y + mo.h) / BS) + 1;
|
const beltY = Math.ceil((mo.y + mo.h) / BS) + 1;
|
||||||
for (let x = bx; x < bx + 9; x++) placeBlock(BP_BELT, 1, x, beltY, true);
|
for (let x = bx; x < bx + 9; x++) placeBlock(BP_BELT, 1, x, beltY, true);
|
||||||
for (let y = by - 11; y < beltY; y++) placeBlock(BP_TUBE, 0, bx + 9, y, true);
|
for (let y = by - 11; y < beltY; y++) placeBlock(BP_SCREW, 0, bx + 9, y, true);
|
||||||
for (let x = bx + 9; x < bx + 16; x++) placeBlock(BP_TUBE, 1, x, by - 12, true);
|
for (let x = bx + 9; x < bx + 16; x++) placeBlock(BP_SCREW, 1, x, by - 12, true);
|
||||||
for (let x = bx + 16; x < bx + 25; x++) placeBlock(BP_BELT, 1, x, by - 11, true);
|
for (let x = bx + 16; x < bx + 25; x++) placeBlock(BP_BELT, 1, x, by - 11, true);
|
||||||
placeBlock(BP_WALL, RSTONE, bx + 25, by - 12, true);
|
placeBlock(BP_WALL, RSTONE, bx + 25, by - 12, true);
|
||||||
mo.cavs[0].forEach(i => setCell(i, MCU, 1090, 0));
|
mo.cavs[0].forEach(i => setCell(i, MCU, 1090, 0));
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// python3 tools/shot.py /tmp/belt-heap.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/belt-heap.js)" --ticks 240 --zoom 7
|
// python3 tools/shot.py /tmp/belt-heap.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/belt-heap.js)" --ticks 240 --zoom 7
|
||||||
const bx = BX + 6, by = BY;
|
const bx = BX + 6, by = BY;
|
||||||
for (let x = bx; x < bx + 12; x++) placeBlock(BP_BELT, 1, x, by, true);
|
for (let x = bx; x < bx + 12; x++) placeBlock(BP_BELT, 1, x, by, true);
|
||||||
for (let y = by - 10; y <= by; y++) placeBlock(BP_TUBE, 0, bx + 12, y, true);
|
for (let y = by - 10; y <= by; y++) placeBlock(BP_SCREW, 0, bx + 12, y, true);
|
||||||
for (let y = by * 4 - 30; y < by * 4; y++) for (let x = (bx + 2) * 4; x < (bx + 10) * 4; x++)
|
for (let y = by * 4 - 30; y < by * 4; y++) for (let x = (bx + 2) * 4; x < (bx + 10) * 4; x++)
|
||||||
setCell(y * W + x, y >= by * 4 - 6 ? SAND : CUORE, AMB[y], 0);
|
setCell(y * W + x, y >= by * 4 - 6 ? SAND : CUORE, AMB[y], 0);
|
||||||
window.CAM = [(bx + 7) * 4, (by - 5) * 4];
|
window.CAM = [(bx + 7) * 4, (by - 5) * 4];
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// Travel join grid: UR/DR/DL/UL, both ends, and all three neighbouring placements.
|
||||||
|
// Screw H and V, then belt H; each tries the side, above/below, and E.
|
||||||
|
// Self-contained, paused and empty. Use --zoom 4 --viewport 2100x1500 --no-light.
|
||||||
|
money = 50000;
|
||||||
|
for (let y = 0; y < 460; y++) for (let x = 0; x < 600; x++) {
|
||||||
|
const i = y * W + x;
|
||||||
|
setCell(i, EMPTY, 20, 0); bgm[i] = 0;
|
||||||
|
}
|
||||||
|
for (let x = 0; x < 600; x++) surfY[x] = 420;
|
||||||
|
window.JOIN_CASES = [];
|
||||||
|
for (let row = 0; row < 8; row++) for (let col = 0; col < 9; col++) {
|
||||||
|
const v = 4 + (row >> 1), end = row & 1, side = end ? 1 : -1;
|
||||||
|
const slope = v === 4 || v === 6 ? -1 : 1, dy = side * slope;
|
||||||
|
const vertical = col >= 3 && col < 6, belt = col >= 6, spot = col % 3;
|
||||||
|
const bx = 34 + col * 12, by = 24 + row * 10;
|
||||||
|
const nx = bx + (spot === 1 ? 0 : side), ny = by + (spot === 0 ? 0 : dy);
|
||||||
|
const code = belt ? BP_BELT : BP_SCREW;
|
||||||
|
const [tx, ty] = SCREW_DIRS[v];
|
||||||
|
const straight = belt ? (tx > 0 ? 1 : 0) : vertical ? (ty < 0 ? 0 : 2) : (tx > 0 ? 1 : 3);
|
||||||
|
const diagonalCode = belt ? (slope < 0 ? BP_BELT_UP : BP_BELT_DOWN) : BP_SCREW;
|
||||||
|
const diagonalV = belt ? (SCREW_DIRS[v][0] > 0 ? 1 : 0) : v;
|
||||||
|
for (let k = 0; k < 2; k++) placeBlock(diagonalCode, diagonalV, bx - k * side, by - k * dy, true);
|
||||||
|
for (let k = 0; k < 2; k++) placeBlock(code, straight, nx + (vertical ? 0 : k * side * (spot === 1 ? -1 : 1)), ny + (vertical ? k * dy * (spot === 0 ? -1 : 1) : 0), true);
|
||||||
|
const joined = endJoin(bx, by, end) >= 0;
|
||||||
|
const kind = joined ? (ny === by ? (end ? 2 : 4) : nx === bx ? (ny < by ? 1 : 3) : vertical ? 6 : 5) : 0;
|
||||||
|
JOIN_CASES.push({v, end, bx, by, nx, ny, vertical, code, straight, joined, kind, spot});
|
||||||
|
}
|
||||||
|
updateRoof();
|
||||||
|
window.CAM = [326, 232];
|
||||||
|
// Labels are part of the scene so screenshots and browser checks use the same layout.
|
||||||
|
drawBot = () => {
|
||||||
|
const ctx = R.ctx;
|
||||||
|
ctx.save(); ctx.font = '12px monospace'; ctx.textBaseline = 'bottom';
|
||||||
|
for (const j of JOIN_CASES) {
|
||||||
|
const p = worldToScreenPx(j.bx * BS - 8, j.by * BS - 14);
|
||||||
|
ctx.fillStyle = '#17222c';
|
||||||
|
const name = ['UR', 'DR', 'DL', 'UL'][j.v - 4], axis = j.code === BP_BELT ? 'belt' : j.vertical ? 'V' : 'H';
|
||||||
|
ctx.fillText(`${name} ${['↗','↘','↙','↖'][j.v-4]} ${j.end ? 'R' : 'L'} ${axis} ${j.vertical ? (SCREW_DIRS[j.v][1]<0?'↑':'↓') : (SCREW_DIRS[j.v][0]>0?'→':'←')} ${['row', 'axis', 'diag'][j.spot]}: ${j.joined ? 'JOIN' : 'free'}`, p.x, p.y);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
};
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
// Run after clear-sky.js; leave paused so the open gate keeps its demonstration charge.
|
// Run after clear-sky.js; leave paused so the open gate keeps its demonstration charge.
|
||||||
for (let k = 0; k < 3; k++) {
|
for (let k = 0; k < 5; k++) {
|
||||||
const bx = BX + 10 + k * 7, by = BY - 5;
|
const bx = BX + 5 + k * 6, by = BY - 5;
|
||||||
placeMachineAt('silo', 0, bx, by);
|
placeMachineAt('silo', 0, bx, by);
|
||||||
const mm = machines.find(m => m && m.type === 'silo' && m.x === bx * 4 && m.y === by * 4);
|
const mm = machines.find(m => m && m.type === 'silo' && m.x === bx * 4 && m.y === by * 4);
|
||||||
mm.sm = k === 2 ? CUORE : SAND; mm.sn = k === 0 ? 0 : Math.floor(SILO_CAP * (k === 1 ? 0.5 : 0.95));
|
mm.sm = k === 2 ? CUORE : k === 3 ? SNOW : COAL;
|
||||||
|
if (k === 1) mm.sm = SAND;
|
||||||
|
mm.sn = [0, 2000, 3800, 2800, 1][k];
|
||||||
mm.open = k === 2;
|
mm.open = k === 2;
|
||||||
}
|
}
|
||||||
window.CAM = [(BX + 18.5) * 4, (BY - 2) * 4];
|
window.CAM = [(BX + 18.5) * 4, (BY - 2) * 4];
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Loaded sidewall conveyor and screen. Use --js 'slopeScene(false)' (belt) or 'slopeScene(true)' (screen).
|
||||||
|
globalThis.slopeScene = function(screen = false) {
|
||||||
|
const bx = Math.floor((SX - 100) / BS), by = Math.floor((SY + 12) / BS);
|
||||||
|
const left = (bx - 12) * BS, right = (bx + 24) * BS;
|
||||||
|
for(let y=by*BS-64;y<by*BS+64;y++)for(let x=left;x<right;x++) {
|
||||||
|
const i=y*W+x;bgm[i]=0;setCell(i,y>=by*BS+56?STONE:EMPTY,20,0);
|
||||||
|
}
|
||||||
|
for(let x=left;x<right;x++){surfY[x]=by*BS+56;dirtY[x]=by*BS+64;}
|
||||||
|
money=1e6;weather.next=1e15;
|
||||||
|
const startY=screen?by-8:by, grade=screen?1:-1;
|
||||||
|
for(let k=0;k<8;k++)placeBlock(screen?BP_FILTER_DOWN:BP_BELT_UP,screen?128:1,bx+k,startY+grade*k,true,{m:[SAND],inv:0});
|
||||||
|
const inlet=startY+(screen?0:1),outlet=startY+grade*7+(screen?1:0);
|
||||||
|
for(let k=1;k<=5;k++)placeBlock(BP_BELT,1,bx-k,inlet,true);
|
||||||
|
for(let k=8;k<13;k++)placeBlock(BP_BELT,1,bx+k,outlet,true);
|
||||||
|
for(let t=0;t<300;t++) {
|
||||||
|
for(let h=1;h<=4;h++){const i=(inlet*BS-h)*W+(bx-4)*BS;if(mat[i]===EMPTY)setCell(i,screen && (t&2)?GRAVEL:SAND,20,0);}
|
||||||
|
simTick();
|
||||||
|
}
|
||||||
|
// Keep loose rejects and passing grains visible underneath the screen.
|
||||||
|
CAM=[(bx+3)*BS,(startY+grade*4)*BS+4];updateRoof();
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Downhill screen whose outlet belt sits level with its last block (the jam the owner saw).
|
||||||
|
globalThis.jointScene = function(level = true) {
|
||||||
|
const bx = Math.floor((SX - 100) / BS), by = Math.floor((SY + 12) / BS) - 8;
|
||||||
|
for(let y=by*BS-64;y<by*BS+96;y++)for(let x=(bx-12)*BS;x<(bx+24)*BS;x++){const i=y*W+x;bgm[i]=0;setCell(i,y>=by*BS+88?STONE:EMPTY,20,0);}
|
||||||
|
money=1e6;weather.next=1e15;
|
||||||
|
for(let k=0;k<5;k++)placeBlock(BP_FILTER_DOWN,128,bx+k,by+k,true,{m:[GRAVEL],inv:0});
|
||||||
|
for(let k=1;k<=5;k++)placeBlock(BP_BELT,1,bx-k,by,true);
|
||||||
|
for(let k=5;k<10;k++)placeBlock(BP_BELT,1,bx+k,by+4+(level?0:1),true);
|
||||||
|
for(let t=0;t<900;t++){for(let h=1;h<=6;h++){const i=(by*BS-h)*W+(bx-4)*BS+(t%3);if(mat[i]===EMPTY)setCell(i,COAL,20,0);}simTick();}
|
||||||
|
CAM=[(bx+4)*BS,(by+3)*BS];updateRoof();
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Paused dug pocket: copper ore, rubble and dirt against stone, granite and dirt.
|
||||||
|
// Use --js "..." to set daylight or R.lens after this setup; CAM frames the pocket.
|
||||||
|
const cx = 360, cy = 300;
|
||||||
|
for (let y = 0; y < 480; y++) for (let x = 100; x < 620; x++) {
|
||||||
|
const dx = x - cx, dy = y - cy;
|
||||||
|
const floor = 20 + Math.round(3 * Math.sin(dx * 0.11));
|
||||||
|
const wall = Math.abs(dx) > 68 || dy < -42 || dy >= floor;
|
||||||
|
const ground = dx < -23 ? STONE : dx < 23 ? GRANITE : DIRT;
|
||||||
|
let m = wall ? ground : EMPTY;
|
||||||
|
if (!wall && dy > floor - 11 - Math.round(3 * Math.sin(dx * 0.18))) {
|
||||||
|
m = dx < -23 ? RSTONE : dx < 23 ? CUORE : LDIRT;
|
||||||
|
if ((x * 7 + y * 11) % 19 === 0) m = CUORE;
|
||||||
|
}
|
||||||
|
const i = y * W + x;
|
||||||
|
bgm[i] = 0;
|
||||||
|
setCell(i, m, 22, wall ? F_NAT : 0);
|
||||||
|
}
|
||||||
|
for (let x = 100; x < 620; x++) {
|
||||||
|
surfY[x] = 100; dirtY[x] = 180; graniteY[x] = 400;
|
||||||
|
}
|
||||||
|
updateRoof();
|
||||||
|
window.CAM = [cx, cy];
|
||||||
|
SET.daylight = 0;
|
||||||
|
SET.lighting = 'full';
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// Run after clear-sky.js: a belt takes sand from a pile, up a diagonal screw to an upper belt.
|
||||||
|
const tx = BX + 12, ty = BY - 1;
|
||||||
|
for (let x = BX + 3; x < tx; x++) placeBlock(BP_BELT, 1, x, BY, true);
|
||||||
|
for (let k = 0; k < 10; k++) placeBlock(BP_SCREW, 4, tx + k, ty - k, true);
|
||||||
|
for (let x = tx + 10; x < tx + 18; x++) placeBlock(BP_BELT, 1, x, ty - 9, true);
|
||||||
|
for (let y = BY * 4 - 22; y < BY * 4; y++) for (let x = (BX + 4) * 4; x < (BX + 11) * 4; x++) setCell(y * W + x, SAND, 20, 0);
|
||||||
|
window.CAM = [(tx + 4) * 4, (ty - 4) * 4];
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Run after clear-sky.js. Paused, empty pipes make casing, flights and mouths easy to inspect.
|
||||||
|
const jx = BX + 5, jy = BY - 3;
|
||||||
|
for (let k = 1; k <= 4; k++) placeBlock(BP_SCREW, 0, jx, jy + k, true);
|
||||||
|
for (let k = 0; k < 6; k++) placeBlock(BP_SCREW, 4, jx + k, jy - k, true);
|
||||||
|
for (let k = 1; k <= 4; k++) placeBlock(BP_SCREW, 0, jx + 5, jy - 5 - k, true);
|
||||||
|
const hx = BX + 21, hy = BY - 3;
|
||||||
|
for (let k = 1; k <= 4; k++) placeBlock(BP_BELT, 1, hx - k, hy, true);
|
||||||
|
for (let k = 0; k < 6; k++) placeBlock(BP_SCREW, 4, hx + k, hy - k, true);
|
||||||
|
for (let k = 1; k <= 4; k++) placeBlock(BP_BELT, 1, hx + 5 + k, hy - 5, true);
|
||||||
|
for (let k = 0; k < 4; k++) placeBlock(BP_SCREW, 5, BX + 32 + k, BY - 10 + k, true);
|
||||||
|
window.JOINTS = {
|
||||||
|
verticalIn: [jx * 4 + 2, jy * 4 + 4],
|
||||||
|
verticalOut: [(jx + 5) * 4 + 2, (jy - 5) * 4],
|
||||||
|
beltIn: [hx * 4, hy * 4 + 2],
|
||||||
|
beltOut: [(hx + 6) * 4, (hy - 5) * 4 + 2],
|
||||||
|
free: [(BX + 35) * 4 + 4, (BY - 7) * 4 + 4]
|
||||||
|
};
|
||||||
|
window.CAM = [(BX + 20) * 4, (BY - 5) * 4];
|
||||||
|
|
||||||
|
// Keep the camera-following bot out of the joint close-ups.
|
||||||
|
drawBot = () => {};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// A twelve-block funnel, one block open at the bottom, draining 400 grains of sand.
|
||||||
|
const fx = Math.floor((SX - 150) / BS), fy = Math.floor((SY - 36) / BS);
|
||||||
|
const cx = (fx + 12) * BS + 2, floor = (fy + 25) * BS;
|
||||||
|
for (let y = fy * BS - 45; y < floor + 8; y++) for (let x = fx * BS - 20; x < (fx + 25) * BS + 20; x++) {
|
||||||
|
const i = y * W + x;
|
||||||
|
bgm[i] = 0;
|
||||||
|
setCell(i, y >= floor ? STONE : EMPTY, 20, y >= floor ? F_NAT : 0);
|
||||||
|
}
|
||||||
|
for (let x = fx * BS - 20; x < (fx + 25) * BS + 20; x++) surfY[x] = floor;
|
||||||
|
bot.x = cx; bot.y = fy * BS + 24;
|
||||||
|
bagAdd(RBRICK, 192, true);
|
||||||
|
GAME.pieceDir.wall = 0;
|
||||||
|
for (const [ax, ay, bx, by] of [[fx, fy, fx + 11, fy + 11], [fx + 24, fy, fx + 13, fy + 11]]) {
|
||||||
|
GAME.drag = { bx: ax, by: ay };
|
||||||
|
placeBlocks(pieceCode(PIECE_BY_ID.wall, { bx, by }), RBRICK, blockLine(ax, ay, bx, by, true));
|
||||||
|
}
|
||||||
|
GAME.drag = null;
|
||||||
|
for (let y = fy * BS - 28; y < fy * BS - 18; y++) for (let x = cx - 20; x < cx + 20; x++) setCell(y * W + x, SAND, 20, 0);
|
||||||
|
updateRoof();
|
||||||
|
window.CAM = [cx, fy * BS + 25];
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Self-contained: a flat feed climbs, crosses the top, and runs downhill; a filter sorts below.
|
||||||
|
const left = Math.max(8, ((SX - 176) >> 2) * 4), base = ((SY - 20) >> 2), bx0 = left >> 2;
|
||||||
|
const ground = (base + 22) * 4;
|
||||||
|
for (let y = (base - 16) * 4; y < ground + 8; y++) for (let x = left; x < left + 188; x++) {
|
||||||
|
const i = y * W + x;
|
||||||
|
mat[i] = y >= ground ? STONE : EMPTY; part[i] = 0; mach[i] = 0; bgm[i] = 0; flags[i] = y >= ground ? F_NAT : 0; temp[i] = 20; wakeXY(x, y);
|
||||||
|
}
|
||||||
|
for (let x = left; x < left + 188; x++) { surfY[x] = ground; dirtY[x] = ground + 8; }
|
||||||
|
AMB.fill(20); money = 50000; weather.next = 1e15;
|
||||||
|
for (let k = 4; k < 10; k++) placeBlock(BP_BELT, 1, bx0 + k, base + 1, true);
|
||||||
|
for (let k = 0; k < 10; k++) placeBlock(BP_BELT_UP, 1, bx0 + 10 + k, base - k, true);
|
||||||
|
for (let k = 20; k < 26; k++) placeBlock(BP_BELT, 1, bx0 + k, base - 9, true);
|
||||||
|
for (let k = 0; k < 10; k++) placeBlock(BP_BELT_DOWN, 1, bx0 + 26 + k, base - 9 + k, true);
|
||||||
|
for (let k = 36; k < 43; k++) placeBlock(BP_BELT, 1, bx0 + k, base + 1, true);
|
||||||
|
const low = base + 16;
|
||||||
|
for (let k = 4; k < 10; k++) placeBlock(BP_BELT, 1, bx0 + k, low + 1, true);
|
||||||
|
for (let k = 0; k < 10; k++) placeBlock(BP_FILTER_UP, 128, bx0 + 10 + k, low - k, true, {m:[SAND],inv:0});
|
||||||
|
for (let k = 20; k < 30; k++) placeBlock(BP_BELT, 1, bx0 + k, low - 9, true);
|
||||||
|
for (let k = 0; k < 380; k++) {
|
||||||
|
for (let l = 1; l <= 4; l++) {
|
||||||
|
let i = ((base + 1) * 4 - l) * W + left + 20;
|
||||||
|
if (mat[i] === EMPTY) setCell(i, SAND, 20, 0);
|
||||||
|
i = ((low + 1) * 4 - l) * W + left + 20;
|
||||||
|
if (mat[i] === EMPTY) setCell(i, (k + l) & 1 ? SAND : GRAVEL, 20, 0);
|
||||||
|
}
|
||||||
|
simTick();
|
||||||
|
}
|
||||||
|
spawnBar(BARCU, left + 92, (base - 9) * 4 - BAR_H, 0, 0, 20);
|
||||||
|
window.CAM = [left + 92, (base + 6) * 4];
|
||||||
|
window.SLOPE_CAM = [left + 64, (base - 4) * 4];
|
||||||
|
updateRoof();
|
||||||
Reference in New Issue
Block a user