132 Commits
Author SHA1 Message Date
Eevee (Evelyn Woods) 5572b3e692 Implement viewport size override debug setting 2020-12-10 18:53:23 -07:00
Eevee (Evelyn Woods) b75253a249 Rearrange actor loop to put movement advancement at the end
I don't know why I ever thought this was a separate pass; I think it was
just the easiest way to make smooth scrolling work when I first
implemented it on like day 2.  Turns out it wasn't ever correct and has
all manner of subtle implications I'll be sorting out for ages.

This does make the turn-based stuff //way// simpler, though.
2020-12-10 18:51:40 -07:00
Eevee (Evelyn Woods) 831a9392e3 Fix debug icon URL in prod 2020-12-10 13:54:59 -07:00
Eevee (Evelyn Woods) 81f7e7fd64 Add encodings for the gift bow and circuit block 2020-12-10 13:51:15 -07:00
Eevee (Evelyn Woods) 823fe4de37 bestowal_bow => gift_bow, directional_block => frame_block 2020-12-10 13:28:40 -07:00
Eevee (Evelyn Woods) bd4cc10b16 Implement foil 2020-12-10 12:48:01 -07:00
Eevee (Evelyn Woods) ef78085e40 Ghosts turn left, not right! 2020-12-10 12:45:15 -07:00
Eevee (Evelyn Woods) 422c702777 Allow blocks to reverse on railroads; partially implement teleporter wiring 2020-12-10 12:25:49 -07:00
Eevee (Evelyn Woods) 6063ea9fba Wired tracks only switch on pulse; wired red teleports only work when powered 2020-12-09 21:07:14 -07:00
Eevee (Evelyn Woods) db2a24319d Fix lightning bolt; implement speed boots 2020-12-09 20:52:58 -07:00
Eevee (Evelyn Woods) b01d50c7e8 Move replay support into the debug panel 2020-12-09 20:36:13 -07:00
Eevee (Evelyn Woods) b2ff27f241 New LL tile: circuit block, which overrides any wiring below it 2020-12-09 20:23:27 -07:00
Eevee (Evelyn Woods) 235cc79e8b Add LL-specific exit poses for Lexy and Cerise 2020-12-09 20:22:35 -07:00
Eevee (Evelyn Woods) 5f804dde14 Finish the tileset! 2020-12-09 20:13:25 -07:00
Eevee (Evelyn Woods) e6d7876679 Fix directional blocks losing their arrows after being cloned 2020-12-08 19:00:50 -07:00
Eevee (Evelyn Woods) a23afe3d90 Editor: Disallow erasing the floor (!) 2020-12-08 18:58:59 -07:00
Eevee (Evelyn Woods) ed58cbac6b Editor: Add a (glitchy) wire tool; add right-click support to existing tools (hurrah) 2020-12-08 18:56:49 -07:00
Eevee (Evelyn Woods) 829184056e Fix activating debug mode before loading a level 2020-12-08 17:54:21 -07:00
Eevee (Evelyn Woods) 9d9c5a3988 Remove the debug checkboxes that don't actually work yet 2020-12-08 17:49:11 -07:00
Eevee (Evelyn Woods) 35bbac9c99 Change the debug trigger to something more accessible 2020-12-08 17:46:48 -07:00
Eevee (Evelyn Woods) a44ed295a2 Fix ball animation 2020-12-08 17:23:27 -07:00
Eevee (Evelyn Woods) 90b583df44 In CC2 (and Lynx, for different reasons), clones are immediately nudged out of the cloner 2020-12-08 17:22:20 -07:00
Eevee (Evelyn Woods) f521bd6d2d New debug options: disable interpolation, show actor bboxes 2020-12-08 16:40:35 -07:00
Eevee (Evelyn Woods) 4ee56fad01 Briefly reveal invisible walls, as in CC2 2020-12-08 12:48:25 -07:00
Eevee (Evelyn Woods) 9735ef93e9 Fix being unable to click the CC2 action buttons more than once 2020-12-08 11:24:59 -07:00
Eevee (Evelyn Woods) 6d39fd3831 Allow "rotating" a counter tile to alter its value 2020-12-07 22:07:20 -07:00
Eevee (Evelyn Woods) 35f040c8d7 Implement saving of logic gates and directional blocks 2020-12-07 21:15:18 -07:00
Eevee (Evelyn Woods) 2849260672 Add swivels and stopwatches to the palette 2020-12-07 12:50:03 -07:00
Eevee (Evelyn Woods) f462ae3394 Add some more complex tiles to the palette; add ,/. shortcuts 2020-12-07 12:45:21 -07:00
Eevee (Evelyn Woods) c4bb1f3df1 Editor: Add a dedicated level browser with previews, and a button to add a new level 2020-12-06 20:57:02 -07:00
Eevee (Evelyn Woods) e754e483ec Update the pack name when changing it in the editor 2020-12-06 19:23:19 -07:00
Eevee (Evelyn Woods) aeac5c285b Finally add and wire up most of the debug panel 2020-12-06 18:51:12 -07:00
Eevee (Evelyn Woods) c1ba299e9f Fix collision for socket and exit 2020-12-06 17:01:47 -07:00
Eevee (Evelyn Woods) 90b6498ff9 Make wires and gray buttons affect swivel doors 2020-12-06 16:57:07 -07:00
Eevee (Evelyn Woods) 30261a106f Mostly implement rover 2020-12-06 16:51:24 -07:00
Eevee (Evelyn Woods) 8428572def Step in reverse order; allow pushing blocks on railroads; gliders ignore turtles 2020-12-06 16:29:07 -07:00
Eevee (Evelyn Woods) 4838bb189b Fix keys accumulating while paused 2020-12-06 16:23:43 -07:00
Eevee (Evelyn Woods) 54381370c0 Add timid teeth; move movement decisions onto tile types; improve doppelganger behavior 2020-12-06 16:16:04 -07:00
Eevee (Evelyn Woods) d981a0a4be Fix some interactions that differ between players/doppelgangers; add Cerise doppelganger movement 2020-12-06 14:59:54 -07:00
Eevee (Evelyn Woods) 3b07c78518 Oops; commit the icon for the tracks tool 2020-12-06 14:03:59 -07:00
Eevee (Evelyn Woods) 076aa9133a Improve splash page slightly; add pack saving in editor 2020-12-06 14:03:36 -07:00
Eevee (Evelyn Woods) 70df85187f Toggle force floors when powered 2020-12-06 11:13:37 -07:00
Eevee (Evelyn Woods) f7080a2697 Editor: Fix shift-pencilling a tile with properties 2020-12-03 22:51:07 -07:00
Eevee (Evelyn Woods) cb62786470 Fix yellow teleport behavior (you pick up even if it itself is not blocked); play pickup sound 2020-12-03 22:43:24 -07:00
Eevee (Evelyn Woods) caf4906176 Fix a crash when trying to drop from an empty inventory 2020-12-03 22:21:44 -07:00
Eevee (Evelyn Woods) 564d247657 Play the thief sound even if you only lost bonus points 2020-12-03 21:58:45 -07:00
Eevee (Evelyn Woods) c4dbdcb650 Update splash commentary about the editor 2020-12-03 21:38:49 -07:00
Eevee (Evelyn Woods) 30062485ab Fix hint saving 2020-12-03 21:38:39 -07:00
Eevee (Evelyn Woods) 87ac6f94a3 Editor: More tiles; more metadata; save hints, more or less 2020-12-03 21:19:47 -07:00
Eevee (Evelyn Woods) 7a710ee5dc Fix editor canvas centering in Chrome 2020-12-03 21:19:33 -07:00
Eevee (Evelyn Woods) 700d3898ab Add a half-baked notion of perception level to tile rendering 2020-12-03 21:18:46 -07:00
Eevee (Evelyn Woods) 8505b132bb Fix some minor bugs introduced into turn-based mode 2020-12-03 21:17:50 -07:00
Eevee (Evelyn Woods) dfb274e85d Tileset: Partial Cerise walk; foil, hook; colorblind blue/green walls; touched up thief 2020-12-03 21:16:53 -07:00
Eevee (Evelyn Woods) b97aaa81a9 Slime doesn't kill blobs; implement bribe; fix player size on level restart 2020-12-03 20:52:43 -07:00
Eevee (Evelyn Woods) 0cd1ea342d Implement yellow teleport pickup behavior 2020-12-03 20:52:00 -07:00
Eevee (Evelyn Woods) 3c43b8d7cd Respect a level's intended viewport size 2020-12-03 17:49:02 -07:00
Eevee (Evelyn Woods) 0885bfc9d2 Make hook load, canopy work, and force floors reverse with gray button 2020-12-03 17:48:24 -07:00
Eevee (Evelyn Woods) 411005eaa6 Editor: Stub out support for actually saving levels 2020-12-03 15:40:44 -07:00
Eevee (Evelyn Woods) 89ae9aa4a3 Fix C2M compression occasionally losing the last byte 2020-12-03 15:39:11 -07:00
Eevee (Evelyn Woods) fa18238e60 Editor: Fix bad rendering when clicking a tool 2020-12-02 17:30:33 -07:00
Eevee (Evelyn Woods) c1452e005f Disable action buttons when appropriate; add inv overflow; partial CC1 support 2020-12-02 17:23:55 -07:00
Eevee (Evelyn Woods) e51665b612 Add basic support for drop/cycle/swap 2020-12-02 17:18:54 -07:00
Eevee (Evelyn Woods) 8d197ce479 Add a basic implementation of doppelgangers 2020-12-02 15:03:13 -07:00
Eevee (Evelyn Woods) f0680ce0c4 Cleaned up several tile properties; added railroad adjusting 2020-12-02 13:54:32 -07:00
Eevee (Evelyn Woods) 72cba627a8 Add a prototype railroad track tool 2020-12-02 12:23:09 -07:00
Eevee (Evelyn Woods) aa41336b90 Add more tiles and more kinds of adjustment 2020-12-02 11:54:15 -07:00
Eevee (Evelyn Woods) ec5d9f7b12 Editor: Add selected tile, tool help, and hint editing; clean up toolbar style 2020-12-02 11:34:46 -07:00
Eevee (Evelyn Woods) 0d376e003e Editor: Teach the adjust tool to edit individual tiles 2020-12-02 09:05:20 -07:00
Eevee (Evelyn Woods) 560a89cfd3 Fix using the wrong tile for the railroad switch 2020-11-30 09:09:21 -07:00
Eevee (Evelyn Woods) e7c9bbe846 Implement most of the railroad behavior 2020-11-30 09:08:55 -07:00
Eevee (Evelyn Woods) 8d26de6915 Implement railroad rendering 2020-11-28 12:56:08 -07:00
Eevee (Evelyn Woods) 14061dec0e Fix the editor's viewport size to match the level 2020-11-28 12:36:35 -07:00
Eevee (Evelyn Woods) 4218657c28 Use consistent drawing behavior in the editor; add shift, ctrl pencil modifiers 2020-11-28 11:59:57 -07:00
Eevee (Evelyn Woods) dff3081194 Editor: center/pad the level, fix some bugs with pencil drawing 2020-11-28 11:07:02 -07:00
Eevee (Evelyn Woods) 349af15e05 Fix being stuck in rewind mode forever when using the button 2020-11-25 03:59:57 -07:00
Eevee (Evelyn Woods) 50c81c5c96 Add directed blob + walker sprites and a custom slime splash (btw slime now destroys stuff) 2020-11-25 03:37:58 -07:00
Eevee (Evelyn Woods) 3a454d77f5 Implement the remaining logic gates and /most/ of their rendering! 2020-11-25 03:14:06 -07:00
Eevee (Evelyn Woods) ac6e33bb6c Simplify blitting; fix arrow blitting; impl light switch; load more items 2020-11-25 01:14:15 -07:00
Eevee (Evelyn Woods) 6c6ce8f344 Fix fireballs being blocked by fire 2020-11-24 23:13:25 -07:00
Eevee (Evelyn Woods) 4b6a8e49ae Fix errors when drawing outside the level in the editor 2020-11-24 01:31:09 -07:00
Eevee (Evelyn Woods) 5cb29c8f7d Overhaul collision
Collision now uses bits and masks.  The main upshot is that ghost and
ice/directional blocks collide much more correctly, now.  And turtles
block fireballs.

Also, monsters can now move over "no" signs, and can trample the player
if she's standing on top of an item.

While I was at it, I finished implementing the "bestowal bow", an item
mod (same layer as the "no" sign) that allows any actor to pick up the
item in that tile.
2020-11-23 23:41:32 -07:00
Eevee (Evelyn Woods) fb301b3b3e Initialize direction when placing actor tiles in the editor 2020-11-23 22:18:40 -07:00
Eevee (Evelyn Woods) ca4eaa86cb Fix some minor aesthetic issues with the editor 2020-11-23 21:54:53 -07:00
Eevee (Evelyn Woods) 39d463932b Remove the "stuck" flag and fix all the repercussions of that 2020-11-23 21:35:28 -07:00
Eevee (Evelyn Woods) e803af2fd2 Handle actors starting on force floors; partially implement item bestowal 2020-11-23 19:22:59 -07:00
Eevee (Evelyn Woods) 3514f25f2b Revert part of the hearts/time style improvements to prevent grid blowout from hints 2020-11-03 14:40:05 -07:00
Eevee (Evelyn Woods) 6804169a8a Spruce up buttons a teeny bit more 2020-11-03 14:07:49 -07:00
Eevee (Evelyn Woods) 37072fa003 Add three new music tracks from notchris 2020-11-03 14:07:25 -07:00
Eevee (Evelyn Woods) c8343f1a23 Replace keys/doors with colorblind friendly versions that heavily use shapes 2020-11-03 13:53:40 -07:00
Eevee (Evelyn Woods) 81c7f97d72 Improve behavior on mobile
- Hide the key hints in portrait mode

- Make auto-scaling more robust; it now handles when the player root is
  wider than the actual play area, it better understands the inventory
  behavior in portrait mode, and it recognizes when it needs to shrink;
  with these changes, the game actually fills the screen on both Firefox
  and Chrome on my phone!

- Replace the text buttons with SVG icons

- Add a little more contrast to button edges

- Fix alignment of the heart/time/score counters in portrait mode

- Detect movement based on where the touch is relative to the level
  viewport, not the entire play area (oof)
2020-11-03 13:50:34 -07:00
Eevee (Evelyn Woods) 1b6bd68879 Bump undo buffer size to 30 seconds 2020-11-03 12:09:12 -07:00
Eevee (Evelyn Woods) 8ff0bd803a Use a ring buffer for undo; don't pause when running out of undo during rewind 2020-11-03 11:57:16 -07:00
Eevee (Evelyn Woods) 350ac08d4d Shrink size of undo buffer by 40%
Using simple maps of changed properties, rather than a big pile of
closures, takes up significantly less space.
2020-11-03 11:48:51 -07:00
Eevee (Evelyn Woods) 84840d2b02 Consider turn-based mode to be aid; switch back to realtime correctly 2020-11-03 10:32:25 -07:00
Eevee (Evelyn Woods) e7e02281a2 Clean up turn-based code
Mostly style nits, but also:

- Renamed some stuff in anticipation of removing GameEnded.

- Actor decisions are independent, so there's no need to do most of them
  in the first part of a tic and the player in the second part; they can
  all happen together in the second part.

- waiting_for_input was merged into turn_based, which I think makes it
  easier to follow what's going on between tics.  Although I just
  realized it introduces a bug, so, better fix that next.

- The canvas didn't need to know if we were waiting or not if we just
  force the tic offset to 1 while waiting.  This also fixed some slight
  jitter with force floors.
2020-11-03 09:50:37 -07:00
Eevee (Evelyn Woods) 83a1dd23ff Merge branch 'master' of github.com:eevee/lexys-labyrinth 2020-11-02 15:40:09 -07:00
Eevee bf74530aa2 Merge pull request #3 from Patashu/master
Implement Turn-Based Mode
2020-11-02 15:39:06 -07:00
Eevee (Evelyn Woods) 131f06ee84 Update tileset with a few more CC2 tiles: slime, transmogrifier, better colorblind keys, other smaller improvements 2020-11-01 11:53:19 -07:00
Eevee (Evelyn Woods) 49ff0d9723 Clean up wiring drawing and logic (zero gates is now a no-op!); begin implementing logic gates 2020-11-01 11:36:17 -07:00
Eevee (Evelyn Woods) 37b44bcca4 Stub out debug controls 2020-11-01 11:29:48 -07:00
Eevee (Evelyn Woods) 060dfdc7d7 Implement drawing and correctly pushing directional blocks 2020-10-28 00:17:41 -06:00
Timothy Stiles f670224460 forgot to change a continue to a return 2020-10-26 16:08:29 +11:00
Timothy Stiles 32b4399683 Merge remote-tracking branch 'upstream/master' 2020-10-26 16:05:34 +11:00
Eevee (Evelyn Woods) dedaa45d07 Fix pushing sliding blocks 2020-10-24 22:30:18 -06:00
Eevee (Evelyn Woods) 368e4676fd Mostly implement orange buttons and flame jets 2020-10-24 21:33:11 -06:00
Timothy Stiles 509b3ca3b7 Merge remote-tracking branch 'upstream/master' 2020-10-25 14:31:32 +11:00
Eevee (Evelyn Woods) 0c9a7e3d07 Implement all three blob modes and fix up some minor details
This makes the replays from the Steam copies of Blobnet and Nice Day
play back correctly!  Neato!
2020-10-24 20:49:14 -06:00
Eevee (Evelyn Woods) a07e10218e Monsters always attempt their last candidate direction, even if blocked 2020-10-24 20:32:22 -06:00
Eevee (Evelyn Woods) f1b040f176 Implement green teleports and the Lynx/CC2 PRNG 2020-10-23 21:09:31 -06:00
Eevee (Evelyn Woods) 603a74a751 Show a hint the player starts on 2020-10-23 17:44:26 -06:00
Eevee (Evelyn Woods) 2820c067c5 Implement the CC2 "no sign" 2020-10-23 17:37:50 -06:00
Timothy Stiles a0e37422b9 Merge remote-tracking branch 'upstream/master' 2020-10-23 20:26:39 +11:00
Timothy Stiles fedbd200fc Merge remote-tracking branch 'upstream/master' 2020-10-22 18:04:43 +11:00
Timothy Stiles 9e53aa75a0 I tried 'snappier' and it was a bad idea, so now you don't have to
it basically skips the first frame of each turn-based movement. it's not TERRIBLE but the smooth movement is already really nice, we don't need to change it
2020-10-14 23:13:38 +11:00
Timothy Stiles 4388402850 Merge remote-tracking branch 'upstream/master' 2020-10-14 22:43:35 +11:00
Timothy Stiles e3de4d59c7 spaceify 2020-10-14 22:34:00 +11:00
Timothy Stiles f7e83342a0 fix a turn based rewinding visual bug 2020-10-14 22:33:45 +11:00
Timothy Stiles be5cc7f97f spaceify 2020-10-14 22:23:13 +11:00
Timothy Stiles e9d542f438 fix a bug when undoing while waiting for input
need to unset it (we used to do this but it got lost in the refactor)
2020-10-14 22:22:27 +11:00
Timothy Stiles a7c38ae0af fix 'keys held for less than a frame are ignored' bug
this was annoying me!
2020-10-14 22:17:07 +11:00
Timothy Stiles bb168d7e1e fix force arrows in turn based mode
if you got misaligned in tic_counter then you'd force arrow forever.
also, turn_based is no longer passed in! yay!
2020-10-14 22:07:29 +11:00
Timothy Stiles e53f00a432 I broke force arrows, in fact!
so much for tooting my own horn
2020-10-14 21:54:13 +11:00
Timothy Stiles a8ce3bca11 fix bugs
we're back at parity now, it looks like
2020-10-14 21:42:51 +11:00
Timothy Stiles 2e1a87199a code refactor part 1: advance_tic is now two parts
seems to work so far
2020-10-14 21:24:46 +11:00
Timothy Stiles c8d80dfc63 another spaceify (oops)
I literally tried to change the setting for this in notepad++ but it crashes every time I do

