Refactor: merge duplicated rules, split the largest files by topic (t21)
- One helper each for wall and mold refunds, tube edges and lanes, pipe text, piece dimensions, font lookup, reach circles and surface averaging. - previews, smooth, notebook, materials, worldgen and selection split into topic files (preview-drill, preview-selection, smooth-shaders, notebook-market, notebook-blueprints, material-tables, worldgen-helpers, selection-pickers, selection-panels). - Every suite's output identical apart from timings; seeds make the same worlds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+69
-31
@@ -196,10 +196,14 @@ Deepgrain Foundry: simulation core (src/sim/). No DOM anywhere in src/sim/: plai
|
|||||||
- functions: rnd, mulberry32, hash2
|
- functions: rnd, mulberry32, hash2
|
||||||
- variables: RS, PH_EMPTY, PH_SOLID, PH_POWDER, PH_LIQUID, PH_GAS, PH_ITEM
|
- variables: RS, PH_EMPTY, PH_SOLID, PH_POWDER, PH_LIQUID, PH_GAS, PH_ITEM
|
||||||
|
|
||||||
### src/sim/materials.js (168 lines)
|
### src/sim/materials.js (119 lines)
|
||||||
Material ids, the material table (def), per-material property arrays, cell flags and part ids, and drill/tank tiers.
|
Material definitions: ids, properties and metal families. ids, and drill/tank tiers.
|
||||||
- functions: def, metalFamily
|
- functions: def, metalFamily
|
||||||
- variables: EMPTY, GRASS, DIRT, LDIRT, SAND, GRAVEL, CLAY, STONE, GRANITE, BASALT, NMAT, MD, FIRETO, 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: EMPTY, GRASS, DIRT, LDIRT, SAND, GRAVEL, CLAY, STONE, GRANITE, BASALT, NMAT, MD, FIRETO
|
||||||
|
|
||||||
|
### src/sim/material-tables.js (49 lines)
|
||||||
|
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
|
||||||
|
|
||||||
### src/sim/pieces.js (143 lines)
|
### src/sim/pieces.js (143 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.
|
||||||
@@ -216,9 +220,9 @@ 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 (101 lines)
|
### src/sim/world.js (102 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, 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, 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, tick, tickStamp, machines, molds, nets, pcells, powerDirty, beltL, beltR, tubeLists, brokenMachines, ghostMachines, ghostBlocked, EV, bot, bag, bagTotal, player, seen, ledger, stats
|
||||||
|
|
||||||
### src/sim/fog.js (52 lines)
|
### src/sim/fog.js (52 lines)
|
||||||
@@ -265,18 +269,18 @@ How belts, filters and screw tubes move what is on or in them. What rests on a b
|
|||||||
- functions: carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, setFilter, filterRun, roomFor, tubeSpread, tubeIntake
|
- functions: carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, setFilter, filterRun, roomFor, tubeSpread, tubeIntake
|
||||||
- variables: CARRY_DEPTH, filterR, filterL, filtersDirty
|
- variables: CARRY_DEPTH, filterR, filterL, filtersDirty
|
||||||
|
|
||||||
### src/sim/blocks.js (149 lines)
|
### src/sim/blocks.js (156 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, removeBlock, blockLine, blockRect, blockInReach, blockIs, placeBlocks
|
- functions: blockOf, blockCellList, pieceCost, canPlaceBlock, placeBlock, blockIntegrity, removeWallCell, removeBlock, blockLine, blockRect, blockInReach, blockIs, placeBlocks
|
||||||
|
|
||||||
### src/sim/molds.js (319 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 (225 lines)
|
### src/sim/items.js (232 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, entTubeEnter, entTubeTick, entTick, entsInCircle, bagAdd, bagTake, giveOrDrop, refundAt, canAfford, payCost, fmtMoney, costText, needText
|
- 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
|
||||||
- variables: BAR_W, BAR_H, entCurM, droppedLast
|
- variables: BAR_W, BAR_H, entCurM, droppedLast
|
||||||
|
|
||||||
### src/sim/tools.js (52 lines)
|
### src/sim/tools.js (52 lines)
|
||||||
@@ -329,19 +333,23 @@ Weather: clouds from steam, rain and snow, and the world water count.
|
|||||||
simTick: one simulation step, in order.
|
simTick: one simulation step, in order.
|
||||||
- functions: simTick
|
- functions: simTick
|
||||||
|
|
||||||
### src/sim/worldgen.js (346 lines)
|
### src/sim/worldgen-helpers.js (61 lines)
|
||||||
World generation: noise, layers, ores, caves, water, trees, and clearing tiny hard specks.
|
World generation helpers: noise and removal of tiny hard rock specks.
|
||||||
- functions: makeNoise, fbm, clearHardSpecks, generateWorld, generateWorldAsync
|
- functions: makeNoise, fbm, clearHardSpecks
|
||||||
- variables: SPECK_MIN
|
- variables: SPECK_MIN
|
||||||
|
|
||||||
|
### src/sim/worldgen.js (286 lines)
|
||||||
|
World generation: staged terrain, ore, caves and placement, with synchronous and async entry points.
|
||||||
|
- functions: generateWorld, generateWorldAsync
|
||||||
|
|
||||||
### src/sim/save.js (141 lines)
|
### src/sim/save.js (141 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
|
- functions: serializeWorld, deserializeWorld
|
||||||
- variables: SAVE_VERSION, TOFF
|
- variables: SAVE_VERSION, TOFF
|
||||||
|
|
||||||
### src/render/setup.js (196 lines)
|
### src/render/setup.js (200 lines)
|
||||||
Drawing (src/render/): browser only. This file: the renderer state R, colour tables (glow, thermal palettes, pipes, trees, sky), canvas setup and the camera.
|
Drawing (src/render/): browser only. This file: the renderer state R, colour tables (glow, thermal palettes, pipes, trees, sky), canvas setup and the camera.
|
||||||
- functions: glowIndex, thermIdx, pipeCellColor, pipeBodyCell, thinPipeColor, beltLike, buildSky, initRender, resizeRender, viewCells, updateCamera, snapCamera, screenToWorld, worldToScreenPx
|
- 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 (79 lines)
|
||||||
@@ -361,10 +369,14 @@ Machine overlays: fans, engines, silo and the rest, drawn on top of the world.
|
|||||||
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, drawRunOutline, drawFilterTags, drawFloaters, moldLabel, drawMolds, drawBot, spawnDebris, spawnSpark, spawnSuck, drawParticles
|
||||||
|
|
||||||
### src/render/smooth.js (430 lines)
|
### src/render/smooth.js (99 lines)
|
||||||
Smooth look: a WebGL2 pass that turns the per-grain buffers drawWorld fills (colour + class, colour behind, trees, occupancy fields, light, glow) into rounded, outlined, softly lit shapes at screen resolution. The 2D canvas then draws this image as its world layer, and every overlay (machines, bot, previews, labels) goes on top as before.
|
Smooth look: WebGL setup, texture uploads, drawing and adaptive resolution.
|
||||||
- functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace
|
- functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace
|
||||||
- variables: SM, SM_VS, SM_FS, smBytes
|
- variables: SM, smBytes
|
||||||
|
|
||||||
|
### src/render/smooth-shaders.js (325 lines)
|
||||||
|
Smooth shaders: vertex and fragment programs for the rounded world look.
|
||||||
|
- variables: SM_VS, SM_FS
|
||||||
|
|
||||||
### src/ui/audio.js (230 lines)
|
### src/ui/audio.js (230 lines)
|
||||||
Audio: chiptune effects and generative music that changes with depth. Everything is synthesized with WebAudio, no samples.
|
Audio: chiptune effects and generative music that changes with depth. Everything is synthesized with WebAudio, no samples.
|
||||||
@@ -400,10 +412,19 @@ The settings screen.
|
|||||||
- functions: openSettings, renderSettings, applySettings, toggleFullscreen, confirmWipe
|
- functions: openSettings, renderSettings, applySettings, toggleFullscreen, confirmWipe
|
||||||
- variables: SET_TABS, setTab
|
- variables: SET_TABS, setTab
|
||||||
|
|
||||||
### src/ui/notebook.js (204 lines)
|
### src/ui/notebook.js (91 lines)
|
||||||
The notebook (N): milestones, market, wallet, blueprints, controls.
|
Notebook: tab navigation, ledger, materials, pieces and controls.
|
||||||
- functions: openNotebook, renderNotebook, fmtClock, orderName, orderLeft, renderMarketTab, fmtPrice, itemsText, renderWallet, renderBlueprintTab, copyCode, controlsHtml
|
- functions: openNotebook, renderNotebook, controlsHtml
|
||||||
- variables: nbTab, MARKET_GROUPS
|
- variables: nbTab
|
||||||
|
|
||||||
|
### src/ui/notebook-market.js (68 lines)
|
||||||
|
Notebook market: prices, orders, claim payments and wallet.
|
||||||
|
- functions: fmtClock, orderName, orderLeft, renderMarketTab, fmtPrice, itemsText, renderWallet
|
||||||
|
- variables: MARKET_GROUPS
|
||||||
|
|
||||||
|
### src/ui/notebook-blueprints.js (47 lines)
|
||||||
|
Notebook blueprints: starter designs, saved library and code sharing.
|
||||||
|
- functions: renderBlueprintTab, copyCode
|
||||||
|
|
||||||
### src/ui/hud.js (34 lines)
|
### src/ui/hud.js (34 lines)
|
||||||
The tool hotbar and what is selected.
|
The tool hotbar and what is selected.
|
||||||
@@ -414,14 +435,22 @@ 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 (242 lines)
|
### src/ui/selection.js (96 lines)
|
||||||
The selected tool and piece: R / Q / E, drill width, filter picker, the selection card, the bag panel.
|
Tool selection: cycling, drill tuning, piece variants and costs.
|
||||||
- functions: pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleBlow, pickFilterUnderCursor, toggleFilterMat, setBrush, pieceV, pieceExtra, filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, openWallPicker, filterBlockAt, chipCost, pieceCostNow, renderSelCard, renderBag
|
- functions: pieceSize, pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleBlow, pickFilterUnderCursor, toggleFilterMat, setBrush, pieceV, pieceExtra, filterBlockAt, chipCost, pieceCostNow
|
||||||
|
|
||||||
|
### src/ui/selection-pickers.js (59 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
|
||||||
|
- functions: filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, openWallPicker
|
||||||
|
|
||||||
|
### src/ui/selection-panels.js (90 lines)
|
||||||
|
Selection panels: current tool card and bag materials.
|
||||||
|
- functions: renderSelCard, renderBag
|
||||||
- variables: lastBagKey
|
- variables: lastBagKey
|
||||||
|
|
||||||
### src/ui/info.js (186 lines)
|
### src/ui/info.js (188 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
|
- functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel, pipeContentsText
|
||||||
|
|
||||||
### src/ui/input.js (91 lines)
|
### src/ui/input.js (91 lines)
|
||||||
Keyboard and mouse listeners.
|
Keyboard and mouse listeners.
|
||||||
@@ -438,10 +467,19 @@ Gamepad input.
|
|||||||
- functions: padPressed, pollPad
|
- functions: padPressed, pollPad
|
||||||
- variables: PAD
|
- variables: PAD
|
||||||
|
|
||||||
### src/ui/previews.js (371 lines)
|
### src/ui/previews.js (190 lines)
|
||||||
Previews drawn under the cursor: build ghosts, drill aim and pulses, selections, paste.
|
Cursor previews: shared grid helpers, air streaks and tool preview dispatch.
|
||||||
- functions: blockRectPx, drawGrid, previewColor, crackSeg, drawCracks, pulseRGB, fanPoints, drawDrillPulses, drawDrillAim, drawAirStreaks, drawPreviews, drawTag, drawSelection, drawPastePreview
|
- functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews
|
||||||
- variables: CRACK_N, CRACK_LV, SEL_COL
|
|
||||||
|
### src/ui/preview-drill.js (104 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.
|
||||||
|
- functions: crackSeg, drawCracks, pulseRGB, fanPoints, drawDrillPulses, drawDrillAim
|
||||||
|
- variables: CRACK_N, CRACK_LV
|
||||||
|
|
||||||
|
### src/ui/preview-selection.js (80 lines)
|
||||||
|
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
|
||||||
|
- variables: SEL_COL
|
||||||
|
|
||||||
### src/ui/events.js (42 lines)
|
### src/ui/events.js (42 lines)
|
||||||
Events from the simulation (emit) turned into sounds, toasts and cards.
|
Events from the simulation (emit) turned into sounds, toasts and cards.
|
||||||
|
|||||||
@@ -4,6 +4,20 @@ 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-24: t21 phase 4: duplicates merged, largest files split (Claude Code, Codex)
|
||||||
|
|
||||||
|
- Merged seven real duplicates: wall refunds (`removeWallCell`, walls and molds), tube edges and
|
||||||
|
lanes (`entTubeEdge`, `entTubeLane`), pipe text, piece dimensions, font lookup, reach circles,
|
||||||
|
surface averaging (`meanSurface`). Look-alikes left alone: data tables, sprite rows, GLSL, and
|
||||||
|
the hot movement and heat code.
|
||||||
|
- Split by topic: `ui/previews.js` -> `preview-drill.js`, `preview-selection.js`;
|
||||||
|
`render/smooth.js` -> `smooth-shaders.js`; `ui/notebook.js` -> `notebook-market.js`,
|
||||||
|
`notebook-blueprints.js`; `sim/materials.js` -> `material-tables.js`; `sim/worldgen.js` ->
|
||||||
|
`worldgen-helpers.js`; `ui/selection.js` -> `selection-pickers.js`, `selection-panels.js`.
|
||||||
|
`render/world.js` left whole (one large function).
|
||||||
|
- Every suite's output identical apart from timings; seeds generate the same worlds.
|
||||||
|
`run_all.py --browser` ALL PASSED.
|
||||||
|
|
||||||
## 2026-10-24: t21 phase 3: speed-ups that change nothing (Claude Code, Codex)
|
## 2026-10-24: t21 phase 3: speed-ups that change nothing (Claude Code, Codex)
|
||||||
|
|
||||||
- Profiled a Huge world (6144x2048) headless and in the browser. After a load every area is
|
- Profiled a Huge world (6144x2048) headless and in the browser. After a load every area is
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"sim": [
|
"sim": [
|
||||||
"sim/base.js",
|
"sim/base.js",
|
||||||
"sim/materials.js",
|
"sim/materials.js",
|
||||||
|
"sim/material-tables.js",
|
||||||
"sim/pieces.js",
|
"sim/pieces.js",
|
||||||
"sim/economy.js",
|
"sim/economy.js",
|
||||||
"sim/progress.js",
|
"sim/progress.js",
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
"sim/ledger.js",
|
"sim/ledger.js",
|
||||||
"sim/weather.js",
|
"sim/weather.js",
|
||||||
"sim/tick.js",
|
"sim/tick.js",
|
||||||
|
"sim/worldgen-helpers.js",
|
||||||
"sim/worldgen.js",
|
"sim/worldgen.js",
|
||||||
"sim/save.js"
|
"sim/save.js"
|
||||||
],
|
],
|
||||||
@@ -43,6 +45,7 @@
|
|||||||
"render/machines.js",
|
"render/machines.js",
|
||||||
"render/overlays.js",
|
"render/overlays.js",
|
||||||
"render/smooth.js",
|
"render/smooth.js",
|
||||||
|
"render/smooth-shaders.js",
|
||||||
"ui/audio.js",
|
"ui/audio.js",
|
||||||
"ui/state.js",
|
"ui/state.js",
|
||||||
"ui/title.js",
|
"ui/title.js",
|
||||||
@@ -51,14 +54,20 @@
|
|||||||
"ui/session.js",
|
"ui/session.js",
|
||||||
"ui/settings.js",
|
"ui/settings.js",
|
||||||
"ui/notebook.js",
|
"ui/notebook.js",
|
||||||
|
"ui/notebook-market.js",
|
||||||
|
"ui/notebook-blueprints.js",
|
||||||
"ui/hud.js",
|
"ui/hud.js",
|
||||||
"ui/build-menu.js",
|
"ui/build-menu.js",
|
||||||
"ui/selection.js",
|
"ui/selection.js",
|
||||||
|
"ui/selection-pickers.js",
|
||||||
|
"ui/selection-panels.js",
|
||||||
"ui/info.js",
|
"ui/info.js",
|
||||||
"ui/input.js",
|
"ui/input.js",
|
||||||
"ui/actions.js",
|
"ui/actions.js",
|
||||||
"ui/gamepad.js",
|
"ui/gamepad.js",
|
||||||
"ui/previews.js",
|
"ui/previews.js",
|
||||||
|
"ui/preview-drill.js",
|
||||||
|
"ui/preview-selection.js",
|
||||||
"ui/events.js",
|
"ui/events.js",
|
||||||
"ui/map.js",
|
"ui/map.js",
|
||||||
"ui/loop.js"
|
"ui/loop.js"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function drawMachines() {
|
|||||||
ctx.fillStyle = 'rgba(30,28,26,0.85)'; ctx.fillRect(sx + 2 * zp, sy + 13 * zp, 20 * zp, zp);
|
ctx.fillStyle = 'rgba(30,28,26,0.85)'; ctx.fillRect(sx + 2 * zp, sy + 13 * zp, 20 * zp, zp);
|
||||||
ctx.fillStyle = 'rgba(150,160,170,0.8)';
|
ctx.fillStyle = 'rgba(150,160,170,0.8)';
|
||||||
for (let k = 0; k < 20; k += 2) ctx.fillRect(sx + (2 + k) * zp, sy + 13 * zp, zp * 0.5, zp);
|
for (let k = 0; k < 20; k += 2) ctx.fillRect(sx + (2 + k) * zp, sy + 13 * zp, zp * 0.5, zp);
|
||||||
if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace';
|
renderMonoFont();
|
||||||
const fs = Math.max(9, Math.round(zp * 2.6));
|
const fs = Math.max(9, Math.round(zp * 2.6));
|
||||||
ctx.font = `700 ${fs}px ${R.monoFont}`;
|
ctx.font = `700 ${fs}px ${R.monoFont}`;
|
||||||
const label = 'TRADE DOCK', tw = ctx.measureText(label).width + fs;
|
const label = 'TRADE DOCK', tw = ctx.measureText(label).width + fs;
|
||||||
@@ -155,7 +155,7 @@ function drawSilo(ctx, mm, sx, sy, zp) {
|
|||||||
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(); }
|
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) {
|
||||||
if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace';
|
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)';
|
||||||
@@ -201,7 +201,7 @@ function drawSiloPixel(ctx, mm, sx, sy, zp) {
|
|||||||
const lid = tick - (mm.fillT || -99) < 10 ? '#e0b43c' : '#9aa4ae';
|
const lid = tick - (mm.fillT || -99) < 10 ? '#e0b43c' : '#9aa4ae';
|
||||||
grain(8, 0, lid); grain(9, -1, lid);
|
grain(8, 0, lid); grain(9, -1, lid);
|
||||||
if (zp >= 3) {
|
if (zp >= 3) {
|
||||||
if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace';
|
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 + 5.6 * zp, sy + 5 * zp);
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ function drawFilterTags(showAll, hoverB) {
|
|||||||
const v = viewCells();
|
const v = viewCells();
|
||||||
const bx0 = Math.max(0, Math.floor(R.camX / BS) - 1), bx1 = Math.min(BW - 1, Math.floor((R.camX + v.w) / BS) + 1);
|
const bx0 = Math.max(0, Math.floor(R.camX / BS) - 1), bx1 = Math.min(BW - 1, Math.floor((R.camX + v.w) / BS) + 1);
|
||||||
const by0 = Math.max(0, Math.floor(R.camY / BS) - 1), by1 = Math.min(BH - 1, Math.floor((R.camY + v.h) / BS) + 1);
|
const by0 = Math.max(0, Math.floor(R.camY / BS) - 1), by1 = Math.min(BH - 1, Math.floor((R.camY + v.h) / BS) + 1);
|
||||||
if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace';
|
renderMonoFont();
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.font = `600 ${Math.round(11 * u)}px ${R.monoFont}`; ctx.textBaseline = 'middle';
|
ctx.font = `600 ${Math.round(11 * u)}px ${R.monoFont}`; ctx.textBaseline = 'middle';
|
||||||
const sw = Math.round(9 * u), gap = Math.round(3 * u), pad = Math.round(5 * u), th = Math.round(17 * u);
|
const sw = Math.round(9 * u), gap = Math.round(3 * u), pad = Math.round(5 * u), th = Math.round(17 * u);
|
||||||
@@ -125,7 +125,7 @@ function drawFilterTags(showAll, hoverB) {
|
|||||||
function drawFloaters() {
|
function drawFloaters() {
|
||||||
const fl = R.floaters; if (!fl || !fl.length) return;
|
const fl = R.floaters; if (!fl || !fl.length) return;
|
||||||
const ctx = R.ctx, zp = R.zp;
|
const ctx = R.ctx, zp = R.zp;
|
||||||
if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace';
|
renderMonoFont();
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.font = `700 ${Math.max(11, Math.round(zp * 3))}px ${R.monoFont}`; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
|
ctx.font = `700 ${Math.max(11, Math.round(zp * 3))}px ${R.monoFont}`; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
|
||||||
for (let k = fl.length - 1; k >= 0; k--) {
|
for (let k = fl.length - 1; k >= 0; k--) {
|
||||||
@@ -141,7 +141,7 @@ function moldLabel(mo) { return mo.state === 'ready' && mo.kind !== 'ingot' ? '1
|
|||||||
function drawMolds() {
|
function drawMolds() {
|
||||||
const ctx = R.ctx, zp = R.zp, fs = Math.max(9, Math.round(zp * 2));
|
const ctx = R.ctx, zp = R.zp, fs = Math.max(9, Math.round(zp * 2));
|
||||||
ctx.save();
|
ctx.save();
|
||||||
if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace';
|
renderMonoFont();
|
||||||
ctx.font = `600 ${fs}px ${R.monoFont}`;
|
ctx.font = `600 ${fs}px ${R.monoFont}`;
|
||||||
ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
|
ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
|
||||||
for (const mo of molds) {
|
for (const mo of molds) {
|
||||||
|
|||||||
+5
-1
@@ -129,7 +129,7 @@ const BG = {
|
|||||||
let SKY_R, SKY_G, SKY_B;
|
let SKY_R, SKY_G, SKY_B;
|
||||||
function buildSky() {
|
function buildSky() {
|
||||||
SKY_R = new Uint8Array(H); SKY_G = new Uint8Array(H); SKY_B = new Uint8Array(H);
|
SKY_R = new Uint8Array(H); SKY_G = new Uint8Array(H); SKY_B = new Uint8Array(H);
|
||||||
let sm = 0; for (let x = 0; x < W; x++) sm += surfY[x]; sm /= W;
|
const sm = meanSurface();
|
||||||
for (let y = 0; y < H; y++) {
|
for (let y = 0; y < H; y++) {
|
||||||
const f = Math.min(1, y / (sm + 20));
|
const f = Math.min(1, y / (sm + 20));
|
||||||
SKY_R[y] = 88 + f * 92; SKY_G[y] = 140 + f * 70; SKY_B[y] = 198 + f * 34;
|
SKY_R[y] = 88 + f * 92; SKY_G[y] = 140 + f * 70; SKY_B[y] = 198 + f * 34;
|
||||||
@@ -194,3 +194,7 @@ function screenToWorld(sx, sy) {
|
|||||||
return { x: R.camX + sx * dpr / R.zp, y: R.camY + sy * dpr / R.zp };
|
return { x: R.camX + sx * dpr / R.zp, y: R.camY + sy * dpr / R.zp };
|
||||||
}
|
}
|
||||||
function worldToScreenPx(x, y) { return { x: (x - R.camX) * R.zp, y: (y - R.camY) * R.zp }; }
|
function worldToScreenPx(x, y) { return { x: (x - R.camX) * R.zp, y: (y - R.camY) * R.zp }; }
|
||||||
|
|
||||||
|
function renderMonoFont() {
|
||||||
|
if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace';
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,325 @@
|
|||||||
|
// Smooth shaders: vertex and fragment programs for the rounded world look.
|
||||||
|
const SM_VS = `#version 300 es
|
||||||
|
void main() {
|
||||||
|
vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
||||||
|
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const SM_FS = `#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
precision highp int;
|
||||||
|
uniform sampler2D tCol, tColL, tBack, tTree, tField, tLight, tGlow;
|
||||||
|
uniform vec2 uSize, uOrigin, uLightSize, uGlowSize;
|
||||||
|
uniform vec4 uView, uLightMap;
|
||||||
|
uniform float uGlowK, uTime, uSurf, uRain;
|
||||||
|
out vec4 o;
|
||||||
|
|
||||||
|
float h21(vec2 p) { p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }
|
||||||
|
float vn(vec2 p) {
|
||||||
|
vec2 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f);
|
||||||
|
return mix(mix(h21(i), h21(i + vec2(1, 0)), f.x), mix(h21(i + vec2(0, 1)), h21(i + vec2(1, 1)), f.x), f.y);
|
||||||
|
}
|
||||||
|
float fbm(vec2 p) { float a = 0.5, s = 0.0; for (int k = 0; k < 4; k++) { s += a * vn(p); p = p * 2.03 + 17.1; a *= 0.5; } return s / 0.9375; }
|
||||||
|
|
||||||
|
vec4 lin(sampler2D t, vec2 q) { return textureLod(t, q / uSize, 0.0); }
|
||||||
|
// cubic B-spline from four bilinear taps: rounds a field of 0s and 1s into smooth shapes
|
||||||
|
vec4 bsp(sampler2D t, vec2 q) {
|
||||||
|
vec2 tc = q - 0.5, i = floor(tc), f = tc - i, f2 = f * f, f3 = f2 * f;
|
||||||
|
vec2 w0 = (1.0 - 3.0 * f + 3.0 * f2 - f3) / 6.0, w1 = (4.0 - 6.0 * f2 + 3.0 * f3) / 6.0;
|
||||||
|
vec2 w2 = (1.0 + 3.0 * f + 3.0 * f2 - 3.0 * f3) / 6.0, w3 = f3 / 6.0;
|
||||||
|
vec2 g0 = w0 + w1, g1 = w2 + w3;
|
||||||
|
vec2 h0 = (i - 0.5 + w1 / g0) / uSize, h1 = (i + 1.5 + w3 / g1) / uSize;
|
||||||
|
return g0.y * (g0.x * textureLod(t, h0, 0.0) + g1.x * textureLod(t, vec2(h1.x, h0.y), 0.0))
|
||||||
|
+ g1.y * (g0.x * textureLod(t, vec2(h0.x, h1.y), 0.0) + g1.x * textureLod(t, h1, 0.0));
|
||||||
|
}
|
||||||
|
vec4 cellAt(sampler2D t, ivec2 c) { return texelFetch(t, clamp(c, ivec2(0), ivec2(uSize) - 1), 0); }
|
||||||
|
int cls(vec4 c) { return int(c.a * 255.0 + 0.5); }
|
||||||
|
float isT(int n) { return (n == 1 || n == 4 || n == 6 || (n >= 7 && n <= 12)) ? 1.0 : 0.0; }
|
||||||
|
float isM(int n) { return n == 2 ? 1.0 : isT(n); } // matter: ground, built pieces or loose grains
|
||||||
|
float isF(int n) { return (n == 13 || n == 14) ? 1.0 : 0.0; }
|
||||||
|
float isBelt(int n) { return (n == 7 || n == 8) ? 1.0 : 0.0; }
|
||||||
|
float isN(int n, int k) { return n == k ? 1.0 : 0.0; }
|
||||||
|
// signed distance (in grains) to the outline of one block of belt: a band 3.7 grains tall; where
|
||||||
|
// the line of belts ends, the end is square with rounded corners. Negative inside.
|
||||||
|
float beltSD(vec2 lb, bool endL, bool endR) {
|
||||||
|
float x0 = endL ? 0.15 : -2.0, x1 = endR ? 3.85 : 6.0, r = 0.75;
|
||||||
|
vec2 c = vec2((x0 + x1) * 0.5, 2.0), hs = vec2((x1 - x0) * 0.5, 1.85);
|
||||||
|
vec2 q = abs(lb - c) - hs + r;
|
||||||
|
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
|
||||||
|
}
|
||||||
|
// 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.
|
||||||
|
float beltLineSD(ivec2 c, vec2 lb) {
|
||||||
|
int nr = cls(cellAt(tCol, c + ivec2(4, 0))), nl = cls(cellAt(tCol, c - ivec2(4, 0)));
|
||||||
|
return beltSD(lb, isBelt(nl) + isF(nl) < 0.5, isBelt(nr) + isF(nr) < 0.5);
|
||||||
|
}
|
||||||
|
// 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.
|
||||||
|
float bandRidge(vec2 w, vec2 lb, float dir) {
|
||||||
|
float run = lb.y < 2.0 ? 1.0 : -1.0;
|
||||||
|
return step(0.72, fract((w.x - dir * run * uTime * 30.0) * 0.5));
|
||||||
|
}
|
||||||
|
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); }
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float zp = uView.z, px = 1.0 / zp;
|
||||||
|
vec2 sp = vec2(gl_FragCoord.x, uView.w - gl_FragCoord.y);
|
||||||
|
vec2 q = sp * px + uView.xy; // position in grains, in the window of grains uploaded
|
||||||
|
vec2 w = q + uOrigin; // position in the world
|
||||||
|
vec2 cam = uOrigin + uView.xy; // world position of the screen's top-left corner
|
||||||
|
vec2 scr = q - uView.xy; // grains from the screen's top-left corner
|
||||||
|
float shc = uView.w * px; // screen height in grains
|
||||||
|
ivec2 c = ivec2(floor(q));
|
||||||
|
vec2 qq = q - 0.5; ivec2 c0 = ivec2(floor(qq)); vec2 t = qq - floor(qq);
|
||||||
|
vec4 k0 = cellAt(tCol, c0), k1 = cellAt(tCol, c0 + ivec2(1, 0)), k2 = cellAt(tCol, c0 + ivec2(0, 1)), k3 = cellAt(tCol, c0 + ivec2(1, 1));
|
||||||
|
int n0 = cls(k0), n1 = cls(k1), n2 = cls(k2), n3 = cls(k3);
|
||||||
|
vec4 own = cellAt(tCol, c); int nc = cls(own);
|
||||||
|
vec2 s = smoothstep(0.1, 0.9, t);
|
||||||
|
vec4 wq = vec4((1.0 - s.x) * (1.0 - s.y), s.x * (1.0 - s.y), (1.0 - s.x) * s.y, s.x * s.y);
|
||||||
|
|
||||||
|
// fields, and everything that needs screen derivatives (worked out before any branching)
|
||||||
|
vec4 Fb = bsp(tField, q);
|
||||||
|
vec4 F = mix(lin(tField, q), Fb, 0.62);
|
||||||
|
// loose-grain occupancy from the four grains around (bilinear): ground and grains together make "matter"
|
||||||
|
vec4 wl = vec4((1.0 - t.x) * (1.0 - t.y), t.x * (1.0 - t.y), (1.0 - t.x) * t.y, t.x * t.y);
|
||||||
|
float pw = dot(wl, vec4(isN(n0, 2), isN(n1, 2), isN(n2, 2), isN(n3, 2)));
|
||||||
|
float M = clamp(F.r + pw, 0.0, 1.0);
|
||||||
|
float lf = clamp(mix(F.g, Fb.g, 0.6) + F.r + pw, 0.0, 1.0);
|
||||||
|
float fwT = max(fwidth(F.r), 1e-4), fwM = max(fwidth(M), 1e-4), fwA = max(fwidth(F.a), 1e-4), fwL = max(fwidth(lf), 1e-4);
|
||||||
|
vec2 gT = vec2(dFdx(mix(Fb.r, M, 0.5)), dFdy(mix(Fb.r, M, 0.5))), gL = vec2(dFdx(F.g), dFdy(F.g));
|
||||||
|
|
||||||
|
// ---- behind everything: sky with clouds and far hills, or the cave's back wall
|
||||||
|
vec4 back = lin(tBack, q);
|
||||||
|
vec3 C = back.rgb;
|
||||||
|
float sky = back.a;
|
||||||
|
if (sky > 0.01) {
|
||||||
|
vec3 S = C;
|
||||||
|
vec2 P0 = scr + cam * 0.08;
|
||||||
|
// clouds; while it rains they thicken and turn grey
|
||||||
|
float cl = smoothstep(0.56 - 0.3 * uRain, 0.78 - 0.2 * uRain, fbm(vec2(P0.x * 0.016 + uTime * 0.006, P0.y * 0.05)));
|
||||||
|
float hi = clamp((uSurf - 40.0 - w.y) / 50.0, 0.0, 1.0);
|
||||||
|
S = mix(S, mix(vec3(0.96, 0.95, 0.93), vec3(0.5, 0.52, 0.56), uRain), cl * (0.6 + 0.3 * uRain) * hi);
|
||||||
|
float k1 = 0.16; vec2 P1 = scr + cam * k1;
|
||||||
|
float base1 = shc * 0.5 + 8.0 + (uSurf - shc * 0.5) * k1;
|
||||||
|
float top1 = base1 - 36.0 * (0.25 + 0.9 * fbm(vec2(P1.x * 0.009, 1.7)));
|
||||||
|
float m1 = smoothstep(top1 - px * 1.5, top1 + px * 1.5, P1.y);
|
||||||
|
vec3 far = mix(vec3(0.50, 0.60, 0.72), S, 0.35) * (0.94 + 0.08 * fbm(P1 * 0.05));
|
||||||
|
S = mix(S, far, m1);
|
||||||
|
float k2 = 0.38; vec2 P2 = scr + cam * k2;
|
||||||
|
float base2 = shc * 0.5 + 14.0 + (uSurf - shc * 0.5) * k2;
|
||||||
|
float top2 = base2 - 20.0 * (0.2 + 0.9 * fbm(vec2(P2.x * 0.016, 7.3)));
|
||||||
|
float m2 = smoothstep(top2 - px * 1.5, top2 + px * 1.5, P2.y);
|
||||||
|
vec3 near = vec3(0.36, 0.5, 0.47) * (0.92 + 0.14 * fbm(P2 * 0.08));
|
||||||
|
near = mix(near, S, 0.42 - 0.2 * smoothstep(top2, top2 + 30.0, P2.y));
|
||||||
|
S = mix(S, near, m2);
|
||||||
|
C = mix(C, S, sky);
|
||||||
|
}
|
||||||
|
// the back wall: rough rock, and darker close to the tunnel walls (ambient occlusion)
|
||||||
|
float notSky = 1.0 - sky;
|
||||||
|
C *= mix(1.0, 1.08 + 0.4 * fbm(w * 0.21), notSky);
|
||||||
|
float near = textureLod(tField, q / uSize, 2.2).r;
|
||||||
|
C *= 1.0 - 0.34 * smoothstep(0.12, 0.85, near) * (1.0 - 0.75 * sky);
|
||||||
|
|
||||||
|
// ---- trees (behind the ground and everything on it)
|
||||||
|
if (F.a > 0.2) {
|
||||||
|
vec4 a0 = cellAt(tTree, c0), a1 = cellAt(tTree, c0 + ivec2(1, 0)), a2 = cellAt(tTree, c0 + ivec2(0, 1)), a3 = cellAt(tTree, c0 + ivec2(1, 1));
|
||||||
|
vec4 m = vec4(step(0.4, a0.a), step(0.4, a1.a), step(0.4, a2.a), step(0.4, a3.a)) * wq;
|
||||||
|
float sm = m.x + m.y + m.z + m.w;
|
||||||
|
if (sm > 1e-4) {
|
||||||
|
vec3 tc = (a0.rgb * m.x + a1.rgb * m.y + a2.rgb * m.z + a3.rgb * m.w) / sm;
|
||||||
|
float leaf = (a0.a * m.x + a1.a * m.y + a2.a * m.z + a3.a * m.w) / sm;
|
||||||
|
float lv = smoothstep(0.6, 0.9, leaf);
|
||||||
|
tc *= mix(0.86 + 0.22 * vn(vec2(w.x * 1.4, w.y * 0.25)), 0.72 + 0.5 * fbm(w * 0.42), lv);
|
||||||
|
float d = (F.a - 0.36) / fwA;
|
||||||
|
tc *= mix(0.55, 1.0, smoothstep(0.4, 1.6, d));
|
||||||
|
C = mix(C, tc, smoothstep(-0.7, 0.7, d));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- smoke, steam and flame: soft, see-through puffs
|
||||||
|
if (Fb.b > 0.02) {
|
||||||
|
vec4 m = vec4(isN(n0, 5), isN(n1, 5), isN(n2, 5), isN(n3, 5)) * wq;
|
||||||
|
float sm = m.x + m.y + m.z + m.w;
|
||||||
|
vec3 gc = sm > 1e-4 ? (k0.rgb * m.x + k1.rgb * m.y + k2.rgb * m.z + k3.rgb * m.w) / sm : own.rgb;
|
||||||
|
if (sm > 1e-4 || nc == 5) C = mix(C, gc, smoothstep(0.03, 0.55, Fb.b) * 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- liquids: smooth surfaces that fill right up against the ground, with a glossy top line
|
||||||
|
float anyL = isN(n0, 3) + isN(n1, 3) + isN(n2, 3) + isN(n3, 3);
|
||||||
|
if (anyL > 0.0 && lf > 0.3) {
|
||||||
|
vec4 m = vec4(isN(n0, 3), isN(n1, 3), isN(n2, 3), isN(n3, 3)) * (wq + 0.02);
|
||||||
|
float sm = m.x + m.y + m.z + m.w;
|
||||||
|
vec3 lc = (k0.rgb * m.x + k1.rgb * m.y + k2.rgb * m.z + k3.rgb * m.w) / sm;
|
||||||
|
lc *= 0.94 + 0.1 * vn(w * 0.55 + vec2(uTime * 0.7, uTime * 0.25));
|
||||||
|
float gl = length(gL);
|
||||||
|
float topSide = gl > 1e-5 ? clamp(gL.y / gl, 0.0, 1.0) : 0.0;
|
||||||
|
float band = 1.0 - smoothstep(0.5, 0.75, F.g);
|
||||||
|
lc += vec3(0.16, 0.18, 0.2) * topSide * band;
|
||||||
|
C = mix(C, lc, smoothstep(0.5 - fwL, 0.5 + fwL, lf));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- the ground: rounded, outlined, with a lit top edge and a shaded underside
|
||||||
|
if (F.r > 0.3) {
|
||||||
|
vec4 m = vec4(isT(n0), isT(n1), isT(n2), isT(n3)) * wq;
|
||||||
|
float sm = m.x + m.y + m.z + m.w;
|
||||||
|
vec3 blur = bsp(tColL, q).rgb, tc = own.rgb;
|
||||||
|
float bd = 1e9;
|
||||||
|
if (m.x > 0.0) { vec3 e = k0.rgb - blur; float dd = dot(e, e); if (dd < bd) { bd = dd; tc = k0.rgb; } }
|
||||||
|
if (m.y > 0.0) { vec3 e = k1.rgb - blur; float dd = dot(e, e); if (dd < bd) { bd = dd; tc = k1.rgb; } }
|
||||||
|
if (m.z > 0.0) { vec3 e = k2.rgb - blur; float dd = dot(e, e); if (dd < bd) { bd = dd; tc = k2.rgb; } }
|
||||||
|
if (m.w > 0.0) { vec3 e = k3.rgb - blur; float dd = dot(e, e); if (dd < bd) { bd = dd; tc = k3.rgb; } }
|
||||||
|
if (sm < 1e-4) tc = own.rgb;
|
||||||
|
tc *= 0.84 + 0.3 * fbm(w * 0.28);
|
||||||
|
tc *= 0.95 + 0.1 * vn(w * 1.6);
|
||||||
|
float gl = length(gT);
|
||||||
|
vec2 nrm = gl > 1e-6 ? -gT / gl : vec2(0.0);
|
||||||
|
float band = 1.0 - smoothstep(0.5, 0.9, M);
|
||||||
|
float lit = dot(nrm, normalize(vec2(-0.5, -1.0)));
|
||||||
|
tc *= 1.0 + 0.42 * lit * band;
|
||||||
|
// the outline follows where ground and grains meet open space, never where rock meets gravel
|
||||||
|
float dm = (M - 0.5) / fwM;
|
||||||
|
tc *= mix(0.4, 1.0, smoothstep(0.5, 1.4 + zp * 0.08, dm));
|
||||||
|
C = mix(C, tc, smoothstep(-0.6, 0.6, (F.r - 0.5) / fwT));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 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));
|
||||||
|
bool closed = isM(n0) + isM(n1) + isM(n2) + isM(n3) > 3.5;
|
||||||
|
if (np > 0) {
|
||||||
|
float best = 1e9; vec3 gc = vec3(0.0); vec2 gd = vec2(0.0); float gr = 0.6;
|
||||||
|
for (int k = 0; k < 4; k++) {
|
||||||
|
ivec2 o4 = ivec2(k & 1, k >> 1);
|
||||||
|
int nk = k == 0 ? n0 : k == 1 ? n1 : k == 2 ? n2 : n3;
|
||||||
|
if (nk != 2) continue;
|
||||||
|
vec4 ck = k == 0 ? k0 : k == 1 ? k1 : k == 2 ? k2 : k3;
|
||||||
|
vec2 ce = vec2(c0 + o4) + 0.5;
|
||||||
|
float r = 0.6 + 0.1 * (h21(ce + uOrigin) - 0.5);
|
||||||
|
vec2 dv = (q - ce) / r;
|
||||||
|
float dd = length(dv);
|
||||||
|
if (dd < best) { best = dd; gc = ck.rgb; gd = dv; gr = r; }
|
||||||
|
}
|
||||||
|
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 (cover > 0.0) {
|
||||||
|
float r2 = min(1.0, dot(gd, gd));
|
||||||
|
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))));
|
||||||
|
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));
|
||||||
|
C = mix(C, pc, cover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- built pieces: crisp, and wall blocks drawn as bevelled tiles with mortar lines
|
||||||
|
if (nc == 4) C = own.rgb;
|
||||||
|
else if (nc == 7 || nc == 8) {
|
||||||
|
// belt: a rubber band round two copper rollers per block; the top run moves the load
|
||||||
|
float dir = nc == 8 ? 1.0 : -1.0;
|
||||||
|
vec2 lb = fract(w * 0.25) * 4.0;
|
||||||
|
// outline: square ends with rounded corners where the line of belts (and filters) ends
|
||||||
|
float sd = beltLineSD(c, lb);
|
||||||
|
float inside = 1.0 - smoothstep(-px, px, sd);
|
||||||
|
if (inside > 0.0) {
|
||||||
|
float edge = -sd;
|
||||||
|
vec3 bc;
|
||||||
|
if (edge < 0.8) {
|
||||||
|
bc = bandColor(lb, bandRidge(w, lb, dir));
|
||||||
|
bc *= mix(0.55, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), edge));
|
||||||
|
} else {
|
||||||
|
vec2 rc = vec2(lb.x < 2.0 ? 1.0 : 3.0, 2.0), dv = lb - rc;
|
||||||
|
float rd = length(dv);
|
||||||
|
bc = vec3(0.15, 0.16, 0.18);
|
||||||
|
if (rd < 0.95) {
|
||||||
|
float ang = atan(dv.y, dv.x) - dir * uTime * 30.0 / 0.95;
|
||||||
|
float spoke = smoothstep(0.55, 0.85, cos(ang * 3.0));
|
||||||
|
bc = 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));
|
||||||
|
bc *= mix(1.0, 0.72, spoke * smoothstep(0.25, 0.4, rd));
|
||||||
|
bc *= mix(1.0, 0.55, smoothstep(0.82, 0.95, rd));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
C = mix(C, bc, inside);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (nc >= 9 && nc <= 12) {
|
||||||
|
// screw tube: a copper pipe; what is inside shows through, and the screw's flights turn
|
||||||
|
bool vert = nc == 9 || nc == 11;
|
||||||
|
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 wall = 0.62;
|
||||||
|
vec3 tc;
|
||||||
|
if (a < wall || a > 4.0 - wall) {
|
||||||
|
float u2 = a < wall ? a / wall : (4.0 - a) / wall; // 0 at the outside, 1 at the bore
|
||||||
|
float cyl = sin(u2 * 1.5708);
|
||||||
|
tc = vec3(0.62, 0.41, 0.27) * (0.6 + 0.55 * cyl);
|
||||||
|
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);
|
||||||
|
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)));
|
||||||
|
} else {
|
||||||
|
bool full = own.r + own.g + own.b > 0.0;
|
||||||
|
float fl = fract((along + a * 0.9 - sp2 * uTime * 14.0) * 0.5);
|
||||||
|
float flight = smoothstep(0.82, 0.92, fl) * (1.0 - smoothstep(0.92, 1.0, fl));
|
||||||
|
vec3 bore = vec3(0.11, 0.1, 0.11) * (0.8 + 0.4 * (1.0 - abs(a - 2.0) / 1.4));
|
||||||
|
tc = full ? own.rgb * (0.9 + 0.1 * vn(w * 1.3)) : bore;
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
C = tc;
|
||||||
|
}
|
||||||
|
else if (nc == 6) {
|
||||||
|
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));
|
||||||
|
vec3 tc = own.rgb * (0.92 + 0.14 * fbm(w * 0.55));
|
||||||
|
float hl = max(1.0 - smoothstep(0.0, 0.8, lb.x), 1.0 - smoothstep(0.0, 0.8, lb.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 *= mix(0.5, 1.0, smoothstep(px * 0.5, px * 0.5 + max(px * 1.2, 0.14), e));
|
||||||
|
C = tc;
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (nc == 13 || nc == 14) {
|
||||||
|
// filter: a belt whose middle is a see-through steel mesh. It has the same band as a belt, so a
|
||||||
|
// row of belts and filters reads as one belt, rounded only where the whole line ends. The
|
||||||
|
// band is perforated; its colour tag sits on the top run for what rides along, on the
|
||||||
|
// bottom run for what drops through.
|
||||||
|
float dir = nc == 14 ? 1.0 : -1.0;
|
||||||
|
vec2 lb = fract(w * 0.25) * 4.0;
|
||||||
|
vec4 tag = cellAt(tTree, c);
|
||||||
|
bool carry = tag.a > 0.16, tinted = tag.r + tag.g + tag.b > 0.0;
|
||||||
|
float sd = beltLineSD(c, lb);
|
||||||
|
float inside = 1.0 - smoothstep(-px, px, sd);
|
||||||
|
if (inside > 0.0) {
|
||||||
|
float edge = -sd;
|
||||||
|
vec3 steel = vec3(0.42, 0.44, 0.5);
|
||||||
|
// through the holes: whatever is behind (water, sky, rock), or what is passing through
|
||||||
|
vec3 behind = own.r + own.g + own.b > 0.0 ? own.rgb : C * 0.8;
|
||||||
|
vec2 cg = abs(fract(w) - 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 fc;
|
||||||
|
if (edge < 0.8) {
|
||||||
|
float rg = bandRidge(w, lb, dir);
|
||||||
|
vec3 band = bandColor(lb, rg);
|
||||||
|
if (tinted && (carry == (lb.y < 2.0))) band = mix(band, tag.rgb * (lb.y < 2.0 ? 1.0 : 0.8), 0.75 * rg);
|
||||||
|
fc = mix(band, behind * 0.85, hole * (1.0 - rg) * 0.75);
|
||||||
|
fc *= mix(0.55, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), edge));
|
||||||
|
} else {
|
||||||
|
fc = mix(steel * 0.78, behind, hole);
|
||||||
|
fc *= mix(0.7, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), edge - 0.8));
|
||||||
|
}
|
||||||
|
C = mix(C, fc, inside);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- light, then the glow of hot things on top
|
||||||
|
if (uLightMap.w > 0.5) C *= textureLod(tLight, (q - uLightMap.xy) / uLightMap.z / uLightSize, 0.0).rgb;
|
||||||
|
if (uGlowK > 0.0) {
|
||||||
|
vec2 gu = q / (2.0 * uGlowSize), d = 1.0 / uGlowSize;
|
||||||
|
vec3 g = textureLod(tGlow, gu, 0.0).rgb * 0.25;
|
||||||
|
g += (textureLod(tGlow, gu + vec2(d.x, 0), 0.0).rgb + textureLod(tGlow, gu - vec2(d.x, 0), 0.0).rgb + textureLod(tGlow, gu + vec2(0, d.y), 0.0).rgb + textureLod(tGlow, gu - vec2(0, d.y), 0.0).rgb) * 0.125;
|
||||||
|
g += (textureLod(tGlow, gu + d, 0.0).rgb + textureLod(tGlow, gu - d, 0.0).rgb + textureLod(tGlow, gu + vec2(d.x, -d.y), 0.0).rgb + textureLod(tGlow, gu + vec2(-d.x, d.y), 0.0).rgb) * 0.0625;
|
||||||
|
C += g * uGlowK;
|
||||||
|
}
|
||||||
|
o = vec4(C, 1.0);
|
||||||
|
}`;
|
||||||
|
|
||||||
+2
-333
@@ -1,337 +1,6 @@
|
|||||||
/* =====================================================================
|
// Smooth look: WebGL setup, texture uploads, drawing and adaptive resolution.
|
||||||
Smooth look: a WebGL2 pass that turns the per-grain buffers drawWorld
|
|
||||||
fills (colour + class, colour behind, trees, occupancy fields, light,
|
|
||||||
glow) into rounded, outlined, softly lit shapes at screen resolution.
|
|
||||||
The 2D canvas then draws this image as its world layer, and every
|
|
||||||
overlay (machines, bot, previews, labels) goes on top as before.
|
|
||||||
===================================================================== */
|
|
||||||
|
|
||||||
const SM = { tried: false, ok: false, why: '', gl: null, canvas: null, prog: null, tex: {}, u: {}, t0: performance.now(), white: null, surf: -1, scale: 1, ms: 0, n: 0 };
|
const SM = { tried: false, ok: false, why: '', gl: null, canvas: null, prog: null, tex: {}, u: {}, t0: performance.now(), white: null, surf: -1, scale: 1, ms: 0, n: 0 };
|
||||||
|
|
||||||
const SM_VS = `#version 300 es
|
|
||||||
void main() {
|
|
||||||
vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
||||||
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const SM_FS = `#version 300 es
|
|
||||||
precision highp float;
|
|
||||||
precision highp int;
|
|
||||||
uniform sampler2D tCol, tColL, tBack, tTree, tField, tLight, tGlow;
|
|
||||||
uniform vec2 uSize, uOrigin, uLightSize, uGlowSize;
|
|
||||||
uniform vec4 uView, uLightMap;
|
|
||||||
uniform float uGlowK, uTime, uSurf, uRain;
|
|
||||||
out vec4 o;
|
|
||||||
|
|
||||||
float h21(vec2 p) { p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }
|
|
||||||
float vn(vec2 p) {
|
|
||||||
vec2 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f);
|
|
||||||
return mix(mix(h21(i), h21(i + vec2(1, 0)), f.x), mix(h21(i + vec2(0, 1)), h21(i + vec2(1, 1)), f.x), f.y);
|
|
||||||
}
|
|
||||||
float fbm(vec2 p) { float a = 0.5, s = 0.0; for (int k = 0; k < 4; k++) { s += a * vn(p); p = p * 2.03 + 17.1; a *= 0.5; } return s / 0.9375; }
|
|
||||||
|
|
||||||
vec4 lin(sampler2D t, vec2 q) { return textureLod(t, q / uSize, 0.0); }
|
|
||||||
// cubic B-spline from four bilinear taps: rounds a field of 0s and 1s into smooth shapes
|
|
||||||
vec4 bsp(sampler2D t, vec2 q) {
|
|
||||||
vec2 tc = q - 0.5, i = floor(tc), f = tc - i, f2 = f * f, f3 = f2 * f;
|
|
||||||
vec2 w0 = (1.0 - 3.0 * f + 3.0 * f2 - f3) / 6.0, w1 = (4.0 - 6.0 * f2 + 3.0 * f3) / 6.0;
|
|
||||||
vec2 w2 = (1.0 + 3.0 * f + 3.0 * f2 - 3.0 * f3) / 6.0, w3 = f3 / 6.0;
|
|
||||||
vec2 g0 = w0 + w1, g1 = w2 + w3;
|
|
||||||
vec2 h0 = (i - 0.5 + w1 / g0) / uSize, h1 = (i + 1.5 + w3 / g1) / uSize;
|
|
||||||
return g0.y * (g0.x * textureLod(t, h0, 0.0) + g1.x * textureLod(t, vec2(h1.x, h0.y), 0.0))
|
|
||||||
+ g1.y * (g0.x * textureLod(t, vec2(h0.x, h1.y), 0.0) + g1.x * textureLod(t, h1, 0.0));
|
|
||||||
}
|
|
||||||
vec4 cellAt(sampler2D t, ivec2 c) { return texelFetch(t, clamp(c, ivec2(0), ivec2(uSize) - 1), 0); }
|
|
||||||
int cls(vec4 c) { return int(c.a * 255.0 + 0.5); }
|
|
||||||
float isT(int n) { return (n == 1 || n == 4 || n == 6 || (n >= 7 && n <= 12)) ? 1.0 : 0.0; }
|
|
||||||
float isM(int n) { return n == 2 ? 1.0 : isT(n); } // matter: ground, built pieces or loose grains
|
|
||||||
float isF(int n) { return (n == 13 || n == 14) ? 1.0 : 0.0; }
|
|
||||||
float isBelt(int n) { return (n == 7 || n == 8) ? 1.0 : 0.0; }
|
|
||||||
float isN(int n, int k) { return n == k ? 1.0 : 0.0; }
|
|
||||||
// signed distance (in grains) to the outline of one block of belt: a band 3.7 grains tall; where
|
|
||||||
// the line of belts ends, the end is square with rounded corners. Negative inside.
|
|
||||||
float beltSD(vec2 lb, bool endL, bool endR) {
|
|
||||||
float x0 = endL ? 0.15 : -2.0, x1 = endR ? 3.85 : 6.0, r = 0.75;
|
|
||||||
vec2 c = vec2((x0 + x1) * 0.5, 2.0), hs = vec2((x1 - x0) * 0.5, 1.85);
|
|
||||||
vec2 q = abs(lb - c) - hs + r;
|
|
||||||
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
|
|
||||||
}
|
|
||||||
// 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.
|
|
||||||
float beltLineSD(ivec2 c, vec2 lb) {
|
|
||||||
int nr = cls(cellAt(tCol, c + ivec2(4, 0))), nl = cls(cellAt(tCol, c - ivec2(4, 0)));
|
|
||||||
return beltSD(lb, isBelt(nl) + isF(nl) < 0.5, isBelt(nr) + isF(nr) < 0.5);
|
|
||||||
}
|
|
||||||
// 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.
|
|
||||||
float bandRidge(vec2 w, vec2 lb, float dir) {
|
|
||||||
float run = lb.y < 2.0 ? 1.0 : -1.0;
|
|
||||||
return step(0.72, fract((w.x - dir * run * uTime * 30.0) * 0.5));
|
|
||||||
}
|
|
||||||
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); }
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
float zp = uView.z, px = 1.0 / zp;
|
|
||||||
vec2 sp = vec2(gl_FragCoord.x, uView.w - gl_FragCoord.y);
|
|
||||||
vec2 q = sp * px + uView.xy; // position in grains, in the window of grains uploaded
|
|
||||||
vec2 w = q + uOrigin; // position in the world
|
|
||||||
vec2 cam = uOrigin + uView.xy; // world position of the screen's top-left corner
|
|
||||||
vec2 scr = q - uView.xy; // grains from the screen's top-left corner
|
|
||||||
float shc = uView.w * px; // screen height in grains
|
|
||||||
ivec2 c = ivec2(floor(q));
|
|
||||||
vec2 qq = q - 0.5; ivec2 c0 = ivec2(floor(qq)); vec2 t = qq - floor(qq);
|
|
||||||
vec4 k0 = cellAt(tCol, c0), k1 = cellAt(tCol, c0 + ivec2(1, 0)), k2 = cellAt(tCol, c0 + ivec2(0, 1)), k3 = cellAt(tCol, c0 + ivec2(1, 1));
|
|
||||||
int n0 = cls(k0), n1 = cls(k1), n2 = cls(k2), n3 = cls(k3);
|
|
||||||
vec4 own = cellAt(tCol, c); int nc = cls(own);
|
|
||||||
vec2 s = smoothstep(0.1, 0.9, t);
|
|
||||||
vec4 wq = vec4((1.0 - s.x) * (1.0 - s.y), s.x * (1.0 - s.y), (1.0 - s.x) * s.y, s.x * s.y);
|
|
||||||
|
|
||||||
// fields, and everything that needs screen derivatives (worked out before any branching)
|
|
||||||
vec4 Fb = bsp(tField, q);
|
|
||||||
vec4 F = mix(lin(tField, q), Fb, 0.62);
|
|
||||||
// loose-grain occupancy from the four grains around (bilinear): ground and grains together make "matter"
|
|
||||||
vec4 wl = vec4((1.0 - t.x) * (1.0 - t.y), t.x * (1.0 - t.y), (1.0 - t.x) * t.y, t.x * t.y);
|
|
||||||
float pw = dot(wl, vec4(isN(n0, 2), isN(n1, 2), isN(n2, 2), isN(n3, 2)));
|
|
||||||
float M = clamp(F.r + pw, 0.0, 1.0);
|
|
||||||
float lf = clamp(mix(F.g, Fb.g, 0.6) + F.r + pw, 0.0, 1.0);
|
|
||||||
float fwT = max(fwidth(F.r), 1e-4), fwM = max(fwidth(M), 1e-4), fwA = max(fwidth(F.a), 1e-4), fwL = max(fwidth(lf), 1e-4);
|
|
||||||
vec2 gT = vec2(dFdx(mix(Fb.r, M, 0.5)), dFdy(mix(Fb.r, M, 0.5))), gL = vec2(dFdx(F.g), dFdy(F.g));
|
|
||||||
|
|
||||||
// ---- behind everything: sky with clouds and far hills, or the cave's back wall
|
|
||||||
vec4 back = lin(tBack, q);
|
|
||||||
vec3 C = back.rgb;
|
|
||||||
float sky = back.a;
|
|
||||||
if (sky > 0.01) {
|
|
||||||
vec3 S = C;
|
|
||||||
vec2 P0 = scr + cam * 0.08;
|
|
||||||
// clouds; while it rains they thicken and turn grey
|
|
||||||
float cl = smoothstep(0.56 - 0.3 * uRain, 0.78 - 0.2 * uRain, fbm(vec2(P0.x * 0.016 + uTime * 0.006, P0.y * 0.05)));
|
|
||||||
float hi = clamp((uSurf - 40.0 - w.y) / 50.0, 0.0, 1.0);
|
|
||||||
S = mix(S, mix(vec3(0.96, 0.95, 0.93), vec3(0.5, 0.52, 0.56), uRain), cl * (0.6 + 0.3 * uRain) * hi);
|
|
||||||
float k1 = 0.16; vec2 P1 = scr + cam * k1;
|
|
||||||
float base1 = shc * 0.5 + 8.0 + (uSurf - shc * 0.5) * k1;
|
|
||||||
float top1 = base1 - 36.0 * (0.25 + 0.9 * fbm(vec2(P1.x * 0.009, 1.7)));
|
|
||||||
float m1 = smoothstep(top1 - px * 1.5, top1 + px * 1.5, P1.y);
|
|
||||||
vec3 far = mix(vec3(0.50, 0.60, 0.72), S, 0.35) * (0.94 + 0.08 * fbm(P1 * 0.05));
|
|
||||||
S = mix(S, far, m1);
|
|
||||||
float k2 = 0.38; vec2 P2 = scr + cam * k2;
|
|
||||||
float base2 = shc * 0.5 + 14.0 + (uSurf - shc * 0.5) * k2;
|
|
||||||
float top2 = base2 - 20.0 * (0.2 + 0.9 * fbm(vec2(P2.x * 0.016, 7.3)));
|
|
||||||
float m2 = smoothstep(top2 - px * 1.5, top2 + px * 1.5, P2.y);
|
|
||||||
vec3 near = vec3(0.36, 0.5, 0.47) * (0.92 + 0.14 * fbm(P2 * 0.08));
|
|
||||||
near = mix(near, S, 0.42 - 0.2 * smoothstep(top2, top2 + 30.0, P2.y));
|
|
||||||
S = mix(S, near, m2);
|
|
||||||
C = mix(C, S, sky);
|
|
||||||
}
|
|
||||||
// the back wall: rough rock, and darker close to the tunnel walls (ambient occlusion)
|
|
||||||
float notSky = 1.0 - sky;
|
|
||||||
C *= mix(1.0, 1.08 + 0.4 * fbm(w * 0.21), notSky);
|
|
||||||
float near = textureLod(tField, q / uSize, 2.2).r;
|
|
||||||
C *= 1.0 - 0.34 * smoothstep(0.12, 0.85, near) * (1.0 - 0.75 * sky);
|
|
||||||
|
|
||||||
// ---- trees (behind the ground and everything on it)
|
|
||||||
if (F.a > 0.2) {
|
|
||||||
vec4 a0 = cellAt(tTree, c0), a1 = cellAt(tTree, c0 + ivec2(1, 0)), a2 = cellAt(tTree, c0 + ivec2(0, 1)), a3 = cellAt(tTree, c0 + ivec2(1, 1));
|
|
||||||
vec4 m = vec4(step(0.4, a0.a), step(0.4, a1.a), step(0.4, a2.a), step(0.4, a3.a)) * wq;
|
|
||||||
float sm = m.x + m.y + m.z + m.w;
|
|
||||||
if (sm > 1e-4) {
|
|
||||||
vec3 tc = (a0.rgb * m.x + a1.rgb * m.y + a2.rgb * m.z + a3.rgb * m.w) / sm;
|
|
||||||
float leaf = (a0.a * m.x + a1.a * m.y + a2.a * m.z + a3.a * m.w) / sm;
|
|
||||||
float lv = smoothstep(0.6, 0.9, leaf);
|
|
||||||
tc *= mix(0.86 + 0.22 * vn(vec2(w.x * 1.4, w.y * 0.25)), 0.72 + 0.5 * fbm(w * 0.42), lv);
|
|
||||||
float d = (F.a - 0.36) / fwA;
|
|
||||||
tc *= mix(0.55, 1.0, smoothstep(0.4, 1.6, d));
|
|
||||||
C = mix(C, tc, smoothstep(-0.7, 0.7, d));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- smoke, steam and flame: soft, see-through puffs
|
|
||||||
if (Fb.b > 0.02) {
|
|
||||||
vec4 m = vec4(isN(n0, 5), isN(n1, 5), isN(n2, 5), isN(n3, 5)) * wq;
|
|
||||||
float sm = m.x + m.y + m.z + m.w;
|
|
||||||
vec3 gc = sm > 1e-4 ? (k0.rgb * m.x + k1.rgb * m.y + k2.rgb * m.z + k3.rgb * m.w) / sm : own.rgb;
|
|
||||||
if (sm > 1e-4 || nc == 5) C = mix(C, gc, smoothstep(0.03, 0.55, Fb.b) * 0.92);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- liquids: smooth surfaces that fill right up against the ground, with a glossy top line
|
|
||||||
float anyL = isN(n0, 3) + isN(n1, 3) + isN(n2, 3) + isN(n3, 3);
|
|
||||||
if (anyL > 0.0 && lf > 0.3) {
|
|
||||||
vec4 m = vec4(isN(n0, 3), isN(n1, 3), isN(n2, 3), isN(n3, 3)) * (wq + 0.02);
|
|
||||||
float sm = m.x + m.y + m.z + m.w;
|
|
||||||
vec3 lc = (k0.rgb * m.x + k1.rgb * m.y + k2.rgb * m.z + k3.rgb * m.w) / sm;
|
|
||||||
lc *= 0.94 + 0.1 * vn(w * 0.55 + vec2(uTime * 0.7, uTime * 0.25));
|
|
||||||
float gl = length(gL);
|
|
||||||
float topSide = gl > 1e-5 ? clamp(gL.y / gl, 0.0, 1.0) : 0.0;
|
|
||||||
float band = 1.0 - smoothstep(0.5, 0.75, F.g);
|
|
||||||
lc += vec3(0.16, 0.18, 0.2) * topSide * band;
|
|
||||||
C = mix(C, lc, smoothstep(0.5 - fwL, 0.5 + fwL, lf));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- the ground: rounded, outlined, with a lit top edge and a shaded underside
|
|
||||||
if (F.r > 0.3) {
|
|
||||||
vec4 m = vec4(isT(n0), isT(n1), isT(n2), isT(n3)) * wq;
|
|
||||||
float sm = m.x + m.y + m.z + m.w;
|
|
||||||
vec3 blur = bsp(tColL, q).rgb, tc = own.rgb;
|
|
||||||
float bd = 1e9;
|
|
||||||
if (m.x > 0.0) { vec3 e = k0.rgb - blur; float dd = dot(e, e); if (dd < bd) { bd = dd; tc = k0.rgb; } }
|
|
||||||
if (m.y > 0.0) { vec3 e = k1.rgb - blur; float dd = dot(e, e); if (dd < bd) { bd = dd; tc = k1.rgb; } }
|
|
||||||
if (m.z > 0.0) { vec3 e = k2.rgb - blur; float dd = dot(e, e); if (dd < bd) { bd = dd; tc = k2.rgb; } }
|
|
||||||
if (m.w > 0.0) { vec3 e = k3.rgb - blur; float dd = dot(e, e); if (dd < bd) { bd = dd; tc = k3.rgb; } }
|
|
||||||
if (sm < 1e-4) tc = own.rgb;
|
|
||||||
tc *= 0.84 + 0.3 * fbm(w * 0.28);
|
|
||||||
tc *= 0.95 + 0.1 * vn(w * 1.6);
|
|
||||||
float gl = length(gT);
|
|
||||||
vec2 nrm = gl > 1e-6 ? -gT / gl : vec2(0.0);
|
|
||||||
float band = 1.0 - smoothstep(0.5, 0.9, M);
|
|
||||||
float lit = dot(nrm, normalize(vec2(-0.5, -1.0)));
|
|
||||||
tc *= 1.0 + 0.42 * lit * band;
|
|
||||||
// the outline follows where ground and grains meet open space, never where rock meets gravel
|
|
||||||
float dm = (M - 0.5) / fwM;
|
|
||||||
tc *= mix(0.4, 1.0, smoothstep(0.5, 1.4 + zp * 0.08, dm));
|
|
||||||
C = mix(C, tc, smoothstep(-0.6, 0.6, (F.r - 0.5) / fwT));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 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));
|
|
||||||
bool closed = isM(n0) + isM(n1) + isM(n2) + isM(n3) > 3.5;
|
|
||||||
if (np > 0) {
|
|
||||||
float best = 1e9; vec3 gc = vec3(0.0); vec2 gd = vec2(0.0); float gr = 0.6;
|
|
||||||
for (int k = 0; k < 4; k++) {
|
|
||||||
ivec2 o4 = ivec2(k & 1, k >> 1);
|
|
||||||
int nk = k == 0 ? n0 : k == 1 ? n1 : k == 2 ? n2 : n3;
|
|
||||||
if (nk != 2) continue;
|
|
||||||
vec4 ck = k == 0 ? k0 : k == 1 ? k1 : k == 2 ? k2 : k3;
|
|
||||||
vec2 ce = vec2(c0 + o4) + 0.5;
|
|
||||||
float r = 0.6 + 0.1 * (h21(ce + uOrigin) - 0.5);
|
|
||||||
vec2 dv = (q - ce) / r;
|
|
||||||
float dd = length(dv);
|
|
||||||
if (dd < best) { best = dd; gc = ck.rgb; gd = dv; gr = r; }
|
|
||||||
}
|
|
||||||
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 (cover > 0.0) {
|
|
||||||
float r2 = min(1.0, dot(gd, gd));
|
|
||||||
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))));
|
|
||||||
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));
|
|
||||||
C = mix(C, pc, cover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- built pieces: crisp, and wall blocks drawn as bevelled tiles with mortar lines
|
|
||||||
if (nc == 4) C = own.rgb;
|
|
||||||
else if (nc == 7 || nc == 8) {
|
|
||||||
// belt: a rubber band round two copper rollers per block; the top run moves the load
|
|
||||||
float dir = nc == 8 ? 1.0 : -1.0;
|
|
||||||
vec2 lb = fract(w * 0.25) * 4.0;
|
|
||||||
// outline: square ends with rounded corners where the line of belts (and filters) ends
|
|
||||||
float sd = beltLineSD(c, lb);
|
|
||||||
float inside = 1.0 - smoothstep(-px, px, sd);
|
|
||||||
if (inside > 0.0) {
|
|
||||||
float edge = -sd;
|
|
||||||
vec3 bc;
|
|
||||||
if (edge < 0.8) {
|
|
||||||
bc = bandColor(lb, bandRidge(w, lb, dir));
|
|
||||||
bc *= mix(0.55, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), edge));
|
|
||||||
} else {
|
|
||||||
vec2 rc = vec2(lb.x < 2.0 ? 1.0 : 3.0, 2.0), dv = lb - rc;
|
|
||||||
float rd = length(dv);
|
|
||||||
bc = vec3(0.15, 0.16, 0.18);
|
|
||||||
if (rd < 0.95) {
|
|
||||||
float ang = atan(dv.y, dv.x) - dir * uTime * 30.0 / 0.95;
|
|
||||||
float spoke = smoothstep(0.55, 0.85, cos(ang * 3.0));
|
|
||||||
bc = 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));
|
|
||||||
bc *= mix(1.0, 0.72, spoke * smoothstep(0.25, 0.4, rd));
|
|
||||||
bc *= mix(1.0, 0.55, smoothstep(0.82, 0.95, rd));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
C = mix(C, bc, inside);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (nc >= 9 && nc <= 12) {
|
|
||||||
// screw tube: a copper pipe; what is inside shows through, and the screw's flights turn
|
|
||||||
bool vert = nc == 9 || nc == 11;
|
|
||||||
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 wall = 0.62;
|
|
||||||
vec3 tc;
|
|
||||||
if (a < wall || a > 4.0 - wall) {
|
|
||||||
float u2 = a < wall ? a / wall : (4.0 - a) / wall; // 0 at the outside, 1 at the bore
|
|
||||||
float cyl = sin(u2 * 1.5708);
|
|
||||||
tc = vec3(0.62, 0.41, 0.27) * (0.6 + 0.55 * cyl);
|
|
||||||
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);
|
|
||||||
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)));
|
|
||||||
} else {
|
|
||||||
bool full = own.r + own.g + own.b > 0.0;
|
|
||||||
float fl = fract((along + a * 0.9 - sp2 * uTime * 14.0) * 0.5);
|
|
||||||
float flight = smoothstep(0.82, 0.92, fl) * (1.0 - smoothstep(0.92, 1.0, fl));
|
|
||||||
vec3 bore = vec3(0.11, 0.1, 0.11) * (0.8 + 0.4 * (1.0 - abs(a - 2.0) / 1.4));
|
|
||||||
tc = full ? own.rgb * (0.9 + 0.1 * vn(w * 1.3)) : bore;
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
C = tc;
|
|
||||||
}
|
|
||||||
else if (nc == 6) {
|
|
||||||
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));
|
|
||||||
vec3 tc = own.rgb * (0.92 + 0.14 * fbm(w * 0.55));
|
|
||||||
float hl = max(1.0 - smoothstep(0.0, 0.8, lb.x), 1.0 - smoothstep(0.0, 0.8, lb.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 *= mix(0.5, 1.0, smoothstep(px * 0.5, px * 0.5 + max(px * 1.2, 0.14), e));
|
|
||||||
C = tc;
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (nc == 13 || nc == 14) {
|
|
||||||
// filter: a belt whose middle is a see-through steel mesh. It has the same band as a belt, so a
|
|
||||||
// row of belts and filters reads as one belt, rounded only where the whole line ends. The
|
|
||||||
// band is perforated; its colour tag sits on the top run for what rides along, on the
|
|
||||||
// bottom run for what drops through.
|
|
||||||
float dir = nc == 14 ? 1.0 : -1.0;
|
|
||||||
vec2 lb = fract(w * 0.25) * 4.0;
|
|
||||||
vec4 tag = cellAt(tTree, c);
|
|
||||||
bool carry = tag.a > 0.16, tinted = tag.r + tag.g + tag.b > 0.0;
|
|
||||||
float sd = beltLineSD(c, lb);
|
|
||||||
float inside = 1.0 - smoothstep(-px, px, sd);
|
|
||||||
if (inside > 0.0) {
|
|
||||||
float edge = -sd;
|
|
||||||
vec3 steel = vec3(0.42, 0.44, 0.5);
|
|
||||||
// through the holes: whatever is behind (water, sky, rock), or what is passing through
|
|
||||||
vec3 behind = own.r + own.g + own.b > 0.0 ? own.rgb : C * 0.8;
|
|
||||||
vec2 cg = abs(fract(w) - 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 fc;
|
|
||||||
if (edge < 0.8) {
|
|
||||||
float rg = bandRidge(w, lb, dir);
|
|
||||||
vec3 band = bandColor(lb, rg);
|
|
||||||
if (tinted && (carry == (lb.y < 2.0))) band = mix(band, tag.rgb * (lb.y < 2.0 ? 1.0 : 0.8), 0.75 * rg);
|
|
||||||
fc = mix(band, behind * 0.85, hole * (1.0 - rg) * 0.75);
|
|
||||||
fc *= mix(0.55, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), edge));
|
|
||||||
} else {
|
|
||||||
fc = mix(steel * 0.78, behind, hole);
|
|
||||||
fc *= mix(0.7, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), edge - 0.8));
|
|
||||||
}
|
|
||||||
C = mix(C, fc, inside);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- light, then the glow of hot things on top
|
|
||||||
if (uLightMap.w > 0.5) C *= textureLod(tLight, (q - uLightMap.xy) / uLightMap.z / uLightSize, 0.0).rgb;
|
|
||||||
if (uGlowK > 0.0) {
|
|
||||||
vec2 gu = q / (2.0 * uGlowSize), d = 1.0 / uGlowSize;
|
|
||||||
vec3 g = textureLod(tGlow, gu, 0.0).rgb * 0.25;
|
|
||||||
g += (textureLod(tGlow, gu + vec2(d.x, 0), 0.0).rgb + textureLod(tGlow, gu - vec2(d.x, 0), 0.0).rgb + textureLod(tGlow, gu + vec2(0, d.y), 0.0).rgb + textureLod(tGlow, gu - vec2(0, d.y), 0.0).rgb) * 0.125;
|
|
||||||
g += (textureLod(tGlow, gu + d, 0.0).rgb + textureLod(tGlow, gu - d, 0.0).rgb + textureLod(tGlow, gu + vec2(d.x, -d.y), 0.0).rgb + textureLod(tGlow, gu + vec2(-d.x, d.y), 0.0).rgb) * 0.0625;
|
|
||||||
C += g * uGlowK;
|
|
||||||
}
|
|
||||||
o = vec4(C, 1.0);
|
|
||||||
}`;
|
|
||||||
|
|
||||||
function smCompile(gl, type, src) {
|
function smCompile(gl, type, src) {
|
||||||
const sh = gl.createShader(type);
|
const sh = gl.createShader(type);
|
||||||
gl.shaderSource(sh, src); gl.compileShader(sh);
|
gl.shaderSource(sh, src); gl.compileShader(sh);
|
||||||
@@ -391,7 +60,7 @@ function smoothDraw(lighting, glowOn) {
|
|||||||
// the GPU pass can run below full resolution on a slow graphics card; it is scaled up smoothly
|
// the GPU pass can run below full resolution on a slow graphics card; it is scaled up smoothly
|
||||||
const gl = SM.gl, cv = SM.canvas, sc = SM.scale, cw = Math.max(1, Math.round(R.canvas.width * sc)), ch = Math.max(1, Math.round(R.canvas.height * sc)), u = SM.u;
|
const gl = SM.gl, cv = SM.canvas, sc = SM.scale, cw = Math.max(1, Math.round(R.canvas.width * sc)), ch = Math.max(1, Math.round(R.canvas.height * sc)), u = SM.u;
|
||||||
if (cv.width !== cw || cv.height !== ch) { cv.width = cw; cv.height = ch; }
|
if (cv.width !== cw || cv.height !== ch) { cv.width = cw; cv.height = ch; }
|
||||||
if (SM.surf < 0 || SM.surfSeed !== worldSeed || SM.surfW !== W) { let s = 0; for (let x = 0; x < W; x++) s += surfY[x]; SM.surf = s / W; SM.surfW = W; SM.surfSeed = worldSeed; }
|
if (SM.surf < 0 || SM.surfSeed !== worldSeed || SM.surfW !== W) { SM.surf = meanSurface(); SM.surfW = W; SM.surfSeed = worldSeed; }
|
||||||
gl.viewport(0, 0, cw, ch);
|
gl.viewport(0, 0, cw, ch);
|
||||||
gl.useProgram(SM.prog);
|
gl.useProgram(SM.prog);
|
||||||
const bw = R.bw, bh = R.bh;
|
const bw = R.bw, bh = R.bh;
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ function drawWorld() {
|
|||||||
R.rain = (R.rain || 0) * 0.98 + rainAt(Math.max(0, Math.min(W - 1, Math.floor(R.camX + bw / 2)))) * 0.02;
|
R.rain = (R.rain || 0) * 0.98 + rainAt(Math.max(0, Math.min(W - 1, Math.floor(R.camX + bw / 2)))) * 0.02;
|
||||||
const rk = R.rain;
|
const rk = R.rain;
|
||||||
// Smooth look: the colours below are still worked out per grain, but each grain also gets a
|
// Smooth look: the colours below are still worked out per grain, but each grain also gets a
|
||||||
// class (alpha byte) and the GPU draws rounded shapes from them (see smooth.js)
|
// class (alpha byte) and the GPU draws rounded shapes from them (see smooth-shaders.js)
|
||||||
const sm = R.smoothOn = SET.look === 'smooth' && !R.lens && typeof smoothAvailable === 'function' && smoothAvailable();
|
const sm = R.smoothOn = SET.look === 'smooth' && !R.lens && typeof smoothAvailable === 'function' && smoothAvailable();
|
||||||
const bpx = R.backPx, tpx = R.treePx, fpx = R.fieldPx, mmask = R.moldMask;
|
const bpx = R.backPx, tpx = R.treePx, fpx = R.fieldPx, mmask = R.moldMask;
|
||||||
if (sm) {
|
if (sm) {
|
||||||
|
|||||||
+10
-3
@@ -71,6 +71,15 @@ function blockIntegrity(b) {
|
|||||||
void v;
|
void v;
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
// One cell of a wall or mold being taken back: metal is counted toward bars (returns 1), other
|
||||||
|
// walls give their rubble back.
|
||||||
|
function removeWallCell(i, wall) {
|
||||||
|
const m = mat[i];
|
||||||
|
if (BAR_OF[m] && PH[wall] === PH_ITEM) { setCell(i, EMPTY, temp[i], 0); return 1; }
|
||||||
|
if (YLD[m] && bagAdd(YLD[m], 1, false) === 0) { setCell(i, YLD[m], temp[i], 0); droppedLast++; } // bag full: it crumbles in place
|
||||||
|
else setCell(i, EMPTY, temp[i], 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
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;
|
||||||
@@ -82,9 +91,7 @@ function removeBlock(bx, by) {
|
|||||||
const m = mat[i], p = part[i];
|
const m = mat[i], p = part[i];
|
||||||
if (code === BP_WALL) {
|
if (code === BP_WALL) {
|
||||||
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) {
|
||||||
if (BAR_OF[m] && PH[v] === PH_ITEM) { metal++; setCell(i, EMPTY, temp[i], 0); }
|
metal += removeWallCell(i, v);
|
||||||
else if (YLD[m] && bagAdd(YLD[m], 1, false) === 0) { setCell(i, YLD[m], temp[i], 0); droppedLast++; } // bag full: it crumbles in place
|
|
||||||
else setCell(i, EMPTY, temp[i], 0);
|
|
||||||
n++;
|
n++;
|
||||||
}
|
}
|
||||||
} else if (isPipeCode(code)) {
|
} else if (isPipeCode(code)) {
|
||||||
|
|||||||
+15
-8
@@ -62,13 +62,19 @@ function entTubeMove(e, x, y, tubeOnly = false) {
|
|||||||
for (let xx = Math.min(e.ix, x); xx <= Math.max(e.ix, x); xx++) if (!entTubeFree(xx, yy)) return false;
|
for (let xx = Math.min(e.ix, x); xx <= Math.max(e.ix, x); xx++) if (!entTubeFree(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 entTubeEdge(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);
|
||||||
|
return x < 0 || y < 0 || x >= W || y >= H ? -1 : y * W + x;
|
||||||
|
}
|
||||||
function entTubeEnter(e, dx, dy) {
|
function entTubeEnter(e, dx, dy) {
|
||||||
for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
|
for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
|
||||||
const xx = e.ix + (dx > 0 ? BAR_W : dx < 0 ? -1 : k), yy = e.iy + (dy > 0 ? BAR_H : dy < 0 ? -1 : k);
|
const i = entTubeEdge(e, dx, dy, k);
|
||||||
if (xx < 0 || yy < 0 || xx >= W || yy >= H) continue;
|
if (i < 0) continue;
|
||||||
const p = part[yy * W + xx]; if (p < P_TU || p > P_TR || (!dx && TDX[p]) || dx * TDX[p] + dy * TDY[p] < 0) continue;
|
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;
|
||||||
// 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 = Math.max(yy & ~3, Math.min((yy & ~3) + 2, e.iy));
|
const x = (xx & ~3), y = entTubeLane(yy, e.iy);
|
||||||
if (entTubeMove(e, x, TDX[p] || dy ? y : e.iy, true)) return true;
|
if (entTubeMove(e, x, TDX[p] || dy ? y : e.iy, true)) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -80,14 +86,15 @@ function entTubeTick(e, p) {
|
|||||||
const dx = TDX[p], dy = TDY[p];
|
const dx = TDX[p], dy = TDY[p];
|
||||||
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 xx = e.ix + (dx > 0 ? BAR_W : dx < 0 ? -1 : k), yy = e.iy + (dy > 0 ? BAR_H : dy < 0 ? -1 : k);
|
const i = entTubeEdge(e, dx, dy, k);
|
||||||
if (xx < 0 || yy < 0 || xx >= W || yy >= H) return;
|
if (i < 0) return;
|
||||||
const q = part[yy * W + xx];
|
const xx = i % W, yy = (i / W) | 0;
|
||||||
|
const q = part[i];
|
||||||
if (q >= P_TU && q <= P_TR && q !== p) { next = q; ax = xx; ay = yy; break; }
|
if (q >= P_TU && q <= P_TR && q !== p) { next = q; ax = xx; ay = yy; break; }
|
||||||
}
|
}
|
||||||
if (next) {
|
if (next) {
|
||||||
if (TDX[next] === -dx && TDY[next] === -dy) return;
|
if (TDX[next] === -dx && TDY[next] === -dy) return;
|
||||||
x = ax & ~3; y = Math.max(ay & ~3, Math.min((ay & ~3) + 2, e.iy));
|
x = ax & ~3; y = entTubeLane(ay, e.iy);
|
||||||
if (entTubeMove(e, x, y, true)) stats.moved++;
|
if (entTubeMove(e, x, y, true)) stats.moved++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Material tables: typed properties, casting groups, flags, parts and tool tiers.
|
||||||
|
// Typed property tables
|
||||||
|
const PH = new Uint8Array(NMAT), DENS = new Float32Array(NMAT), KC = new Float32Array(NMAT), ICAP = new Float32Array(NMAT),
|
||||||
|
HARD = new Uint8Array(NMAT), YLD = new Uint8Array(NMAT), MELT = new Float32Array(NMAT), MELTTO = new Uint8Array(NMAT),
|
||||||
|
SMELTP = new Float32Array(NMAT), FRZ = new Float32Array(NMAT), FRZTO = new Uint8Array(NMAT), BOIL = new Float32Array(NMAT),
|
||||||
|
BOILTO = new Uint8Array(NMAT), CONDT = new Float32Array(NMAT), CONDTO = new Uint8Array(NMAT), IGN = new Float32Array(NMAT),
|
||||||
|
FLAME = new Float32Array(NMAT), FLAMEF = new Float32Array(NMAT), FUEL = new Uint8Array(NMAT), BURNTO = new Uint8Array(NMAT),
|
||||||
|
ASHP = new Float32Array(NMAT), FLUID = new Float32Array(NMAT), SPREAD = new Uint8Array(NMAT), STICK = new Float32Array(NMAT),
|
||||||
|
ALPHA = new Float32Array(NMAT), LIQT = new Float32Array(NMAT), BAGOK = new Uint8Array(NMAT), PARTMAT = new Uint8Array(NMAT),
|
||||||
|
RELAX = new Float32Array(NMAT), COLR = new Uint8Array(NMAT), COLG = new Uint8Array(NMAT), COLB = new Uint8Array(NMAT),
|
||||||
|
CVAR = new Uint8Array(NMAT);
|
||||||
|
for (let i = 0; i < NMAT; i++) {
|
||||||
|
const d = MD[i];
|
||||||
|
PH[i] = d.ph; DENS[i] = d.dens; KC[i] = d.k; ICAP[i] = 1 / d.cap; HARD[i] = d.hard; YLD[i] = d.yld;
|
||||||
|
MELT[i] = d.melt; MELTTO[i] = d.meltTo; SMELTP[i] = d.smelt; FRZ[i] = d.freeze; FRZTO[i] = d.freezeTo;
|
||||||
|
BOIL[i] = d.boil; BOILTO[i] = d.boilTo; CONDT[i] = d.condT; CONDTO[i] = d.condTo; IGN[i] = d.ign; FLAME[i] = d.flame;
|
||||||
|
FLAMEF[i] = d.flameF; FUEL[i] = d.fuel; BURNTO[i] = d.burnTo; ASHP[i] = d.ashP; FLUID[i] = d.fluid; SPREAD[i] = d.spread;
|
||||||
|
STICK[i] = d.stick; ALPHA[i] = d.alpha; LIQT[i] = d.liqT; BAGOK[i] = d.bag ? 1 : 0; PARTMAT[i] = d.partMat ? 1 : 0;
|
||||||
|
COLR[i] = d.col[0]; COLG[i] = d.col[1]; COLB[i] = d.col[2]; CVAR[i] = d.v;
|
||||||
|
RELAX[i] = d.ph === PH_LIQUID ? 0.0006 : d.ph === PH_POWDER ? 0.0005 : d.ph === PH_GAS ? 0.003 : 0.0004;
|
||||||
|
}
|
||||||
|
// what molds cast (molten metals, and molten glass) and what they set into
|
||||||
|
const CASTMETAL = new Uint8Array(NMAT); for (const m of [MCU, MFE, MST, MSN, MZN, MAG, MAU, MBZ, MBS, MGL]) CASTMETAL[m] = 1;
|
||||||
|
// tier as a drill bit or tank (0: too soft to be a bit)
|
||||||
|
const METAL_TIER = new Int8Array(NMAT).fill(-1); METAL_TIER[COPPER] = 1; METAL_TIER[IRON] = 2; METAL_TIER[STEEL] = 3;
|
||||||
|
METAL_TIER[TIN] = 0; METAL_TIER[GLASS] = 0; METAL_TIER[ZINC] = 0; METAL_TIER[SILVER] = 0; METAL_TIER[GOLD] = 0; METAL_TIER[BRONZE] = 1; METAL_TIER[BRASS] = 1;
|
||||||
|
const SOLIDMETAL = new Uint8Array(NMAT); for (const m of [COPPER, IRON, STEEL, TIN, ZINC, SILVER, GOLD, BRONZE, BRASS, GLASS]) SOLIDMETAL[m] = 1;
|
||||||
|
|
||||||
|
// ---------- Flags & parts ----------
|
||||||
|
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 TDX = [0, 0, 0, -1, 1], TDY = [0, -1, 1, 0, 0];
|
||||||
|
|
||||||
|
// ---------- Tool tiers ----------
|
||||||
|
// reach: how far from the bot the drill works. Harder rock takes longer, and
|
||||||
|
// every new bit cuts faster and further.
|
||||||
|
const BIT_TIERS = [
|
||||||
|
{ name: 'Stone bit', hard: 3, speed: 0.7, reach: 16 },
|
||||||
|
{ name: 'Copper bit', hard: 4, speed: 1.0, reach: 24 },
|
||||||
|
{ name: 'Iron bit', hard: 5, speed: 1.5, reach: 34 },
|
||||||
|
{ name: 'Steel bit', hard: 6, speed: 2.2, reach: 46 },
|
||||||
|
];
|
||||||
|
const TANK_TIERS = [
|
||||||
|
{ name: 'Canvas sack', cap: 1500 },
|
||||||
|
{ name: 'Copper tank', cap: 4000 },
|
||||||
|
{ name: 'Iron tank', cap: 10000 },
|
||||||
|
{ name: 'Steel tank', cap: 25000 },
|
||||||
|
];
|
||||||
|
const REACH = 64;
|
||||||
+1
-50
@@ -1,4 +1,4 @@
|
|||||||
// Material ids, the material table (def), per-material property arrays, cell flags and part
|
// Material definitions: ids, properties and metal families.
|
||||||
// ids, and drill/tank tiers.
|
// ids, and drill/tank tiers.
|
||||||
// ---------- Material ids ----------
|
// ---------- Material ids ----------
|
||||||
const EMPTY = 0, GRASS = 1, DIRT = 2, LDIRT = 3, SAND = 4, GRAVEL = 5, CLAY = 6, STONE = 7, GRANITE = 8, BASALT = 9,
|
const EMPTY = 0, GRASS = 1, DIRT = 2, LDIRT = 3, SAND = 4, GRAVEL = 5, CLAY = 6, STONE = 7, GRANITE = 8, BASALT = 9,
|
||||||
@@ -117,52 +117,3 @@ def(BARSB, { name: 'Stone block', ph: PH_ITEM, col: [172, 166, 154], v: 0, dens:
|
|||||||
def(PIPE, { name: 'Pipe', col: [176, 112, 72], v: 4, k: 0.04, cap: 1.5, hard: 4, melt: 1085, meltTo: MCU, partMat: true });
|
def(PIPE, { name: 'Pipe', col: [176, 112, 72], v: 4, k: 0.04, cap: 1.5, hard: 4, melt: 1085, meltTo: MCU, partMat: true });
|
||||||
def(DOCKW, { name: 'Trade dock', col: [70, 82, 96], v: 6, k: 0.01, cap: 2, hard: 9, yld: 0, partMat: true, desc: 'The claim office\'s trade dock. Whatever drops through the grate in its floor is sold.' });
|
def(DOCKW, { name: 'Trade dock', col: [70, 82, 96], v: 6, k: 0.01, cap: 2, hard: 9, yld: 0, partMat: true, desc: 'The claim office\'s trade dock. Whatever drops through the grate in its floor is sold.' });
|
||||||
def(HULL, { name: 'Machine', col: [84, 92, 102], v: 6, k: 0.03, cap: 1.5, hard: 4, melt: 1085, meltTo: MCU, partMat: true });
|
def(HULL, { name: 'Machine', col: [84, 92, 102], v: 6, k: 0.03, cap: 1.5, hard: 4, melt: 1085, meltTo: MCU, partMat: true });
|
||||||
|
|
||||||
// Typed property tables
|
|
||||||
const PH = new Uint8Array(NMAT), DENS = new Float32Array(NMAT), KC = new Float32Array(NMAT), ICAP = new Float32Array(NMAT),
|
|
||||||
HARD = new Uint8Array(NMAT), YLD = new Uint8Array(NMAT), MELT = new Float32Array(NMAT), MELTTO = new Uint8Array(NMAT),
|
|
||||||
SMELTP = new Float32Array(NMAT), FRZ = new Float32Array(NMAT), FRZTO = new Uint8Array(NMAT), BOIL = new Float32Array(NMAT),
|
|
||||||
BOILTO = new Uint8Array(NMAT), CONDT = new Float32Array(NMAT), CONDTO = new Uint8Array(NMAT), IGN = new Float32Array(NMAT),
|
|
||||||
FLAME = new Float32Array(NMAT), FLAMEF = new Float32Array(NMAT), FUEL = new Uint8Array(NMAT), BURNTO = new Uint8Array(NMAT),
|
|
||||||
ASHP = new Float32Array(NMAT), FLUID = new Float32Array(NMAT), SPREAD = new Uint8Array(NMAT), STICK = new Float32Array(NMAT),
|
|
||||||
ALPHA = new Float32Array(NMAT), LIQT = new Float32Array(NMAT), BAGOK = new Uint8Array(NMAT), PARTMAT = new Uint8Array(NMAT),
|
|
||||||
RELAX = new Float32Array(NMAT), COLR = new Uint8Array(NMAT), COLG = new Uint8Array(NMAT), COLB = new Uint8Array(NMAT),
|
|
||||||
CVAR = new Uint8Array(NMAT);
|
|
||||||
for (let i = 0; i < NMAT; i++) {
|
|
||||||
const d = MD[i];
|
|
||||||
PH[i] = d.ph; DENS[i] = d.dens; KC[i] = d.k; ICAP[i] = 1 / d.cap; HARD[i] = d.hard; YLD[i] = d.yld;
|
|
||||||
MELT[i] = d.melt; MELTTO[i] = d.meltTo; SMELTP[i] = d.smelt; FRZ[i] = d.freeze; FRZTO[i] = d.freezeTo;
|
|
||||||
BOIL[i] = d.boil; BOILTO[i] = d.boilTo; CONDT[i] = d.condT; CONDTO[i] = d.condTo; IGN[i] = d.ign; FLAME[i] = d.flame;
|
|
||||||
FLAMEF[i] = d.flameF; FUEL[i] = d.fuel; BURNTO[i] = d.burnTo; ASHP[i] = d.ashP; FLUID[i] = d.fluid; SPREAD[i] = d.spread;
|
|
||||||
STICK[i] = d.stick; ALPHA[i] = d.alpha; LIQT[i] = d.liqT; BAGOK[i] = d.bag ? 1 : 0; PARTMAT[i] = d.partMat ? 1 : 0;
|
|
||||||
COLR[i] = d.col[0]; COLG[i] = d.col[1]; COLB[i] = d.col[2]; CVAR[i] = d.v;
|
|
||||||
RELAX[i] = d.ph === PH_LIQUID ? 0.0006 : d.ph === PH_POWDER ? 0.0005 : d.ph === PH_GAS ? 0.003 : 0.0004;
|
|
||||||
}
|
|
||||||
// what molds cast (molten metals, and molten glass) and what they set into
|
|
||||||
const CASTMETAL = new Uint8Array(NMAT); for (const m of [MCU, MFE, MST, MSN, MZN, MAG, MAU, MBZ, MBS, MGL]) CASTMETAL[m] = 1;
|
|
||||||
// tier as a drill bit or tank (0: too soft to be a bit)
|
|
||||||
const METAL_TIER = new Int8Array(NMAT).fill(-1); METAL_TIER[COPPER] = 1; METAL_TIER[IRON] = 2; METAL_TIER[STEEL] = 3;
|
|
||||||
METAL_TIER[TIN] = 0; METAL_TIER[GLASS] = 0; METAL_TIER[ZINC] = 0; METAL_TIER[SILVER] = 0; METAL_TIER[GOLD] = 0; METAL_TIER[BRONZE] = 1; METAL_TIER[BRASS] = 1;
|
|
||||||
const SOLIDMETAL = new Uint8Array(NMAT); for (const m of [COPPER, IRON, STEEL, TIN, ZINC, SILVER, GOLD, BRONZE, BRASS, GLASS]) SOLIDMETAL[m] = 1;
|
|
||||||
|
|
||||||
// ---------- Flags & parts ----------
|
|
||||||
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 TDX = [0, 0, 0, -1, 1], TDY = [0, -1, 1, 0, 0];
|
|
||||||
|
|
||||||
// ---------- Tool tiers ----------
|
|
||||||
// reach: how far from the bot the drill works. Harder rock takes longer, and
|
|
||||||
// every new bit cuts faster and further.
|
|
||||||
const BIT_TIERS = [
|
|
||||||
{ name: 'Stone bit', hard: 3, speed: 0.7, reach: 16 },
|
|
||||||
{ name: 'Copper bit', hard: 4, speed: 1.0, reach: 24 },
|
|
||||||
{ name: 'Iron bit', hard: 5, speed: 1.5, reach: 34 },
|
|
||||||
{ name: 'Steel bit', hard: 6, speed: 2.2, reach: 46 },
|
|
||||||
];
|
|
||||||
const TANK_TIERS = [
|
|
||||||
{ name: 'Canvas sack', cap: 1500 },
|
|
||||||
{ name: 'Copper tank', cap: 4000 },
|
|
||||||
{ name: 'Iron tank', cap: 10000 },
|
|
||||||
{ name: 'Steel tank', cap: 25000 },
|
|
||||||
];
|
|
||||||
const REACH = 64;
|
|
||||||
|
|||||||
+1
-3
@@ -309,9 +309,7 @@ function removeMold(mo) {
|
|||||||
for (const i of walls) {
|
for (const i of walls) {
|
||||||
const m = mat[i];
|
const m = mat[i];
|
||||||
if (PH[m] === PH_SOLID && !(flags[i] & F_NAT) && !PARTMAT[m]) {
|
if (PH[m] === PH_SOLID && !(flags[i] & F_NAT) && !PARTMAT[m]) {
|
||||||
if (BAR_OF[m] && PH[mo.wall] === PH_ITEM) { metal++; setCell(i, EMPTY, temp[i], 0); }
|
metal += removeWallCell(i, mo.wall);
|
||||||
else if (YLD[m] && bagAdd(YLD[m], 1, false) === 0) { setCell(i, YLD[m], temp[i], 0); droppedLast++; }
|
|
||||||
else setCell(i, EMPTY, temp[i], 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (metal) giveOrDrop(BAR_OF[WALL_OF[mo.wall]] || BARCU, Math.floor(metal / GRAINS_PER_BAR), mo.x + 8, mo.y);
|
if (metal) giveOrDrop(BAR_OF[WALL_OF[mo.wall]] || BARCU, Math.floor(metal / GRAINS_PER_BAR), mo.x + 8, mo.y);
|
||||||
|
|||||||
+2
-1
@@ -67,8 +67,9 @@ function depthU(y) {
|
|||||||
}
|
}
|
||||||
// the y at a fraction u of the underground depth (the inverse of depthU)
|
// the y at a fraction u of the underground depth (the inverse of depthU)
|
||||||
function depthY(u) { return Math.round(surfBase + (!shallowD ? u * underD : u <= 0.3 ? u / 0.3 * shallowD : shallowD + (u - 0.3) / 0.7 * (underD - shallowD))); }
|
function depthY(u) { return Math.round(surfBase + (!shallowD ? u * underD : u <= 0.3 ? u / 0.3 * shallowD : shallowD + (u - 0.3) / 0.7 * (underD - shallowD))); }
|
||||||
|
function meanSurface() { let s = 0; for (let x = 0; x < W; x++) s += surfY[x]; return s / W; }
|
||||||
function computeAmbient() {
|
function computeAmbient() {
|
||||||
let sm = 0; for (let x = 0; x < W; x++) sm += surfY[x]; sm /= W;
|
const sm = meanSurface();
|
||||||
for (let y = 0; y < H; y++) {
|
for (let y = 0; y < H; y++) {
|
||||||
let a = 18;
|
let a = 18;
|
||||||
if (y > sm) a += Math.min(60, (y - sm) * 0.04);
|
if (y > sm) a += Math.min(60, (y - sm) * 0.04);
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// World generation helpers: noise and removal of tiny hard rock specks.
|
||||||
|
// ---------- Perlin noise ----------
|
||||||
|
function makeNoise(rng) {
|
||||||
|
const perm = new Uint8Array(512), gx = new Float32Array(256), gy = new Float32Array(256);
|
||||||
|
const p = []; for (let i = 0; i < 256; i++) p.push(i);
|
||||||
|
for (let i = 255; i > 0; i--) { const j = Math.floor(rng() * (i + 1)); const t = p[i]; p[i] = p[j]; p[j] = t; }
|
||||||
|
for (let i = 0; i < 512; i++) perm[i] = p[i & 255];
|
||||||
|
for (let i = 0; i < 256; i++) { const a = rng() * Math.PI * 2; gx[i] = Math.cos(a); gy[i] = Math.sin(a); }
|
||||||
|
return function (x, y) {
|
||||||
|
const xi = Math.floor(x), yi = Math.floor(y), xf = x - xi, yf = y - yi;
|
||||||
|
const X = xi & 255, Y = yi & 255;
|
||||||
|
const p0 = perm[Y], p1 = perm[Y + 1];
|
||||||
|
const h00 = perm[X + p0], h10 = perm[X + 1 + p0], h01 = perm[X + p1], h11 = perm[X + 1 + p1];
|
||||||
|
const n00 = gx[h00] * xf + gy[h00] * yf, n10 = gx[h10] * (xf - 1) + gy[h10] * yf;
|
||||||
|
const n01 = gx[h01] * xf + gy[h01] * (yf - 1), n11 = gx[h11] * (xf - 1) + gy[h11] * (yf - 1);
|
||||||
|
const u = xf * xf * xf * (xf * (xf * 6 - 15) + 10), v = yf * yf * yf * (yf * (yf * 6 - 15) + 10);
|
||||||
|
return (n00 + u * (n10 - n00) + v * (n01 - n00) + u * v * (n00 - n10 - n01 + n11)) * 1.41;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function fbm(n, x, y, oct) { let s = 0, a = 1, f = 1, t = 0; for (let o = 0; o < oct; o++) { s += n(x * f, y * f) * a; t += a; a *= 0.5; f *= 2; } return s / t; }
|
||||||
|
|
||||||
|
// Small specks of rock harder than the rock around them (a few grains of iron ore in stone, a
|
||||||
|
// chip of granite, a sliver of basalt) are not worth having: a bit too weak to break them leaves
|
||||||
|
// them hanging in the way. Any such piece of SPECK_MIN grains or fewer becomes the softer rock
|
||||||
|
// around it. Bigger veins and pockets stay. live: the world is in play (an older save).
|
||||||
|
const SPECK_MIN = 30;
|
||||||
|
function clearHardSpecks(live) {
|
||||||
|
const seen = new Uint8Array(N), st = new Int32Array(N), cells = new Int32Array(SPECK_MIN + 1);
|
||||||
|
let n = 0;
|
||||||
|
for (const h0 of [6, 5, 4]) {
|
||||||
|
seen.fill(0);
|
||||||
|
const mem = (i) => { const m = mat[i]; return (flags[i] & F_NAT) !== 0 && PH[m] === PH_SOLID && HARD[m] >= h0 && HARD[m] <= 6 && YLD[m] !== 0 && !PARTMAT[m]; };
|
||||||
|
const soft = (i) => { const m = mat[i]; return (flags[i] & F_NAT) !== 0 && PH[m] === PH_SOLID && HARD[m] >= 1 && HARD[m] < h0 && YLD[m] !== 0 && !PARTMAT[m]; };
|
||||||
|
for (let i0 = 0; i0 < N; i0++) {
|
||||||
|
if (seen[i0] || !mem(i0)) continue;
|
||||||
|
let sp = 0, sz = 0, x0 = W, y0 = H, x1 = 0, y1 = 0;
|
||||||
|
const hosts = new Map();
|
||||||
|
st[sp++] = i0; seen[i0] = 1;
|
||||||
|
while (sp) {
|
||||||
|
const i = st[--sp], x = i % W, y = (i / W) | 0;
|
||||||
|
if (sz <= SPECK_MIN) cells[sz] = i;
|
||||||
|
sz++;
|
||||||
|
if (x < x0) x0 = x; if (x > x1) x1 = x; if (y < y0) y0 = y; if (y > y1) y1 = y;
|
||||||
|
for (const j of [x > 0 ? i - 1 : -1, x < W - 1 ? i + 1 : -1, i - W, i + W]) {
|
||||||
|
if (j < 0 || j >= N || seen[j]) continue;
|
||||||
|
if (mem(j)) { seen[j] = 1; st[sp++] = j; }
|
||||||
|
else if (soft(j)) hosts.set(mat[j], (hosts.get(mat[j]) || 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sz > SPECK_MIN) continue;
|
||||||
|
// a speck left floating (in a cave, or dug round) takes the rock nearest round it
|
||||||
|
if (!hosts.size) for (let y = Math.max(0, y0 - 5); y <= Math.min(H - 1, y1 + 5); y++) for (let x = Math.max(0, x0 - 5); x <= Math.min(W - 1, x1 + 5); x++) { const j = y * W + x; if (soft(j)) hosts.set(mat[j], (hosts.get(mat[j]) || 0) + 1); }
|
||||||
|
let host = 0, best = 0;
|
||||||
|
for (const [m, c] of hosts) if (c > best) { best = c; host = m; }
|
||||||
|
if (!host) continue;
|
||||||
|
for (let k = 0; k < sz; k++) { const i = cells[k]; if (live) setCell(i, host, temp[i], F_NAT); else mat[i] = host; }
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
+1
-61
@@ -1,64 +1,4 @@
|
|||||||
// World generation: noise, layers, ores, caves, water, trees, and clearing tiny hard specks.
|
// World generation: staged terrain, ore, caves and placement, with synchronous and async entry points.
|
||||||
// ---------- Perlin noise ----------
|
|
||||||
function makeNoise(rng) {
|
|
||||||
const perm = new Uint8Array(512), gx = new Float32Array(256), gy = new Float32Array(256);
|
|
||||||
const p = []; for (let i = 0; i < 256; i++) p.push(i);
|
|
||||||
for (let i = 255; i > 0; i--) { const j = Math.floor(rng() * (i + 1)); const t = p[i]; p[i] = p[j]; p[j] = t; }
|
|
||||||
for (let i = 0; i < 512; i++) perm[i] = p[i & 255];
|
|
||||||
for (let i = 0; i < 256; i++) { const a = rng() * Math.PI * 2; gx[i] = Math.cos(a); gy[i] = Math.sin(a); }
|
|
||||||
return function (x, y) {
|
|
||||||
const xi = Math.floor(x), yi = Math.floor(y), xf = x - xi, yf = y - yi;
|
|
||||||
const X = xi & 255, Y = yi & 255;
|
|
||||||
const p0 = perm[Y], p1 = perm[Y + 1];
|
|
||||||
const h00 = perm[X + p0], h10 = perm[X + 1 + p0], h01 = perm[X + p1], h11 = perm[X + 1 + p1];
|
|
||||||
const n00 = gx[h00] * xf + gy[h00] * yf, n10 = gx[h10] * (xf - 1) + gy[h10] * yf;
|
|
||||||
const n01 = gx[h01] * xf + gy[h01] * (yf - 1), n11 = gx[h11] * (xf - 1) + gy[h11] * (yf - 1);
|
|
||||||
const u = xf * xf * xf * (xf * (xf * 6 - 15) + 10), v = yf * yf * yf * (yf * (yf * 6 - 15) + 10);
|
|
||||||
return (n00 + u * (n10 - n00) + v * (n01 - n00) + u * v * (n00 - n10 - n01 + n11)) * 1.41;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function fbm(n, x, y, oct) { let s = 0, a = 1, f = 1, t = 0; for (let o = 0; o < oct; o++) { s += n(x * f, y * f) * a; t += a; a *= 0.5; f *= 2; } return s / t; }
|
|
||||||
|
|
||||||
// Small specks of rock harder than the rock around them (a few grains of iron ore in stone, a
|
|
||||||
// chip of granite, a sliver of basalt) are not worth having: a bit too weak to break them leaves
|
|
||||||
// them hanging in the way. Any such piece of SPECK_MIN grains or fewer becomes the softer rock
|
|
||||||
// around it. Bigger veins and pockets stay. live: the world is in play (an older save).
|
|
||||||
const SPECK_MIN = 30;
|
|
||||||
function clearHardSpecks(live) {
|
|
||||||
const seen = new Uint8Array(N), st = new Int32Array(N), cells = new Int32Array(SPECK_MIN + 1);
|
|
||||||
let n = 0;
|
|
||||||
for (const h0 of [6, 5, 4]) {
|
|
||||||
seen.fill(0);
|
|
||||||
const mem = (i) => { const m = mat[i]; return (flags[i] & F_NAT) !== 0 && PH[m] === PH_SOLID && HARD[m] >= h0 && HARD[m] <= 6 && YLD[m] !== 0 && !PARTMAT[m]; };
|
|
||||||
const soft = (i) => { const m = mat[i]; return (flags[i] & F_NAT) !== 0 && PH[m] === PH_SOLID && HARD[m] >= 1 && HARD[m] < h0 && YLD[m] !== 0 && !PARTMAT[m]; };
|
|
||||||
for (let i0 = 0; i0 < N; i0++) {
|
|
||||||
if (seen[i0] || !mem(i0)) continue;
|
|
||||||
let sp = 0, sz = 0, x0 = W, y0 = H, x1 = 0, y1 = 0;
|
|
||||||
const hosts = new Map();
|
|
||||||
st[sp++] = i0; seen[i0] = 1;
|
|
||||||
while (sp) {
|
|
||||||
const i = st[--sp], x = i % W, y = (i / W) | 0;
|
|
||||||
if (sz <= SPECK_MIN) cells[sz] = i;
|
|
||||||
sz++;
|
|
||||||
if (x < x0) x0 = x; if (x > x1) x1 = x; if (y < y0) y0 = y; if (y > y1) y1 = y;
|
|
||||||
for (const j of [x > 0 ? i - 1 : -1, x < W - 1 ? i + 1 : -1, i - W, i + W]) {
|
|
||||||
if (j < 0 || j >= N || seen[j]) continue;
|
|
||||||
if (mem(j)) { seen[j] = 1; st[sp++] = j; }
|
|
||||||
else if (soft(j)) hosts.set(mat[j], (hosts.get(mat[j]) || 0) + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sz > SPECK_MIN) continue;
|
|
||||||
// a speck left floating (in a cave, or dug round) takes the rock nearest round it
|
|
||||||
if (!hosts.size) for (let y = Math.max(0, y0 - 5); y <= Math.min(H - 1, y1 + 5); y++) for (let x = Math.max(0, x0 - 5); x <= Math.min(W - 1, x1 + 5); x++) { const j = y * W + x; if (soft(j)) hosts.set(mat[j], (hosts.get(mat[j]) || 0) + 1); }
|
|
||||||
let host = 0, best = 0;
|
|
||||||
for (const [m, c] of hosts) if (c > best) { best = c; host = m; }
|
|
||||||
if (!host) continue;
|
|
||||||
for (let k = 0; k < sz; k++) { const i = cells[k]; if (live) setCell(i, host, temp[i], F_NAT); else mat[i] = host; }
|
|
||||||
n++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
// ---------- World generation ----------
|
// ---------- World generation ----------
|
||||||
function* worldSteps(seedIn, w, h) {
|
function* worldSteps(seedIn, w, h) {
|
||||||
yield [0, 'Shaping the land'];
|
yield [0, 'Shaping the land'];
|
||||||
|
|||||||
+1
-1
@@ -124,7 +124,7 @@ function primaryPress(shift) {
|
|||||||
const P = piece();
|
const P = piece();
|
||||||
if (P.kind === 'block') { GAME.drag = aimBlock(); }
|
if (P.kind === 'block') { GAME.drag = aimBlock(); }
|
||||||
else {
|
else {
|
||||||
const bw = P.kind === 'mold' ? MOLDS[P.mold].bw : P.bw, bh = P.kind === 'mold' ? MOLDS[P.mold].bh : P.bh;
|
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 (!inReach(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);
|
||||||
|
|||||||
+4
-2
@@ -114,13 +114,13 @@ function hoverInfo(ax, ay) {
|
|||||||
} 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(pn && pn.n ? `line holds ${pn.n.toLocaleString('en-US')} / ${pn.cap.toLocaleString('en-US')} ${MD[pn.m].name.toLowerCase()}` : `line empty (room for ${pn ? pn.cap : PIPE_CAP})`);
|
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 (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 (m === BELTL || m === BELTR) t1 = 'Belt ' + (m === BELTL ? '←' : '→');
|
else if (m === BELTL || m === BELTR) t1 = 'Belt ' + (m === BELTL ? '←' : '→');
|
||||||
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';
|
||||||
t2.push(pn && pn.n ? `line holds ${pn.n.toLocaleString('en-US')} / ${pn.cap.toLocaleString('en-US')} ${MD[pn.m].name.toLowerCase()}` : `line empty (room for ${pn ? pn.cap : PIPE_CAP})`);
|
t2.push(pipeContentsText(pn));
|
||||||
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');
|
||||||
}
|
}
|
||||||
@@ -184,3 +184,5 @@ function drawCursorLabel(px, py, r) {
|
|||||||
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' : '#ffd27a'; ctx.fillText(sub, x + padX + w1 + fs * 0.6, y + h / 2); }
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pipeContentsText(pn) { return pn && pn.n ? `line holds ${pn.n.toLocaleString('en-US')} / ${pn.cap.toLocaleString('en-US')} ${MD[pn.m].name.toLowerCase()}` : `line empty (room for ${pn ? pn.cap : PIPE_CAP})`; }
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// Notebook blueprints: starter designs, saved library and code sharing.
|
||||||
|
function renderBlueprintTab(body) {
|
||||||
|
const lib = libLoad();
|
||||||
|
let h = `<p class="note">Select the Blueprint tool (5), drag over something you built to copy it, then click to place a ghost. Your bot builds ghosts from the materials in your bag when it is within reach. Right-click clears ghosts.</p>`;
|
||||||
|
h += `<p class="label grouphead">Starter designs</p><p class="note">Optional. Tested builds to learn from or copy; place one as a ghost and your bot builds it from your bag. Clear the space first: ghosts wait for open air.</p><div class="cards">`;
|
||||||
|
STARTER_DESIGNS.forEach((d, k) => {
|
||||||
|
const bp = d.bp();
|
||||||
|
h += `<div class="card"><div class="h"><span>${esc(d.name)}</span><span class="chip">${bp.bw}×${bp.bh} blocks</span></div><div class="p">${esc(d.desc)}</div><div class="p">Build cost: ${esc(costText(bpCost(bp), 1))}</div><div style="display:flex;gap:6px;flex-wrap:wrap"><button class="btn" data-starter="${k}">Place it</button></div></div>`;
|
||||||
|
});
|
||||||
|
h += '</div><p class="label grouphead">Your blueprints</p>';
|
||||||
|
h += `<div class="set"><div><div>Clipboard</div><div class="d">${GAME.clip ? `${GAME.clip.bw} × ${GAME.clip.bh} blocks, ${GAME.clip.blocks.length} pieces${GAME.clip.machines.length ? `, ${GAME.clip.machines.length} machines` : ''}${GAME.clip.molds.length ? `, ${GAME.clip.molds.length} molds` : ''}. Needs ${esc(costText(bpCost(GAME.clip), 1) || 'nothing')}` : 'Empty'}</div></div><div class="ctl"><button class="btn" id="bp-save" ${GAME.clip ? '' : 'disabled'}>Save to library</button><button class="btn" id="bp-code" ${GAME.clip ? '' : 'disabled'}>Copy code</button></div></div>`;
|
||||||
|
h += `<div class="set"><div><div>Import a blueprint code</div><div class="d">Paste a code that starts with DGBP2.</div></div><div class="ctl"><button class="btn" id="bp-import">Paste code…</button></div></div>`;
|
||||||
|
h += `<p class="label grouphead">Library</p>`;
|
||||||
|
if (!lib.length) h += `<p class="note">No saved blueprints yet.</p>`;
|
||||||
|
else {
|
||||||
|
h += '<div class="cards">';
|
||||||
|
lib.forEach((it, k) => {
|
||||||
|
const ok = bpValid(it.bp);
|
||||||
|
h += `<div class="card"><div class="h"><span>${esc(it.name)}</span><span class="chip">${ok ? `${it.bp.bw}×${it.bp.bh}` : 'old format'}</span></div><div class="p">${ok ? esc(costText(bpCost(it.bp), 1) || 'free') : 'Made before the block grid. It cannot be placed.'}</div><div style="display:flex;gap:6px;flex-wrap:wrap">${ok ? `<button class="btn" data-bpload="${k}">Use</button><button class="btn ghost" data-bpcode="${k}">Copy code</button>` : ''}<button class="btn ghost danger" data-bpdel="${k}">Delete</button></div></div>`;
|
||||||
|
});
|
||||||
|
h += '</div>';
|
||||||
|
}
|
||||||
|
body.innerHTML = h;
|
||||||
|
const sv = $('bp-save'); if (sv) sv.onclick = () => {
|
||||||
|
dialog('Name this blueprint', '<input class="text" id="bp-name" maxlength="40" placeholder="Copper smelter">', [
|
||||||
|
{ label: 'Cancel', cls: 'ghost' },
|
||||||
|
{ label: 'Save', cls: 'primary', fn: () => { const n = ($('bp-name').value || 'Untitled').trim(); const l = libLoad(); l.push({ name: n, bp: GAME.clip }); if (libSave(l)) toast(`Saved “${n}”.`); renderNotebook(); } },
|
||||||
|
]);
|
||||||
|
setTimeout(() => $('bp-name') && $('bp-name').focus(), 30);
|
||||||
|
};
|
||||||
|
const cc = $('bp-code'); if (cc) cc.onclick = () => copyCode(bpEncode(GAME.clip));
|
||||||
|
$('bp-import').onclick = () => {
|
||||||
|
dialog('Import blueprint', '<textarea class="code" id="bp-paste" placeholder="DGBP1:…"></textarea>', [
|
||||||
|
{ label: 'Cancel', cls: 'ghost' },
|
||||||
|
{ label: 'Import', cls: 'primary', fn: () => { try { GAME.clip = bpDecode($('bp-paste').value); GAME.paste = true; hudDirty = true; toast('Blueprint ready. Click to place it.'); closeAllModals(); } catch (e) { toast(e.message); return true; } } },
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
body.querySelectorAll('[data-bpload]').forEach(b => b.onclick = () => { GAME.clip = libLoad()[+b.dataset.bpload].bp; GAME.paste = true; hudDirty = true; closeAllModals(); toast('Blueprint ready. Click to place it.'); });
|
||||||
|
body.querySelectorAll('[data-starter]').forEach(b => b.onclick = () => { GAME.clip = STARTER_DESIGNS[+b.dataset.starter].bp(); GAME.paste = true; hudDirty = true; closeAllModals(); toast('Click to place it. R flips it.'); });
|
||||||
|
body.querySelectorAll('[data-bpcode]').forEach(b => b.onclick = () => copyCode(bpEncode(libLoad()[+b.dataset.bpcode].bp)));
|
||||||
|
body.querySelectorAll('[data-bpdel]').forEach(b => b.onclick = () => { const l = libLoad(); l.splice(+b.dataset.bpdel, 1); libSave(l); renderNotebook(); });
|
||||||
|
}
|
||||||
|
function copyCode(code) {
|
||||||
|
const done = () => toast('Blueprint code copied.');
|
||||||
|
const fallback = () => { dialog('Blueprint code', `<p class="note">Select the text and copy it.</p><textarea class="code" id="bp-out" readonly>${esc(code)}</textarea>`, [{ label: 'Done', cls: 'primary' }]); setTimeout(() => { const t = $('bp-out'); if (t) { t.focus(); t.select(); } }, 30); };
|
||||||
|
try { navigator.clipboard.writeText(code).then(done, fallback); } catch (e) { fallback(); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// Notebook market: prices, orders, claim payments and wallet.
|
||||||
|
function fmtClock(ticks) { const s = Math.max(0, Math.ceil(ticks / 60)); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`; }
|
||||||
|
function orderName(o) { return `${o.who ? o.who + ': ' : ''}${o.qty.toLocaleString('en-US')} ${MD[o.m].name.toLowerCase()}${PH[o.m] === PH_ITEM && o.qty !== 1 ? 's' : ''}`; }
|
||||||
|
function orderLeft(o) { return Math.max(0, o.due - tick); }
|
||||||
|
const MARKET_GROUPS = [
|
||||||
|
['Raw', [COAL, LWOOD, ACORN, SAND, LCLAY, RGRAN, RBAS, CUORE, FEORE, SNORE, ZNORE]],
|
||||||
|
['Bulk (builders)', [RSTONE, GRAVEL, BARSB, PLANK, GREENB, BARBR]],
|
||||||
|
['Refined', [CHARCOAL, RBRICK, CULLET, BARGL, SCRCU, SCRFE, SCRST, SCRSN, SCRZN, BARCU, BARSN, BARZN, BARFE, BARST]],
|
||||||
|
['Alloys', [SCRBZ, SCRBS, BARBZ, BARBS]],
|
||||||
|
['Precious', [AGORE, AUORE, SCRAG, SCRAU, BARAG, BARAU]],
|
||||||
|
];
|
||||||
|
function renderMarketTab(body) {
|
||||||
|
let h = `<div class="mkt-sum"><div><div class="k">Money</div><div class="v">${fmtMoney(money)}</div></div>`
|
||||||
|
+ `<div><div class="k">${debt > 0 ? 'Still owed on the claim' : 'The claim'}</div><div class="v" style="color:${debt > 0 ? 'var(--melt)' : 'var(--ok)'}">${debt > 0 ? fmtMoney(debt) : 'Paid off'}</div></div>`
|
||||||
|
+ `<div><div class="k">Earned in total</div><div class="v">${fmtMoney(earned)}</div></div></div>`;
|
||||||
|
if (debt > 0) h += `<div class="mkt-pay"><span class="note" style="margin:0 6px 0 0">Pay toward the claim:</span>${[1000, 5000].map(a => `<button class="btn" data-pay="${a}" ${money >= Math.min(a, debt) ? '' : 'disabled'}>${fmtMoney(Math.min(a, debt))}</button>`).join('')}<button class="btn primary" data-pay="all" ${money > 0 ? '' : 'disabled'}>All I can (${fmtMoney(Math.min(money, debt))})</button></div>`;
|
||||||
|
else h += `<p class="note">The land is yours. Keep building: the ledger has bigger goals.</p>`;
|
||||||
|
h += `<p class="note">Anything that drops through the trade dock's grate is sold on the spot, once it is cool enough to handle. Junk like dirt, ash and slag goes for nothing. Rock you dig still sells, builders order it by the bagful, and a Grinder finds ore in it. Build walls around the dock to funnel more in.</p>`;
|
||||||
|
h += `<p class="label grouphead">Orders · ${esc(STANDING[standing()])}</p>`;
|
||||||
|
if (orders.length) {
|
||||||
|
h += '<div class="orderlist">' + orders.map(o => `<div class="o"><span class="t"><span class="sw" style="display:inline-block;vertical-align:-2px;margin-right:6px;background:${rgbOf(o.m)}"></span>${esc(orderName(o))}</span><span class="pay">${fmtMoney(o.pay)}</span>`
|
||||||
|
+ `<span class="d">${o.got} of ${o.qty} delivered · ${o.mult}× market · ${fmtClock(orderLeft(o))} left</span><span></span><span class="pb"><i style="width:${(o.got / o.qty * 100).toFixed(1)}%"></i></span></div>`).join('') + '</div>';
|
||||||
|
} else h += `<p class="note">No open orders. Buyers post new ones every minute or two, for goods you have found.</p>`;
|
||||||
|
h += `<p class="note">Goods dropped into the dock fill a matching order first. A missed deadline costs nothing: what you delivered is paid at market price. ${ordersDone} filled so far; bigger buyers come with more.</p>`;
|
||||||
|
h += `<p class="label grouphead">Prices</p><p class="note">Per grain, or per bar. Selling a lot of one thing lowers its price for a while; it recovers over about half an hour.</p>`;
|
||||||
|
let rows = '', unknown = 0;
|
||||||
|
for (const [g, list] of MARKET_GROUPS) {
|
||||||
|
const known = list.filter(m => seen[m]);
|
||||||
|
unknown += list.length - known.length;
|
||||||
|
if (!known.length) continue;
|
||||||
|
rows += `<tr><td colspan="3" class="label" style="padding-top:8px">${esc(g)}</td></tr>`;
|
||||||
|
for (const m of known) {
|
||||||
|
const f = priceFactor(m), now = priceNow(m);
|
||||||
|
rows += `<tr><td><span class="sw" style="display:inline-block;vertical-align:-2px;margin-right:6px;background:${rgbOf(m)}"></span>${esc(MD[m].name)}</td><td class="num">${fmtPrice(PRICE[m])}</td><td class="num ${f < 0.97 ? 'dip' : ''}">${fmtPrice(now)}${f < 0.97 ? ` (${Math.round(f * 100)}%)` : ''}</td></tr>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h += rows ? `<div class="tablewrap"><table class="mt"><thead><tr><th>Goods</th><th>Base</th><th>Now</th></tr></thead><tbody>${rows}</tbody></table></div>` : '<p class="note">Find something worth selling first.</p>';
|
||||||
|
if (unknown) h += `<p class="note">${unknown} more goods to discover.</p>`;
|
||||||
|
h += `<p class="label grouphead">Sales, minute by minute</p>`;
|
||||||
|
const recent = saleAcc.total > 0 ? [{ total: saleAcc.total, items: saleAcc.items, now: true }].concat(shipLog) : shipLog;
|
||||||
|
h += recent.length ? `<div class="shiplog">${recent.map(sl => `<div><span class="what">${sl.now ? 'This minute: ' : ''}${esc(itemsText(sl.items))}</span><b class="num">+${fmtMoney(sl.total)}</b></div>`).join('')}</div>` : '<p class="note">Nothing sold yet.</p>';
|
||||||
|
body.innerHTML = h;
|
||||||
|
body.querySelectorAll('[data-pay]').forEach(b => b.onclick = () => {
|
||||||
|
const v = b.dataset.pay === 'all' ? money : +b.dataset.pay;
|
||||||
|
const paid = payClaim(v);
|
||||||
|
if (paid > 0) { playSfx('sale'); toast(`Paid ${fmtMoney(paid)} toward the claim.`); }
|
||||||
|
renderNotebook(); hudDirty = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function fmtPrice(v) { return v >= 10 ? '$' + Math.round(v) : '$' + v.toFixed(2); }
|
||||||
|
function itemsText(items) {
|
||||||
|
const e = Object.entries(items).map(([m, n]) => [+m, n]).sort((a, b) => b[1] * PRICE[b[0]] - a[1] * PRICE[a[0]]);
|
||||||
|
const parts = e.slice(0, 3).map(([m, n]) => PH[m] === PH_ITEM ? `${n} ${MD[m].name.toLowerCase()}${n === 1 ? '' : 's'}` : `${n} ${MD[m].name.toLowerCase()}`);
|
||||||
|
if (e.length > 3) parts.push(`${e.length - 3} more`);
|
||||||
|
return parts.join(', ');
|
||||||
|
}
|
||||||
|
function renderWallet() {
|
||||||
|
$('w-money').textContent = fmtMoney(money);
|
||||||
|
const c = $('w-claim');
|
||||||
|
c.className = 'line' + (debt <= 0 ? ' paid' : '');
|
||||||
|
c.innerHTML = debt > 0 ? `<span>Claim owed</span><b>${fmtMoney(debt)}</b>` : `<span>Claim</span><b>paid off ✓</b>`;
|
||||||
|
const h = orders.length ? orders.map(o => {
|
||||||
|
const left = orderLeft(o);
|
||||||
|
return `<span class="ord"><span class="sw" style="background:${rgbOf(o.m)}"></span><span class="nm">${o.got}/${esc(orderName(o))}</span><span class="tm${left < 3600 ? ' late' : ''}">${fmtClock(left)}</span><span class="pb"><i style="width:${(o.got / o.qty * 100).toFixed(1)}%"></i></span></span>`;
|
||||||
|
}).join('') : '<span class="none">No orders yet</span>';
|
||||||
|
const box = $('w-orders');
|
||||||
|
if (box.innerHTML !== h) box.innerHTML = h;
|
||||||
|
}
|
||||||
+1
-114
@@ -1,4 +1,4 @@
|
|||||||
// The notebook (N): milestones, market, wallet, blueprints, controls.
|
// Notebook: tab navigation, ledger, materials, pieces and controls.
|
||||||
// ---------- Notebook ----------
|
// ---------- Notebook ----------
|
||||||
let nbTab = 'Ledger';
|
let nbTab = 'Ledger';
|
||||||
function openNotebook(tab) { if (tab) nbTab = tab; renderNotebook(); openModal('m-notebook'); }
|
function openNotebook(tab) { if (tab) nbTab = tab; renderNotebook(); openModal('m-notebook'); }
|
||||||
@@ -55,119 +55,6 @@ function renderNotebook() {
|
|||||||
body.innerHTML = controlsHtml();
|
body.innerHTML = controlsHtml();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function fmtClock(ticks) { const s = Math.max(0, Math.ceil(ticks / 60)); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`; }
|
|
||||||
function orderName(o) { return `${o.who ? o.who + ': ' : ''}${o.qty.toLocaleString('en-US')} ${MD[o.m].name.toLowerCase()}${PH[o.m] === PH_ITEM && o.qty !== 1 ? 's' : ''}`; }
|
|
||||||
function orderLeft(o) { return Math.max(0, o.due - tick); }
|
|
||||||
const MARKET_GROUPS = [
|
|
||||||
['Raw', [COAL, LWOOD, ACORN, SAND, LCLAY, RGRAN, RBAS, CUORE, FEORE, SNORE, ZNORE]],
|
|
||||||
['Bulk (builders)', [RSTONE, GRAVEL, BARSB, PLANK, GREENB, BARBR]],
|
|
||||||
['Refined', [CHARCOAL, RBRICK, CULLET, BARGL, SCRCU, SCRFE, SCRST, SCRSN, SCRZN, BARCU, BARSN, BARZN, BARFE, BARST]],
|
|
||||||
['Alloys', [SCRBZ, SCRBS, BARBZ, BARBS]],
|
|
||||||
['Precious', [AGORE, AUORE, SCRAG, SCRAU, BARAG, BARAU]],
|
|
||||||
];
|
|
||||||
function renderMarketTab(body) {
|
|
||||||
let h = `<div class="mkt-sum"><div><div class="k">Money</div><div class="v">${fmtMoney(money)}</div></div>`
|
|
||||||
+ `<div><div class="k">${debt > 0 ? 'Still owed on the claim' : 'The claim'}</div><div class="v" style="color:${debt > 0 ? 'var(--melt)' : 'var(--ok)'}">${debt > 0 ? fmtMoney(debt) : 'Paid off'}</div></div>`
|
|
||||||
+ `<div><div class="k">Earned in total</div><div class="v">${fmtMoney(earned)}</div></div></div>`;
|
|
||||||
if (debt > 0) h += `<div class="mkt-pay"><span class="note" style="margin:0 6px 0 0">Pay toward the claim:</span>${[1000, 5000].map(a => `<button class="btn" data-pay="${a}" ${money >= Math.min(a, debt) ? '' : 'disabled'}>${fmtMoney(Math.min(a, debt))}</button>`).join('')}<button class="btn primary" data-pay="all" ${money > 0 ? '' : 'disabled'}>All I can (${fmtMoney(Math.min(money, debt))})</button></div>`;
|
|
||||||
else h += `<p class="note">The land is yours. Keep building: the ledger has bigger goals.</p>`;
|
|
||||||
h += `<p class="note">Anything that drops through the trade dock's grate is sold on the spot, once it is cool enough to handle. Junk like dirt, ash and slag goes for nothing. Rock you dig still sells, builders order it by the bagful, and a Grinder finds ore in it. Build walls around the dock to funnel more in.</p>`;
|
|
||||||
h += `<p class="label grouphead">Orders · ${esc(STANDING[standing()])}</p>`;
|
|
||||||
if (orders.length) {
|
|
||||||
h += '<div class="orderlist">' + orders.map(o => `<div class="o"><span class="t"><span class="sw" style="display:inline-block;vertical-align:-2px;margin-right:6px;background:${rgbOf(o.m)}"></span>${esc(orderName(o))}</span><span class="pay">${fmtMoney(o.pay)}</span>`
|
|
||||||
+ `<span class="d">${o.got} of ${o.qty} delivered · ${o.mult}× market · ${fmtClock(orderLeft(o))} left</span><span></span><span class="pb"><i style="width:${(o.got / o.qty * 100).toFixed(1)}%"></i></span></div>`).join('') + '</div>';
|
|
||||||
} else h += `<p class="note">No open orders. Buyers post new ones every minute or two, for goods you have found.</p>`;
|
|
||||||
h += `<p class="note">Goods dropped into the dock fill a matching order first. A missed deadline costs nothing: what you delivered is paid at market price. ${ordersDone} filled so far; bigger buyers come with more.</p>`;
|
|
||||||
h += `<p class="label grouphead">Prices</p><p class="note">Per grain, or per bar. Selling a lot of one thing lowers its price for a while; it recovers over about half an hour.</p>`;
|
|
||||||
let rows = '', unknown = 0;
|
|
||||||
for (const [g, list] of MARKET_GROUPS) {
|
|
||||||
const known = list.filter(m => seen[m]);
|
|
||||||
unknown += list.length - known.length;
|
|
||||||
if (!known.length) continue;
|
|
||||||
rows += `<tr><td colspan="3" class="label" style="padding-top:8px">${esc(g)}</td></tr>`;
|
|
||||||
for (const m of known) {
|
|
||||||
const f = priceFactor(m), now = priceNow(m);
|
|
||||||
rows += `<tr><td><span class="sw" style="display:inline-block;vertical-align:-2px;margin-right:6px;background:${rgbOf(m)}"></span>${esc(MD[m].name)}</td><td class="num">${fmtPrice(PRICE[m])}</td><td class="num ${f < 0.97 ? 'dip' : ''}">${fmtPrice(now)}${f < 0.97 ? ` (${Math.round(f * 100)}%)` : ''}</td></tr>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h += rows ? `<div class="tablewrap"><table class="mt"><thead><tr><th>Goods</th><th>Base</th><th>Now</th></tr></thead><tbody>${rows}</tbody></table></div>` : '<p class="note">Find something worth selling first.</p>';
|
|
||||||
if (unknown) h += `<p class="note">${unknown} more goods to discover.</p>`;
|
|
||||||
h += `<p class="label grouphead">Sales, minute by minute</p>`;
|
|
||||||
const recent = saleAcc.total > 0 ? [{ total: saleAcc.total, items: saleAcc.items, now: true }].concat(shipLog) : shipLog;
|
|
||||||
h += recent.length ? `<div class="shiplog">${recent.map(sl => `<div><span class="what">${sl.now ? 'This minute: ' : ''}${esc(itemsText(sl.items))}</span><b class="num">+${fmtMoney(sl.total)}</b></div>`).join('')}</div>` : '<p class="note">Nothing sold yet.</p>';
|
|
||||||
body.innerHTML = h;
|
|
||||||
body.querySelectorAll('[data-pay]').forEach(b => b.onclick = () => {
|
|
||||||
const v = b.dataset.pay === 'all' ? money : +b.dataset.pay;
|
|
||||||
const paid = payClaim(v);
|
|
||||||
if (paid > 0) { playSfx('sale'); toast(`Paid ${fmtMoney(paid)} toward the claim.`); }
|
|
||||||
renderNotebook(); hudDirty = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function fmtPrice(v) { return v >= 10 ? '$' + Math.round(v) : '$' + v.toFixed(2); }
|
|
||||||
function itemsText(items) {
|
|
||||||
const e = Object.entries(items).map(([m, n]) => [+m, n]).sort((a, b) => b[1] * PRICE[b[0]] - a[1] * PRICE[a[0]]);
|
|
||||||
const parts = e.slice(0, 3).map(([m, n]) => PH[m] === PH_ITEM ? `${n} ${MD[m].name.toLowerCase()}${n === 1 ? '' : 's'}` : `${n} ${MD[m].name.toLowerCase()}`);
|
|
||||||
if (e.length > 3) parts.push(`${e.length - 3} more`);
|
|
||||||
return parts.join(', ');
|
|
||||||
}
|
|
||||||
function renderWallet() {
|
|
||||||
$('w-money').textContent = fmtMoney(money);
|
|
||||||
const c = $('w-claim');
|
|
||||||
c.className = 'line' + (debt <= 0 ? ' paid' : '');
|
|
||||||
c.innerHTML = debt > 0 ? `<span>Claim owed</span><b>${fmtMoney(debt)}</b>` : `<span>Claim</span><b>paid off ✓</b>`;
|
|
||||||
const h = orders.length ? orders.map(o => {
|
|
||||||
const left = orderLeft(o);
|
|
||||||
return `<span class="ord"><span class="sw" style="background:${rgbOf(o.m)}"></span><span class="nm">${o.got}/${esc(orderName(o))}</span><span class="tm${left < 3600 ? ' late' : ''}">${fmtClock(left)}</span><span class="pb"><i style="width:${(o.got / o.qty * 100).toFixed(1)}%"></i></span></span>`;
|
|
||||||
}).join('') : '<span class="none">No orders yet</span>';
|
|
||||||
const box = $('w-orders');
|
|
||||||
if (box.innerHTML !== h) box.innerHTML = h;
|
|
||||||
}
|
|
||||||
function renderBlueprintTab(body) {
|
|
||||||
const lib = libLoad();
|
|
||||||
let h = `<p class="note">Select the Blueprint tool (5), drag over something you built to copy it, then click to place a ghost. Your bot builds ghosts from the materials in your bag when it is within reach. Right-click clears ghosts.</p>`;
|
|
||||||
h += `<p class="label grouphead">Starter designs</p><p class="note">Optional. Tested builds to learn from or copy; place one as a ghost and your bot builds it from your bag. Clear the space first: ghosts wait for open air.</p><div class="cards">`;
|
|
||||||
STARTER_DESIGNS.forEach((d, k) => {
|
|
||||||
const bp = d.bp();
|
|
||||||
h += `<div class="card"><div class="h"><span>${esc(d.name)}</span><span class="chip">${bp.bw}×${bp.bh} blocks</span></div><div class="p">${esc(d.desc)}</div><div class="p">Build cost: ${esc(costText(bpCost(bp), 1))}</div><div style="display:flex;gap:6px;flex-wrap:wrap"><button class="btn" data-starter="${k}">Place it</button></div></div>`;
|
|
||||||
});
|
|
||||||
h += '</div><p class="label grouphead">Your blueprints</p>';
|
|
||||||
h += `<div class="set"><div><div>Clipboard</div><div class="d">${GAME.clip ? `${GAME.clip.bw} × ${GAME.clip.bh} blocks, ${GAME.clip.blocks.length} pieces${GAME.clip.machines.length ? `, ${GAME.clip.machines.length} machines` : ''}${GAME.clip.molds.length ? `, ${GAME.clip.molds.length} molds` : ''}. Needs ${esc(costText(bpCost(GAME.clip), 1) || 'nothing')}` : 'Empty'}</div></div><div class="ctl"><button class="btn" id="bp-save" ${GAME.clip ? '' : 'disabled'}>Save to library</button><button class="btn" id="bp-code" ${GAME.clip ? '' : 'disabled'}>Copy code</button></div></div>`;
|
|
||||||
h += `<div class="set"><div><div>Import a blueprint code</div><div class="d">Paste a code that starts with DGBP2.</div></div><div class="ctl"><button class="btn" id="bp-import">Paste code…</button></div></div>`;
|
|
||||||
h += `<p class="label grouphead">Library</p>`;
|
|
||||||
if (!lib.length) h += `<p class="note">No saved blueprints yet.</p>`;
|
|
||||||
else {
|
|
||||||
h += '<div class="cards">';
|
|
||||||
lib.forEach((it, k) => {
|
|
||||||
const ok = bpValid(it.bp);
|
|
||||||
h += `<div class="card"><div class="h"><span>${esc(it.name)}</span><span class="chip">${ok ? `${it.bp.bw}×${it.bp.bh}` : 'old format'}</span></div><div class="p">${ok ? esc(costText(bpCost(it.bp), 1) || 'free') : 'Made before the block grid. It cannot be placed.'}</div><div style="display:flex;gap:6px;flex-wrap:wrap">${ok ? `<button class="btn" data-bpload="${k}">Use</button><button class="btn ghost" data-bpcode="${k}">Copy code</button>` : ''}<button class="btn ghost danger" data-bpdel="${k}">Delete</button></div></div>`;
|
|
||||||
});
|
|
||||||
h += '</div>';
|
|
||||||
}
|
|
||||||
body.innerHTML = h;
|
|
||||||
const sv = $('bp-save'); if (sv) sv.onclick = () => {
|
|
||||||
dialog('Name this blueprint', '<input class="text" id="bp-name" maxlength="40" placeholder="Copper smelter">', [
|
|
||||||
{ label: 'Cancel', cls: 'ghost' },
|
|
||||||
{ label: 'Save', cls: 'primary', fn: () => { const n = ($('bp-name').value || 'Untitled').trim(); const l = libLoad(); l.push({ name: n, bp: GAME.clip }); if (libSave(l)) toast(`Saved “${n}”.`); renderNotebook(); } },
|
|
||||||
]);
|
|
||||||
setTimeout(() => $('bp-name') && $('bp-name').focus(), 30);
|
|
||||||
};
|
|
||||||
const cc = $('bp-code'); if (cc) cc.onclick = () => copyCode(bpEncode(GAME.clip));
|
|
||||||
$('bp-import').onclick = () => {
|
|
||||||
dialog('Import blueprint', '<textarea class="code" id="bp-paste" placeholder="DGBP1:…"></textarea>', [
|
|
||||||
{ label: 'Cancel', cls: 'ghost' },
|
|
||||||
{ label: 'Import', cls: 'primary', fn: () => { try { GAME.clip = bpDecode($('bp-paste').value); GAME.paste = true; hudDirty = true; toast('Blueprint ready. Click to place it.'); closeAllModals(); } catch (e) { toast(e.message); return true; } } },
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
body.querySelectorAll('[data-bpload]').forEach(b => b.onclick = () => { GAME.clip = libLoad()[+b.dataset.bpload].bp; GAME.paste = true; hudDirty = true; closeAllModals(); toast('Blueprint ready. Click to place it.'); });
|
|
||||||
body.querySelectorAll('[data-starter]').forEach(b => b.onclick = () => { GAME.clip = STARTER_DESIGNS[+b.dataset.starter].bp(); GAME.paste = true; hudDirty = true; closeAllModals(); toast('Click to place it. R flips it.'); });
|
|
||||||
body.querySelectorAll('[data-bpcode]').forEach(b => b.onclick = () => copyCode(bpEncode(libLoad()[+b.dataset.bpcode].bp)));
|
|
||||||
body.querySelectorAll('[data-bpdel]').forEach(b => b.onclick = () => { const l = libLoad(); l.splice(+b.dataset.bpdel, 1); libSave(l); renderNotebook(); });
|
|
||||||
}
|
|
||||||
function copyCode(code) {
|
|
||||||
const done = () => toast('Blueprint code copied.');
|
|
||||||
const fallback = () => { dialog('Blueprint code', `<p class="note">Select the text and copy it.</p><textarea class="code" id="bp-out" readonly>${esc(code)}</textarea>`, [{ label: 'Done', cls: 'primary' }]); setTimeout(() => { const t = $('bp-out'); if (t) { t.focus(); t.select(); } }, 30); };
|
|
||||||
try { navigator.clipboard.writeText(code).then(done, fallback); } catch (e) { fallback(); }
|
|
||||||
}
|
|
||||||
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'],
|
['Fly', 'W A S D'], ['Boost while flying', 'Shift'], ['Tool modifier (while not flying)', 'Shift'], ['Use tool', 'Left mouse'], ['Second action', 'Right mouse'],
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// 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.
|
||||||
|
const CRACK_N = [makeNoise(mulberry32(0xc7ac1)), makeNoise(mulberry32(0xc7ac2)), makeNoise(mulberry32(0xc7ac3))];
|
||||||
|
const CRACK_LV = [{ s: 1 / 7, k: 0.1, w: 0.2 }, { s: 1 / 3.6, k: 0.42, w: 0.14 }, { s: 1 / 2, k: 0.72, w: 0.09 }];
|
||||||
|
function crackSeg(ctx, x, y, n, sc, zp, oy) {
|
||||||
|
const v = (cx, cy) => n(cx * sc + 0.37, cy * sc + 0.61);
|
||||||
|
const a = v(x, y), b = v(x + 1, y), c = v(x + 1, y + 1), d = v(x, y + 1);
|
||||||
|
const pts = [];
|
||||||
|
if ((a < 0) !== (b < 0)) pts.push(x + a / (a - b), y);
|
||||||
|
if ((b < 0) !== (c < 0)) pts.push(x + 1, y + b / (b - c));
|
||||||
|
if ((d < 0) !== (c < 0)) pts.push(x + d / (d - c), y + 1);
|
||||||
|
if ((a < 0) !== (d < 0)) pts.push(x, y + a / (a - d));
|
||||||
|
for (let k = 0; k + 3 < pts.length; k += 4) {
|
||||||
|
ctx.moveTo((pts[k] - R.camX) * zp, (pts[k + 1] - R.camY) * zp + oy);
|
||||||
|
ctx.lineTo((pts[k + 2] - R.camX) * zp, (pts[k + 3] - R.camY) * zp + oy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function drawCracks() {
|
||||||
|
const ctx = R.ctx, zp = R.zp;
|
||||||
|
if (zp < 2.5) return;
|
||||||
|
const x0 = Math.max(0, Math.floor(R.camX)), y0 = Math.max(0, Math.floor(R.camY));
|
||||||
|
const x1 = Math.min(W - 1, Math.ceil(R.camX + R.canvas.width / zp)), y1 = Math.min(H - 1, Math.ceil(R.camY + R.canvas.height / zp));
|
||||||
|
const cells = [];
|
||||||
|
for (let y = y0; y <= y1; y++) { const row = y * W; for (let x = x0; x <= x1; x++) { const dp = digp[row + x]; if (dp && PH[mat[row + x]] === PH_SOLID) cells.push(x, y, dp / 255); } }
|
||||||
|
if (!cells.length) return;
|
||||||
|
ctx.save(); ctx.lineCap = 'round';
|
||||||
|
for (let l = 0; l < CRACK_LV.length; l++) {
|
||||||
|
const L = CRACK_LV[l];
|
||||||
|
// a soft lit lip just below each crack, then the dark crack itself
|
||||||
|
for (const pass of [0, 1]) {
|
||||||
|
ctx.beginPath();
|
||||||
|
for (let k = 0; k < cells.length; k += 3) if (cells[k + 2] >= L.k) crackSeg(ctx, cells[k], cells[k + 1], CRACK_N[l], L.s, zp, pass ? 0 : Math.max(1, zp * 0.12));
|
||||||
|
ctx.strokeStyle = pass ? `rgba(18,12,10,${0.78 - l * 0.12})` : 'rgba(255,240,220,0.13)';
|
||||||
|
ctx.lineWidth = Math.max(1, zp * L.w * (pass ? 1 : 1.3));
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
// The pulses in flight: a wavefront running out from the face of the bot, fading as it goes
|
||||||
|
function pulseRGB(tune) {
|
||||||
|
if (!tune) return '150,215,255';
|
||||||
|
return `${Math.round((COLR[tune] + 255) / 2)},${Math.round((COLG[tune] + 255) / 2)},${Math.round((COLB[tune] + 255) / 2)}`;
|
||||||
|
}
|
||||||
|
// screen points across a fan (mouth at the bot, or out at distance sd along each ray)
|
||||||
|
function fanPoints(ox, oy, ang, Wd, sd, n) {
|
||||||
|
const pts = [];
|
||||||
|
for (let k = 0; k <= n; k++) { const r = pulseRay(ox, oy, ang, Wd, -1 + 2 * k / n); pts.push(worldToScreenPx(r.x + r.ux * sd, r.y + r.uy * sd)); }
|
||||||
|
return pts;
|
||||||
|
}
|
||||||
|
function drawDrillPulses() {
|
||||||
|
if (!drillPulses.length) return;
|
||||||
|
const ctx = R.ctx, zp = R.zp;
|
||||||
|
ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.lineJoin = 'round'; ctx.lineCap = 'round';
|
||||||
|
for (const q of drillPulses) {
|
||||||
|
const Wd = PULSE_W[q.w], rr = Math.min(q.r, q.range), a = Math.max(0, 1 - q.r / (q.range + 3)), col = pulseRGB(q.tune);
|
||||||
|
if (rr < 3) continue;
|
||||||
|
const front = fanPoints(q.ox, q.oy, q.ang, Wd, rr, 10), back = fanPoints(q.ox, q.oy, q.ang, Wd, Math.max(2.5, rr - 3), 10);
|
||||||
|
const line = (pts) => { ctx.beginPath(); pts.forEach((pt, k) => k ? ctx.lineTo(pt.x, pt.y) : ctx.moveTo(pt.x, pt.y)); };
|
||||||
|
// a faint band behind the front, then the bright front itself
|
||||||
|
ctx.fillStyle = `rgba(${col},${(0.08 * a).toFixed(3)})`;
|
||||||
|
line(front); for (let k = back.length - 1; k >= 0; k--) ctx.lineTo(back[k].x, back[k].y); ctx.closePath(); ctx.fill();
|
||||||
|
ctx.strokeStyle = `rgba(${col},${(0.2 * a).toFixed(3)})`; ctx.lineWidth = Math.max(3, zp * 1.3); line(front); ctx.stroke();
|
||||||
|
ctx.strokeStyle = `rgba(${col},${(0.85 * a).toFixed(3)})`; ctx.lineWidth = Math.max(1.5, zp * 0.3); line(front); ctx.stroke();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
// With the drill in hand: the fan a pulse will sweep (as wide as the bot where it leaves it),
|
||||||
|
// out to where it stops, and a crosshair
|
||||||
|
function drawDrillAim(ax, ay) {
|
||||||
|
const ctx = R.ctx, zp = R.zp;
|
||||||
|
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 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 mouth = fanPoints(o.x, o.y, ang, Wd, 2.5, 1), far = fanPoints(o.x, o.y, ang, Wd, range, 10);
|
||||||
|
ctx.save();
|
||||||
|
// the outline gets a dark under-stroke so it reads on bright sky as well as dark rock
|
||||||
|
const outline = () => { ctx.beginPath(); ctx.moveTo(mouth[0].x, mouth[0].y); for (const pt of far) ctx.lineTo(pt.x, pt.y); ctx.lineTo(mouth[1].x, mouth[1].y); ctx.closePath(); };
|
||||||
|
ctx.fillStyle = `rgba(${col},0.06)`; outline(); ctx.fill();
|
||||||
|
ctx.setLineDash([zp * 0.8, zp * 0.8]); ctx.lineDashOffset = -(R.frame * 0.3) % (zp * 1.6);
|
||||||
|
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.setLineDash([]); ctx.lineDashOffset = 0;
|
||||||
|
// the emitter glows across the bot's face while it fires
|
||||||
|
const hot = Math.max(0, 1 - (tick - lastPulseTick) / PULSE_EVERY);
|
||||||
|
if (GAME.lmb && hot > 0) {
|
||||||
|
ctx.globalCompositeOperation = 'lighter';
|
||||||
|
ctx.strokeStyle = `rgba(${col},${(0.7 * hot).toFixed(3)})`; ctx.lineWidth = Math.max(2, zp * 0.6); ctx.lineCap = 'round';
|
||||||
|
ctx.beginPath(); ctx.moveTo(mouth[0].x, mouth[0].y); ctx.lineTo(mouth[1].x, mouth[1].y); ctx.stroke();
|
||||||
|
const g = ctx.createRadialGradient(c.x, c.y, 0, c.x, c.y, zp * 3.5);
|
||||||
|
g.addColorStop(0, `rgba(${col},${(0.35 * hot).toFixed(3)})`); g.addColorStop(1, `rgba(${col},0)`);
|
||||||
|
ctx.fillStyle = g; ctx.fillRect(c.x - zp * 3.5, c.y - zp * 3.5, zp * 7, zp * 7);
|
||||||
|
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);
|
||||||
|
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.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();
|
||||||
|
if (GAME.drillTune) { const sz = Math.max(6, zp * 1.4); ctx.fillStyle = rgbOf(GAME.drillTune); ctx.fillRect(p.x + k * 1.8, p.y - k * 1.8 - sz, sz, sz); ctx.strokeStyle = 'rgba(0,0,0,0.5)'; ctx.lineWidth = 1; ctx.strokeRect(p.x + k * 1.8 + 0.5, p.y - k * 1.8 - sz + 0.5, sz - 1, sz - 1); }
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// Selection previews: labelled tags, copy and cut boxes, and paste ghosts.
|
||||||
|
// a small labelled tag drawn on the world (screen pixels)
|
||||||
|
function drawTag(text, col, x, y, sub) {
|
||||||
|
const ctx = R.ctx, u = Math.max(1, R.canvas.width / window.innerWidth);
|
||||||
|
renderMonoFont();
|
||||||
|
ctx.save();
|
||||||
|
ctx.font = `700 ${Math.round(11 * u)}px ${R.monoFont}`; ctx.textBaseline = 'middle';
|
||||||
|
const tw = ctx.measureText(text).width;
|
||||||
|
ctx.font = `500 ${Math.round(11 * u)}px ${R.monoFont}`;
|
||||||
|
const sw = sub ? ctx.measureText(sub).width + 8 * u : 0;
|
||||||
|
const h = Math.round(19 * u), pad = 7 * u, w = tw + sw + pad * 2;
|
||||||
|
x = Math.max(2, Math.min(R.canvas.width - w - 2, x)); y = Math.max(2, Math.min(R.canvas.height - h - 2, y));
|
||||||
|
ctx.fillStyle = 'rgba(14,12,11,0.9)'; ctx.fillRect(x, y, w, h);
|
||||||
|
ctx.fillStyle = `rgb(${col})`; ctx.fillRect(x, y, 3 * u, h);
|
||||||
|
ctx.strokeStyle = `rgba(${col},0.8)`; ctx.lineWidth = u; ctx.strokeRect(x + u / 2, y + u / 2, w - u, h - u);
|
||||||
|
ctx.font = `700 ${Math.round(11 * u)}px ${R.monoFont}`; ctx.fillStyle = `rgb(${col})`; ctx.fillText(text, x + pad, y + h / 2 + u * 0.5);
|
||||||
|
if (sub) { ctx.font = `500 ${Math.round(11 * u)}px ${R.monoFont}`; ctx.fillStyle = 'rgba(231,220,203,0.9)'; ctx.fillText(sub, x + pad + tw + 8 * u, y + h / 2 + u * 0.5); }
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
// Copy (teal) and cut (red, hatched): the box being dragged, what it would pick up, and a tag
|
||||||
|
const SEL_COL = { copy: '92,186,166', cut: '239,106,85' };
|
||||||
|
function drawSelection(mode, ax, ay) {
|
||||||
|
const ctx = R.ctx, zp = R.zp, col = SEL_COL[mode], sd = GAME.selDrag;
|
||||||
|
const sx = sd ? sd.x : ax, sy = sd ? sd.y : ay;
|
||||||
|
const a = blockOf(Math.min(sx, ax), Math.min(sy, ay)), c = blockOf(Math.max(sx, ax), Math.max(sy, ay));
|
||||||
|
const bw = c.bx - a.bx + 1, bh = c.by - a.by + 1;
|
||||||
|
drawGrid(blockOf(ax, ay).bx, blockOf(ax, ay).by, 7);
|
||||||
|
ctx.save();
|
||||||
|
// what is inside: built blocks, and machines and molds that fit wholly inside
|
||||||
|
let n = 0;
|
||||||
|
if (sd) {
|
||||||
|
ctx.fillStyle = `rgba(${col},${mode === 'cut' ? 0.42 : 0.3})`;
|
||||||
|
for (let by = a.by; by <= c.by; by++) for (let bx = a.bx; bx <= c.bx; bx++) {
|
||||||
|
if (bx < 0 || by < 0 || bx >= BW || by >= BH) continue;
|
||||||
|
const b = by * BW + bx;
|
||||||
|
if (blockP[b] && blockIntegrity(b) >= 8) { const [x, y, w, h] = blockRectPx(bx, by, 1, 1); ctx.fillRect(x, y, w, h); n++; }
|
||||||
|
}
|
||||||
|
ctx.strokeStyle = `rgba(${col},0.95)`; ctx.lineWidth = Math.max(1, zp * 0.35);
|
||||||
|
for (const mm of machines) {
|
||||||
|
if (!mm || PIECE_BY_ID[mm.type].fixed) continue;
|
||||||
|
const P = PIECE_BY_ID[mm.type], mbx = mm.x / BS, mby = mm.y / BS;
|
||||||
|
if (mbx >= a.bx && mby >= a.by && mbx + P.bw - 1 <= c.bx && mby + P.bh - 1 <= c.by) { const [x, y, w, h] = blockRectPx(mbx, mby, P.bw, P.bh); ctx.fillRect(x, y, w, h); ctx.strokeRect(x, y, w, h); n++; }
|
||||||
|
}
|
||||||
|
for (const mo of molds) {
|
||||||
|
const md = MOLDS[mo.kind], mbx = mo.x / BS, mby = mo.y / BS;
|
||||||
|
if (mbx >= a.bx && mby >= a.by && mbx + md.bw - 1 <= c.bx && mby + md.bh - 1 <= c.by) { const [x, y, w, h] = blockRectPx(mbx, mby, md.bw, md.bh); ctx.fillRect(x, y, w, h); ctx.strokeRect(x, y, w, h); n++; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const [x, y, w, h] = blockRectPx(a.bx, a.by, bw, bh);
|
||||||
|
ctx.fillStyle = `rgba(${col},0.08)`; ctx.fillRect(x, y, w, h);
|
||||||
|
if (mode === 'cut') {
|
||||||
|
// red hatching: this will be taken down
|
||||||
|
ctx.save(); ctx.beginPath(); ctx.rect(x, y, w, h); ctx.clip();
|
||||||
|
ctx.strokeStyle = `rgba(${col},0.35)`; ctx.lineWidth = Math.max(1, zp * 0.25);
|
||||||
|
const step = Math.max(8, zp * 2.5);
|
||||||
|
ctx.beginPath(); for (let k = -h; k < w; k += step) { ctx.moveTo(x + k, y + h); ctx.lineTo(x + k + h, y); } ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
ctx.strokeStyle = `rgba(${col},0.98)`; ctx.lineWidth = Math.max(1.5, zp * (mode === 'cut' ? 0.45 : 0.35));
|
||||||
|
ctx.setLineDash([zp * 1.2, zp * 0.8]); ctx.lineDashOffset = -(R.frame * 0.4) % (zp * 2);
|
||||||
|
ctx.strokeRect(x, y, w, h); ctx.setLineDash([]); ctx.lineDashOffset = 0;
|
||||||
|
ctx.restore();
|
||||||
|
const label = mode === 'cut' ? '✂ CUT' : '⧉ COPY';
|
||||||
|
const sub = !sd ? 'drag a box' : n ? (mode === 'cut' ? `${plural(n, 'piece')} · let go to take down` : `${plural(n, 'piece')} · let go to copy`) : 'nothing built in here';
|
||||||
|
drawTag(label, col, x, y - Math.round(23 * Math.max(1, R.canvas.width / window.innerWidth)), sub);
|
||||||
|
}
|
||||||
|
// Pasting: the clipboard follows the cursor, snapped to the grid
|
||||||
|
function drawPastePreview() {
|
||||||
|
const ctx = R.ctx, zp = R.zp, bp = GAME.clip, { bx: ox, by: oy } = stampOrigin(bp.bw, bp.bh);
|
||||||
|
drawGrid(aimBlock().bx, aimBlock().by, 7);
|
||||||
|
ctx.save();
|
||||||
|
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.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); }
|
||||||
|
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); }
|
||||||
|
ctx.restore();
|
||||||
|
drawTag('PASTE', '92,186,166', x, y - Math.round(23 * Math.max(1, R.canvas.width / window.innerWidth)), 'click to place · R flips · Esc stops');
|
||||||
|
}
|
||||||
+9
-190
@@ -1,4 +1,4 @@
|
|||||||
// Previews drawn under the cursor: build ghosts, drill aim and pulses, selections, paste.
|
// Cursor previews: shared grid helpers, air streaks and tool preview dispatch.
|
||||||
// ---------- Overlay previews ----------
|
// ---------- Overlay previews ----------
|
||||||
function blockRectPx(bx, by, bw, bh) { const q = worldToScreenPx(bx * BS, by * BS); return [q.x, q.y, bw * BS * R.zp, bh * BS * R.zp]; }
|
function blockRectPx(bx, by, bw, bh) { const q = worldToScreenPx(bx * BS, by * BS); return [q.x, q.y, bw * BS * R.zp, bh * BS * R.zp]; }
|
||||||
function drawGrid(cbx, cby, radius) {
|
function drawGrid(cbx, cby, radius) {
|
||||||
@@ -18,109 +18,6 @@ function previewColor(code, v) {
|
|||||||
if (code === BP_FILTER) return '96,100,110';
|
if (code === BP_FILTER) 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';
|
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';
|
||||||
}
|
}
|
||||||
// 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.
|
|
||||||
const CRACK_N = [makeNoise(mulberry32(0xc7ac1)), makeNoise(mulberry32(0xc7ac2)), makeNoise(mulberry32(0xc7ac3))];
|
|
||||||
const CRACK_LV = [{ s: 1 / 7, k: 0.1, w: 0.2 }, { s: 1 / 3.6, k: 0.42, w: 0.14 }, { s: 1 / 2, k: 0.72, w: 0.09 }];
|
|
||||||
function crackSeg(ctx, x, y, n, sc, zp, oy) {
|
|
||||||
const v = (cx, cy) => n(cx * sc + 0.37, cy * sc + 0.61);
|
|
||||||
const a = v(x, y), b = v(x + 1, y), c = v(x + 1, y + 1), d = v(x, y + 1);
|
|
||||||
const pts = [];
|
|
||||||
if ((a < 0) !== (b < 0)) pts.push(x + a / (a - b), y);
|
|
||||||
if ((b < 0) !== (c < 0)) pts.push(x + 1, y + b / (b - c));
|
|
||||||
if ((d < 0) !== (c < 0)) pts.push(x + d / (d - c), y + 1);
|
|
||||||
if ((a < 0) !== (d < 0)) pts.push(x, y + a / (a - d));
|
|
||||||
for (let k = 0; k + 3 < pts.length; k += 4) {
|
|
||||||
ctx.moveTo((pts[k] - R.camX) * zp, (pts[k + 1] - R.camY) * zp + oy);
|
|
||||||
ctx.lineTo((pts[k + 2] - R.camX) * zp, (pts[k + 3] - R.camY) * zp + oy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function drawCracks() {
|
|
||||||
const ctx = R.ctx, zp = R.zp;
|
|
||||||
if (zp < 2.5) return;
|
|
||||||
const x0 = Math.max(0, Math.floor(R.camX)), y0 = Math.max(0, Math.floor(R.camY));
|
|
||||||
const x1 = Math.min(W - 1, Math.ceil(R.camX + R.canvas.width / zp)), y1 = Math.min(H - 1, Math.ceil(R.camY + R.canvas.height / zp));
|
|
||||||
const cells = [];
|
|
||||||
for (let y = y0; y <= y1; y++) { const row = y * W; for (let x = x0; x <= x1; x++) { const dp = digp[row + x]; if (dp && PH[mat[row + x]] === PH_SOLID) cells.push(x, y, dp / 255); } }
|
|
||||||
if (!cells.length) return;
|
|
||||||
ctx.save(); ctx.lineCap = 'round';
|
|
||||||
for (let l = 0; l < CRACK_LV.length; l++) {
|
|
||||||
const L = CRACK_LV[l];
|
|
||||||
// a soft lit lip just below each crack, then the dark crack itself
|
|
||||||
for (const pass of [0, 1]) {
|
|
||||||
ctx.beginPath();
|
|
||||||
for (let k = 0; k < cells.length; k += 3) if (cells[k + 2] >= L.k) crackSeg(ctx, cells[k], cells[k + 1], CRACK_N[l], L.s, zp, pass ? 0 : Math.max(1, zp * 0.12));
|
|
||||||
ctx.strokeStyle = pass ? `rgba(18,12,10,${0.78 - l * 0.12})` : 'rgba(255,240,220,0.13)';
|
|
||||||
ctx.lineWidth = Math.max(1, zp * L.w * (pass ? 1 : 1.3));
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
// The pulses in flight: a wavefront running out from the face of the bot, fading as it goes
|
|
||||||
function pulseRGB(tune) {
|
|
||||||
if (!tune) return '150,215,255';
|
|
||||||
return `${Math.round((COLR[tune] + 255) / 2)},${Math.round((COLG[tune] + 255) / 2)},${Math.round((COLB[tune] + 255) / 2)}`;
|
|
||||||
}
|
|
||||||
// screen points across a fan (mouth at the bot, or out at distance sd along each ray)
|
|
||||||
function fanPoints(ox, oy, ang, Wd, sd, n) {
|
|
||||||
const pts = [];
|
|
||||||
for (let k = 0; k <= n; k++) { const r = pulseRay(ox, oy, ang, Wd, -1 + 2 * k / n); pts.push(worldToScreenPx(r.x + r.ux * sd, r.y + r.uy * sd)); }
|
|
||||||
return pts;
|
|
||||||
}
|
|
||||||
function drawDrillPulses() {
|
|
||||||
if (!drillPulses.length) return;
|
|
||||||
const ctx = R.ctx, zp = R.zp;
|
|
||||||
ctx.save(); ctx.globalCompositeOperation = 'lighter'; ctx.lineJoin = 'round'; ctx.lineCap = 'round';
|
|
||||||
for (const q of drillPulses) {
|
|
||||||
const Wd = PULSE_W[q.w], rr = Math.min(q.r, q.range), a = Math.max(0, 1 - q.r / (q.range + 3)), col = pulseRGB(q.tune);
|
|
||||||
if (rr < 3) continue;
|
|
||||||
const front = fanPoints(q.ox, q.oy, q.ang, Wd, rr, 10), back = fanPoints(q.ox, q.oy, q.ang, Wd, Math.max(2.5, rr - 3), 10);
|
|
||||||
const line = (pts) => { ctx.beginPath(); pts.forEach((pt, k) => k ? ctx.lineTo(pt.x, pt.y) : ctx.moveTo(pt.x, pt.y)); };
|
|
||||||
// a faint band behind the front, then the bright front itself
|
|
||||||
ctx.fillStyle = `rgba(${col},${(0.08 * a).toFixed(3)})`;
|
|
||||||
line(front); for (let k = back.length - 1; k >= 0; k--) ctx.lineTo(back[k].x, back[k].y); ctx.closePath(); ctx.fill();
|
|
||||||
ctx.strokeStyle = `rgba(${col},${(0.2 * a).toFixed(3)})`; ctx.lineWidth = Math.max(3, zp * 1.3); line(front); ctx.stroke();
|
|
||||||
ctx.strokeStyle = `rgba(${col},${(0.85 * a).toFixed(3)})`; ctx.lineWidth = Math.max(1.5, zp * 0.3); line(front); ctx.stroke();
|
|
||||||
}
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
// With the drill in hand: the fan a pulse will sweep (as wide as the bot where it leaves it),
|
|
||||||
// out to where it stops, and a crosshair
|
|
||||||
function drawDrillAim(ax, ay) {
|
|
||||||
const ctx = R.ctx, zp = R.zp;
|
|
||||||
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 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 mouth = fanPoints(o.x, o.y, ang, Wd, 2.5, 1), far = fanPoints(o.x, o.y, ang, Wd, range, 10);
|
|
||||||
ctx.save();
|
|
||||||
// the outline gets a dark under-stroke so it reads on bright sky as well as dark rock
|
|
||||||
const outline = () => { ctx.beginPath(); ctx.moveTo(mouth[0].x, mouth[0].y); for (const pt of far) ctx.lineTo(pt.x, pt.y); ctx.lineTo(mouth[1].x, mouth[1].y); ctx.closePath(); };
|
|
||||||
ctx.fillStyle = `rgba(${col},0.06)`; outline(); ctx.fill();
|
|
||||||
ctx.setLineDash([zp * 0.8, zp * 0.8]); ctx.lineDashOffset = -(R.frame * 0.3) % (zp * 1.6);
|
|
||||||
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.setLineDash([]); ctx.lineDashOffset = 0;
|
|
||||||
// the emitter glows across the bot's face while it fires
|
|
||||||
const hot = Math.max(0, 1 - (tick - lastPulseTick) / PULSE_EVERY);
|
|
||||||
if (GAME.lmb && hot > 0) {
|
|
||||||
ctx.globalCompositeOperation = 'lighter';
|
|
||||||
ctx.strokeStyle = `rgba(${col},${(0.7 * hot).toFixed(3)})`; ctx.lineWidth = Math.max(2, zp * 0.6); ctx.lineCap = 'round';
|
|
||||||
ctx.beginPath(); ctx.moveTo(mouth[0].x, mouth[0].y); ctx.lineTo(mouth[1].x, mouth[1].y); ctx.stroke();
|
|
||||||
const g = ctx.createRadialGradient(c.x, c.y, 0, c.x, c.y, zp * 3.5);
|
|
||||||
g.addColorStop(0, `rgba(${col},${(0.35 * hot).toFixed(3)})`); g.addColorStop(1, `rgba(${col},0)`);
|
|
||||||
ctx.fillStyle = g; ctx.fillRect(c.x - zp * 3.5, c.y - zp * 3.5, zp * 7, zp * 7);
|
|
||||||
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);
|
|
||||||
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.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();
|
|
||||||
if (GAME.drillTune) { const sz = Math.max(6, zp * 1.4); ctx.fillStyle = rgbOf(GAME.drillTune); ctx.fillRect(p.x + k * 1.8, p.y - k * 1.8 - sz, sz, sz); ctx.strokeStyle = 'rgba(0,0,0,0.5)'; ctx.lineWidth = 1; ctx.strokeRect(p.x + k * 1.8 + 0.5, p.y - k * 1.8 - sz + 0.5, sz - 1, sz - 1); }
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
// 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) {
|
||||||
if (!SET.particles || (SET.fog && fogSeen(ax, ay) < 64)) return;
|
if (!SET.particles || (SET.fog && fogSeen(ax, ay) < 64)) return;
|
||||||
@@ -157,6 +54,11 @@ function drawAirStreaks(ax, ay, p, r, blowing) {
|
|||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
function drawReachCircle(radius, alpha) {
|
||||||
|
const ctx = R.ctx, zp = R.zp, c = worldToScreenPx(bot.x + bot.w / 2, bot.y + bot.h / 2);
|
||||||
|
ctx.strokeStyle = `rgba(239,106,85,${alpha})`; ctx.setLineDash([zp * 2, zp * 2]); ctx.lineWidth = Math.max(1, zp * 0.3);
|
||||||
|
ctx.beginPath(); ctx.arc(c.x, c.y, radius * zp, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]);
|
||||||
|
}
|
||||||
function drawPreviews() {
|
function drawPreviews() {
|
||||||
const ctx = R.ctx, zp = R.zp;
|
const ctx = R.ctx, zp = R.zp;
|
||||||
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);
|
||||||
@@ -165,14 +67,10 @@ function drawPreviews() {
|
|||||||
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' && !sel) {
|
||||||
const c = worldToScreenPx(bot.x + bot.w / 2, bot.y + bot.h / 2);
|
drawReachCircle(REACH, 0.35);
|
||||||
ctx.strokeStyle = 'rgba(239,106,85,0.35)'; ctx.setLineDash([zp * 2, zp * 2]); ctx.lineWidth = Math.max(1, zp * 0.3);
|
|
||||||
ctx.beginPath(); ctx.arc(c.x, c.y, REACH * zp, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]);
|
|
||||||
}
|
}
|
||||||
if (t === 'drill' && !sel && !pasting && !inDrillReach(ax, ay, GAME.drillW)) {
|
if (t === 'drill' && !sel && !pasting && !inDrillReach(ax, ay, GAME.drillW)) {
|
||||||
const c = worldToScreenPx(bot.x + bot.w / 2, bot.y + bot.h / 2);
|
drawReachCircle(drillReach(GAME.drillW), 0.45);
|
||||||
ctx.strokeStyle = 'rgba(239,106,85,0.45)'; ctx.setLineDash([zp * 2, zp * 2]); ctx.lineWidth = Math.max(1, zp * 0.3);
|
|
||||||
ctx.beginPath(); ctx.arc(c.x, c.y, drillReach(GAME.drillW) * zp, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]);
|
|
||||||
}
|
}
|
||||||
drawDrillPulses();
|
drawDrillPulses();
|
||||||
if (t === 'drill' && !sel && !pasting) drawDrillAim(ax, ay);
|
if (t === 'drill' && !sel && !pasting) drawDrillAim(ax, ay);
|
||||||
@@ -251,7 +149,7 @@ function drawPreviews() {
|
|||||||
ctx.fillRect(p.x + zp * 6, p.y - zp * 6 - 14, tw + 10, 18); ctx.fillStyle = '#e7dccb'; ctx.fillText(label, p.x + zp * 6 + 5, p.y - zp * 6);
|
ctx.fillRect(p.x + zp * 6, p.y - zp * 6 - 14, tw + 10, 18); ctx.fillStyle = '#e7dccb'; ctx.fillText(label, p.x + zp * 6 + 5, p.y - zp * 6);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const bw = P.kind === 'mold' ? MOLDS[P.mold].bw : P.bw, bh = P.kind === 'mold' ? MOLDS[P.mold].bh : P.bh;
|
const { bw, bh } = pieceSize(P);
|
||||||
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);
|
||||||
@@ -290,82 +188,3 @@ 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);
|
||||||
}
|
}
|
||||||
// a small labelled tag drawn on the world (screen pixels)
|
|
||||||
function drawTag(text, col, x, y, sub) {
|
|
||||||
const ctx = R.ctx, u = Math.max(1, R.canvas.width / window.innerWidth);
|
|
||||||
if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace';
|
|
||||||
ctx.save();
|
|
||||||
ctx.font = `700 ${Math.round(11 * u)}px ${R.monoFont}`; ctx.textBaseline = 'middle';
|
|
||||||
const tw = ctx.measureText(text).width;
|
|
||||||
ctx.font = `500 ${Math.round(11 * u)}px ${R.monoFont}`;
|
|
||||||
const sw = sub ? ctx.measureText(sub).width + 8 * u : 0;
|
|
||||||
const h = Math.round(19 * u), pad = 7 * u, w = tw + sw + pad * 2;
|
|
||||||
x = Math.max(2, Math.min(R.canvas.width - w - 2, x)); y = Math.max(2, Math.min(R.canvas.height - h - 2, y));
|
|
||||||
ctx.fillStyle = 'rgba(14,12,11,0.9)'; ctx.fillRect(x, y, w, h);
|
|
||||||
ctx.fillStyle = `rgb(${col})`; ctx.fillRect(x, y, 3 * u, h);
|
|
||||||
ctx.strokeStyle = `rgba(${col},0.8)`; ctx.lineWidth = u; ctx.strokeRect(x + u / 2, y + u / 2, w - u, h - u);
|
|
||||||
ctx.font = `700 ${Math.round(11 * u)}px ${R.monoFont}`; ctx.fillStyle = `rgb(${col})`; ctx.fillText(text, x + pad, y + h / 2 + u * 0.5);
|
|
||||||
if (sub) { ctx.font = `500 ${Math.round(11 * u)}px ${R.monoFont}`; ctx.fillStyle = 'rgba(231,220,203,0.9)'; ctx.fillText(sub, x + pad + tw + 8 * u, y + h / 2 + u * 0.5); }
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
// Copy (teal) and cut (red, hatched): the box being dragged, what it would pick up, and a tag
|
|
||||||
const SEL_COL = { copy: '92,186,166', cut: '239,106,85' };
|
|
||||||
function drawSelection(mode, ax, ay) {
|
|
||||||
const ctx = R.ctx, zp = R.zp, col = SEL_COL[mode], sd = GAME.selDrag;
|
|
||||||
const sx = sd ? sd.x : ax, sy = sd ? sd.y : ay;
|
|
||||||
const a = blockOf(Math.min(sx, ax), Math.min(sy, ay)), c = blockOf(Math.max(sx, ax), Math.max(sy, ay));
|
|
||||||
const bw = c.bx - a.bx + 1, bh = c.by - a.by + 1;
|
|
||||||
drawGrid(blockOf(ax, ay).bx, blockOf(ax, ay).by, 7);
|
|
||||||
ctx.save();
|
|
||||||
// what is inside: built blocks, and machines and molds that fit wholly inside
|
|
||||||
let n = 0;
|
|
||||||
if (sd) {
|
|
||||||
ctx.fillStyle = `rgba(${col},${mode === 'cut' ? 0.42 : 0.3})`;
|
|
||||||
for (let by = a.by; by <= c.by; by++) for (let bx = a.bx; bx <= c.bx; bx++) {
|
|
||||||
if (bx < 0 || by < 0 || bx >= BW || by >= BH) continue;
|
|
||||||
const b = by * BW + bx;
|
|
||||||
if (blockP[b] && blockIntegrity(b) >= 8) { const [x, y, w, h] = blockRectPx(bx, by, 1, 1); ctx.fillRect(x, y, w, h); n++; }
|
|
||||||
}
|
|
||||||
ctx.strokeStyle = `rgba(${col},0.95)`; ctx.lineWidth = Math.max(1, zp * 0.35);
|
|
||||||
for (const mm of machines) {
|
|
||||||
if (!mm || PIECE_BY_ID[mm.type].fixed) continue;
|
|
||||||
const P = PIECE_BY_ID[mm.type], mbx = mm.x / BS, mby = mm.y / BS;
|
|
||||||
if (mbx >= a.bx && mby >= a.by && mbx + P.bw - 1 <= c.bx && mby + P.bh - 1 <= c.by) { const [x, y, w, h] = blockRectPx(mbx, mby, P.bw, P.bh); ctx.fillRect(x, y, w, h); ctx.strokeRect(x, y, w, h); n++; }
|
|
||||||
}
|
|
||||||
for (const mo of molds) {
|
|
||||||
const md = MOLDS[mo.kind], mbx = mo.x / BS, mby = mo.y / BS;
|
|
||||||
if (mbx >= a.bx && mby >= a.by && mbx + md.bw - 1 <= c.bx && mby + md.bh - 1 <= c.by) { const [x, y, w, h] = blockRectPx(mbx, mby, md.bw, md.bh); ctx.fillRect(x, y, w, h); ctx.strokeRect(x, y, w, h); n++; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const [x, y, w, h] = blockRectPx(a.bx, a.by, bw, bh);
|
|
||||||
ctx.fillStyle = `rgba(${col},0.08)`; ctx.fillRect(x, y, w, h);
|
|
||||||
if (mode === 'cut') {
|
|
||||||
// red hatching: this will be taken down
|
|
||||||
ctx.save(); ctx.beginPath(); ctx.rect(x, y, w, h); ctx.clip();
|
|
||||||
ctx.strokeStyle = `rgba(${col},0.35)`; ctx.lineWidth = Math.max(1, zp * 0.25);
|
|
||||||
const step = Math.max(8, zp * 2.5);
|
|
||||||
ctx.beginPath(); for (let k = -h; k < w; k += step) { ctx.moveTo(x + k, y + h); ctx.lineTo(x + k + h, y); } ctx.stroke();
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
ctx.strokeStyle = `rgba(${col},0.98)`; ctx.lineWidth = Math.max(1.5, zp * (mode === 'cut' ? 0.45 : 0.35));
|
|
||||||
ctx.setLineDash([zp * 1.2, zp * 0.8]); ctx.lineDashOffset = -(R.frame * 0.4) % (zp * 2);
|
|
||||||
ctx.strokeRect(x, y, w, h); ctx.setLineDash([]); ctx.lineDashOffset = 0;
|
|
||||||
ctx.restore();
|
|
||||||
const label = mode === 'cut' ? '✂ CUT' : '⧉ COPY';
|
|
||||||
const sub = !sd ? 'drag a box' : n ? (mode === 'cut' ? `${plural(n, 'piece')} · let go to take down` : `${plural(n, 'piece')} · let go to copy`) : 'nothing built in here';
|
|
||||||
drawTag(label, col, x, y - Math.round(23 * Math.max(1, R.canvas.width / window.innerWidth)), sub);
|
|
||||||
}
|
|
||||||
// Pasting: the clipboard follows the cursor, snapped to the grid
|
|
||||||
function drawPastePreview() {
|
|
||||||
const ctx = R.ctx, zp = R.zp, bp = GAME.clip, { bx: ox, by: oy } = stampOrigin(bp.bw, bp.bh);
|
|
||||||
drawGrid(aimBlock().bx, aimBlock().by, 7);
|
|
||||||
ctx.save();
|
|
||||||
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.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); }
|
|
||||||
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); }
|
|
||||||
ctx.restore();
|
|
||||||
drawTag('PASTE', '92,186,166', x, y - Math.round(23 * Math.max(1, R.canvas.width / window.innerWidth)), 'click to place · R flips · Esc stops');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// Selection panels: current tool card and bag materials.
|
||||||
|
function renderSelCard() {
|
||||||
|
const t = toolId(), c = $('selcard');
|
||||||
|
let h = '';
|
||||||
|
const sel = GAME.selDrag ? GAME.selDrag.mode : selModeHeld();
|
||||||
|
c.hidden = false;
|
||||||
|
const ng = ghostBlocks.size + ghostMachines.length + ghostMolds.length;
|
||||||
|
if (sel === 'cut') {
|
||||||
|
h = `<div class="row"><span class="chip bad">✂ Cut</span><span class="name grow">Hold X and drag a box</span></div>
|
||||||
|
<div class="sub">Let go to take down everything you built in the box (refunded to your bag) and pick it up to paste somewhere else. <kbd>Ctrl</kbd>+<kbd>Z</kbd> puts it back.</div>`;
|
||||||
|
} else if (sel === 'copy') {
|
||||||
|
h = `<div class="row"><span class="chip ok">⧉ Copy</span><span class="name grow">Hold C and drag a box</span></div>
|
||||||
|
<div class="sub">Let go to copy what you built in the box. Then click to place ghosts of it; your bot builds them from your bag.</div>`;
|
||||||
|
} else if (GAME.paste && GAME.clip) {
|
||||||
|
h = `<div class="row"><span class="chip ok">Paste</span><span class="name grow">Blueprint</span><span class="chip">${GAME.clip.bw}×${GAME.clip.bh} blocks</span></div>
|
||||||
|
<div class="sub">Click to place a ghost; your bot builds ghosts from your bag when it is close. <kbd>R</kbd> flips it. Right click or <kbd>Esc</kbd> stops pasting, <kbd>V</kbd> starts again.</div><div class="sub">Needs ${esc(costText(bpCost(GAME.clip), 1) || 'nothing')}</div>`;
|
||||||
|
if (ng) h += `<div class="sub" style="color:var(--patina)">${ng} ghost pieces waiting${ghostMissing ? ', some short on materials' : ''}.</div>`;
|
||||||
|
} else if (t === 'drill') {
|
||||||
|
const b = BIT_TIERS[player.bitTier];
|
||||||
|
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>
|
||||||
|
<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="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>`;
|
||||||
|
} else if (t === 'build') {
|
||||||
|
// the build tool in general; the piece itself is described in the menu's hover popup
|
||||||
|
const P = piece();
|
||||||
|
const chips = buildVariantChips(P) + (GAME.replace ? '<span class="chip hot">Replace · G</span>' : '')
|
||||||
|
+ (ng ? `<span class="chip ok">${ng} ghost pieces waiting${ghostMissing ? ', some short on materials' : ''}.</span>` : '');
|
||||||
|
h = '<div class="row"><span class="name grow">Build</span></div>'
|
||||||
|
+ (chips ? `<div class="row" style="flex-wrap:wrap;gap:4px">${chips}</div>` : '')
|
||||||
|
+ '<div class="sub">Place pieces from the menu below. Drag for a line, <kbd>Shift</kbd>-drag for a box; out of reach becomes ghosts.</div>'
|
||||||
|
+ '<div class="sub"><kbd>Q</kbd>/<kbd>E</kbd> piece · <kbd>Shift</kbd>+<kbd>Q</kbd>/<kbd>E</kbd> tab · <kbd>R</kbd> turn or material · <kbd>F</kbd> material menu · hold <kbd>G</kbd> replace · right click take back · hold <kbd>C</kbd>/<kbd>X</kbd> copy/cut · <kbd>V</kbd> paste · <kbd>Ctrl</kbd>+<kbd>Z</kbd> undo</div>';
|
||||||
|
} else if (t === 'vacuum') {
|
||||||
|
const all = blowingAll(), list = all ? blowAllList() : blowList();
|
||||||
|
const sws = ms => ms.slice(0, 4).map(m => `<span class="sw" title="${esc(MD[m].name)}" style="background:${rgbOf(m)}"></span>`).join('') + (ms.length > 4 ? `+${ms.length - 4}` : '');
|
||||||
|
const count = list.reduce((n, m) => n + bag[m], 0);
|
||||||
|
h = `<div class="row"><span class="name grow">Pneumatic nozzle</span><span class="chip">width ${GAME.brush[1]}</span></div>
|
||||||
|
<div class="row vacrow"><b>Pick up</b><span>${GAME.blow.size ? 'Selected' : 'What you click'} · Shift: everything</span>${sws(GAME.blow.size ? [...GAME.blow] : GAME.lmb && GAME.vacFilter > 0 ? [GAME.vacFilter] : [])}</div>
|
||||||
|
<div class="row vacrow"><b>Blow out</b><kbd>Q</kbd><span class="name grow vacchoice">${all ? '' : sws(list)}${all ? list.length ? `Everything · mix of ${list.length}` : 'Everything · nothing loose' : list.length > 1 ? `Mix of ${list.length}` : list.length ? esc(MD[list[0]].name) : GAME.blow.size ? 'none in bag' : 'nothing selected'}</span><span class="num chip${count ? '' : ' bad'}">${count ? count.toLocaleString('en-US') : 'none'}</span><kbd>E</kbd></div>
|
||||||
|
<div class="sub">Left picks up the selection (or what you click if nothing is selected), right blows it out. Hold Ctrl in the bag to pick from everything you've found. Shift: everything.</div>`;
|
||||||
|
} else if (t === 'ignite') {
|
||||||
|
h = `<div class="row"><span class="name">Igniter</span><span class="chip hot">up to 640°C</span></div><div class="sub">Hold on fuel to light it. Coal catches around 420°C, wood around 300°C. Fuel needs open air next to it.</div>`;
|
||||||
|
}
|
||||||
|
if (c.innerHTML !== h) c.innerHTML = h;
|
||||||
|
}
|
||||||
|
let lastBagKey = '';
|
||||||
|
function renderBag() {
|
||||||
|
const cap = bagCap(), pct = Math.min(100, bagTotal / cap * 100);
|
||||||
|
$('bagcap').textContent = `${bagTotal.toLocaleString('en-US')} / ${cap.toLocaleString('en-US')}`;
|
||||||
|
$('bagbar').firstChild.style.width = pct + '%';
|
||||||
|
$('bagbar').classList.toggle('full', bagTotal >= cap);
|
||||||
|
const t = toolId(), all = blowingAll(), blowing = all ? blowAllList() : [...GAME.blow];
|
||||||
|
const missing = [...GAME.blow].filter(m => bag[m] <= 0);
|
||||||
|
if (GAME.ctrl && t === 'vacuum') for (const m of BAG_ORDER) if (seen[m] && bag[m] <= 0 && !GAME.blow.has(m) && (PH[m] === PH_POWDER || PH[m] === PH_ITEM || PH[m] === PH_LIQUID && player.flask)) missing.push(m);
|
||||||
|
const l = bagMaterials().concat(missing);
|
||||||
|
const key = l.map(m => m + ':' + bag[m] + (GAME.blow.has(m) ? 'b' : '')).join(',') + '|' + GAME.wallMat + '|' + GAME.filterCfg.m.join('.') + '|' + t + '|' + GAME.pieceIdx + '|' + all + '|' + (t === 'vacuum' && GAME.ctrl) + '|' + missing.join('.');
|
||||||
|
if (key === lastBagKey) return;
|
||||||
|
lastBagKey = key;
|
||||||
|
const box = $('baglist'); box.innerHTML = '';
|
||||||
|
// the hint and Clear sit under the list, outside its scroll, so they never scroll away
|
||||||
|
const foot = $('bagfoot'); foot.innerHTML = '';
|
||||||
|
if (t === 'vacuum') {
|
||||||
|
foot.appendChild(el('div', { class: 'baghint' }, "Click: select only this · Ctrl+click: add to the selection (hold Ctrl to see everything you've found) · Q/E: next"));
|
||||||
|
if (GAME.blow.size) {
|
||||||
|
const clear = el('button', { class: 'bagclear', 'data-bag-clear': '' }, 'Clear selection');
|
||||||
|
clear.onclick = () => { GAME.blow.clear(); lastBagKey = ''; hudDirty = true; playSfx('ui'); };
|
||||||
|
foot.appendChild(clear);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!l.length) { box.appendChild(el('div', { class: 'empty' }, 'Empty. Break rock with the Drill, then use the Pneumatic nozzle.')); return; }
|
||||||
|
for (const m of l) {
|
||||||
|
if (m === missing[0]) box.appendChild(el('div', { class: 'bagsep' }, 'Not in bag'));
|
||||||
|
const sel = (t === 'vacuum' && blowing.includes(m)) || (t === 'build' && m === GAME.wallMat && pieceUsesWall(piece())) || (t === 'build' && piece().filter && GAME.filterCfg.m.includes(m));
|
||||||
|
const row = el('div', { class: 'bagrow' + (bag[m] > 0 ? '' : ' none') + (sel ? ' on' : '') });
|
||||||
|
const b = el('button', { class: 'pick', title: 'Select ' + MD[m].name });
|
||||||
|
b.innerHTML = `<span class="sw" style="background:${rgbOf(m)}"></span><span class="nm">${esc(MD[m].name)}</span><span class="n num">${bag[m] > 0 ? bag[m].toLocaleString('en-US') : '–'}</span>`;
|
||||||
|
// Ctrl+click adds to or removes from the selection. GAME.ctrl too: on some Linux setups the click
|
||||||
|
// arrives without ctrlKey even though the Ctrl keydown (which opened the full list) was seen.
|
||||||
|
b.oncontextmenu = e => e.preventDefault();
|
||||||
|
b.onclick = e => {
|
||||||
|
if (e.ctrlKey || e.metaKey || GAME.ctrl) { toggleBlow(m); if (t !== 'vacuum') selectTool(1); return; }
|
||||||
|
if (t === 'build' && piece().filter && filterable(m)) { toggleFilterMat(m); return; }
|
||||||
|
else if (t === 'build' && WALL_OF[m]) { GAME.wallMat = m; if (!pieceUsesWall(piece())) GAME.pieceIdx = 0; }
|
||||||
|
else { GAME.blow = GAME.blow.size === 1 && GAME.blow.has(m) ? new Set() : new Set([m]); if (t !== 'vacuum') selectTool(1); }
|
||||||
|
hudDirty = true; lastBagKey = ''; playSfx('ui');
|
||||||
|
};
|
||||||
|
row.appendChild(b);
|
||||||
|
box.appendChild(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// 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
|
||||||
|
function filterChoices(cur) { return BAG_ORDER.filter(m => filterable(m) && (seen[m] || cur.includes(m))); }
|
||||||
|
function matList(ms) {
|
||||||
|
const n = ms.map(m => MD[m].name.toLowerCase());
|
||||||
|
return n.length <= 3 ? n.join(n.length === 2 ? ' and ' : ', ') : `${n.slice(0, 2).join(', ')} and ${n.length - 2} more`;
|
||||||
|
}
|
||||||
|
// What a filter does, in words: what falls through and what rides along
|
||||||
|
function filterParts(f) {
|
||||||
|
if (!f.m.length) return f.inv ? { drops: 'everything', rides: 'nothing' } : { drops: 'nothing', rides: 'everything' };
|
||||||
|
return f.inv ? { drops: 'everything else', rides: matList(f.m) } : { drops: matList(f.m), rides: 'everything else' };
|
||||||
|
}
|
||||||
|
function filterSentence(f) { const p = filterParts(f); return `drop ${p.drops} and carry ${p.rides}`; }
|
||||||
|
// Change the whole run of matching filters at once
|
||||||
|
function applyFilterRun(run, f) {
|
||||||
|
for (const b of run) { blockV[b] = f.dir ? 128 : 0; setFilter(b, f); }
|
||||||
|
filtersDirty = true;
|
||||||
|
}
|
||||||
|
function openFilterPicker(cur, onDone, title) {
|
||||||
|
const sel = new Set(cur.m);
|
||||||
|
let inv = cur.inv ? 1 : 0, dir = cur.dir ? 1 : 0;
|
||||||
|
const list = filterChoices(cur.m);
|
||||||
|
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="1"><b>→ Carry these</b><span>They ride along; the rest falls through</span></button>
|
||||||
|
</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>`
|
||||||
|
+ (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>`;
|
||||||
|
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; } }]);
|
||||||
|
const box = $('dlg-body');
|
||||||
|
function sync() {
|
||||||
|
box.querySelectorAll('[data-inv]').forEach(b => { const on = +b.dataset.inv === inv; 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); });
|
||||||
|
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.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-m]').forEach(b => b.onclick = () => { const m = +b.dataset.m; if (sel.has(m)) sel.delete(m); else sel.add(m); playSfx('ui'); sync(); });
|
||||||
|
sync();
|
||||||
|
}
|
||||||
|
function openWallPicker() {
|
||||||
|
const P = piece(), title = P.kind === 'mold' ? `${P.name} material` : 'Wall material';
|
||||||
|
const list = WALL_LIST.filter(m => bag[m] > 0).concat(WALL_LIST.filter(m => !bag[m]));
|
||||||
|
const body = `<div class="wpick" role="radiogroup" aria-label="${esc(title)}">${list.map(m => {
|
||||||
|
const d = MD[WALL_OF[m]], cost = pieceCostNow(P, m), on = m === GAME.wallMat;
|
||||||
|
// two sources build the same wall (glass from shards or from bars): say which
|
||||||
|
const name = WALL_LIST.filter(k => WALL_OF[k] === WALL_OF[m]).length > 1 ? `${d.name} from ${MD[m].name.toLowerCase()}` : d.name;
|
||||||
|
const heat = d.ign ? `burns at ${fmtT(d.ign)}` : d.melt ? `melts at ${fmtT(d.melt)}` : "won't melt";
|
||||||
|
return `<button class="wp${on ? ' on' : ''}${canAfford(cost, 1) ? '' : ' short'}" role="radio" aria-checked="${on}" data-wall="${m}"><span class="sw" style="background:${rgbOf(WALL_OF[m])}"></span><span><b>${esc(name)}</b><span>you have ${bag[m].toLocaleString('en-US')}${PH[m] === PH_ITEM ? ` ${esc(MD[m].name.toLowerCase())}${bag[m] === 1 ? '' : 's'}` : ''}</span><span>cost: ${esc(costText(cost, 1))}</span><span>${heat}</span></span></button>`;
|
||||||
|
}).join('')}</div>`;
|
||||||
|
dialog(title, body, [{ label: 'Cancel' }]);
|
||||||
|
$('dlg-body').querySelectorAll('[data-wall]').forEach(b => b.onclick = () => {
|
||||||
|
GAME.wallMat = +b.dataset.wall; closeModal('m-dialog'); playSfx('ui'); hudDirty = true; lastBagKey = '';
|
||||||
|
setStatus(`${P.kind === 'mold' ? P.name : 'Walls'}: ${MD[WALL_OF[GAME.wallMat]].name.toLowerCase()}.`, 'info');
|
||||||
|
});
|
||||||
|
}
|
||||||
+2
-148
@@ -1,4 +1,5 @@
|
|||||||
// The selected tool and piece: R / Q / E, drill width, filter picker, the selection card, the bag panel.
|
// Tool selection: cycling, drill tuning, piece variants and costs.
|
||||||
|
function pieceSize(P) { return P.kind === 'mold' ? MOLDS[P.mold] : P; }
|
||||||
function pieceUsesWall(P) { return P.variants === 'wall'; }
|
function pieceUsesWall(P) { return P.variants === 'wall'; }
|
||||||
function setDrillWidth(w) {
|
function setDrillWidth(w) {
|
||||||
const n = Math.max(0, Math.min(PULSE_W.length - 1, w));
|
const n = Math.max(0, Math.min(PULSE_W.length - 1, w));
|
||||||
@@ -86,64 +87,6 @@ function pieceV(P) {
|
|||||||
return GAME.pieceDir[P.id] || 0;
|
return GAME.pieceDir[P.id] || 0;
|
||||||
}
|
}
|
||||||
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; }
|
||||||
// What a filter can hold: loose things and bars you have found, plus anything it already holds
|
|
||||||
function filterChoices(cur) { return BAG_ORDER.filter(m => filterable(m) && (seen[m] || cur.includes(m))); }
|
|
||||||
function matList(ms) {
|
|
||||||
const n = ms.map(m => MD[m].name.toLowerCase());
|
|
||||||
return n.length <= 3 ? n.join(n.length === 2 ? ' and ' : ', ') : `${n.slice(0, 2).join(', ')} and ${n.length - 2} more`;
|
|
||||||
}
|
|
||||||
// What a filter does, in words: what falls through and what rides along
|
|
||||||
function filterParts(f) {
|
|
||||||
if (!f.m.length) return f.inv ? { drops: 'everything', rides: 'nothing' } : { drops: 'nothing', rides: 'everything' };
|
|
||||||
return f.inv ? { drops: 'everything else', rides: matList(f.m) } : { drops: matList(f.m), rides: 'everything else' };
|
|
||||||
}
|
|
||||||
function filterSentence(f) { const p = filterParts(f); return `drop ${p.drops} and carry ${p.rides}`; }
|
|
||||||
// Change the whole run of matching filters at once
|
|
||||||
function applyFilterRun(run, f) {
|
|
||||||
for (const b of run) { blockV[b] = f.dir ? 128 : 0; setFilter(b, f); }
|
|
||||||
filtersDirty = true;
|
|
||||||
}
|
|
||||||
function openFilterPicker(cur, onDone, title) {
|
|
||||||
const sel = new Set(cur.m);
|
|
||||||
let inv = cur.inv ? 1 : 0, dir = cur.dir ? 1 : 0;
|
|
||||||
const list = filterChoices(cur.m);
|
|
||||||
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="1"><b>→ Carry these</b><span>They ride along; the rest falls through</span></button>
|
|
||||||
</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>`
|
|
||||||
+ (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>`;
|
|
||||||
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; } }]);
|
|
||||||
const box = $('dlg-body');
|
|
||||||
function sync() {
|
|
||||||
box.querySelectorAll('[data-inv]').forEach(b => { const on = +b.dataset.inv === inv; 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); });
|
|
||||||
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.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-m]').forEach(b => b.onclick = () => { const m = +b.dataset.m; if (sel.has(m)) sel.delete(m); else sel.add(m); playSfx('ui'); sync(); });
|
|
||||||
sync();
|
|
||||||
}
|
|
||||||
function openWallPicker() {
|
|
||||||
const P = piece(), title = P.kind === 'mold' ? `${P.name} material` : 'Wall material';
|
|
||||||
const list = WALL_LIST.filter(m => bag[m] > 0).concat(WALL_LIST.filter(m => !bag[m]));
|
|
||||||
const body = `<div class="wpick" role="radiogroup" aria-label="${esc(title)}">${list.map(m => {
|
|
||||||
const d = MD[WALL_OF[m]], cost = pieceCostNow(P, m), on = m === GAME.wallMat;
|
|
||||||
// two sources build the same wall (glass from shards or from bars): say which
|
|
||||||
const name = WALL_LIST.filter(k => WALL_OF[k] === WALL_OF[m]).length > 1 ? `${d.name} from ${MD[m].name.toLowerCase()}` : d.name;
|
|
||||||
const heat = d.ign ? `burns at ${fmtT(d.ign)}` : d.melt ? `melts at ${fmtT(d.melt)}` : "won't melt";
|
|
||||||
return `<button class="wp${on ? ' on' : ''}${canAfford(cost, 1) ? '' : ' short'}" role="radio" aria-checked="${on}" data-wall="${m}"><span class="sw" style="background:${rgbOf(WALL_OF[m])}"></span><span><b>${esc(name)}</b><span>you have ${bag[m].toLocaleString('en-US')}${PH[m] === PH_ITEM ? ` ${esc(MD[m].name.toLowerCase())}${bag[m] === 1 ? '' : 's'}` : ''}</span><span>cost: ${esc(costText(cost, 1))}</span><span>${heat}</span></span></button>`;
|
|
||||||
}).join('')}</div>`;
|
|
||||||
dialog(title, body, [{ label: 'Cancel' }]);
|
|
||||||
$('dlg-body').querySelectorAll('[data-wall]').forEach(b => b.onclick = () => {
|
|
||||||
GAME.wallMat = +b.dataset.wall; closeModal('m-dialog'); playSfx('ui'); hudDirty = true; lastBagKey = '';
|
|
||||||
setStatus(`${P.kind === 'mold' ? P.name : 'Walls'}: ${MD[WALL_OF[GAME.wallMat]].name.toLowerCase()}.`, 'info');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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 blockP[b] === BP_FILTER ? 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) {
|
||||||
@@ -151,92 +94,3 @@ function pieceCostNow(P, m = GAME.wallMat) {
|
|||||||
if (P.kind === 'block' && P.code === BP_WALL) return wallCost(m, BS * BS);
|
if (P.kind === 'block' && P.code === BP_WALL) return wallCost(m, BS * BS);
|
||||||
return P.cost;
|
return P.cost;
|
||||||
}
|
}
|
||||||
function renderSelCard() {
|
|
||||||
const t = toolId(), c = $('selcard');
|
|
||||||
let h = '';
|
|
||||||
const sel = GAME.selDrag ? GAME.selDrag.mode : selModeHeld();
|
|
||||||
c.hidden = false;
|
|
||||||
const ng = ghostBlocks.size + ghostMachines.length + ghostMolds.length;
|
|
||||||
if (sel === 'cut') {
|
|
||||||
h = `<div class="row"><span class="chip bad">✂ Cut</span><span class="name grow">Hold X and drag a box</span></div>
|
|
||||||
<div class="sub">Let go to take down everything you built in the box (refunded to your bag) and pick it up to paste somewhere else. <kbd>Ctrl</kbd>+<kbd>Z</kbd> puts it back.</div>`;
|
|
||||||
} else if (sel === 'copy') {
|
|
||||||
h = `<div class="row"><span class="chip ok">⧉ Copy</span><span class="name grow">Hold C and drag a box</span></div>
|
|
||||||
<div class="sub">Let go to copy what you built in the box. Then click to place ghosts of it; your bot builds them from your bag.</div>`;
|
|
||||||
} else if (GAME.paste && GAME.clip) {
|
|
||||||
h = `<div class="row"><span class="chip ok">Paste</span><span class="name grow">Blueprint</span><span class="chip">${GAME.clip.bw}×${GAME.clip.bh} blocks</span></div>
|
|
||||||
<div class="sub">Click to place a ghost; your bot builds ghosts from your bag when it is close. <kbd>R</kbd> flips it. Right click or <kbd>Esc</kbd> stops pasting, <kbd>V</kbd> starts again.</div><div class="sub">Needs ${esc(costText(bpCost(GAME.clip), 1) || 'nothing')}</div>`;
|
|
||||||
if (ng) h += `<div class="sub" style="color:var(--patina)">${ng} ghost pieces waiting${ghostMissing ? ', some short on materials' : ''}.</div>`;
|
|
||||||
} else if (t === 'drill') {
|
|
||||||
const b = BIT_TIERS[player.bitTier];
|
|
||||||
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>
|
|
||||||
<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="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>`;
|
|
||||||
} else if (t === 'build') {
|
|
||||||
// the build tool in general; the piece itself is described in the menu's hover popup
|
|
||||||
const P = piece();
|
|
||||||
const chips = buildVariantChips(P) + (GAME.replace ? '<span class="chip hot">Replace · G</span>' : '')
|
|
||||||
+ (ng ? `<span class="chip ok">${ng} ghost pieces waiting${ghostMissing ? ', some short on materials' : ''}.</span>` : '');
|
|
||||||
h = '<div class="row"><span class="name grow">Build</span></div>'
|
|
||||||
+ (chips ? `<div class="row" style="flex-wrap:wrap;gap:4px">${chips}</div>` : '')
|
|
||||||
+ '<div class="sub">Place pieces from the menu below. Drag for a line, <kbd>Shift</kbd>-drag for a box; out of reach becomes ghosts.</div>'
|
|
||||||
+ '<div class="sub"><kbd>Q</kbd>/<kbd>E</kbd> piece · <kbd>Shift</kbd>+<kbd>Q</kbd>/<kbd>E</kbd> tab · <kbd>R</kbd> turn or material · <kbd>F</kbd> material menu · hold <kbd>G</kbd> replace · right click take back · hold <kbd>C</kbd>/<kbd>X</kbd> copy/cut · <kbd>V</kbd> paste · <kbd>Ctrl</kbd>+<kbd>Z</kbd> undo</div>';
|
|
||||||
} else if (t === 'vacuum') {
|
|
||||||
const all = blowingAll(), list = all ? blowAllList() : blowList();
|
|
||||||
const sws = ms => ms.slice(0, 4).map(m => `<span class="sw" title="${esc(MD[m].name)}" style="background:${rgbOf(m)}"></span>`).join('') + (ms.length > 4 ? `+${ms.length - 4}` : '');
|
|
||||||
const count = list.reduce((n, m) => n + bag[m], 0);
|
|
||||||
h = `<div class="row"><span class="name grow">Pneumatic nozzle</span><span class="chip">width ${GAME.brush[1]}</span></div>
|
|
||||||
<div class="row vacrow"><b>Pick up</b><span>${GAME.blow.size ? 'Selected' : 'What you click'} · Shift: everything</span>${sws(GAME.blow.size ? [...GAME.blow] : GAME.lmb && GAME.vacFilter > 0 ? [GAME.vacFilter] : [])}</div>
|
|
||||||
<div class="row vacrow"><b>Blow out</b><kbd>Q</kbd><span class="name grow vacchoice">${all ? '' : sws(list)}${all ? list.length ? `Everything · mix of ${list.length}` : 'Everything · nothing loose' : list.length > 1 ? `Mix of ${list.length}` : list.length ? esc(MD[list[0]].name) : GAME.blow.size ? 'none in bag' : 'nothing selected'}</span><span class="num chip${count ? '' : ' bad'}">${count ? count.toLocaleString('en-US') : 'none'}</span><kbd>E</kbd></div>
|
|
||||||
<div class="sub">Left picks up the selection (or what you click if nothing is selected), right blows it out. Hold Ctrl in the bag to pick from everything you've found. Shift: everything.</div>`;
|
|
||||||
} else if (t === 'ignite') {
|
|
||||||
h = `<div class="row"><span class="name">Igniter</span><span class="chip hot">up to 640°C</span></div><div class="sub">Hold on fuel to light it. Coal catches around 420°C, wood around 300°C. Fuel needs open air next to it.</div>`;
|
|
||||||
}
|
|
||||||
if (c.innerHTML !== h) c.innerHTML = h;
|
|
||||||
}
|
|
||||||
let lastBagKey = '';
|
|
||||||
function renderBag() {
|
|
||||||
const cap = bagCap(), pct = Math.min(100, bagTotal / cap * 100);
|
|
||||||
$('bagcap').textContent = `${bagTotal.toLocaleString('en-US')} / ${cap.toLocaleString('en-US')}`;
|
|
||||||
$('bagbar').firstChild.style.width = pct + '%';
|
|
||||||
$('bagbar').classList.toggle('full', bagTotal >= cap);
|
|
||||||
const t = toolId(), all = blowingAll(), blowing = all ? blowAllList() : [...GAME.blow];
|
|
||||||
const missing = [...GAME.blow].filter(m => bag[m] <= 0);
|
|
||||||
if (GAME.ctrl && t === 'vacuum') for (const m of BAG_ORDER) if (seen[m] && bag[m] <= 0 && !GAME.blow.has(m) && (PH[m] === PH_POWDER || PH[m] === PH_ITEM || PH[m] === PH_LIQUID && player.flask)) missing.push(m);
|
|
||||||
const l = bagMaterials().concat(missing);
|
|
||||||
const key = l.map(m => m + ':' + bag[m] + (GAME.blow.has(m) ? 'b' : '')).join(',') + '|' + GAME.wallMat + '|' + GAME.filterCfg.m.join('.') + '|' + t + '|' + GAME.pieceIdx + '|' + all + '|' + (t === 'vacuum' && GAME.ctrl) + '|' + missing.join('.');
|
|
||||||
if (key === lastBagKey) return;
|
|
||||||
lastBagKey = key;
|
|
||||||
const box = $('baglist'); box.innerHTML = '';
|
|
||||||
// the hint and Clear sit under the list, outside its scroll, so they never scroll away
|
|
||||||
const foot = $('bagfoot'); foot.innerHTML = '';
|
|
||||||
if (t === 'vacuum') {
|
|
||||||
foot.appendChild(el('div', { class: 'baghint' }, "Click: select only this · Ctrl+click: add to the selection (hold Ctrl to see everything you've found) · Q/E: next"));
|
|
||||||
if (GAME.blow.size) {
|
|
||||||
const clear = el('button', { class: 'bagclear', 'data-bag-clear': '' }, 'Clear selection');
|
|
||||||
clear.onclick = () => { GAME.blow.clear(); lastBagKey = ''; hudDirty = true; playSfx('ui'); };
|
|
||||||
foot.appendChild(clear);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!l.length) { box.appendChild(el('div', { class: 'empty' }, 'Empty. Break rock with the Drill, then use the Pneumatic nozzle.')); return; }
|
|
||||||
for (const m of l) {
|
|
||||||
if (m === missing[0]) box.appendChild(el('div', { class: 'bagsep' }, 'Not in bag'));
|
|
||||||
const sel = (t === 'vacuum' && blowing.includes(m)) || (t === 'build' && m === GAME.wallMat && pieceUsesWall(piece())) || (t === 'build' && piece().filter && GAME.filterCfg.m.includes(m));
|
|
||||||
const row = el('div', { class: 'bagrow' + (bag[m] > 0 ? '' : ' none') + (sel ? ' on' : '') });
|
|
||||||
const b = el('button', { class: 'pick', title: 'Select ' + MD[m].name });
|
|
||||||
b.innerHTML = `<span class="sw" style="background:${rgbOf(m)}"></span><span class="nm">${esc(MD[m].name)}</span><span class="n num">${bag[m] > 0 ? bag[m].toLocaleString('en-US') : '–'}</span>`;
|
|
||||||
// Ctrl+click adds to or removes from the selection. GAME.ctrl too: on some Linux setups the click
|
|
||||||
// arrives without ctrlKey even though the Ctrl keydown (which opened the full list) was seen.
|
|
||||||
b.oncontextmenu = e => e.preventDefault();
|
|
||||||
b.onclick = e => {
|
|
||||||
if (e.ctrlKey || e.metaKey || GAME.ctrl) { toggleBlow(m); if (t !== 'vacuum') selectTool(1); return; }
|
|
||||||
if (t === 'build' && piece().filter && filterable(m)) { toggleFilterMat(m); return; }
|
|
||||||
else if (t === 'build' && WALL_OF[m]) { GAME.wallMat = m; if (!pieceUsesWall(piece())) GAME.pieceIdx = 0; }
|
|
||||||
else { GAME.blow = GAME.blow.size === 1 && GAME.blow.has(m) ? new Set() : new Set([m]); if (t !== 'vacuum') selectTool(1); }
|
|
||||||
hudDirty = true; lastBagKey = ''; playSfx('ui');
|
|
||||||
};
|
|
||||||
row.appendChild(b);
|
|
||||||
box.appendChild(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user