ha ha
2020-09-26 23:48:47 +10:00
Timothy Stiles ccfd5c30ce fix animation ugliness in turn-based mode
Uguhughugh it looks SO SMOOTH NOW. I can go to bed happy.
2020-09-26 23:45:53 +10:00
Timothy Stiles e908434a20 only restart on fresh press of spacebar 2020-09-26 23:01:42 +10:00
Timothy Stiles 5b7273e9d9 Turn-Based: add space to wait 2020-09-26 22:40:38 +10:00
Timothy Stiles e6a4e88935 spaceify previous commit 2020-09-26 22:32:01 +10:00
Timothy Stiles d2e900dc3a fix a rewind bug in non turn based mode
Was caused by not making these setters undoable.
2020-09-26 22:31:18 +10:00
Timothy Stiles 30a145599c fix a graphical undoing bug in Turn-Based Mode
turns out we were smuggling pending_undo to previous moves. aha!
2020-09-26 22:18:19 +10:00
Timothy Stiles 5c6cd01b39 Implement turn based mode
Seems to work mechanically though I haven't extensively stress tested it yet. Force floors work the way you'd want them to though (you're given control whenever you can make an input and not otherwise).
There are some graphical bugs with rewinding, but there were some without turn based mode anyway...
2020-09-26 22:10:42 +10:00
42 changed files with 5755 additions and 1365 deletions

No files matched your search

+3 -1
View File
@@ -38,7 +38,9 @@ Give it a try, I guess! [https://c.eev.ee/lexys-labyrinth/](https://c.eev.ee/le
## Special thanks
- The incredible nerds who put together the [Chip Wiki](https://wiki.bitbusters.club/) and also reside on the Bit Busters Discord
- The incredible nerds who put together the [Chip Wiki](https://wiki.bitbusters.club/) and also reside on the Bit Busters Discord, including:
- ruben for documenting the CC2 PRNG
- The Architect for documenting the CC2 C2G parser
- Everyone who worked on [Chip's Challenge Level Pack 1](https://wiki.bitbusters.club/Chip%27s_Challenge_Level_Pack_1), the default set of levels
- [Tile World](https://wiki.bitbusters.club/Tile_World) for being an incredible reference on Lynx mechanics
- Everyone who contributed music — see [`js/soundtrack.js`](js/soundtrack.js) for a list!
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

+136 -39
View File
@@ -10,11 +10,11 @@
<meta name="og:image" content="https://c.eev.ee/lexys-labyrinth/og-preview.png">
<meta name="og:title" content="Lexy's Labyrinth">
<meta name="og:description" content="A (work in progress) reimplementation of Chip's Challenge 1 and 2, using entirely free assets.">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
</head>
<body data-mode="splash">
<header id="header-main">
<img src="icon.png" alt="">
<img id="header-icon" src="icon.png" alt="">
<h1>Lexy's Labyrinth</h1>
<p>— a game by <a href="https://eev.ee/">eevee</a></p>
<nav>
@@ -34,32 +34,35 @@
<header id="header-level">
<h3 id="level-name">Level 1 — Key Pyramid</h3>
<nav>
<button id="main-prev-level" type="button">←</button>
<button id="main-prev-level" type="button">
<svg class="svg-icon" viewBox="0 0 16 16" title="previous"><path d="M14,1 2,8 14,14 z"></svg>
</button>
<button id="main-choose-level" type="button">Level select</button>
<button id="main-next-level" type="button">→</button>
<button id="main-next-level" type="button">
<svg class="svg-icon" viewBox="0 0 16 16" title="next"><path d="M2,1 14,8 2,14 z"></svg>
</button>
</nav>
</header>
<main id="splash">
<div class="drag-overlay"></div>
<header>
<h1><img src="og-preview.png" alt="">Lexy's Labyrinth</h1>
<p>an unapproved Chip's Challenge emulator</p>
</header>
<section id="splash-intro">
<p><strong>Welcome</strong> to Lexy's Labyrinth, an open source puzzle game that is curiously similar to — but legally distinct from — the Atari classic <a href="https://en.wikipedia.org/wiki/Chip%27s_Challenge">Chip's Challenge</a>!</p>
<p>(This is a Chip's Challenge <em>emulator</em>, designed to be an accessible way to play community-made levels with free assets. It's 99% compatible with Chip's Challenge 1, and support for Chip's Challenge 2 is underway. But you can safely ignore all that and treat this as its own game.)</p>
<p>Please note that <em>levels themselves</em> may contain hints or lore referring to a guy named Chip collecting computer chips, even though you are clearly a fox named Lexy collecting hearts. Weird, right? Sorry for any confusion!</p>
<p>Pick a level pack to get started! You can also get more technical details or report bugs on <a href="https://github.com/eevee/lexys-labyrinth">GitHub</a>, find out more about Chip's Challenge via the <a href="https://bitbusters.club/">Bit Busters Club</a> fansite, or support this endeavor (and other things I do) via <a href="https://www.patreon.com/eevee">Patreon</a>!</p>
<!-- TODO i want to make clear this is a chip's challenge emulator without bogging people down too much about what that means -->
<p>Pick a level pack to get started! You can also load and play any levels you've got lying around, or brave the level editor and make one of your own.</p>
<p>If you're not familiar with the game, read up on <a href="https://github.com/eevee/lexys-labyrinth/wiki/How-To-Play">how to play</a>. You can also get more technical details or report bugs on <a href="https://github.com/eevee/lexys-labyrinth">GitHub</a>, find out more about Chip's Challenge via the <a href="https://bitbusters.club/">Bit Busters Club</a> fansite, or support this endeavor (and other things I do) on <a href="https://www.patreon.com/eevee">Patreon</a>.</p>
</section>
<section id="splash-stock-levels">
<h2>Just play something</h2>
<h2>Play community levels</h2>
<!-- populated by js -->
</section>
<section id="splash-upload-levels">
<h2>Other levels</h2>
<p>Load and play any levels you have on hand — both the original levels and any custom ones, perhaps ones you found on the <a href="https://sets.bitbusters.club/">Bit Busters Club set list</a>. Supports CCL/DAT, C2G, and individual C2Ms (though scores aren't saved for those).</p>
<h2>Load other levels</h2>
<p>Load and play any levels you have on hand, including the original levels. Supports CCL/DAT, C2G, and individual C2Ms (though scores aren't saved for those). Find more on the <a href="https://sets.bitbusters.club/">Bit Busters Club set list</a>.</p>
<!-- TODO zip files! -->
<p>You can also drag and drop files or directories into this window.</p>
<input id="splash-upload-file" type="file" accept=".dat,.ccl,.c2m,.ccs" multiple>
@@ -77,8 +80,9 @@
<section id="splash-your-levels">
<h2>Make your own (WIP lol)</h2>
<p>Please note that the level editor is <strong>extremely</strong> unfinished, and can't even save yet.</p>
<p><button type="button" id="splash-create-level" class="button-big">Create a level</button></p>
<p>Please note that the level editor is <strong>very</strong> unfinished, and doesn't even have undo yet. It may eat your work! Beware!</p>
<p><button type="button" id="splash-create-pack" class="button-big">New pack</button></p>
<p><button type="button" id="splash-create-level" class="button-big">New scratch level<br>(won't be saved!)</button></p>
</section>
</main>
<main id="player" hidden>
@@ -105,6 +109,88 @@
</div>
<div class="inventory"></div>
</section>
<form id="player-debug">
<div class="-time">
<table class="-time-controls">
<tr>
<td><button type="button" class="-time-button" data-dt="-1">← 1 tic</button></td>
<td id="player-debug-time-tics">0</td>
<td>tics</td>
<td><button type="button" class="-time-button" data-dt="1">1 tic →</button></td>
</tr>
<tr>
<td><button type="button" class="-time-button" data-dt="-4">← 1 move</button></td>
<td id="player-debug-time-moves">0</td>
<td>moves</td>
<td><button type="button" class="-time-button" data-dt="4">1 move →</button></td>
</tr>
<tr>
<td><button type="button" class="-time-button" data-dt="-20">← 1 s</button></td>
<td id="player-debug-time-secs">0</td>
<td>seconds</td>
<td><button type="button" class="-time-button" data-dt="20">1 s →</button></td>
</tr>
</table>
<p>Game speed:
<select name="speed">
<option value="6">6× — 120 tics per second</option>
<option value="3">3× — 60 tics per second</option>
<option value="2">2× — 40 tics per second</option>
<option value="3/2">1½× — 30 tics per second</option>
<option value="1" selected>1× — 20 tics per second</option>
<option value="1/2">½× — 10 tics per second</option>
<option value="1/3">⅓× — 6⅔ tics per second</option>
<option value="1/4">¼× — 5 tics per second</option>
</select>
</p>
</div>
<div class="input"></div>
<div class="-options">
<p> Viewport size:
<select id="player-debug-viewport">
<option value="default" selected>Level default (9 or 10)</option>
<option value="12">12 × 12</option>
<option value="16">16 × 16</option>
<option value="24">24 × 24</option>
<option value="32">32 × 32</option>
<option value="max">Entire level</option>
</select>
</p>
<ul>
<li><label><input type="checkbox" name="show_actor_bboxes"> Show actor bounding boxes</label></li>
<li><label><input type="checkbox" name="disable_interpolation"> Disable interpolation</label></li>
<!--
<li><label><input type="checkbox" disabled> Show actor info</label></li>
<li><label><input type="checkbox" disabled> Freeze time for everything else</label></li>
<li><label><input type="checkbox" disabled> Player is immortal</label></li>
<li><label><input type="checkbox" disabled> Player ignores collision</label></li>
<li><label><input type="checkbox" disabled> Player levitates</label></li>
-->
</ul>
<p>(Middle-click to teleport.)</p>
</div>
<div class="-inventory">
<!-- populated in js -->
</div>
<div class="-buttons">
<!-- populated in js -->
</div>
<!-- TODO?
- inspect with mouse
- list of actors, or currently pointed-to actor?
- activate something manually?
- click a button ingame?
- pan viewport (like editor)
- show connections, directions, other editor features
- look under anything
- other game info?
- count tiles?
- total hearts?
- total bonus flags?
-->
</form>
<div id="player-music">
<div id="player-music-left">
🎵 <a id="player-music-title">title</a> by <a id="player-music-author">author</a>
@@ -117,28 +203,39 @@
</div>
<div class="controls">
<div class="play-controls">
<button class="control-pause" type="button">Pause <span class="keyhint">(p)</span></button>
<button class="control-restart" type="button">Restart</button>
<button class="control-undo" type="button">Undo</button>
<button class="control-rewind" type="button">Rewind <span class="keyhint">(z)</span></button>
<button class="control-pause" type="button">
<svg class="svg-icon" viewBox="0 0 16 16" title="pause"><path d="M2,1 h4 v14 h-4 z M10,1 h4 v14 h-4 z"></svg>
<span class="keyhint">p</span></button>
<button class="control-restart" type="button">
<svg class="svg-icon" viewBox="0 0 16 16" title="restart"><path d="M13,13 A 7,7 270 1,1 13,3 L15,1 15,7 9,7 11,5 A 4,4 270 1,0 11,11 z"></svg>
</button>
<button class="control-undo" type="button">
<svg class="svg-icon" viewBox="0 0 16 16" title="undo"><path d="M6,5 6,2 1,7 6,12 6,9 A 10,10 60 0,1 15,12 A 10,10 90 0,0 6,5"></svg>
</button>
<button class="control-rewind" type="button">
<svg class="svg-icon" viewBox="0 0 16 16" title="rewind"><path d="M1,8 7,2 7,14 z M9,8 15,2 15,14 z"></svg>
<span class="keyhint">z</span></button>
<label><input class="control-turn-based" type="checkbox"> Turn-based mode</label>
</div>
<div class="demo-controls">
<button class="demo-play" type="button">View replay</button>
<button class="demo-step-1" type="button">Step 1 tic</button>
<button class="demo-step-4" type="button">Step 1 move</button>
<div class="input"></div>
<div class="actions">
<button class="action-drop" type="button">
<svg class="svg-icon" viewBox="0 0 16 16" title="pause"><path d="M6,0 h4 v9 h3 l-5,5 h7 v2 h-14 v-2 h7 l-5,-5 h3"></svg>
drop <span class="keyhint">q</span></button>
<button class="action-cycle" type="button">
<svg class="svg-icon" viewBox="0 0 16 16" title="pause">
<path d="M2,3 H11 V1 l4,4 -4,4 V7 H2 Z"></path>
<path d="M14,9 H5 V7 l-4,4 4,4 v-2 h9 z"></path>
</svg>
cycle <span class="keyhint">e</span></button>
<button class="action-swap" type="button">
<svg class="svg-icon" viewBox="0 0 16 16" title="pause">
<path d="m 7,1 h 2 l 1,1 V 6 L 9,7 v 4 L 8,11.5 7,11 V 7 L 6,6 V 2 Z"></path>
<path d="M 8,13 13,11 8,9 3,11 Z m 0,2 7,-3 V 11 L 8,8 1,11 v 1 z"></path>
<ellipse cx="5.5" cy="11" rx="0.75" ry="0.5"></ellipse>
</svg>
switch <span class="keyhint">c</span></button>
</div>
</div>
<!-- TODO debug panel?
- current tic
- list of actors, or currently pointed-to actor?
<div class="debug">
<button>« 4 tics</button>
<button>« 1 tic</button>
<button>1 tic »</button>
<button>4 tics »</button>
</div>
-->
</main>
<main id="editor" hidden>
<header>
@@ -152,14 +249,14 @@
also deal with levels vs level /packs/ somehow, not sure how that'll work (including downloading them, yeargh?)
-->
</header>
<div class="level"><!-- level canvas and any overlays go here --></div>
<nav class="controls">
<div id="editor-toolbar">
<!-- tools go here -->
<div class="editor-canvas">
<div class="-container">
<!-- level canvas and any overlays go here -->
<!-- the container is to allow them to scroll as a single unit -->
</div>
<button id="editor-share-url" type="button">Share?</button>
<div id="editor-tool-help">
<strong>Pencil</strong> — <span>Select a tile and draw with the left mouse button. Erase with the right mouse button.</span>
</div>
<nav class="controls">
<div id="editor-tile">
</div>
<!--
<p style>
+43
View File
@@ -4,6 +4,7 @@ export const DIRECTIONS = {
north: {
movement: [0, -1],
bit: 0x01,
index: 0,
action: 'up',
left: 'west',
right: 'east',
@@ -12,6 +13,7 @@ export const DIRECTIONS = {
south: {
movement: [0, 1],
bit: 0x04,
index: 2,
action: 'down',
left: 'east',
right: 'west',
@@ -20,6 +22,7 @@ export const DIRECTIONS = {
west: {
movement: [-1, 0],
bit: 0x08,
index: 3,
action: 'left',
left: 'south',
right: 'north',
@@ -28,9 +31,49 @@ export const DIRECTIONS = {
east: {
movement: [1, 0],
bit: 0x02,
index: 1,
action: 'right',
left: 'north',
right: 'south',
opposite: 'west',
},
};
// Should match the bit ordering above, and CC2's order
export const DIRECTION_ORDER = ['north', 'east', 'south', 'west'];
// TODO cc2 order is: swivel, thinwalls, canopy (and yes you can have them all in the same tile)
export const DRAW_LAYERS = {
terrain: 0,
item: 1,
item_mod: 2,
actor: 3,
overlay: 4,
MAX: 5,
};
export const COLLISION = {
player1: 0x0001,
player2: 0x0002,
player: 0x0003,
block_cc1: 0x0004,
block_cc2: 0x0008, // ice + directional
// Monsters are a little complicated, because some of them have special rules, e.g. fireballs
// aren't blocked by fire.
// For a monster's MASK, you should use ONLY ONE of these specific monster bits (if
// appropriate), OR the generic bit -- DO NOT combine them!
monster_generic: 0x0100,
fireball: 0x0200,
bug: 0x0400,
rover: 0x1000,
ghost: 0x8000,
// For a tile's COLLISION, use one of these bit combinations
monster_solid: 0x7f00, // everything but ghost
monster_any: 0xff00, // everything including ghost
// Combo masks used for matching
all_but_ghost: 0xffff & ~0x8000,
all_but_player: 0xffff & ~0x0003,
all: 0xffff,
};
+253
View File
@@ -0,0 +1,253 @@
import { TransientOverlay } from './main-base.js';
import { mk, mk_svg } from './util.js';
// FIXME could very much stand to have a little animation when appearing
class TileEditorOverlay extends TransientOverlay {
constructor(conductor) {
let root = mk('form.editor-popup-tile-editor');
super(conductor, root);
this.editor = conductor.editor;
this.tile = null;
}
edit_tile(tile) {
this.tile = tile;
}
static configure_tile_defaults(tile) {
// FIXME maybe this should be on the tile type, so it functions as documentation there?
}
}
class LetterTileEditor extends TileEditorOverlay {
constructor(conductor) {
super(conductor);
this.root.append(mk('h3', "Letter tile"));
let list = mk('ol.editor-letter-tile-picker');
this.root.append(list);
this.glyph_elements = {};
let add = glyph => {
let input = mk('input', {type: 'radio', name: 'glyph', value: glyph});
this.glyph_elements[glyph] = input;
let item = mk('li', mk('label', input, mk('span.-glyph', glyph)));
list.append(item);
};
let arrows = ["⬆", "➡", "⬇", "⬅"];
for (let c = 32; c < 96; c++) {
let glyph = String.fromCharCode(c);
add(glyph);
// Add the arrows to the ends of the rows
if (c % 16 === 15) {
add(arrows[(c - 47) / 16]);
}
}
list.addEventListener('change', ev => {
if (this.tile) {
this.tile.overlaid_glyph = this.root.elements['glyph'].value;
this.editor.mark_tile_dirty(this.tile);
}
});
}
edit_tile(tile) {
super.edit_tile(tile);
this.root.elements['glyph'].value = tile.overlaid_glyph;
}
static configure_tile_defaults(tile) {
tile.type.populate_defaults(tile);
}
}
class HintTileEditor extends TileEditorOverlay {
constructor(conductor) {
super(conductor);
this.root.append(mk('h3', "Hint text"));
this.text = mk('textarea.editor-hint-tile-text');
this.root.append(this.text);
this.text.addEventListener('input', ev => {
if (this.tile) {
this.tile.hint_text = this.text.value;
}
});
}
edit_tile(tile) {
super.edit_tile(tile);
this.text.value = tile.hint_text ?? "";
}
static configure_tile_defaults(tile) {
tile.hint_text = "";
}
}
class FrameBlockTileEditor extends TileEditorOverlay {
constructor(conductor) {
super(conductor);
let svg_icons = [];
for (let center of [[16, 0], [16, 16], [0, 16], [0, 0]]) {
let symbol = mk_svg('svg', {viewBox: '0 0 16 16'},
mk_svg('circle', {cx: center[0], cy: center[1], r: 3}),
mk_svg('circle', {cx: center[0], cy: center[1], r: 13}),
);
svg_icons.push(symbol);
}
svg_icons.push(mk_svg('svg', {viewBox: '0 0 16 16'},
mk_svg('rect', {x: -2, y: 3, width: 20, height: 10}),
));
svg_icons.push(mk_svg('svg', {viewBox: '0 0 16 16'},
mk_svg('rect', {x: 3, y: -2, width: 10, height: 20}),
));
this.root.append(mk('h3', "Arrows"));
let arrow_list = mk('ol.editor-directional-block-tile-arrows.editor-tile-editor-svg-parts');
// Arrange the arrows in a grid
for (let [direction, icon] of [
[null, mk_svg('path', {d: 'M 8,16 v -8 h 8'})],
['north', mk_svg('path', {d: 'M 0,12 h 16 l -8,-8 z'})],
[null, mk_svg('path', {d: 'M 0,8 h 8 v 8'})],
['west', mk_svg('path', {d: 'M 12,16 v -16 l -8,8 z'})],
[null, null],
['east', mk_svg('path', {d: 'M 4,0 v 16 l 8,-8 z'})],
[null, mk_svg('path', {d: 'M 16,8 h -8 v -8'})],
['south', mk_svg('path', {d: 'M 16,4 h -16 l 8,8 z'})],
[null, mk_svg('path', {d: 'M 8,0 v 8 h -8'})],
]) {
let li = mk('li');
let svg;
if (icon) {
svg = mk_svg('svg', {viewBox: '0 0 16 16'}, icon);
}
if (direction === null) {
if (svg) {
li.append(svg);
}
}
else {
let input = mk('input', {type: 'checkbox', name: 'direction', value: direction});
li.append(mk('label', input, svg));
}
arrow_list.append(li);
}
arrow_list.addEventListener('change', ev => {
if (! this.tile)
return;
if (ev.target.checked) {
this.tile.arrows.add(ev.target.value);
}
else {
this.tile.arrows.delete(ev.target.value);
}
this.editor.mark_tile_dirty(this.tile);
});
this.root.append(arrow_list);
}
edit_tile(tile) {
super.edit_tile(tile);
for (let input of this.root.elements['direction']) {
input.checked = tile.arrows.has(input.value);
}
}
static configure_tile_defaults(tile) {
}
}
class RailroadTileEditor extends TileEditorOverlay {
constructor(conductor) {
super(conductor);
let svg_icons = [];
for (let center of [[16, 0], [16, 16], [0, 16], [0, 0]]) {
let symbol = mk_svg('svg', {viewBox: '0 0 16 16'},
mk_svg('circle', {cx: center[0], cy: center[1], r: 3}),
mk_svg('circle', {cx: center[0], cy: center[1], r: 13}),
);
svg_icons.push(symbol);
}
svg_icons.push(mk_svg('svg', {viewBox: '0 0 16 16'},
mk_svg('rect', {x: -2, y: 3, width: 20, height: 10}),
));
svg_icons.push(mk_svg('svg', {viewBox: '0 0 16 16'},
mk_svg('rect', {x: 3, y: -2, width: 10, height: 20}),
));
this.root.append(mk('h3', "Tracks"));
let track_list = mk('ul.editor-railroad-tile-tracks.editor-tile-editor-svg-parts');
// Shown as two rows, this puts the straight parts first and the rest in a circle
let track_order = [4, 1, 2, 5, 0, 3];
for (let i of track_order) {
let input = mk('input', {type: 'checkbox', name: 'track', value: i});
track_list.append(mk('li', mk('label', input, svg_icons[i])));
}
track_list.addEventListener('change', ev => {
if (this.tile) {
let bit = 1 << ev.target.value;
if (ev.target.checked) {
this.tile.tracks |= bit;
}
else {
this.tile.tracks &= ~bit;
}
this.editor.mark_tile_dirty(this.tile);
}
});
this.root.append(track_list);
this.root.append(mk('h3', "Switch"));
let switch_list = mk('ul.editor-railroad-tile-tracks.--switch');
for (let i of track_order) {
let input = mk('input', {type: 'radio', name: 'switch', value: i});
switch_list.append(mk('li', mk('label', input, svg_icons[i].cloneNode(true))));
}
// TODO if they remove a track it should change the switch
// TODO if they pick a track that's missing it should add it
switch_list.addEventListener('change', ev => {
if (this.tile) {
this.tile.track_switch = ev.target.value;
this.editor.mark_tile_dirty(this.tile);
}
});
this.root.append(switch_list);
// TODO need a way to set no actor at all
// TODO initial actor facing (maybe only if there's an actor in the cell)
}
edit_tile(tile) {
super.edit_tile(tile);
for (let input of this.root.elements['track']) {
input.checked = !! (tile.tracks & (1 << input.value));
}
if (tile.track_switch === null) {
this.root.elements['switch'].value = '';
}
else {
this.root.elements['switch'].value = tile.track_switch;
}
}
static configure_tile_defaults(tile) {
}
}
export const TILES_WITH_PROPS = {
floor_letter: LetterTileEditor,
hint: HintTileEditor,
frame_block: FrameBlockTileEditor,
railroad: RailroadTileEditor,
// TODO various wireable tiles (hmm not sure how that ui works)
// TODO initial value of counter
// TODO cloner arrows
// TODO later, custom floor/wall selection
};
+11 -3
View File
@@ -8,6 +8,7 @@ export class StoredLevel {
// TODO still not sure this belongs here
this.number = number; // one-based
this.title = '';
this.author = '';
this.password = null;
this.hint = '';
this.chips_required = 0;
@@ -16,6 +17,10 @@ export class StoredLevel {
this.extra_chunks = [];
this.use_cc1_boots = false;
this.use_ccl_compat = false;
// 0 - deterministic (PRNG + simple convolution)
// 1 - 4 patterns (default; PRNG + rotating through 0-3)
// 2 - extra random (like deterministic, but initial seed is "actually" random)
this.blob_behavior = 1;
this.size_x = 0;
this.size_y = 0;
@@ -44,12 +49,13 @@ export class StoredLevel {
}
}
export class StoredGame {
export class StoredPack {
constructor(identifier, level_loader) {
this.identifier = identifier;
this.title = "";
this._level_loader = level_loader;
// Simple objects containing keys:
// Simple objects containing keys that are usually:
// title: level title
// index: level index, used internally only
// number: level number (may not match index due to C2G shenanigans)
@@ -72,7 +78,9 @@ export class StoredGame {
}
else {
// Otherwise, attempt to load the level
return this._level_loader(meta.bytes, meta.number);
return this._level_loader(meta);
}
}
}
export const StoredGame = StoredPack;
+182 -48
View File
@@ -1,4 +1,4 @@
import { DIRECTIONS } from './defs.js';
import { DIRECTIONS, DIRECTION_ORDER } from './defs.js';
import * as format_base from './format-base.js';
import TILE_TYPES from './tiletypes.js';
import * as util from './util.js';
@@ -372,12 +372,30 @@ const TILE_ENCODING = {
0x4f: {
name: 'railroad',
modifier: {
_parts: ['ne', 'se', 'sw', 'ne', 'ew', 'ns'],
decode(tile, mask) {
// TODO railroad props
// Leave the track parts alone as a bitmask; the type has a list of them
tile.tracks = mask & 0x3f;
// Check for a switch, which is a bit number in the above mask
if (mask & 0x40) {
tile.track_switch = (mask >> 8) & 0x0f;
}
else {
tile.track_switch = null;
}
// Initial actor facing is in the highest nybble
tile.entered_direction = (mask >> 12) & 0x03;
},
encode(tile) {
// TODO
return 0;
let ret = tile.tracks & 0x3f;
if (tile.track_switch !== null) {
ret |= 0x40;
ret |= tile.track_switch << 8;
}
if (tile.entered_direction) {
ret |= DIRECTION_ORDER.indexOf(tile.entered_direction) << 12;
}
return ret;
},
},
},
@@ -402,10 +420,9 @@ const TILE_ENCODING = {
name: 'teeth_timid',
has_next: true,
extra_args: [arg_direction],
error: "Timid chomper is not yet implemented, sorry!",
},
0x58: {
// TODO??? unused in main levels -- name: 'doppelganger2',
// TODO??? unused in main levels -- name: '',
has_next: true,
extra_args: [arg_direction],
error: "Explosion animation is not implemented, sorry!",
@@ -421,9 +438,60 @@ const TILE_ENCODING = {
name: 'no_player1_sign',
},
0x5c: {
// TODO (modifier chooses logic gate) name: 'doppelganger2',
// TODO modifier: ...
error: "Logic gates are not yet implemented, sorry!",
name: 'logic_gate',
modifier: {
decode(tile, modifier) {
if (modifier >= 0x1e && modifier <= 0x27) {
// Counter, which can't be rotated
tile.direction = 'north';
tile.gate_type = 'counter';
tile.memory = modifier - 0x1e;
}
else {
tile.direction = ['north', 'east', 'south', 'west'][modifier & 0x03];
let type = modifier >> 2;
if (type < 6) {
tile.gate_type = ['not', 'and', 'or', 'xor', 'latch-cw', 'nand'][type];
}
else if (type === 16) {
tile.gate_type = 'latch-ccw';
}
else {
tile.gate_type = 'bogus';
}
}
},
encode(tile) {
let direction_offset = DIRECTIONS[tile.direction].index;
if (tile.gate_type === 'not') {
return 0 + direction_offset;
}
else if (tile.gate_type === 'and') {
return 4 + direction_offset;
}
else if (tile.gate_type === 'or') {
return 8 + direction_offset;
}
else if (tile.gate_type === 'xor') {
return 12 + direction_offset;
}
else if (tile.gate_type === 'latch-cw') {
return 16 + direction_offset;
}
else if (tile.gate_type === 'nand') {
return 20 + direction_offset;
}
else if (tile.gate_type === 'counter') {
return 30 + tile.memory;
}
else if (tile.gate_type === 'latch-ccw') {
return 64 + direction_offset;
}
else {
return 0xff;
}
},
},
},
0x5e: {
name: 'button_pink',
@@ -441,7 +509,6 @@ const TILE_ENCODING = {
0x62: {
name: 'lightning_bolt',
has_next: true,
error: "The lightning bolt is not yet implemented, sorry!",
},
0x63: {
name: 'tank_yellow',
@@ -455,13 +522,11 @@ const TILE_ENCODING = {
name: 'doppelganger1',
has_next: true,
extra_args: [arg_direction],
error: "Doppelganger Lexy is not yet implemented, sorry!",
},
0x66: {
name: 'doppelganger2',
has_next: true,
extra_args: [arg_direction],
error: "Doppelganger Cerise is not yet implemented, sorry!",
},
0x68: {
name: 'bowling_ball',
@@ -495,18 +560,33 @@ const TILE_ENCODING = {
name: 'floor_letter',
modifier: {
decode(tile, ascii_code) {
tile.ascii_code = ascii_code;
if (ascii_code < 28 || ascii_code >= 96) {
// Invalid
tile.overlaid_glyph = "?";
}
else if (ascii_code < 32) {
// Arrows are stored goofily
tile.overlaid_glyph = ["⬆", "➡", "⬇", "⬅"][ascii_code - 28];
}
else {
tile.overlaid_glyph = String.fromCharCode(ascii_code);
}
},
encode(tile) {
return tile.ascii_code;
let arrow_index = ["⬆", "➡", "⬇", "⬅"].indexOf(tile.overlaid_glyph);
if (arrow_index >= 0) {
return arrow_index + 28;
}
return tile.overlaid_glyph.charCodeAt(0);
},
},
},
0x72: {
name: 'purple_wall',
name: 'purple_floor',
},
0x73: {
name: 'purple_floor',
name: 'purple_wall',
},
0x76: {
name: '#mod8',
@@ -536,7 +616,7 @@ const TILE_ENCODING = {
name: 'popdown_floor',
},
0x7f: {
name: 'forbidden',
name: 'no_sign',
has_next: true,
},
0x80: {
@@ -544,7 +624,8 @@ const TILE_ENCODING = {
has_next: true,
},
0x81: {
name: 'directional_block',
name: 'frame_block',
has_next: true,
extra_args: [
arg_direction,
{
@@ -559,12 +640,14 @@ const TILE_ENCODING = {
tile.arrows = arrows;
},
encode(tile) {
// TODO
return 0;
let bits = 0;
for (let direction of tile.arrows) {
bits |= DIRECTIONS[direction].bit;
}
return bits;
},
},
],
has_next: true,
},
0x82: {
name: 'floor_mimic',
@@ -585,11 +668,11 @@ const TILE_ENCODING = {
},
0x88: {
name: 'light_switch_off',
error: "The light switch is not yet implemented, sorry!",
modifier: modifier_wire,
},
0x89: {
name: 'light_switch_on',
error: "The light switch is not yet implemented, sorry!",
modifier: modifier_wire,
},
0x8a: {
name: 'thief_keys',
@@ -613,17 +696,26 @@ const TILE_ENCODING = {
0x8f: {
name: 'bribe',
has_next: true,
error: "The bribe is not yet implemented, sorry!",
},
0x90: {
name: 'speed_boots',
has_next: true,
error: "The speed boots are not yet implemented, sorry!",
},
0x91: {
0x92: {
name: 'hook',
has_next: true,
error: "The hook is not yet implemented, sorry!",
},
// LL-specific tiles
0xe0: {
name: 'gift_bow',
has_next: true,
},
0xe1: {
name: 'circuit_block',
has_next: true,
modifier: modifier_wire,
extra_args: [arg_direction],
},
};
const REVERSE_TILE_ENCODING = {};
@@ -776,11 +868,10 @@ export function parse_level(buf, number = 1) {
level.hint = str;
}
else if (type === 'NOTE') {
// Author's comments... but might also include multiple hints
// for levels with multiple hint tiles, delineated by [CLUE].
// For my purposes, extra hints are associated with the
// individual tiles, so we'll map those later
[level.comment, ...extra_hints] = str.split(/^\[CLUE\]$/mg);
// Author's comments... but might also include multiple hints for levels with
// multiple hint tiles, delineated by [CLUE] (anywhere in the line (!)).
// LL treats extra hints as tile properties, so store them for later
[level.comment, ...extra_hints] = str.split(/\n?^.*\[CLUE\].*$\n?/mg);
}
continue;
}
@@ -793,6 +884,7 @@ export function parse_level(buf, number = 1) {
level.time_limit = view.getUint16(0, true);
// TODO 0 - 10x10, 1 - 9x9, 2 - split, otherwise unknown which needs handling
// FIXME does this default to 0 if no OPTN block is present?
let viewport = view.getUint8(2, true);
if (viewport === 0) {
level.viewport_size = 10;
@@ -835,7 +927,7 @@ export function parse_level(buf, number = 1) {
if (view.byteLength <= 24)
continue;
//level.blob_behavior = view.getUint8(24, true);
level.blob_behavior = view.getUint8(24, true);
}
else if (type === 'MAP ' || type === 'PACK') {
if (type === 'PACK') {
@@ -996,18 +1088,24 @@ export function parse_level(buf, number = 1) {
}
// Connect extra hints
let h = 0;
for (let tile of hint_tiles) {
if (h > extra_hints.length)
break;
tile.specific_hint = extra_hints[h];
h++;
for (let [i, tile] of hint_tiles.entries()) {
if (i < extra_hints.length) {
tile.hint_text = extra_hints[i];
}
else {
// Fall back to regular hint
tile.hint_text = null;
}
}
return level;
}
// This thin wrapper is passed to StoredGame as the parser function
function _parse_level_from_stored_meta(meta) {
return parse_level(meta.bytes, meta.number);
}
// Write 1, 2, or 4 bytes to a DataView
function write_n_bytes(view, start, n, value) {
@@ -1065,10 +1163,18 @@ function compress(buf) {
// If we found a match that's worth copying (i.e. shorter than just writing a data block),
// then do so
let do_copy = (best_length > 3);
// If we're not copying, add this byte to a pending data block /now/, so the next block can
// catch it if it happens to be the last byte
if (! do_copy) {
pending_data_length += 1;
p++;
}
// Write out any pending data block if necessary -- i.e. if we're about to write a copy
// block, if we're at the max size of a data block, or if this is the end of the data
if (pending_data_length > 0 &&
(do_copy || pending_data_length === 127 || p === buf.byteLength - 1))
(do_copy || pending_data_length === 127 || p >= buf.byteLength))
{
outbytes[q] = pending_data_length;
q++;
@@ -1079,6 +1185,7 @@ function compress(buf) {
pending_data_length = 0;
}
// Finally, do a copy
if (do_copy) {
outbytes[q] = 0x80 + best_length;
outbytes[q + 1] = p - best_start;
@@ -1086,11 +1193,6 @@ function compress(buf) {
// Update p, noting that we might've done a copy into the future
p += best_length;
}
else {
// Otherwise, add this to a pending data block
pending_data_length += 1;
p++;
}
// If we ever exceed the uncompressed length, don't even bother
if (q > buf.byteLength) {
@@ -1156,6 +1258,26 @@ export function synthesize_level(stored_level) {
let c2m = new C2M;
c2m.add_section('CC2M', '133');
if (stored_level.title) {
c2m.add_section('TITL', stored_level.title);
}
if (stored_level.author) {
c2m.add_section('AUTH', stored_level.author);
}
// Options block
let options = new Uint8Array(3);
new DataView(options.buffer).setUint16(0, stored_level.time_limit, true);
if (stored_level.viewport_size === 10) {
options[2] = 0;
}
else if (stored_level.viewport_size === 9) {
options[2] = 1;
}
// TODO split
// TODO for size purposes, omit the block entirely if all options are defaults?
c2m.add_section('OPTN', options);
// Store camera regions
// TODO LL feature, should be distinguished somehow
if (stored_level.camera_regions.length > 0) {
@@ -1176,6 +1298,7 @@ export function synthesize_level(stored_level) {
let map_view = new DataView(map_bytes.buffer);
map_bytes[0] = stored_level.size_x;
map_bytes[1] = stored_level.size_y;
let hints = [];
let p = 2;
for (let cell of stored_level.linear_cells) {
for (let i = cell.length - 1; i >= 0; i--) {
@@ -1219,10 +1342,21 @@ export function synthesize_level(stored_level) {
}
}
if (tile.type.name === 'hint') {
hints.push(tile.hint_text);
}
// TODO assert that the bottom tile has no next, and all the others do
}
}
// Collect hints first so we can put them in the comment field
// FIXME this does not respect global hint, but then, neither does the editor.
hints = hints.map(hint => hint ?? '');
hints.push('');
hints.unshift('');
c2m.add_section('NOTE', hints.join('\n[CLUE]\n'));
// FIXME ack, ArrayBuffer.slice makes a copy actually! and i use it a lot in this file i think!!
let map_buf = map_bytes.buffer.slice(0, p);
let compressed_map = compress(map_buf);
@@ -1621,7 +1755,7 @@ const MAX_SIMULTANEOUS_REQUESTS = 5;
let resolve;
let promise = new Promise((res, rej) => { resolve = res });
let game = new format_base.StoredGame(undefined, parse_level);
let game = new format_base.StoredGame(undefined, _parse_level_from_stored_meta);
let parser;
let active_map_fetches = new Set;
let pending_map_fetches = [];
@@ -1698,7 +1832,7 @@ const MAX_SIMULTANEOUS_REQUESTS = 5;
// Individual levels don't make sense on their own, but we can wrap them in a dummy one-level game
export function wrap_individual_level(buf) {
let game = new format_base.StoredGame(undefined, parse_level);
let game = new format_base.StoredGame(undefined, _parse_level_from_stored_meta);
let meta = {
index: 0,
number: 1,
+6 -1
View File
@@ -329,8 +329,13 @@ function parse_level(bytes, number) {
return level;
}
// This thin wrapper is passed to StoredGame as the parser function
function _parse_level_from_stored_meta(meta) {
return parse_level(meta.bytes, meta.number);
}
export function parse_game(buf) {
let game = new format_base.StoredGame(null, parse_level);
let game = new format_base.StoredGame(null, _parse_level_from_stored_meta);
let full_view = new DataView(buf);
let magic = full_view.getUint32(0, true);
+831 -433
View File
File diff suppressed because it is too large. Load diff
+25 -1
View File
@@ -37,6 +37,10 @@ export class Overlay {
this.root.addEventListener('click', ev => {
ev.stopPropagation();
});
// Block any window-level key handlers from firing when we type
this.root.addEventListener('keydown', ev => {
ev.stopPropagation();
});
}
open() {
@@ -54,6 +58,8 @@ export class Overlay {
overlay.addEventListener('click', ev => {
this.close();
});
return overlay;
}
close() {
@@ -61,10 +67,20 @@ export class Overlay {
}
}
// Overlay styled like a popup of some sort
export class TransientOverlay extends Overlay {
open() {
// TODO i don't like how vaguely arbitrary this feels.
let overlay = super.open();
overlay.classList.add('--transient');
return overlay;
}
}
// Overlay styled like a dialog box
export class DialogOverlay extends Overlay {
constructor(conductor) {
super(conductor, mk('div.dialog'));
super(conductor, mk('form.dialog'));
this.root.append(
this.header = mk('header'),
@@ -103,3 +119,11 @@ export class ConfirmOverlay extends DialogOverlay {
this.footer.append(yes, no);
}
}
export function load_json_from_storage(key) {
return JSON.parse(window.localStorage.getItem(key));
}
export function save_json_to_storage(key, value) {
window.localStorage.setItem(key, JSON.stringify(value));
}
+1426 -129
View File
File diff suppressed because it is too large. Load diff
+464 -119
View File
@@ -243,6 +243,8 @@ class Player extends PrimaryView {
ArrowRight: 'right',
ArrowUp: 'up',
ArrowDown: 'down',
Spacebar: 'wait',
" ": 'wait',
w: 'up',
a: 'left',
s: 'down',
@@ -254,6 +256,7 @@ class Player extends PrimaryView {
this.scale = 1;
this.play_speed = 1;
this.compat = {
popwalls_react_on_arrive: false,
auto_convert_ccl_popwalls: true,
@@ -305,6 +308,21 @@ class Player extends PrimaryView {
}
});
// 0: normal realtime mode
// 1: turn-based mode
// 2: turn-based mode, but we know the game is frozen waiting for input
this.turn_mode = 0;
this.turn_based_checkbox = this.root.querySelector('.controls .control-turn-based');
this.turn_based_checkbox.checked = false;
this.turn_based_checkbox.addEventListener('change', ev => {
if (this.turn_based_checkbox.checked) {
this.turn_mode = 1;
}
else {
this.turn_mode = 0;
}
});
// Bind buttons
this.pause_button = this.root.querySelector('.controls .control-pause');
this.pause_button.addEventListener('click', ev => {
@@ -321,13 +339,14 @@ class Player extends PrimaryView {
this.undo_button = this.root.querySelector('.controls .control-undo');
this.undo_button.addEventListener('click', ev => {
let player_cell = this.level.player.cell;
// Keep undoing until (a) we're on another cell and (b) we're not
// sliding, i.e. we're about to make a conscious move
// Keep undoing until (a) we're on another cell and (b) we're not sliding, i.e. we're
// about to make a conscious move. Note that this means undoing all the way through
// force floors, even if you could override them!
let moved = false;
while (this.level.undo_stack.length > 0 &&
while (this.level.has_undo() &&
! (moved && this.level.player.slide_mode === null))
{
this.level.undo();
this.undo();
if (player_cell !== this.level.player.cell) {
moved = true;
}
@@ -344,41 +363,47 @@ class Player extends PrimaryView {
});
this.rewind_button = this.root.querySelector('.controls .control-rewind');
this.rewind_button.addEventListener('click', ev => {
if (this.level.undo_stack.length > 0) {
this.state = 'rewinding';
if (this.state === 'rewinding') {
this.set_state('playing');
}
else if (this.level.has_undo()) {
this.set_state('rewinding');
}
});
// Demo playback
this.root.querySelector('.demo-controls .demo-play').addEventListener('click', ev => {
if (this.state === 'playing' || this.state === 'paused' || this.state === 'rewinding') {
new ConfirmOverlay(this.conductor, "Abandon your progress and watch the replay?", () => {
this.play_demo();
});
}
else {
this.play_demo();
}
// Game actions
// TODO do these need buttons?? feel like they're not discoverable otherwise
this.drop_button = this.root.querySelector('.actions .action-drop');
this.drop_button.addEventListener('click', ev => {
// Use the set of "buttons pressed between tics" because it's cleared automatically;
// otherwise these will stick around forever
this.current_keys_new.add('q');
ev.target.blur();
});
this.root.querySelector('.demo-controls .demo-step-1').addEventListener('click', ev => {
this.advance_by(1);
this._redraw();
this.cycle_button = this.root.querySelector('.actions .action-cycle');
this.cycle_button.addEventListener('click', ev => {
this.current_keys_new.add('e');
ev.target.blur();
});
this.root.querySelector('.demo-controls .demo-step-4').addEventListener('click', ev => {
this.advance_by(4);
this._redraw();
this.swap_button = this.root.querySelector('.actions .action-swap');
this.swap_button.addEventListener('click', ev => {
this.current_keys_new.add('c');
ev.target.blur();
});
this.use_interpolation = true;
this.renderer = new CanvasRenderer(this.conductor.tileset);
this.level_el.append(this.renderer.canvas);
this.renderer.canvas.addEventListener('auxclick', ev => {
if (ev.button !== 1)
return;
// TODO make a real debug flag? allow enabling this but consider it aid level 3?
if (! location.host.match(/localhost/))
if (! this.debug.enabled)
return;
let [x, y] = this.renderer.cell_coords_from_event(ev);
this.level.move_to(this.level.player, this.level.cells[y][x], 1);
// TODO this behaves a bit weirdly when paused (doesn't redraw even with a force), i
// think because we're still claiming a speed of 1 so time has to pass before the move
// actually "happens"
});
// Populate inventory
@@ -409,8 +434,12 @@ class Player extends PrimaryView {
this.previous_action = null; // last direction we were moving, if any
this.using_touch = false; // true if using touch controls
this.current_keys = new Set; // keys that are currently held
this.current_keys_new = new Set; // keys that were pressed since input was last read
// TODO this could all probably be more rigorous but it's fine for now
key_target.addEventListener('keydown', ev => {
if (! this.active)
return;
if (ev.key === 'p' || ev.key === 'Pause') {
this.toggle_pause();
return;
@@ -429,7 +458,9 @@ class Player extends PrimaryView {
}
else {
// Restart
this.restart_level();
if (!this.current_keys.has(ev.key)) {
this.restart_level();
}
}
return;
}
@@ -438,7 +469,7 @@ class Player extends PrimaryView {
}
if (ev.key === 'z') {
if (this.level.undo_stack.length > 0 &&
if (this.level.has_undo() &&
(this.state === 'stopped' || this.state === 'playing' || this.state === 'paused'))
{
this.set_state('rewinding');
@@ -447,6 +478,7 @@ class Player extends PrimaryView {
if (this.key_mapping[ev.key]) {
this.current_keys.add(ev.key);
this.current_keys_new.add(ev.key);
ev.stopPropagation();
ev.preventDefault();
@@ -457,6 +489,9 @@ class Player extends PrimaryView {
}
});
key_target.addEventListener('keyup', ev => {
if (! this.active)
return;
if (ev.key === 'z') {
if (this.state === 'rewinding') {
this.set_state('playing');
@@ -500,7 +535,7 @@ class Player extends PrimaryView {
// Figure out where these touches are, relative to the game area
// TODO allow starting a level without moving?
let rect = touch_target.getBoundingClientRect();
let rect = this.level_el.getBoundingClientRect();
for (let touch of ev.changedTouches) {
// Normalize touch coordinates to [-1, 1]
let rx = (touch.clientX - rect.left) / rect.width * 2 - 1;
@@ -564,6 +599,7 @@ class Player extends PrimaryView {
});
// Populate input debugger
this.debug = { enabled: false };
this.input_el = this.root.querySelector('.input');
this.input_action_elements = {};
for (let [action, label] of Object.entries(ACTION_LABELS)) {
@@ -575,7 +611,6 @@ class Player extends PrimaryView {
this._advance_bound = this.advance.bind(this);
this._redraw_bound = this.redraw.bind(this);
// Used to determine where within a tic we are, for animation purposes
this.tic_offset = 0;
this.last_advance = 0; // performance.now timestamp
// Auto-size the level canvas on resize
@@ -591,6 +626,155 @@ class Player extends PrimaryView {
setup() {
}
// Link up the debug panel and enable debug features
// (note that this might be called /before/ setup!)
setup_debug() {
this.root.classList.add('--debug');
let debug_el = this.root.querySelector('#player-debug');
this.debug = {
enabled: true,
time_tics_el: this.root.querySelector('#player-debug-time-tics'),
time_moves_el: this.root.querySelector('#player-debug-time-moves'),
time_secs_el: this.root.querySelector('#player-debug-time-secs'),
};
// Add a button for every kind of inventory item
let inventory_el = debug_el.querySelector('.-inventory');
let make_button = (label, onclick) => {
let button = mk('button', {type: 'button'}, label);
button.addEventListener('click', onclick);
return button;
};
for (let name of [
'key_blue', 'key_red', 'key_yellow', 'key_green',
'flippers', 'fire_boots', 'cleats', 'suction_boots',
'bribe', 'railroad_sign', 'hiking_boots', 'speed_boots',
'xray_eye', 'helmet', 'foil', 'lightning_bolt',
]) {
inventory_el.append(make_button(
mk('img', {src: this.render_inventory_tile(name)}),
() => {
this.level.give_actor(this.level.player, name);
this.update_ui();
}));
}
let clear_button = mk('button.-wide', {type: 'button'}, "clear inventory");
clear_button.addEventListener('click', ev => {
this.level.take_all_keys_from_actor(this.level.player);
this.level.take_all_tools_from_actor(this.level.player);
this.update_ui();
});
inventory_el.append(clear_button);
debug_el.querySelector('.-time-controls').addEventListener('click', ev => {
let button = ev.target.closest('button.-time-button');
if (! button)
return;
let dt = parseInt(button.getAttribute('data-dt'));
if (dt > 0) {
this.advance_by(dt);
}
else if (dt < 0) {
for (let i = 0; i < -dt; i++) {
if (! this.level.has_undo())
break;
this.undo();
}
}
this._redraw();
this.update_ui();
});
let speed_el = debug_el.elements.speed;
speed_el.value = "1";
speed_el.addEventListener('change', ev => {
let speed = ev.target.value;
let [numer, denom] = speed.split('/');
this.play_speed = parseInt(numer, 10) / parseInt(denom ?? '1', 10);
});
let viewport_el = this.root.querySelector('#player-debug-viewport');
viewport_el.value = "default";
viewport_el.addEventListener('change', ev => {
let viewport = ev.target.value;
if (viewport === 'default') {
this.debug.viewport_size_override = null;
}
else if (viewport === 'max') {
this.debug.viewport_size_override = 'max';
}
else {
this.debug.viewport_size_override = parseInt(viewport, 10);
}
this.update_viewport_size();
this._redraw();
});
this.debug.replay_button = make_button("view replay", () => {
if (this.state === 'playing' || this.state === 'paused' || this.state === 'rewinding') {
new ConfirmOverlay(this.conductor, "Restart this level and watch the replay?", () => {
this.play_demo();
}).open();
}
else {
this.play_demo();
}
});
this._update_replay_button_enabled();
debug_el.querySelector('.-buttons').append(
this.debug.replay_button,
make_button("green button", () => {
TILE_TYPES['button_green'].do_button(this.level);
this._redraw();
}),
make_button("blue button", () => {
TILE_TYPES['button_blue'].do_button(this.level);
this._redraw();
}),
make_button("toggle clock", () => {
this.level.pause_timer();
this.update_ui();
}),
make_button("+10s", () => {
this.level.adjust_timer(+10);
this.update_ui();
}),
make_button("−10s", () => {
this.level.adjust_timer(-10);
this.update_ui();
}),
make_button("stop clock", () => {
this.level.time_remaining = null;
this.update_ui();
}),
);
// Link up some options checkboxes
let wire_checkbox = (name, onclick) => {
let checkbox = debug_el.elements[name];
checkbox.checked = false; // override browser memory
checkbox.addEventListener('click', onclick);
};
wire_checkbox('show_actor_bboxes', ev => {
this.renderer.show_actor_bboxes = ev.target.checked;
this._redraw();
});
wire_checkbox('disable_interpolation', ev => {
this.use_interpolation = ! ev.target.checked;
this._redraw();
});
if (this.level) {
this.update_ui();
}
}
_update_replay_button_enabled() {
if (this.debug.replay_button) {
this.debug.replay_button.disabled = ! (this.level && this.level.stored_level.demo);
}
}
activate() {
// We can't resize when we're not visible, so do it now
super.activate();
@@ -622,10 +806,33 @@ class Player extends PrimaryView {
this.level = new Level(stored_level, this.gather_compat_options(stored_level));
this.level.sfx = this.sfx_player;
this.renderer.set_level(this.level);
this.root.classList.toggle('--has-demo', !!this.level.stored_level.demo);
this.update_viewport_size();
// TODO base this on a hash of the UA + some identifier for the pack + the level index. StoredLevel doesn't know its own index atm...
this.change_music(this.conductor.level_index % SOUNDTRACK.length);
this._clear_state();
this._update_replay_button_enabled();
}
update_viewport_size() {
let w, h;
if (this.debug.enabled && this.debug.viewport_size_override) {
if (this.debug.viewport_size_override === 'max') {
w = this.level.size_x;
h = this.level.size_y;
}
else {
w = h = this.debug.viewport_size_override;
}
}
else {
w = h = this.conductor.stored_level.viewport_size;
}
this.renderer.set_viewport_size(w, h);
this.renderer.canvas.style.setProperty('--viewport-width', w);
this.renderer.canvas.style.setProperty('--viewport-height', h);
// TODO only if the size changed?
this.adjust_scale();
}
restart_level() {
@@ -647,7 +854,7 @@ class Player extends PrimaryView {
_clear_state() {
this.set_state('waiting');
this.tic_offset = 0;
this.turn_mode = this.turn_based_checkbox.checked ? 1 : 0;
this.last_advance = 0;
this.demo_faucet = null;
this.current_keyring = {};
@@ -664,10 +871,16 @@ class Player extends PrimaryView {
this._redraw();
}
open_level_browser() {
new LevelBrowserOverlay(this.conductor).open();
}
play_demo() {
this.restart_level();
this.demo_faucet = this.level.stored_level.demo[Symbol.iterator]();
this.level.force_floor_direction = this.level.stored_level.demo.initial_force_floor_direction;
let demo = this.level.stored_level.demo;
this.demo_faucet = demo[Symbol.iterator]();
this.level.force_floor_direction = demo.initial_force_floor_direction;
this.level._blob_modifier = demo.blob_seed;
// FIXME should probably start playback on first real input
this.set_state('playing');
}
@@ -691,6 +904,10 @@ class Player extends PrimaryView {
for (let key of this.current_keys) {
input.add(this.key_mapping[key]);
}
for (let key of this.current_keys_new) {
input.add(this.key_mapping[key]);
}
this.current_keys_new.clear();
for (let action of Object.values(this.current_touches)) {
input.add(action);
}
@@ -715,6 +932,9 @@ class Player extends PrimaryView {
// newly pressed secondary action; remember, there can't be two opposing keys held,
// because we already checked for that above, so this is only necessary if there's
// not already a secondary action
if (this.secondary_action && ! input.has(this.secondary_action)) {
this.secondary_action = null;
}
if (! this.secondary_action) {
for (let action of ['down', 'left', 'right', 'up']) {
if (action !== this.primary_action &&
@@ -757,13 +977,36 @@ class Player extends PrimaryView {
}
}
let player_actions = {
primary: this.primary_action ? ACTION_DIRECTIONS[this.primary_action] : null,
secondary: this.secondary_action ? ACTION_DIRECTIONS[this.secondary_action] : null,
cycle: input.has('cycle') && ! this.previous_input.has('cycle'),
drop: input.has('drop') && ! this.previous_input.has('drop'),
swap: input.has('swap') && ! this.previous_input.has('swap'),
}
this.previous_input = input;
this.sfx_player.advance_tic();
this.level.advance_tic(
this.primary_action ? ACTION_DIRECTIONS[this.primary_action] : null,
this.secondary_action ? ACTION_DIRECTIONS[this.secondary_action] : null,
);
// Turn-based mode is considered assistance, but only if the game actually attempts to
// progress while it's enabled
if (this.turn_mode > 0) {
this.level.aid = Math.max(1, this.level.aid);
}
let has_input = input.has('wait') || Object.values(player_actions).some(x => x);
if (this.turn_mode > 0 && this.level.can_accept_input() && ! has_input) {
// If we're in turn-based mode and could provide input here, but don't have any,
// then wait until we do
this.turn_mode = 2;
}
else {
this.level.advance_tic(player_actions);
if (this.turn_mode > 1) {
this.turn_mode = 1;
}
}
if (this.level.state !== 'playing') {
// We either won or lost!
@@ -783,24 +1026,25 @@ class Player extends PrimaryView {
}
this.last_advance = performance.now();
if (this.state === 'playing') {
this.advance_by(1);
}
else if (this.state === 'rewinding') {
if (this.level.undo_stack.length === 0) {
// TODO detect if we hit the start of the level (rather than just running the undo
// buffer dry) and change to 'waiting' instead
// TODO pausing seems rude actually, it should just hover in-place?
this._advance_handle = null;
this.set_state('paused');
}
else {
if (this.level.has_undo()) {
// Rewind by undoing one tic every tic
this.level.undo();
this.undo();
this.update_ui();
}
// If there are no undo entries left, freeze in place until the player stops rewinding,
// which I think is ye olde VHS behavior
// TODO detect if we hit the start of the level (rather than just running the undo
// buffer dry) and change to 'waiting' instead?
}
let dt = 1000 / TICS_PER_SECOND;
// FIXME if play speed is sufficiently high, we need to advance multiple frames at once or
// the timeout will get capped
let dt = 1000 / (TICS_PER_SECOND * this.play_speed);
if (this.state === 'rewinding') {
// Rewind faster than normal time
dt *= 0.5;
@@ -808,19 +1052,32 @@ class Player extends PrimaryView {
this._advance_handle = window.setTimeout(this._advance_bound, dt);
}
undo() {
this.level.undo();
}
// Redraws every frame, unless the game isn't running
redraw() {
// Calculate this here, not in _redraw, because that's called at weird
// times when the game might not have actually advanced at all yet
// TODO this is not gonna be right while pausing lol
// TODO i'm not sure it'll be right when rewinding either
// TODO or if the game's speed changes. wow!
this.tic_offset = Math.min(0.9999, (performance.now() - this.last_advance) / 1000 / (1 / TICS_PER_SECOND));
if (this.state === 'rewinding') {
this.tic_offset = 1 - this.tic_offset;
let tic_offset;
if (this.turn_mode === 2) {
// We're dawdling between tics, so nothing is actually animating, but the clock hasn't
// advanced yet; pretend whatever's currently animating has finished
tic_offset = 1;
}
else if (this.use_interpolation) {
tic_offset = Math.min(0.9999, (performance.now() - this.last_advance) / 1000 * TICS_PER_SECOND * this.play_speed);
if (this.state === 'rewinding') {
tic_offset = 1 - tic_offset;
}
}
else {
tic_offset = 0;
}
this._redraw();
this._redraw(tic_offset);
// Check for a stopped game *after* drawing, so that if the game ends, we still draw its
// final result before stopping the draw loop
@@ -833,9 +1090,10 @@ class Player extends PrimaryView {
}
}
// Actually redraw. Used to force drawing outside of normal play
_redraw() {
this.renderer.draw(this.tic_offset);
// Actually redraw. Used to force drawing outside of normal play, in which case we don't
// interpolate (because we're probably paused)
_redraw(tic_offset = 0) {
this.renderer.draw(tic_offset);
}
render_inventory_tile(name) {
@@ -848,8 +1106,18 @@ class Player extends PrimaryView {
}
update_ui() {
this.pause_button.disabled = !(this.state === 'playing' || this.state === 'paused');
this.pause_button.disabled = ! (this.state === 'playing' || this.state === 'paused' || this.state === 'rewinding');
this.restart_button.disabled = (this.state === 'waiting');
this.undo_button.disabled = ! this.level.has_undo();
this.rewind_button.disabled = ! (this.level.has_undo() || this.state === 'rewinding');
this.drop_button.disabled = ! (
this.state === 'playing' && ! this.level.stored_level.use_cc1_boots &&
this.level.player.toolbelt && this.level.player.toolbelt.length > 0);
this.cycle_button.disabled = ! (
this.state === 'playing' && ! this.level.stored_level.use_cc1_boots &&
this.level.player.toolbelt && this.level.player.toolbelt.length > 1);
this.swap_button.disabled = ! (this.state === 'playing' && this.level.players.length > 1);
// TODO can we do this only if they actually changed?
this.chips_el.textContent = this.level.chips_remaining;
@@ -902,6 +1170,13 @@ class Player extends PrimaryView {
for (let action of Object.keys(ACTION_LABELS)) {
this.input_action_elements[action].classList.toggle('--pressed', this.previous_input.has(action));
}
if (this.debug.enabled) {
let t = this.level.tic_counter;
this.debug.time_tics_el.textContent = `${t}`;
this.debug.time_moves_el.textContent = `${Math.floor(t/4)}`;
this.debug.time_secs_el.textContent = (t / 20).toFixed(2);
}
}
toggle_pause() {
@@ -928,6 +1203,12 @@ class Player extends PrimaryView {
this.state = new_state;
// Drop any "new" keys when switching into playing, since they accumulate freely as long as
// the game isn't actually running
if (new_state === 'playing') {
this.current_keys_new.clear();
}
// Populate the overlay
let overlay_reason = '';
let overlay_top = '';
@@ -970,22 +1251,24 @@ class Player extends PrimaryView {
let scorecard = this.level.get_scorecard();
let savefile = this.conductor.current_pack_savefile;
let old_scorecard;
if (! savefile.scorecards[level_index] ||
savefile.scorecards[level_index].score < scorecard.score ||
(savefile.scorecards[level_index].score === scorecard.score &&
savefile.scorecards[level_index].aid > scorecard.aid))
{
old_scorecard = savefile.scorecards[level_index];
if (! this.debug.enabled) {
if (! savefile.scorecards[level_index] ||
savefile.scorecards[level_index].score < scorecard.score ||
(savefile.scorecards[level_index].score === scorecard.score &&
savefile.scorecards[level_index].aid > scorecard.aid))
{
old_scorecard = savefile.scorecards[level_index];
// Adjust the total score
savefile.total_score = savefile.total_score ?? 0;
if (old_scorecard) {
savefile.total_score -= old_scorecard.score;
// Adjust the total score
savefile.total_score = savefile.total_score ?? 0;
if (old_scorecard) {
savefile.total_score -= old_scorecard.score;
}
savefile.total_score += scorecard.score;
savefile.scorecards[level_index] = scorecard;
this.conductor.save_savefile();
}
savefile.total_score += scorecard.score;
savefile.scorecards[level_index] = scorecard;
this.conductor.save_savefile();
}
overlay_reason = 'success';
@@ -1022,7 +1305,7 @@ class Player extends PrimaryView {
overlay_top = random_choice([
"you did it!", "nice going!", "great job!", "good work!",
"onwards!", "tubular!", "yeehaw!", "hot damn!",
"alphanumeric!", "nice dynamic typing!",
"alphanumeric!", "nice dynamic typing!",
]);
}
if (this.using_touch) {
@@ -1163,10 +1446,13 @@ class Player extends PrimaryView {
adjust_scale() {
// TODO make this optional
let style = window.getComputedStyle(this.root);
// If we're not visible, no layout information is available and this is impossible
if (style['display'] === 'none')
return;
let is_portrait = !! style.getPropertyValue('--is-portrait');
// The base size is the size of the canvas, i.e. the viewport size times the tile size --
// but note that we have 2x4 extra tiles for the inventory depending on layout
// TODO if there's ever a portrait view for phones, this will need adjusting
let base_x, base_y;
if (is_portrait) {
base_x = this.conductor.tileset.size_x * this.renderer.viewport_size_x;
@@ -1176,24 +1462,43 @@ class Player extends PrimaryView {
base_x = this.conductor.tileset.size_x * (this.renderer.viewport_size_x + 4);
base_y = this.conductor.tileset.size_y * this.renderer.viewport_size_y;
}
// The main UI is centered in a flex item with auto margins, so the extra space available is
// the size of those margins (which naturally discounts the size of the buttons and music
// title and whatnot, so those automatically reserve their own space)
if (style['display'] === 'none') {
// the computed margins can be 'auto' in this case
return;
// Unfortunately, finding the available space is a little tricky. The container is a CSS
// flex item, and the flex cell doesn't correspond directly to any element, so there's no
// way for us to query its size directly. We also have various stuff up top and down below
// that shouldn't count as available space. So instead we take a rough guess by adding up:
// - the space currently taken up by the canvas
let avail_x = this.renderer.canvas.offsetWidth;
let avail_y = this.renderer.canvas.offsetHeight;
// - the space currently taken up by the inventory, depending on orientation
if (is_portrait) {
avail_y += this.inventory_el.offsetHeight;
}
let extra_x = parseFloat(style['margin-left']) + parseFloat(style['margin-right']);
let extra_y = parseFloat(style['margin-top']) + parseFloat(style['margin-bottom']);
// The total available space, then, is the current size of the canvas (and inventory, when
// appropriate) plus the size of the margins
let total_x = extra_x + this.renderer.canvas.offsetWidth + this.inventory_el.offsetWidth;
let total_y = extra_y + this.renderer.canvas.offsetHeight;
else {
avail_x += this.inventory_el.offsetWidth;
}
// - the difference between the size of the play area and the size of our root (which will
// add in any gap around the player, e.g. if the controls stretch the root to be wider)
let root_rect = this.root.getBoundingClientRect();
let player_rect = this.root.querySelector('.-main-area').getBoundingClientRect();
avail_x += root_rect.width - player_rect.width;
avail_y += root_rect.height - player_rect.height;
// - the margins around our root, which consume all the extra space
let margin_x = parseFloat(style['margin-left']) + parseFloat(style['margin-right']);
let margin_y = parseFloat(style['margin-top']) + parseFloat(style['margin-bottom']);
avail_x += margin_x;
avail_y += margin_y;
// If those margins are zero, by the way, we risk being too big for the viewport already,
// and we need to subtract any extra scroll on the body
if (margin_x === 0 || margin_y === 0) {
avail_x -= document.body.scrollWidth - document.body.clientWidth;
avail_y -= document.body.scrollHeight - document.body.clientHeight;
}
let dpr = window.devicePixelRatio || 1.0;
// Divide to find the biggest scale that still fits. But don't exceed 90% of the available
// space, or it'll feel cramped (except on small screens, where being too small HURTS).
let maxfrac = total_x < 800 ? 1 : 0.9;
let scale = Math.floor(maxfrac * dpr * Math.min(total_x / base_x, total_y / base_y));
let maxfrac = is_portrait ? 1 : 0.9;
let scale = Math.floor(maxfrac * dpr * Math.min(avail_x / base_x, avail_y / base_y));
if (scale <= 1) {
scale = 1;
}
@@ -1315,28 +1620,38 @@ class Splash extends PrimaryView {
await this.search_multi_source(new util.EntryFileSource(entries));
},
});
}
// Bind to "create level" button
this.root.querySelector('#splash-create-level').addEventListener('click', ev => {
let stored_level = new format_base.StoredLevel(1);
stored_level.size_x = 32;
stored_level.size_y = 32;
for (let i = 0; i < 1024; i++) {
let cell = new format_base.StoredCell;
cell.push({type: TILE_TYPES['floor']});
stored_level.linear_cells.push(cell);
}
stored_level.linear_cells[0].push({type: TILE_TYPES['player']});
// FIXME definitely gonna need a name here chief
let stored_game = new format_base.StoredGame(null);
stored_game.level_metadata.push({
stored_level: stored_level,
});
this.conductor.load_game(stored_game);
this.conductor.switch_to_editor();
setup() {
// Editor interface
// (this has to be handled here because we need to examine the editor,
// which hasn't yet been created in our constructor)
// Bind to "create" buttons
this.root.querySelector('#splash-create-pack').addEventListener('click', ev => {
this.conductor.editor.create_pack();
});
this.root.querySelector('#splash-create-level').addEventListener('click', ev => {
this.conductor.editor.create_scratch_level();
});
// Add buttons for any existing packs
let packs = this.conductor.editor.stash.packs;
let pack_keys = Object.keys(packs);
pack_keys.sort((a, b) => packs[b].last_modified - packs[a].last_modified);
let editor_section = this.root.querySelector('#splash-your-levels');
let editor_list = editor_section;
for (let key of pack_keys) {
let pack = packs[key];
let button = mk('button.button-big.level-pack-button', {type: 'button'},
mk('h3', pack.title),
// TODO whether it's yours or not?
// TODO number of levels?
);
// TODO make a container so this can be 1 event
button.addEventListener('click', ev => {
this.conductor.editor.load_editor_pack(key);
});
editor_list.append(button);
}
}
// Look for something we can load, and load it
@@ -1574,7 +1889,7 @@ class OptionsOverlay extends DialogOverlay {
}
}
// List of levels
// List of levels, used in the player
class LevelBrowserOverlay extends DialogOverlay {
constructor(conductor) {
super(conductor);
@@ -1668,8 +1983,16 @@ class LevelBrowserOverlay extends DialogOverlay {
}
// Central dispatcher of what we're doing and what we've got loaded
// We store several kinds of things in localStorage:
// Main storage:
// packs
// options
const STORAGE_KEY = "Lexy's Labyrinth";
// Records for playing a pack
const STORAGE_PACK_PREFIX = "Lexy's Labyrinth: ";
// Metadata for an edited pack
// - list of the levels they own and basic metadata like name
// Stored individual levels: given dummy names, all indexed on their own
class Conductor {
constructor(tileset) {
this.stored_game = null;
@@ -1723,9 +2046,10 @@ class Conductor {
ev.target.blur();
});
this.nav_choose_level_button.addEventListener('click', ev => {
if (this.stored_game) {
new LevelBrowserOverlay(this).open();
}
if (! this.stored_game)
return;
this.current.open_level_browser();
ev.target.blur();
});
document.querySelector('#main-change-pack').addEventListener('click', ev => {
@@ -1746,6 +2070,20 @@ class Conductor {
this.switch_to_player();
});
// Bind the secret debug button: the icon in the lower left
document.querySelector('#header-icon').addEventListener('auxclick', ev => {
if (ev.button === 1 && ! this.player.debug.enabled) {
new ConfirmOverlay(this,
"Enable debug mode in the player? This will give you lots of toys to play with, " +
"but disable all saving of scores until you reload the page!",
() => {
this.player.setup_debug();
ev.target.src = 'icon-debug.png';
},
).open();
}
});
this.update_nav_buttons();
this.switch_to_splash();
}
@@ -1815,16 +2153,14 @@ class Conductor {
this.stored_level = this.stored_game.load_level(level_index);
}
catch (e) {
console.error(e);
new LevelErrorOverlay(this, e).open();
return false;
}
this.level_index = level_index;
// FIXME do better
this.level_name_el.textContent = `Level ${level_index + 1} — ${this.stored_level.title}`;
document.title = `${PAGE_TITLE} - ${this.stored_level.title}`;
this.update_level_title();
this.update_nav_buttons();
this.player.load_level(this.stored_level);
@@ -1832,6 +2168,13 @@ class Conductor {
return true;
}
update_level_title() {
this.level_pack_name_el.textContent = this.stored_game.title;
this.level_name_el.textContent = `Level ${this.stored_level.number} — ${this.stored_level.title}`;
document.title = `${this.stored_level.title} [#${this.stored_level.number}] — ${this.stored_game.title} — ${PAGE_TITLE}`;
}
update_nav_buttons() {
this.nav_choose_level_button.disabled = !this.stored_game;
this.nav_prev_button.disabled = !this.stored_game || this.level_index <= 0;
@@ -1879,7 +2222,7 @@ class Conductor {
// TODO handle errors
// TODO cancel a download if we start another one?
let buf = await util.fetch(path);
await this.parse_and_load_game(buf, new util.HTTPFileSource(new URL(location)), path);
await this.parse_and_load_game(buf, new util.HTTPFileSource(new URL(location)), path, undefined, title);
}
async parse_and_load_game(buf, source, path, identifier, title) {
@@ -1887,9 +2230,6 @@ class Conductor {
identifier = this.extract_identifier_from_path(path);
}
// TODO get title out of C2G when it's supported
this.level_pack_name_el.textContent = title ?? identifier ?? '(untitled)';
// TODO also support tile world's DAC when reading from local??
// TODO ah, there's more metadata in CCX, crapola
let magic = String.fromCharCode.apply(null, new Uint8Array(buf.slice(0, 4)));
@@ -1905,7 +2245,6 @@ class Conductor {
else if (magic.toLowerCase() === 'game') {
// TODO this isn't really a magic number and isn't required to be first, so, maybe
// this one should just go by filename
console.log(path);
let dir;
if (! path.match(/[/]/)) {
dir = '';
@@ -1918,6 +2257,12 @@ class Conductor {
else {
throw new Error("Unrecognized file format");
}
// TODO load title for a C2G
if (! stored_game.title) {
stored_game.title = title ?? identifier ?? "Untitled pack";
}
if (this.load_game(stored_game, identifier)) {
this.switch_to_player();
}
+56 -12
View File
@@ -1,4 +1,4 @@
import { DIRECTIONS } from './defs.js';
import { DIRECTIONS, DRAW_LAYERS } from './defs.js';
import { mk } from './util.js';
import TILE_TYPES from './tiletypes.js';
@@ -11,7 +11,6 @@ export class CanvasRenderer {
// to do so, but then we wouldn't make a canvas so it couldn't be
// hooked, yadda yadda
if (fixed_size) {
this.viewport_is_fixed = true;
this.viewport_size_x = fixed_size;
this.viewport_size_y = fixed_size;
}
@@ -25,7 +24,10 @@ export class CanvasRenderer {
this.ctx = this.canvas.getContext('2d');
this.viewport_x = 0;
this.viewport_y = 0;
this.viewport_dirty = false;
this.show_actor_bboxes = false;
this.use_rewind_effect = false;
this.perception = 0; // 0 normal, 1 secret eye, 2 editor
}
set_level(level) {
@@ -33,6 +35,13 @@ export class CanvasRenderer {
// TODO update viewport size... or maybe Game should do that since you might be cheating
}
// Change the viewport size. DOES NOT take effect until the next redraw!
set_viewport_size(x, y) {
this.viewport_size_x = x;
this.viewport_size_y = y;
this.viewport_dirty = true;
}
cell_coords_from_event(ev) {
let rect = this.canvas.getBoundingClientRect();
let scale_x = rect.width / this.canvas.width;
@@ -67,6 +76,14 @@ export class CanvasRenderer {
return;
}
if (this.viewport_dirty) {
this.viewport_dirty = false;
this.canvas.setAttribute('width', this.tileset.size_x * this.viewport_size_x);
this.canvas.setAttribute('height', this.tileset.size_y * this.viewport_size_y);
this.canvas.style.setProperty('--viewport-width', this.viewport_size_x);
this.canvas.style.setProperty('--viewport-height', this.viewport_size_y);
}
let tic = (this.level.tic_counter ?? 0) + tic_offset;
let tw = this.tileset.size_x;
let th = this.tileset.size_y;
@@ -125,36 +142,63 @@ export class CanvasRenderer {
let y1 = Math.min(this.level.size_y - 1, Math.ceil(y0 + this.viewport_size_y));
// Draw one layer at a time, so animated objects aren't overdrawn by
// neighboring terrain
// XXX layer count hardcoded here
for (let layer = 0; layer < 4; layer++) {
// FIXME this is a bit inefficient when there are a lot of rarely-used layers; consider
// instead drawing everything under actors, then actors, then everything above actors?
for (let layer = 0; layer < DRAW_LAYERS.MAX; layer++) {
for (let x = xf0; x <= x1; x++) {
for (let y = yf0; y <= y1; y++) {
for (let tile of this.level.cells[y][x]) {
if (tile.type.draw_layer !== layer)
continue;
let vx, vy;
if (tile.type.is_actor &&
// FIXME kind of a hack for the editor, which uses bare tile objects
tile.visual_position)
{
// Handle smooth scrolling
let [vx, vy] = tile.visual_position(tic_offset);
[vx, vy] = tile.visual_position(tic_offset);
// Round this to the pixel grid too!
vx = Math.floor(vx * tw + 0.5) / tw;
vy = Math.floor(vy * th + 0.5) / th;
this.tileset.draw(tile, tic, (sx, sy, dx = 0, dy = 0, w = 1, h = w) =>
this.blit(this.ctx, sx, sy, vx - x0 + dx, vy - y0 + dy, w, h));
}
else {
// Non-actors can't move
this.tileset.draw(tile, tic, (sx, sy, dx = 0, dy = 0, w = 1, h = w) =>
this.blit(this.ctx, sx, sy, x - x0 + dx, y - y0 + dy, w, h));
vx = x;
vy = y;
}
// Note that the blit we pass to the tileset has a different signature:
// blit(
// source_tile_x, source_tile_y,
// mask_x = 0, mask_y = 0, mask_width = 1, mask_height = mask_width,
// mask_dx = mask_x, mask_dy = mask_y)
// This makes it easier to use in the extremely common case of drawing
// part of one tile atop another tile, but still aligned to the grid.
this.tileset.draw(tile, tic, (tx, ty, mx = 0, my = 0, mw = 1, mh = mw, mdx = mx, mdy = my) =>
this.blit(this.ctx,
tx + mx, ty + my,
vx - x0 + mdx, vy - y0 + mdy,
mw, mh));
}
}
}
}
if (this.show_actor_bboxes && ! this.level.linear_cells) { // FIXME dumb hack so this doesn't happen in editor
this.ctx.fillStyle = '#f004';
for (let x = xf0; x <= x1; x++) {
for (let y = yf0; y <= y1; y++) {
let actor = this.level.cells[y][x].get_actor();
if (! actor)
continue;
let [vx, vy] = actor.visual_position(tic_offset);
// Don't round to the pixel grid; we want to know if the bbox is misaligned!
this.ctx.fillRect((vx - x0) * tw, (vy - y0) * th, 1 * tw, 1 * th);
}
}
}
if (this.use_rewind_effect) {
this.draw_rewind_effect(tic);
}
@@ -174,11 +218,11 @@ export class CanvasRenderer {
}
}
create_tile_type_canvas(name) {
create_tile_type_canvas(name, tile = null) {
let canvas = mk('canvas', {width: this.tileset.size_x, height: this.tileset.size_y});
let ctx = canvas.getContext('2d');
this.tileset.draw_type(name, null, 0, (sx, sy, dx = 0, dy = 0, w = 1, h = w) =>
this.blit(ctx, sx, sy, dx, dy, w, h));
this.tileset.draw_type(name, tile, 0, this.perception, (tx, ty, mx = 0, my = 0, mw = 1, mh = mw, mdx = mx, mdy = my) =>
this.blit(ctx, tx + mx, ty + my, mdx, mdy, mw, mh));
return canvas;
}
}
+15
View File
@@ -61,4 +61,19 @@ export default [{
author: "jneen",
twitter: "jneen_",
path: 'music/escape-on-star-road.ogg',
}, {
title: "inner orbit",
author: "notchris",
twitter: "chrismcgrane",
path: 'music/inner-orbit.ogg',
}, {
title: "canopy",
author: "notchris",
twitter: "chrismcgrane",
path: 'music/canopy.ogg',
}, {
title: "asteroid prairie",
author: "notchris",
twitter: "chrismcgrane",
path: 'music/asteroid-prairie.ogg',
}];
+573 -112
View File
@@ -3,14 +3,36 @@ import TILE_TYPES from './tiletypes.js';
// TODO really need to specify this format more concretely, whoof
// XXX special kinds of drawing i know this has for a fact:
// - letter tiles draw from a block of half-tiles onto the center of the base
// - slime and walkers have double-size tiles when moving
// - wired tiles are a whole thing
// - letter tiles draw from a block (one of two blocks!) of half-tiles onto the center of the base
// - force floors are cropped from a double-size tile
// - wired tiles are a whole thing (floor)
// - thin walls are packed into just two tiles
// - rover has a half-tile overlay for its direction?
// - railroad tracks overlay a Lot
// - directional blocks have arrows in an awkward layout, not 4x4 grid but actually positioned on the edges
// - green and purple toggle walls use an overlay
// - turtles use an overlay, seem to pick a tile at random every so often
// - animations are common, should maybe have configurable timing??
// - custom floors and walls /should/ be consolidated into a single tile probably
// - thin walls should probably be consolidated?
// - traps have a state
// special features i currently have
// - directions for actors, can be used anywhere
// - arrows: for directional blocks
// - mask: for thin walls (though the idea is useful in many more places)
// - wired: for wired tiles
// - overlay: for green/purple walls mostly, also some bogus cc1 tiles
// things that are currently NOT handled
// - bomb is supposed to have a fuse
// - critters should only animate when moving
// - rover animation depends on behavior, also has a quarter-tile overlay for its direction
// - slime and walkers have double-size tiles when moving
// - logic gates draw over the stuff underneath them
// - railroad tracks overlay a Lot
// - canopy, at all
// - swivel's floor (eugh)
// - xray vision
// - editor vision
export const CC2_TILESET_LAYOUT = {
'#wire-width': 1/16,
@@ -22,8 +44,13 @@ export const CC2_TILESET_LAYOUT = {
key_blue: [5, 1],
key_yellow: [6, 1],
key_green: [7, 1],
dirt_block: [8, 1],
// xray
// FIXME these shouldn't be drawn with a hole when drawn in isolation, gruh
dirt_block: {
special: 'perception',
threshold: 2,
hidden: [8, 1],
revealed: [9, 1],
},
ice: [10, 1],
ice_se: [11, 1],
ice_sw: [12, 1],
@@ -35,23 +62,44 @@ export const CC2_TILESET_LAYOUT = {
// Wiring!
base: [0, 2],
wired: [8, 26],
wired_cross: [10, 26],
is_wired_optional: true,
},
wall_invisible: [0, 2],
wall_appearing: [0, 2],
wall: [1, 2],
floor_letter: [2, 2],
'floor_letter#ascii': {
x0: 0,
y0: 0,
width: 16,
height: 1,
// FIXME i think these are visible with the secret eye, but my tileset puts text on them, whoops
wall_invisible: {
special: 'perception',
threshold: 2,
hidden: [0, 2],
revealed: [9, 31],
},
'floor_letter#arrows': {
north: [14, 31],
east: [14.5, 31],
south: [15, 31],
west: [15.5, 31],
// FIXME this shouldn't be visible with seeing eye (or should it not spawn at all?)
wall_invisible_revealed: [1, 2],
wall_appearing: {
special: 'perception',
threshold: 2,
hidden: [0, 2],
revealed: [11, 31],
},
wall: [1, 2],
floor_letter: {
special: 'letter',
base: [2, 2],
letter_glyphs: {
// Arrows
"⬆": [14, 31],
"➡": [14.5, 31],
"⬇": [15, 31],
"⬅": [15.5, 31],
},
letter_ranges: [{
// ASCII text (only up through uppercase)
range: [32, 96],
x0: 0,
y0: 0,
w: 0.5,
h: 0.5,
columns: 32,
}],
},
thief_tools: [3, 2],
socket: [4, 2],
@@ -62,8 +110,13 @@ export const CC2_TILESET_LAYOUT = {
[8, 2],
[9, 2],
],
ice_block: [10, 2],
// TODO ice block xray
// FIXME these shouldn't be drawn with a hole when drawn in isolation, gruh
ice_block: {
special: 'perception',
threshold: 2,
hidden: [10, 2],
revealed: [11, 2],
},
score_1000: [12, 2],
score_100: [13, 2],
score_10: [14, 2],
@@ -73,9 +126,10 @@ export const CC2_TILESET_LAYOUT = {
green_chip: [9, 3],
chip_extra: [10, 3],
chip: [11, 3],
// bribe
// mercury boot
// canopy, xray
bribe: [12, 3],
speed_boots: [13, 3],
canopy: [14, 3],
// canopy xray
// TODO lit
dynamite: [0, 4],
@@ -92,24 +146,40 @@ export const CC2_TILESET_LAYOUT = {
wall_custom_blue: [15, 4],
explosion: [[0, 5], [1, 5], [2, 5], [3, 5]],
splash_slime: [[0, 5], [1, 5], [2, 5], [3, 5]],
splash: [[4, 5], [5, 5], [6, 5], [7, 5]],
flame_jet_off: [8, 5],
flame_jet_on: [[9, 5], [10, 5], [11, 5]],
popdown_wall: [12, 5],
popdown_floor: [12, 5],
popdown_floor: {
special: 'perception',
threshold: 2,
hidden: [12, 5],
revealed: [13, 5],
},
popdown_floor_visible: [13, 5],
forbidden: [14, 5],
// TODO arrows overlay at [3, 10]
directional_block: [15, 5],
no_sign: [14, 5],
frame_block: {
base: [15, 5],
arrows: [3, 10],
},
flippers: [0, 6],
fire_boots: [1, 6],
cleats: [2, 6],
suction_boots: [3, 6],
hiking_boots: [4, 6],
// speed boots...? not boots though
lightning_bolt: [5, 6],
// weird translucent spiral
// weird translucent red
// TODO dopps can push but i don't think they have any other visuals
doppelganger1: {
base: [7, 6],
overlay: 'player',
},
doppelganger2: {
base: [7, 6],
overlay: 'player2',
},
button_blue: [8, 6],
button_green: [9, 6],
button_red: [10, 6],
@@ -168,7 +238,13 @@ export const CC2_TILESET_LAYOUT = {
],
fake_wall: [0, 10],
fake_floor: [0, 10],
// FIXME i think these are visible with the secret eye, but my tileset puts text on them, whoops
fake_floor: {
special: 'perception',
threshold: 2,
hidden: [0, 10],
revealed: [10, 31],
},
// Thin walls are built piecemeal from these two tiles; the first is N/S,
// the second is E/W
thinwall_n: {
@@ -199,22 +275,33 @@ export const CC2_TILESET_LAYOUT = {
popwall: [8, 10],
popwall2: [8, 10],
gravel: [9, 10],
ball: [[10, 10], [11, 10], [12, 10], [13, 10], [14, 10]],
ball: [
// appropriately, this animation ping-pongs
[10, 10], [11, 10], [12, 10], [13, 10],
[14, 10], [13, 10], [12, 10], [11, 10],
// FIXME the ball bounces so it specifically needs to play its animation every move; this
// defeats the ½x slowdown. it's dumb and means this anim as written doesn't match cc2
[10, 10], [11, 10], [12, 10], [13, 10],
[14, 10], [13, 10], [12, 10], [11, 10],
],
steel: {
// Wiring!
base: [15, 10],
wired: [9, 26],
wired_cross: [11, 26],
is_wired_optional: true,
},
// TODO only animates while moving
// TODO should explicitly set the non-moving tile, so we can have the walk tile start with
// immediate movement?
// TODO this shouldn't run at half speed, it's already designed to be one step, and when teeth
// move at half speed it looks clumsy
teeth: {
// NOTE: CC2 inexplicably dropped north teeth and just uses the south
// sprites instead
north: [[0, 11], [1, 11], [2, 11]],
east: [[3, 11], [4, 11], [5, 11]],
south: [[0, 11], [1, 11], [2, 11]],
west: [[6, 11], [7, 11], [8, 11]],
// NOTE: CC2 inexplicably dropped north teeth and just uses the south sprites instead
north: [[1, 11], [0, 11], [1, 11], [2, 11]],
east: [[4, 11], [3, 11], [4, 11], [5, 11]],
south: [[1, 11], [0, 11], [1, 11], [2, 11]],
west: [[7, 11], [6, 11], [7, 11], [8, 11]],
},
swivel_sw: [9, 11],
swivel_nw: [10, 11],
@@ -237,18 +324,31 @@ export const CC2_TILESET_LAYOUT = {
},
walker: [0, 13],
// TODO walker animations span multiple tiles, rgh
// FIXME walker animations span multiple tiles
helmet: [0, 14],
stopwatch_toggle: [14, 14],
stopwatch_bonus: [15, 14],
blob: [0, 15],
// TODO blob animations also span multiple tiles
// FIXME blob animations span multiple tiles
// TODO [0, 16] some kinda red/blue outline
floor_mimic: [0, 2], // TODO [14, 16] with xray
floor_mimic: {
special: 'perception',
threshold: 1,
hidden: [0, 2],
revealed: [14, 16],
},
// TODO [15, 16] some kinda yellow/black outline
// timid teeth
teeth_timid: {
// NOTE: CC2 inexplicably dropped north teeth and just uses the south sprites instead
// NOTE: it also skimped on timid teeth frames
north: [[1, 17], [0, 17]],
east: [[3, 17], [2, 17]],
south: [[1, 17], [0, 17]],
west: [[5, 17], [4, 17]],
},
bowling_ball: [6, 17], // TODO also +18 when rolling
tank_yellow: {
north: [[8, 17], [9, 17]],
@@ -257,8 +357,26 @@ export const CC2_TILESET_LAYOUT = {
west: [[14, 17], [15, 17]],
},
// TODO rover has layers and moves and stuff
rover: [0, 18],
// TODO rover has an overlay showing its direction
rover: {
inert: [0, 18],
teeth: [[0, 18], [8, 18]],
glider: [
// quite fast
[0, 18], [1, 18], [2, 18], [3, 18], [4, 18], [5, 18], [6, 18], [7, 18],
[0, 18], [1, 18], [2, 18], [3, 18], [4, 18], [5, 18], [6, 18], [7, 18],
],
bug: [[0, 18], [1, 18], [2, 18], [3, 18], [4, 18], [5, 18], [6, 18], [7, 18]],
ball: [[0, 18], [4, 18]],
teeth_timid: [[0, 18], [9, 18]],
fireball: [
// quite fast
[7, 18], [6, 18], [5, 18], [4, 18], [3, 18], [2, 18], [1, 18], [0, 18],
[7, 18], [6, 18], [5, 18], [4, 18], [3, 18], [2, 18], [1, 18], [0, 18],
],
paramecium: [[7, 18], [6, 18], [5, 18], [4, 18], [3, 18], [2, 18], [1, 18], [0, 18]],
walker: [[8, 18], [9, 18]],
},
xray_eye: [11, 18],
ghost: {
north: [12, 18],
@@ -294,7 +412,14 @@ export const CC2_TILESET_LAYOUT = {
force_floor_all: [[0, 21], [1, 21], [2, 21], [3, 21], [4, 21], [5, 21], [6, 21], [7, 21]],
// latches
// switch
light_switch_off: {
base: [14, 21],
wired: [12, 21],
},
light_switch_on: {
base: [14, 21],
wired: [13, 21],
},
thief_keys: [15, 21],
player: {
@@ -332,8 +457,10 @@ export const CC2_TILESET_LAYOUT = {
south: [2, 23],
west: [10, 23],
},
exited: 'normal',
// These are frames from the splash/explosion animations
drowned: [5, 5],
slimed: [5, 5],
burned: [1, 5],
exploded: [1, 5],
failed: [1, 5],
@@ -367,6 +494,55 @@ export const CC2_TILESET_LAYOUT = {
[15, 24],
],
logic_gate: {
special: 'logic-gate',
logic_gate_tiles: {
'latch-ccw': {
north: [8, 21],
east: [9, 21],
south: [10, 21],
west: [11, 21],
},
not: {
north: [0, 25],
east: [1, 25],
south: [2, 25],
west: [3, 25],
},
and: {
north: [4, 25],
east: [5, 25],
south: [6, 25],
west: [7, 25],
},
or: {
north: [8, 25],
east: [9, 25],
south: [10, 25],
west: [11, 25],
},
xor: {
north: [12, 25],
east: [13, 25],
south: [14, 25],
west: [15, 25],
},
'latch-cw': {
north: [0, 26],
east: [1, 26],
south: [2, 26],
west: [3, 26],
},
nand: {
north: [4, 26],
east: [5, 26],
south: [6, 26],
west: [7, 26],
},
counter: [14, 26],
},
},
'#unpowered': [13, 26],
'#powered': [15, 26],
@@ -405,8 +581,10 @@ export const CC2_TILESET_LAYOUT = {
south: [2, 28],
west: [10, 28],
},
exited: 'normal',
// These are frames from the splash/explosion animations
drowned: [5, 5],
slimed: [5, 5],
burned: [1, 5],
exploded: [1, 5],
failed: [1, 5],
@@ -419,11 +597,40 @@ export const CC2_TILESET_LAYOUT = {
],
// TODO handle train tracks! this is gonna be complicated.
railroad: [9, 10],
railroad_sign: [4, 31],
railroad: {
special: 'railroad',
base: [9, 10],
railroad_ties: {
ne: [0, 30],
se: [1, 30],
sw: [2, 30],
nw: [3, 30],
ew: [4, 30],
ns: [5, 30],
},
railroad_switch: [6, 30],
railroad_inactive: {
ne: [7, 30],
se: [8, 30],
sw: [9, 30],
nw: [10, 30],
ew: [11, 30],
ns: [12, 30],
},
railroad_active: {
ne: [13, 30],
se: [14, 30],
sw: [15, 30],
nw: [0, 31],
ew: [1, 31],
ns: [2, 31],
},
},
railroad_sign: [3, 31],
dirt: [4, 31],
no_player2_sign: [5, 31],
no_player1_sign: [6, 31],
hook: [7, 31],
// misc other stuff
};
@@ -435,6 +642,7 @@ export const TILE_WORLD_TILESET_LAYOUT = {
water: [0, 3],
fire: [0, 4],
wall_invisible: [0, 5],
wall_invisible_revealed: [0, 1],
thinwall_n: [0, 6],
thinwall_w: [0, 7],
thinwall_s: [0, 8],
@@ -596,9 +804,14 @@ export const TILE_WORLD_TILESET_LAYOUT = {
export const LL_TILESET_LAYOUT = Object.assign({}, CC2_TILESET_LAYOUT, {
// Completed teeth sprites
teeth: Object.assign({}, CC2_TILESET_LAYOUT.teeth, {
north: [[0, 32], [1, 32], [2, 32], [1, 32]],
north: [[1, 32], [0, 32], [1, 32], [2, 32]],
}),
popwall2: [9, 32],
teeth_timid: {
north: [[7, 32], [6, 32], [7, 32], [8, 32]],
east: [[4, 32], [2, 17], [4, 32], [3, 17]],
south: [[3, 32], [0, 17], [3, 32], [1, 17]],
west: [[5, 32], [4, 17], [5, 32], [5, 17]],
},
// Extra player sprites
player: Object.assign({}, CC2_TILESET_LAYOUT.player, {
@@ -609,14 +822,26 @@ export const LL_TILESET_LAYOUT = Object.assign({}, CC2_TILESET_LAYOUT, {
west: [3, 33],
},
forced: 'skating',
exited: [14, 32],
burned: {
north: [4, 33],
east: [5, 33],
south: [6, 33],
west: [7, 33],
},
slimed: [1, 38],
}),
player2: Object.assign({}, CC2_TILESET_LAYOUT.player2, {
// TODO skating
exited: [15, 32],
burned: {
north: [12, 33],
east: [13, 33],
south: [14, 33],
west: [15, 33],
},
slimed: [1, 38],
}),
// TODO player2 equivalents
bogus_player_burned_fire: {
overlay: [6, 33],
base: 'fire',
@@ -625,6 +850,33 @@ export const LL_TILESET_LAYOUT = Object.assign({}, CC2_TILESET_LAYOUT, {
overlay: [6, 33],
base: 'floor',
},
// Custom tiles
popwall2: [9, 32],
gift_bow: [10, 32],
circuit_block: {
base: [13, 32],
wired: [11, 32],
wired_cross: [12, 32],
},
// Blob and walker in all four directions
blob: {
north: [[0, 35], [1, 35], [2, 35], [3, 35], [4, 35], [5, 35], [6, 35], [7, 35]],
east: [[8, 35], [9, 35], [10, 35], [11, 35], [12, 35], [13, 35], [14, 35], [15, 35]],
south: [[0, 36], [1, 36], [2, 36], [3, 36], [4, 36], [5, 36], [6, 36], [7, 36]],
west: [[8, 36], [9, 36], [10, 36], [11, 36], [12, 36], [13, 36], [14, 36], [15, 36]],
},
walker: {
north: [[0, 37], [1, 37], [2, 37], [3, 37]],
east: [[4, 37], [5, 37], [6, 37], [7, 37]],
// Same animations but played backwards
south: [[2, 37], [1, 37], [0, 37], [3, 37]],
west: [[6, 37], [5, 37], [4, 37], [7, 37]],
},
// Custom VFX
splash_slime: [[0, 38], [1, 38], [2, 38], [3, 38]],
});
export class Tileset {
@@ -637,12 +889,163 @@ export class Tileset {
}
draw(tile, tic, blit) {
this.draw_type(tile.type.name, tile, tic, blit);
// FIXME perception
this.draw_type(tile.type.name, tile, tic, 0, blit);
}
// Draw a "standard" drawspec, which is either:
// - a single tile: [x, y]
// - an animation: [[x0, y0], [x1, y1], ...]
// - a directional tile: { north: T, east: T, ... } where T is either of the above
_draw_standard(drawspec, tile, tic, blit, mask = []) {
// If we have an object, it must be a table of directions
let coords = drawspec;
if (!(coords instanceof Array)) {
coords = coords[(tile && tile.direction) ?? 'south'];
}
// Deal with animation
if (coords[0] instanceof Array) {
if (tic !== null) {
if (tile && tile.animation_speed) {
// This tile reports its own animation timing (in tics), so trust that, and just
// use the current tic's fraction.
// That said: adjusting animation speed complicates this slightly. Consider the
// player's walk animation, which takes 4 tics to complete, during which time we
// cycle through 8 frames. Playing that at half speed means only half the
// animation actually plays, but if the player continues walking, then on the
// NEXT four tics, we should play the other half. To make this work, use the
// tic as a global timer as well: if the animation started on tics 0-4, play the
// first half; if it started on tics 5-8, play the second half. They could get
// out of sync if the player hesitates, but no one will notice that, and this
// approach minimizes storing extra state.
let i = (tile.animation_progress + tic % 1) / tile.animation_speed;
// But do NOT do this for explosions or splashes, which have a fixed duration
// and only play once
if (this.animation_slowdown > 1 && ! tile.type.ttl) {
// i ranges from [0, 1), but a slowdown of N means we'll only play the first
// 1/N of it before the game ends (or loops) the animation.
// So increase by [0..N-1] to get it in some other range, then divide by N
// to scale back down to [0, 1)
i += Math.floor(tic / tile.animation_speed % this.animation_slowdown);
i /= this.animation_slowdown;
}
coords = coords[Math.floor(i * coords.length)];
}
else {
// This tile animates on a global timer, one cycle every quarter of a second
coords = coords[Math.floor(tic / this.animation_slowdown % 5 / 5 * coords.length)];
}
}
else {
coords = coords[0];
}
}
blit(coords[0], coords[1], ...mask);
}
_draw_letter(drawspec, tile, tic, blit) {
this._draw_standard(drawspec.base, tile, tic, blit);
let glyph = tile ? tile.overlaid_glyph : "?";
if (drawspec.letter_glyphs[glyph]) {
let [x, y] = drawspec.letter_glyphs[glyph];
// XXX size is hardcoded here, but not below, meh
blit(x, y, 0, 0, 0.5, 0.5, 0.25, 0.25);
}
else {
// Look for a range
let u = glyph.charCodeAt(0);
for (let rangedef of drawspec.letter_ranges) {
if (rangedef.range[0] <= u && u < rangedef.range[1]) {
let t = u - rangedef.range[0];
let x = rangedef.x0 + rangedef.w * (t % rangedef.columns);
let y = rangedef.y0 + rangedef.h * Math.floor(t / rangedef.columns);
blit(x, y, 0, 0, rangedef.w, rangedef.h,
(1 - rangedef.w) / 2, (1 - rangedef.h) / 2);
break;
}
}
}
}
_draw_logic_gate(drawspec, tile, tic, blit) {
// Layer 1: wiring state
// Always draw the unpowered wire base
let unpowered_coords = this.layout['#unpowered'];
let powered_coords = this.layout['#powered'];
blit(...unpowered_coords);
if (tile && tile.cell) {
// What goes on top varies a bit...
// FIXME implement for NOT and counter!
let r = this.layout['#wire-width'] / 2;
if (tile.cell.powered_edges & DIRECTIONS[tile.direction].bit) {
// Output (on top)
let [x0, y0, x1, y1] = this._rotate(tile.direction, 0.5 - r, 0, 0.5 + r, 0.5);
blit(powered_coords[0], powered_coords[1], x0, y0, x1 - x0, y1 - y0);
}
if (tile.cell.powered_edges & DIRECTIONS[DIRECTIONS[tile.direction].right].bit) {
// Right input, which includes the middle
let [x0, y0, x1, y1] = this._rotate(tile.direction, 0.5 - r, 0.5 - r, 1, 1);
blit(powered_coords[0], powered_coords[1], x0, y0, x1 - x0, y1 - y0);
}
if (tile.cell.powered_edges & DIRECTIONS[DIRECTIONS[tile.direction].left].bit) {
// Left input, which does not include the middle
let [x0, y0, x1, y1] = this._rotate(tile.direction, 0, 0.5 - r, 0.5 - r, 1);
blit(powered_coords[0], powered_coords[1], x0, y0, x1 - x0, y1 - y0);
}
}
// Layer 2: the tile itself
this._draw_standard(drawspec.logic_gate_tiles[tile.gate_type], tile, tic, blit);
// Layer 3: counter number
if (tile.gate_type === 'counter') {
blit(0, 3, tile.memory * 0.75, 0, 0.75, 1, 0.125, 0);
}
}
_draw_railroad(drawspec, tile, tic, blit) {
// All railroads have regular gravel underneath
// TODO would be nice to disambiguate since it's possible to have nothing visible
this._draw_standard(this.layout['gravel'], tile, tic, blit);
// FIXME what do i draw if there's no tile?
let part_order = ['ne', 'se', 'sw', 'nw', 'ew', 'ns'];
let visible_parts = [];
let topmost_part = null;
for (let [i, part] of part_order.entries()) {
if (tile && (tile.tracks & (1 << i))) {
if (tile.track_switch === i) {
topmost_part = part;
}
visible_parts.push(part);
}
}
let has_switch = (tile && tile.track_switch !== null);
for (let part of visible_parts) {
this._draw_standard(drawspec.railroad_ties[part], tile, tic, blit);
}
let tracks = has_switch ? drawspec.railroad_inactive : drawspec.railroad_active;
for (let part of visible_parts) {
if (part !== topmost_part) {
this._draw_standard(tracks[part], tile, tic, blit);
}
}
if (topmost_part) {
this._draw_standard(drawspec.railroad_active[topmost_part], tile, tic, blit);
}
if (has_switch) {
this._draw_standard(drawspec.railroad_switch, tile, tic, blit);
}
}
// Draws a tile type, given by name. Passing in a tile is optional, but
// without it you'll get defaults.
draw_type(name, tile, tic, blit) {
draw_type(name, tile, tic, perception, blit) {
let drawspec = this.layout[name];
if (! drawspec) {
console.error(`Don't know how to draw tile type ${name}!`);
@@ -651,12 +1054,17 @@ export class Tileset {
if (drawspec.overlay) {
// Goofy overlay thing used for green/purple toggle tiles and
// southeast thin walls. Draw the base (a type name), then draw
// southeast thin walls. Draw the base (a type name or drawspec), then draw
// the overlay (either a type name or a regular draw spec).
// TODO chance of infinite recursion here
this.draw_type(drawspec.base, tile, tic, blit);
if (typeof drawspec.base === 'string') {
this.draw_type(drawspec.base, tile, tic, perception, blit);
}
else {
this._draw_standard(drawspec.base, tile, tic, blit);
}
if (typeof drawspec.overlay === 'string') {
this.draw_type(drawspec.overlay, tile, tic, blit);
this.draw_type(drawspec.overlay, tile, tic, perception, blit);
return;
}
else {
@@ -664,6 +1072,30 @@ export class Tileset {
}
}
// TODO shift everything to use this style, this is ridiculous
if (drawspec.special) {
if (drawspec.special === 'letter') {
this._draw_letter(drawspec, tile, tic, blit);
return;
}
else if (drawspec.special === 'perception') {
if (perception >= drawspec.threshold) {
drawspec = drawspec.revealed;
}
else {
drawspec = drawspec.hidden;
}
}
else if (drawspec.special === 'logic-gate') {
this._draw_logic_gate(drawspec, tile, tic, blit);
return;
}
else if (drawspec.special === 'railroad') {
this._draw_railroad(drawspec, tile, tic, blit);
return;
}
}
let coords = drawspec;
if (drawspec.mask) {
// Some tiles (OK, just the thin walls) don't actually draw a full
@@ -671,31 +1103,51 @@ export class Tileset {
coords = drawspec.tile;
}
else if (drawspec.wired) {
if (tile && tile.wire_directions) {
// TODO all four is a different thing entirely with two separate parts, ugh
// Draw the appropriate wire underlay
this.draw_type(tile.cell.is_powered ? '#powered' : '#unpowered', tile, tic, blit);
// This /should/ match CC2's draw order exactly, based on experimentation
let wire_radius = this.layout['#wire-width'] / 2;
if (tile && tile.wire_directions === 0x0f) {
// This is a wired tile with crossing wires, which acts a little differently
// Draw the base tile
blit(drawspec.base[0], drawspec.base[1]);
// Draw a masked part of the base tile
let wiredir = tile.wire_directions;
let wire_radius = this.layout['#wire-width'] / 2;
let wire0 = 0.5 - wire_radius;
let wire1 = 0.5 + wire_radius;
let [bx, by] = drawspec.base;
if ((wiredir & DIRECTIONS['north'].bit) === 0) {
blit(bx, by, 0, 0, 1, wire0);
}
if ((wiredir & DIRECTIONS['south'].bit) === 0) {
blit(bx, by + wire1, 0, wire1, 1, wire0);
}
if ((wiredir & DIRECTIONS['west'].bit) === 0) {
blit(bx, by, 0, 0, wire0, 1);
}
if ((wiredir & DIRECTIONS['east'].bit) === 0) {
blit(bx + wire1, by, wire1, 0, wire0, 1);
}
// Draw the two wires as separate rectangles, NS then EW
let wire_inset = 0.5 - wire_radius;
let wire_coords_ns = this.layout[
tile.cell && tile.cell.powered_edges & DIRECTIONS['north'].bit ? '#powered' : '#unpowered'];
let wire_coords_ew = this.layout[
tile.cell && tile.cell.powered_edges & DIRECTIONS['east'].bit ? '#powered' : '#unpowered'];
blit(wire_coords_ns[0], wire_coords_ns[1], wire_inset, 0, wire_radius * 2, 1);
blit(wire_coords_ew[0], wire_coords_ew[1], 0, wire_inset, 1, wire_radius * 2);
// Then draw the wired tile as normal
// Draw the cross tile on top
coords = drawspec.wired_cross ?? drawspec.wired;
}
else if (tile && tile.wire_directions) {
// Draw the base tile
blit(drawspec.base[0], drawspec.base[1]);
// Draw the wire part as a single rectangle, initially just a small dot in the
// center, but extending out to any edge that has a wire present
let x0 = 0.5 - wire_radius;
let x1 = 0.5 + wire_radius;
let y0 = 0.5 - wire_radius;
let y1 = 0.5 + wire_radius;
if (tile.wire_directions & DIRECTIONS['north'].bit) {
y0 = 0;
}
if (tile.wire_directions & DIRECTIONS['east'].bit) {
x1 = 1;
}
if (tile.wire_directions & DIRECTIONS['south'].bit) {
y1 = 1;
}
if (tile.wire_directions & DIRECTIONS['west'].bit) {
x0 = 0;
}
let wire_coords = this.layout[tile.cell && tile.cell.powered_edges ? '#powered' : '#unpowered'];
blit(wire_coords[0], wire_coords[1], x0, y0, x1 - x0, y1 - y0);
// Then draw the wired tile on top of it all
coords = drawspec.wired;
}
else {
@@ -711,6 +1163,10 @@ export class Tileset {
}
}
}
else if (drawspec.arrows) {
// Directional blocks have a specific overlay, but draw the base first
coords = drawspec.base;
}
else if (drawspec.animate_width) {
// Force floors animate their... cutout, I guess?
let [x, y] = drawspec.base;
@@ -795,7 +1251,7 @@ export class Tileset {
// Continue on with masking
coords = drawspec.tile;
let [x0, y0, w, h] = drawspec.mask;
blit(coords[0] + x0, coords[1] + y0, x0, y0, w, h);
blit(coords[0], coords[1], x0, y0, w, h);
}
else {
if (!coords) console.error(name, tile);
@@ -809,51 +1265,56 @@ export class Tileset {
let tunnel_length = 12/32;
let tunnel_offset = (1 - tunnel_width) / 2;
if (tile.wire_tunnel_directions & DIRECTIONS['north'].bit) {
blit(tunnel_coords[0] + tunnel_offset, tunnel_coords[1],
blit(tunnel_coords[0], tunnel_coords[1],
tunnel_offset, 0, tunnel_width, tunnel_length);
}
if (tile.wire_tunnel_directions & DIRECTIONS['south'].bit) {
blit(tunnel_coords[0] + tunnel_offset, tunnel_coords[1] + 1 - tunnel_length,
blit(tunnel_coords[0], tunnel_coords[1],
tunnel_offset, 1 - tunnel_length, tunnel_width, tunnel_length);
}
if (tile.wire_tunnel_directions & DIRECTIONS['west'].bit) {
blit(tunnel_coords[0], tunnel_coords[1] + tunnel_offset,
blit(tunnel_coords[0], tunnel_coords[1],
0, tunnel_offset, tunnel_length, tunnel_width);
}
if (tile.wire_tunnel_directions & DIRECTIONS['east'].bit) {
blit(tunnel_coords[0] + 1 - tunnel_length, tunnel_coords[1] + tunnel_offset,
blit(tunnel_coords[0], tunnel_coords[1],
1 - tunnel_length, tunnel_offset, tunnel_length, tunnel_width);
}
}
// Special behavior for special objects
// TODO? hardcode this less?
if (name === 'floor_letter') {
let n = tile.ascii_code - 32;
let scale = 0.5;
let sx, sy;
if (n < 0) {
// Arrows
if (n < -4) {
// Default to south
n = -2;
}
// Directional blocks have arrows drawn on top
// TODO does cc2 draw even if there are no arrows?
if (drawspec.arrows && tile && tile.arrows) {
let [x, y] = drawspec.arrows;
let x0 = 0.25, y0 = 0.25, x1 = 0.75, y1 = 0.75;
if (tile.arrows.has('north')) {
y0 = 0;
}
if (tile.arrows.has('east')) {
x1 = 1;
}
if (tile.arrows.has('south')) {
y1 = 1;
}
if (tile.arrows.has('west')) {
x0 = 0;
}
blit(x, y, x0, y0, x1 - x0, y1 - y0);
}
}
let direction = ['north', 'east', 'south', 'west'][n + 4];
[sx, sy] = this.layout['floor_letter#arrows'][direction];
}
else {
// ASCII text (only up through uppercase)
let letter_spec = this.layout['floor_letter#ascii'];
if (n > letter_spec.width / scale * letter_spec.height / scale) {
n = 0;
}
let w = letter_spec.width / scale;
sx = (letter_spec.x0 + n % w) * scale;
sy = (letter_spec.y0 + Math.floor(n / w)) * scale;
}
let offset = (1 - scale) / 2;
blit(sx, sy, offset, offset, scale, scale);
_rotate(direction, x0, y0, x1, y1) {
if (direction === 'east') {
return [1 - y1, x0, 1 - y0, x1];
}
else if (direction === 'south') {
return [1 - x1, 1 - y1, 1 - x0, 1 - y0];
}
else if (direction === 'west') {
return [y0, 1 - x1, y1, 1 - x0];
}
else {
return [x0, y0, x1, y1];
}
}
}
+1268 -417
View File
File diff suppressed because it is too large. Load diff
+21 -12
View File
@@ -24,14 +24,19 @@ function _mk(el, children) {
export function mk(tag_selector, ...children) {
let [tag, ...classes] = tag_selector.split('.');
let el = document.createElement(tag);
el.classList = classes.join(' ');
if (classes.length > 0) {
el.classList = classes.join(' ');
}
return _mk(el, children);
}
export const SVG_NS = 'http://www.w3.org/2000/svg';
export function mk_svg(tag_selector, ...children) {
let [tag, ...classes] = tag_selector.split('.');
let el = document.createElementNS('http://www.w3.org/2000/svg', tag);
el.classList = classes.join(' ');
let el = document.createElementNS(SVG_NS, tag);
if (classes.length > 0) {
el.classList = classes.join(' ');
}
return _mk(el, children);
}
@@ -152,8 +157,13 @@ export function string_from_buffer_ascii(buf, start = 0, len) {
return String.fromCharCode.apply(null, new Uint8Array(buf, start, len));
}
// Converts a string to a buffer, using NO ENCODING, assuming single-byte characters
export function bytestring_to_buffer(bytestring) {
return Uint8Array.from(bytestring, c => c.charCodeAt(0)).buffer;
}
// Cast a line through a grid and yield every cell it touches
export function* walk_grid(x0, y0, x1, y1) {
export function* walk_grid(x0, y0, x1, y1, min_a, min_b, max_a, max_b) {
// TODO if the ray starts outside the grid (extremely unlikely), we should
// find the point where it ENTERS the grid, otherwise the 'while'
// conditions below will stop immediately
@@ -193,27 +203,24 @@ export function* walk_grid(x0, y0, x1, y1) {
step_a = -step_a;
offset_x = 1 - offset_x;
}
// Zero offset means we're on a grid line, so we're actually a full cell
// away from the next grid line
if (offset_x === 0) {
else if (offset_x === 0) {
// Zero offset means we're on a grid line, so we're a full cell away from the next grid line
// (if we're moving forward; if we're moving backward, the next cell really is 0 away)
offset_x = 1;
}
let step_b = 1;
let offset_y = 1 - (y0 - b);
let offset_y = 1 - (y0 - b);
if (dy < 0) {
dy = -dy;
step_b = -step_b;
offset_y = 1 - offset_y;
}
if (offset_y === 0) {
else if (offset_y === 0) {
offset_y = 1;
}
let err = dy * offset_x - dx * offset_y;
let min_a = 0, min_b = 0;
// TODO get these passed in fool
let max_a = 31, max_b = 31;
if (dx > dy) {
// Main axis is x/a
while (min_a <= a && a <= max_a && min_b <= b && b <= max_b) {
@@ -252,6 +259,8 @@ export function* walk_grid(x0, y0, x1, y1) {
}
}
}
window.walk_grid = walk_grid;
// console.table(Array.from(walk_grid(13, 27.133854389190674, 12.90625, 27.227604389190674)))
// Root class to indirect over where we might get files from
// - a pool of uploaded in-memory files
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+442 -38
View File
@@ -37,7 +37,10 @@ button {
font-family: inherit;
color: white;
background: var(--button-bg-color);
border: none;
border: 1px solid hsl(225, 10%, 15%);
box-shadow:
inset 0 0 0 1px hsl(225, 10%, 33%),
0 1px 1px hsl(225, 10%, 10%);
border-radius: 0.25em;
text-transform: lowercase;
cursor: pointer;
@@ -45,9 +48,19 @@ button {
button:hover {
background: var(--button-bg-hover-color);
}
button:active {
box-shadow:
inset 0 0 2px 1px hsl(225, 10%, 33%);
transform: translateY(1px);
/* Need this for the editor's tool help things and i'm not questioning it */
z-index: 1;
}
button:disabled {
color: #606060;
background: #202020;
box-shadow:
inset 0 0 2px 1px hsl(225, 0%, 10%),
0 1px 0 hsl(225, 10%, 10%);
cursor: auto;
}
button.button-big {
@@ -97,10 +110,10 @@ a:visited {
text-decoration: underline dotted;
}
a:link {
color: hsl(225, 50%, 60%);
color: hsl(225, 50%, 75%);
}
a:visited {
color: hsl(300, 50%, 60%);
color: hsl(255, 50%, 75%);
}
a:link:hover,
a:visited:hover {
@@ -110,6 +123,16 @@ a:active {
color: hsl(0, 50%, 60%);
}
svg.svg-icon {
width: 1em;
height: 1em;
vertical-align: middle;
stroke: none;
fill: currentColor;
fill-rule: evenodd;
}
/* Overlay styling */
.overlay {
display: flex;
@@ -123,6 +146,11 @@ a:active {
right: 0;
background: #fff4;
}
.overlay.--transient {
align-items: start;
justify-content: start;
background: none;
}
.dialog {
display: flex;
flex-direction: column;
@@ -165,6 +193,26 @@ a:active {
background: #f0d0d0;
padding: 0.5em 1em;
}
.dialog a:link {
color: hsl(225, 50%, 50%);
}
.dialog a:visited {
color: hsl(255, 50%, 50%);
}
dl.formgrid {
display: grid;
grid: auto-flow min-content / 1fr 4fr;
align-items: baseline;
gap: 1em;
margin: 0;
}
dl.formgrid > dt {
grid-column: 1;
}
dl.formgrid > dd {
grid-column: 2;
margin: 0;
}
/* Individual overlays */
table.level-browser {
@@ -269,6 +317,13 @@ label.option .option-label {
/* TODO */
}
@media (max-width: 800px) {
.dialog {
max-width: 90%;
max-height: 90%;
}
}
/* Bits and pieces */
img.compat-icon {
margin: 0 0.25em 0.125em;
@@ -403,6 +458,11 @@ body[data-mode=player] #editor-play {
#splash > header h1 {
font-size: 3em;
}
#splash > header p {
margin: 0;
font-style: italic;
color: #909090;
}
#splash h2 {
border-bottom: 1px solid #404040;
color: #909090;
@@ -415,7 +475,7 @@ body[data-mode=player] #editor-play {
}
#splash > #splash-intro {
grid-area: intro;
font-size: 18px;
font-size: 20px;
}
#splash > #splash-stock-levels {
grid-area: stock;
@@ -504,8 +564,9 @@ button.level-pack-button p {
"level bonus" min-content
"level message" 1fr
"level inventory" min-content
"level actions" min-content
/* Need explicit min-content to force the hint to wrap */
/ min-content min-content
/ min-content min-content min-content
;
column-gap: 2em;
row-gap: 0.5em;
@@ -547,6 +608,10 @@ button.level-pack-button p {
text-align: center;
text-shadow: 0 2px 1px black;
}
/* Allow clicking through the overlay in debug mode */
#player.--debug .overlay-message {
pointer-events: none;
}
#player .overlay-message p {
margin: 0;
}
@@ -687,7 +752,7 @@ dl.score-chart .-sum {
background-size: calc(var(--tile-width) * var(--scale)) calc(var(--tile-height) * var(--scale));
width: calc(4 * var(--tile-width) * var(--scale));
min-height: calc(2 * var(--tile-height) * var(--scale));
height: calc(2 * var(--tile-height) * var(--scale));
}
#player .inventory img {
width: calc(var(--tile-width) * var(--scale));
@@ -711,6 +776,15 @@ dl.score-chart .-sum {
background: #0009;
color: white;
}
#player .actions {
grid-area: actions;
display: flex;
gap: 0.5em;
}
#player .actions button {
flex: 1;
white-space: nowrap;
}
#player-music {
grid-area: music;
@@ -734,25 +808,111 @@ dl.score-chart .-sum {
#player .controls {
grid-area: controls;
display: flex;
gap: 0.25em;
justify-content: space-between;
}
.play-controls,
.demo-controls {
#player button {
position: relative;
}
#player button .keyhint {
position: absolute;
left: 0;
right: 0;
top: -2em;
width: 1em;
margin: auto;
color: #404040;
border: 1px solid #202020;
border-radius: 0.25em;
box-shadow: 0 2px 0 #202020;
text-align: center;
text-transform: uppercase;
}
#player button:disabled .keyhint {
display: none;
}
.play-controls {
display: flex;
align-items: center;
gap: 0.25em;
}
.play-controls {
align-self: start;
}
.demo-controls {
display: none;
flex: 1;
justify-content: flex-end;
}
main.--has-demo .demo-controls {
display: flex;
}
/* Debug stuff */
#player.--debug #player-debug {
display: grid;
}
#player-debug {
display: none;
grid:
"time inventory options"
"buttons buttons buttons"
/ 2fr 1fr 2fr
;
gap: 0.25em;
padding: 0.5em;
background-image: repeating-linear-gradient(135deg,
hsl(0, 0%, 4%) 0em,
hsl(0, 0%, 4%) 1em,
hsl(15, 25%, 8%) 1em,
hsl(15, 25%, 8%) 2em);
border: 0.25em solid hsla(30, 100%, 50%, 0.5);
box-shadow: inset 0 0 0.25em 0.25em #0004;
}
#player-debug > .-time {
grid-area: time;
}
#player-debug > .-time table.-time-controls {
}
#player-debug > .-time table.-time-controls button {
width: 100%;
text-align: inherit;
}
#player-debug > .-time table.-time-controls td:nth-child(1) {
/* go backwards button */
text-align: left;
}
#player-debug > .-time table.-time-controls td:nth-child(2) {
/* value */
width: 6em;
text-align: right;
}
#player-debug > .-time table.-time-controls td:nth-child(3) {
/* label */
}
#player-debug > .-time table.-time-controls td:nth-child(4) {
/* go forward button */
text-align: right;
}
#player-debug > .-options {
grid-area: options;
}
#player-debug > .-inventory {
grid-area: inventory;
display: grid;
margin: auto;
grid: auto-fill var(--tile-height) / repeat(4, var(--tile-width));
gap: 2px;
}
#player-debug > .-inventory > button {
padding: 0;
}
#player-debug > .-inventory > button > img {
display: block;
}
#player-debug > .-inventory > button.-wide {
grid-column: span 4;
padding: 0.25em;
}
#player-debug > .-buttons {
grid-area: buttons;
display: flex;
gap: 0.25em;
}
.input {
display: grid;
display: none;
@@ -790,13 +950,21 @@ main.--has-demo .demo-controls {
/* The play area isn't necessarily the biggest thing any more, and it's ugly when stretched */
align-items: center;
}
#player .controls {
flex-direction: column;
}
#player > .-main-area {
/* Rearrange the grid to be vertical */
grid:
"level level"
"chips inventory"
"time inventory"
"bonus inventory"
"level level"
"chips inventory" calc((var(--tile-height) * var(--scale) * 2 - 1em) / 3)
"time inventory" calc((var(--tile-height) * var(--scale) * 2 - 1em) / 3)
"bonus inventory" calc((var(--tile-height) * var(--scale) * 2 - 1em) / 3)
"actions actions" min-content
/* FIXME ideally the first column would be 1fr so the hearts/time have space, but that
* allows hints to grow to the entire width of the window, which incredibly sucks. i
* don't know how to get around this except by giving the grid a fixed width, which i
* guess wouldn't be that hard */
/ min-content min-content
;
row-gap: 0.5em;
@@ -815,6 +983,10 @@ main.--has-demo .demo-controls {
z-index: 1;
font-size: calc(var(--tile-height) * var(--scale) / 2.5);
}
#player .keyhint {
/* Hide key hints, they take up surprisingly a lot of space */
display: none;
}
#player-music {
/* Stack the title/artist on the volume, since they don't fit well side by side */
font-size: 0.875em;
@@ -831,32 +1003,48 @@ main.--has-demo .demo-controls {
grid:
"controls controls" min-content
"palette level" 1fr
/ minmax(25%, auto) 1fr
/ min-content 1fr
;
gap: 0.5em;
min-height: 0;
margin: auto;
padding: 1em;
margin: auto 1em;
}
#editor .level {
#editor .editor-canvas {
grid-area: level;
position: relative;
overflow: auto;
/* Padding and background make it easier to tell when we're at the edge of the map */
/* TODO padding should be half a cell, and svg should respect it too */
/* padding: 1em; */
/* Flex layout to center the canvas and overlay together */
display: flex;
/* This isn't supported in Chrome (still!!), so use auto margins on the child instead
align-items: safe center;
justify-content: safe center;
*/
background: #202020;
border: 0.125em solid black;
}
#editor .editor-canvas .-container {
position: relative;
margin: auto;
/* Give the canvas/overlay a bit of a margin; it has to be a border because, due to some quirk
* of overflowing flexboxes I guess, padding and margins won't extend the scroll area on the
* right and bottom */
/* TODO padding should be half a cell? */
border: 2em solid transparent;
}
#editor .editor-canvas canvas {
display: block;
width: calc(var(--viewport-width) * var(--tile-width) * var(--scale));
--viewport-width: 9;
--viewport-height: 9;
}
/* SVG overlays */
#editor svg.level-editor-overlay {
position: absolute;
top: 0;
bottom: 0;
left: 0;
/* FIXME get real size */
width: 1024px;
height: 1024px;
right: 0;
/* allow clicks to go through us! */
pointer-events: none;
@@ -885,10 +1073,64 @@ main.--has-demo .demo-controls {
}
#editor .controls {
/* TODO with the hint area gone i don't think this needs to be a grid? could just flex */
grid-area: controls;
display: grid;
grid:
"tile toolbar layer direction . menu" auto
/ auto auto auto auto 1fr auto
;
align-items: center;
column-gap: 1em;
}
#editor .controls p img {
background: url(icons/tool-bg-unselected.png);
#editor .controls #editor-tile {
grid-area: tile;
}
#editor .controls #editor-tile canvas {
display: block;
}
#editor .controls #editor-toolbar {
grid-area: toolbar;
}
#editor-toolbar .-help {
opacity: 0;
visibility: hidden;
z-index: 1;
position: absolute;
width: max-content;
margin-top: -0.25em;
margin-left: -0.5em;
padding: 0.33em 0.75em;
pointer-events: none;
border: 1px solid black;
white-space: pre-wrap;
line-height: 1.5;
text-transform: none;
text-align: left;
background: hsl(225, 10%, 20%);
box-shadow: 0 1px 2px 1px #0004;
transition-property: margin-top, opacity, visibility;
transition-timing-function: ease-out;
transition-duration: 0.125s, 0.125s, 0s;
transition-delay: 0s, 0s, 0.125s;
}
#editor-toolbar .-help h3 {
font-size: 1.25em;
margin-bottom: 0.25rem;
border-bottom: 1px solid currentColor;
}
#editor-toolbar button:hover .-help {
opacity: 1;
z-index: 2; /* show above any that are in mid-fade */
visibility: visible;
margin-top: 0.25em;
transition-delay: 0.5s;
transition-timing-function: ease-in;
}
#editor .controls .-buttons {
grid-area: menu;
}
.icon-button-set {
display: flex;
@@ -903,6 +1145,7 @@ main.--has-demo .demo-controls {
background: url(icons/tool-bg-unselected.png) no-repeat;
border: none;
border-radius: 0;
box-shadow: none;
}
.icon-button-set button.-selected {
background-image: url(icons/tool-bg-selected.png);
@@ -910,9 +1153,10 @@ main.--has-demo .demo-controls {
.icon-button-set button img {
display: block;
}
#editor .palette {
isolation: isolate;
grid-area: palette;
min-width: 20vw;
padding-right: 0.25em; /* in case of scrollbar */
overflow-y: auto;
}
@@ -927,12 +1171,172 @@ main.--has-demo .demo-controls {
}
#editor .palette section {
display: grid;
grid: auto-flow 32px / repeat(auto-fit, 32px);
gap: 3px;
margin: 0.33em 4px; /* matches gap */
grid: auto-flow 32px / repeat(8, 32px);
gap: 4px;
}
.palette-entry {
margin: 0.25em;
}
.palette-entry:hover {
box-shadow: 0 0 0 1px black, 0 0 0 3px hsl(225, 100%, 75%);
}
.palette-entry.--selected {
box-shadow: 0 0 0 1px black, 0 0 0 2px white;
z-index: 1;
box-shadow: 0 0 0 1px black, 0 0 0 3px white;
}
.editor-level-browser {
display: grid;
grid: auto-flow auto / repeat(auto-fill, minmax(13em, 1fr)); /* 12em preview wdith + padding */
gap: 0.5em;
width: 70vw;
/* seems to go into the parent's right padding fsr, i guess because the scrollbar is there */
margin-right: 1em;
list-style: none;
}
.editor-level-browser li {
display: grid;
grid:
"preview preview"
"number title"
/ min-content 1fr
;
gap: 0.25em;
padding: 0.5em;
}
.editor-level-browser li:hover {
background: hsl(225, 60%, 85%);
}
.editor-level-browser li > .-preview {
grid-area: preview;
display: flex;
align-items: center;
justify-content: center;
width: 12em;
height: 12em;
margin: auto;
}
.editor-level-browser li > .-preview:empty::before {
content: '···';
display: block;
font-size: 5em;
color: #c0c0c0;
}
.editor-level-browser li > .-preview canvas {
display: block;
max-width: 100%;
max-height: 100%;
}
.editor-level-browser li > .-number {
grid-area: number;
font-size: 2em;
}
.editor-level-browser li > .-title {
grid-area: title;
align-self: center;
}
/* Mini editors for specific tiles with complex properties */
/* FIXME should this stuff be on an overlay container class? */
form.editor-popup-tile-editor {
position: relative;
padding: 0.5em;
color: black;
background: white;
border: 1px solid black;
box-shadow: 0 2px 4px #0004;
margin-top: 1em;
--chevron-offset: 0px;
}
form.editor-popup-tile-editor.--above {
margin-top: -1em;
}
form.editor-popup-tile-editor h3 {
border-bottom: 1px dotted #606060;
}
form.editor-popup-tile-editor * + h3 {
margin-top: 0.25em;
}
/* Use ::before for a chevron pointing at the tile in question */
form.editor-popup-tile-editor::before {
content: '';
display: block;
position: absolute;
border: 1em solid transparent;
left: var(--chevron-offset);
margin-left: -1em;
top: -1em;
border-top: none;
border-bottom-color: white;
filter: drop-shadow(0 -1px 0 black);
}
form.editor-popup-tile-editor.--above::before {
top: auto;
bottom: -1em;
border: 1em solid transparent;
border-bottom: none;
border-top-color: white;
filter: drop-shadow(0 1px 0 black);
}
/* Letter floor tiles, which let you pick the character to use; show them as a grid. Note the
* characters are preceded by radio buttons, which we hide for simplicity */
ol.editor-letter-tile-picker {
display: grid;
grid: auto-flow 1.5em / repeat(17, 1.5em);
text-align: center;
font-family: monospace;
}
ol.editor-letter-tile-picker label,
ol.editor-letter-tile-picker .-glyph {
display: block;
height: 100%;
}
ol.editor-letter-tile-picker input[type=radio] {
display: none;
}
ol.editor-letter-tile-picker input[type=radio]:checked + .-glyph {
background: hsl(225, 75%, 90%);
outline: 2px solid hsl(225, 75%, 80%);
}
/* Hint tiles accept prose */
textarea.editor-hint-tile-text {
font-size: 1.5em;
width: 20vw;
height: 20vh;
min-width: 15rem;
min-height: 5rem;
border: none;
font-family: serif;
}
/* Class for a list that uses hidden inputs with svg icons, shared by directional blocks and
* railroad tracks */
.editor-tile-editor-svg-parts input {
display: none;
}
.editor-tile-editor-svg-parts svg {
display: block;
width: 3em;
fill: none;
stroke: #c0c0c0;
stroke-width: 2;
}
.editor-tile-editor-svg-parts input:checked + svg {
stroke: hsl(225, 90%, 50%);
}
/* Directional blocks have arrows */
ol.editor-directional-block-tile-arrows {
display: grid;
grid: auto-flow 3em / repeat(3, 3em);
}
/* Railroad tracks are... complicated */
ul.editor-railroad-tile-tracks {
display: grid;
grid: auto-flow 3em / repeat(3, 3em);
gap: 0.25em;
}
ul.editor-railroad-tile-tracks.--switch input:checked + svg {
stroke: hsl(15, 90%, 50%);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.