225 Commits
Author SHA1 Message Date
Eevee (Evelyn Woods) 86764612d3 [WIP] Switch to a more accurate frame-based model
This seems to match how CC2 actually works, and it fixes the replays for
the CC1 levels BLOCK BUSTER, THE PRISONER, CATACOMBS, and GOLDKEY.

Unfortunately, it also regresses ON THE ROCKS, GRAIL, and ALPHABET SOUP,
and I do not know why.  I'm not even sure why it fixes CATACOMBS and
GOLDKEY.  It makes a mess of force floor handling with mixed results.
It's also some 40% slower, which is not ideal.

I doubt I'll revisit this particular branch, since it's the sort of mass
code rearrangement that breaks everything, but I'm keeping it for future
reference in case I try to revisit this idea.
2020-12-15 21:07:47 -07:00
Eevee (Evelyn Woods) eec7ab2e1a Fix rover behavior at last; lesson 5 now syncs 2020-12-15 17:50:13 -07:00
Eevee (Evelyn Woods) 25b4b32f94 Add teleport overriding and seriously clean up teleport code 2020-12-15 16:44:37 -07:00
Eevee (Evelyn Woods) 7c82a4cdf9 Only do CC2 actions per press, not per held tic 2020-12-15 01:07:02 -07:00
Eevee (Evelyn Woods) 0f02e270f2 Catch level parse errors in the bulk test dialog 2020-12-15 00:55:40 -07:00
Eevee (Evelyn Woods) 07c9a83f75 Fix crash with teeth and Cerise 2020-12-15 00:48:28 -07:00
Eevee (Evelyn Woods) bf81738e19 Partly implement helmet; get rover very close to correct (?) 2020-12-15 00:44:07 -07:00
Eevee (Evelyn Woods) 55abe8b53a Allow the bowling ball to slide normally 2020-12-15 00:26:00 -07:00
Eevee (Evelyn Woods) f858668ca8 Clumsily teach bowling balls to destroy actors 2020-12-15 00:10:23 -07:00
Eevee (Evelyn Woods) 2d7df413ee Allow ghosts to pass through thin walls 2020-12-14 23:59:38 -07:00
Eevee (Evelyn Woods) dae66de160 Thieves will, however, take bribes from anyone 2020-12-14 23:49:47 -07:00
Eevee (Evelyn Woods) 8b60a44b09 Fix loading of the initial entry direction for railroads 2020-12-14 23:39:07 -07:00
Eevee (Evelyn Woods) 2103c649f6 Implement a few quirks of ghost movement 2020-12-14 23:35:11 -07:00
Eevee (Evelyn Woods) 6d4326fe5b Thieves only steal from the player 2020-12-14 23:29:05 -07:00
Eevee (Evelyn Woods) 3e18e38f15 Add rough implementations of dynamite and bowling ball 2020-12-14 23:25:48 -07:00
Eevee (Evelyn Woods) 9ade84c6fe Generally only lose if the real player dies, not a doppelganger 2020-12-14 23:20:46 -07:00
Eevee (Evelyn Woods) bf3c501353 Fix yellow tank behavior to be faux simultaneous 2020-12-14 23:14:31 -07:00
Eevee (Evelyn Woods) a529414e42 Cycle an RFF after a failed override; move slide turnaround to move phase 2020-12-14 22:06:42 -07:00
Eevee (Evelyn Woods) e4ab7dde86 Fix loading custom floors with junk in their modifier's high nybble 2020-12-14 22:06:01 -07:00
Eevee (Evelyn Woods) a865647eae Non-players move instantly while sliding 2020-12-14 17:52:59 -07:00
Eevee (Evelyn Woods) f02fa1a9bb Split up the wiring updates 2020-12-14 17:42:31 -07:00
Eevee (Evelyn Woods) fea93aa9ec Fix wire tool only working in the upper-left quadrant 2020-12-14 17:32:48 -07:00
Eevee (Evelyn Woods) 00ac94ac8c Add a further hack atop the cooldown delay hack to fix adjacent trap release 2020-12-14 17:25:15 -07:00
Eevee (Evelyn Woods) c34aaadf06 Attempt, unsuccessfully, to implement ice block melting 2020-12-14 17:07:35 -07:00
Eevee (Evelyn Woods) efd25294ac Restore the notion of an out-of-turn move
This fixes a lot of replay sync issues with cloners; in CC2, actors
advance only one frame (1/3 tic) at a time, so when a cloned object
happens to get a turn later in the same tic that it was cloned, it only
ends up 1 frame ahead of everything else.  Since actors can only begin
moves on tic-aligned frames, even though it does get where it was going
sooner, it has to wait for a frame before moving, so the advantage
doesn't change anything.

The problem is that LL counts movement in tics, not frames, so that kind
of bonus turn puts the clone an entire tic ahead which can gum things
up.

This is still not perfect, but it's much closer.
2020-12-14 17:05:01 -07:00
Eevee (Evelyn Woods) beb5a5c743 Change the debug mode trigger to a regular click 2020-12-14 17:02:14 -07:00
Eevee (Evelyn Woods) f3f73a5e41 Move input handling into Level and clean it up a ton; add a bulk test gizmo 2020-12-14 17:02:14 -07:00
Eevee (Evelyn Woods) 189ab96e3c C2G strings can be empty 2020-12-13 23:56:13 -07:00
Eevee (Evelyn Woods) 16f11f3a9b Exits block CC1 blocks 2020-12-13 22:22:22 -07:00
Eevee (Evelyn Woods) c8ed4b9fba Fix the spelling of Chuck Sommerville's name lol whoops 2020-12-13 20:43:01 -07:00
Eevee (Evelyn Woods) 2fa231a6cd Oops; include HTML/CSS changes for replays too 2020-12-13 20:39:55 -07:00
Eevee (Evelyn Woods) 1c9dee1213 Add support for recording replays, with a bunch of refactoring along the way 2020-12-13 20:36:12 -07:00
Eevee (Evelyn Woods) 85a81878cc Add a button to download a level from the editor 2020-12-13 16:23:45 -07:00
Eevee (Evelyn Woods) df14b62b94 Auto-grow the map save buffer when necessary; do less slicing 2020-12-13 16:09:04 -07:00
Eevee (Evelyn Woods) 5e6784c235 Fix encoding and decoding of swivels 2020-12-13 15:56:24 -07:00
Eevee (Evelyn Woods) 0f5b8098f6 Split teleporting into its own pass, like Lynx 2020-12-13 02:23:14 -07:00
Eevee (Evelyn Woods) 93954135d2 Set splash delay back to 6; is correct after all I guess 2020-12-13 01:54:52 -07:00
Eevee (Evelyn Woods) 90008c3a89 Make the player push blocks at decision time
It turns out the player explores all their decisions in a very physical
way, which is the real source of block slapping and also means the
player can push blocks before anything else can move, regardless of
actor order.

This fixes at least half a dozen CC1 replays, which is just
mindboggling.
2020-12-13 00:39:36 -07:00
Eevee (Evelyn Woods) bd4c04c1d8 Fix wiring order; relax player/monster collision 2020-12-12 21:00:28 -07:00
Eevee (Evelyn Woods) f95913b6d8 Teeth chase the player's apparent position 2020-12-12 20:24:28 -07:00
Eevee (Evelyn Woods) 3a04b6276c Go back to toggling green objects instantly 2020-12-12 19:22:04 -07:00
Eevee (Evelyn Woods) 4d23acb27e Add on_approach; use it to erase animations and fix popdown floors 2020-12-12 19:00:58 -07:00
Eevee (Evelyn Woods) 077ac65650 Restore turn-based mode 2020-12-12 18:16:52 -07:00
Eevee (Evelyn Woods) 299b1578a7 Mostly revert actor loop reorg
I was right the first time, and I've proven it to myself now.  I
originally made the change because I couldn't see any other way to fix
the ICEBERG replay from Steam CC1, but now, I do!
2020-12-12 17:57:47 -07:00
Eevee (Evelyn Woods) 413fdce590 Rejigger input parsing to be stateless and better match CC2; syncs SCAVENGER HUNT! 2020-12-12 01:08:08 -07:00
Eevee (Evelyn Woods) cfdbe0705a Add even faster playback options; fix some demo decoding bugs 2020-12-12 00:22:51 -07:00
Eevee (Evelyn Woods) 769d424dde Fix rendering breakage caused by a couple more out-of-turn moves 2020-12-11 22:54:01 -07:00
Eevee (Evelyn Woods) 8671bee08b Run wiring thrice per tic; recognize some tiles have odd propagation rules 2020-12-11 22:49:23 -07:00
Eevee (Evelyn Woods) c17169f49d Rearranged debug panel a bit; added progress bar for replay playback 2020-12-11 22:15:39 -07:00
Eevee (Evelyn Woods) fde7d9a11c Remove some old stuff; stub out replay progress bar 2020-12-11 21:40:40 -07:00
Eevee (Evelyn Woods) fec09c03ba Support running the game at ludicrous speed 2020-12-11 21:31:07 -07:00
Eevee (Evelyn Woods) 0f0c7437a6 Allow rewinding a replay without desyncing it 2020-12-11 21:14:19 -07:00
Eevee (Evelyn Woods) 410af788fc Fix teleporters, and more generally out-of-turn movement 2020-12-11 20:58:50 -07:00
Eevee (Evelyn Woods) a96c089d7f Move the debug panel to a sidebar 2020-12-11 19:43:22 -07:00
Eevee (Evelyn Woods) b8dd0ae41e Add a delay before restarting a failed level by tapping the viewport 2020-12-11 14:39:14 -07:00
Eevee (Evelyn Woods) 75e981335f Restore the input viewer 2020-12-11 14:25:02 -07:00
Eevee (Evelyn Woods) 6c2afd7e32 Fix flicker when pushing a block across multiple cells 2020-12-11 13:59:23 -07:00
Eevee (Evelyn Woods) 2f130861d6 Remove animation_{progress,speed} and fix interpolation 2020-12-11 13:56:41 -07:00
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
Eevee (Evelyn Woods) 8c2f71294f Fix crash caused by last fix dammit (fixes #9) 2020-10-22 14:44:05 -06:00
Timothy Stiles fedbd200fc Merge remote-tracking branch 'upstream/master' 2020-10-22 18:04:43 +11:00
Eevee (Evelyn Woods) d03d61516f Fix NaNs sneaking into the save file 2020-10-21 23:34:59 -06:00
Eevee (Evelyn Woods) 09eb03dad6 Iterate actors in reverse order to match Lynx/CC2 (fixes #7) 2020-10-21 21:24:59 -06:00
Eevee (Evelyn Woods) 8073604271 Add... error handling... sort of... for levels at least 2020-10-21 21:07:50 -06:00
Eevee (Evelyn Woods) edbe32c148 Add support for drag/drop, dir upload, C2G, AND lazy level loading! 2020-10-21 20:47:07 -06: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
Eevee (Evelyn Woods) 36b9f2efd7 Make (most) actors pick up blue keys 2020-10-07 15:28:51 -06:00
Eevee (Evelyn Woods) 197113c842 Add two new tracks from jneen 2020-10-07 15:16:20 -06:00
Eevee (Evelyn Woods) b20e2cac8c Tanks in traps turn around 2020-10-04 09:52:59 -06:00
Eevee (Evelyn Woods) 5c2fc32546 Tanks always decide to move in their given direction, even when blocked 2020-10-04 09:47:03 -06:00
Eevee (Evelyn Woods) 7f8efaa4e0 Fix flicker when moving in sync with a N/W actor at the edge of the viewport 2020-10-04 09:40:52 -06:00
Eevee (Evelyn Woods) 8adb630862 Add partial wiring support 2020-10-01 06:46:07 -06:00
Eevee (Evelyn Woods) 4cd0585d0b Revert the blue wall compat fix and replace them with popwalls instead (fixes #5) 2020-10-01 03:22:49 -06:00
Eevee (Evelyn Woods) 4f5d169d06 Reduce SFX volume in levels with a lot of off-screen button presses 2020-10-01 03:08:54 -06:00
Eevee (Evelyn Woods) 9b873764fb Shim around several compat issues that affect CCLP levels
- CCLP1 #81 requires pushing blocks off of blue walls, which is
  impossible in CC2 but allowed in TW Lynx (unclear if this is a lynx
  behavior or a tw bug)

- CCLP1 #89 has a tank start on a recessed wall and drive off of it,
  expecting the recessed wall to be left alone, but under CC2 rules it
  becomes a wall; such walls are now automatically converted to a new
  tile, the "doubly recessed wall", which restores the expected behavior
  without changing how recessed walls work in general

- CCLP4 #135 expects pressing a blue button to not affect blue tanks
  that are currently in mid-slide

In addition, the behavior of blue buttons now matches the Lynx/Steam
behavior: the press is stored as a flag and queued until the tank is
next able to move.
2020-10-01 03:08:25 -06:00
Eevee (Evelyn Woods) 8326b42bc7 Pad "real time" seconds to two places; allow tied scores without aid to replace scores with aid 2020-09-30 02:37:43 -06:00
Eevee (Evelyn Woods) f99b9826be Detect trap buttons that are held down when the level begins 2020-09-30 02:20:33 -06:00
Eevee (Evelyn Woods) f4363b8fda Rewrite how connections work
- Teleporters now connect on the fly, rather than having fixed
  connections (important because dynamite can destroy teleporters!)

- If custom connections are present, red and brown buttons ONLY use
  those, rather than falling back to CC2 connection rules

- Multiple brown buttons connected to the same trap should now work
  correctly
2020-09-30 02:11:17 -06:00
Eevee (Evelyn Woods) 6df0c96d1b Make the green key and lock more colorblind-friendly 2020-09-29 10:58:37 -06:00
Eevee (Evelyn Woods) db3af69ff7 Make gray buttons undoable 2020-09-28 20:44:46 -06:00
Eevee (Evelyn Woods) dfa0fa4a9e Add gray buttons to the C2M implementation 2020-09-28 20:43:22 -06:00
Eevee (Evelyn Woods) 325a06395d Partly implement gray buttons; expose them and green stuff in the editor 2020-09-28 04:09:24 -06:00
Eevee (Evelyn Woods) 76051870b7 Basically finish the camera region editing tool; add save/load support for it 2020-09-28 04:00:55 -06:00
Eevee (Evelyn Woods) 432bb881e6 Split out mouse operations; add camera regions, our first custom feature 2020-09-28 02:58:11 -06:00
Eevee (Evelyn Woods) 8711d87a36 Split the editor into its own module 2020-09-28 00:58:31 -06:00
Eevee (Evelyn Woods) ade135514b Undo actor animation (fixes #4) 2020-09-28 00:38:24 -06:00
Eevee (Evelyn Woods) 684021f93d Fix typo in drowned player tile 2020-09-27 04:03:15 -06:00
Eevee (Evelyn Woods) 78bb1f2dc1 Populate player 2 state sprites so she can actually be drawn 2020-09-26 20:38:50 -06:00
Eevee (Evelyn Woods) 70d6739465 Ice blocks splash in water 2020-09-26 20:21:00 -06:00
Eevee (Evelyn Woods) 760ca374ce Fall back to webkitAudioContext if necessary, sigh 2020-09-26 19:47:51 -06: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
Eevee (Evelyn Woods) 4a0ba44116 Restore floors under thin walls in CCL maps 2020-09-26 04:49:58 -06:00
Eevee (Evelyn Woods) 967520c1bd Change fake floors to not be blocking; causes bad visual/audio feedback 2020-09-26 03:15:01 -06:00
Eevee (Evelyn Woods) b40805c02e Take a rough swing at phone support 2020-09-26 02:55:39 -06:00
Eevee (Evelyn Woods) a2e1ed4820 Remove unused sound effect 2020-09-26 02:55:23 -06:00
Eevee (Evelyn Woods) fb71dd1ff1 Fix conundrum, which I accidentally corrupted a bit 2020-09-26 01:44:06 -06:00
Eevee (Evelyn Woods) 12066072ec Disable the big ol cheat whoops 2020-09-26 00:17:15 -06:00
47 changed files with 11287 additions and 3325 deletions

No files matched your search

+4 -2
View File
@@ -38,9 +38,11 @@ Give it a try, I guess! [https://c.eev.ee/lexys-labyrinth/](https://c.eev.ee/le
## Special thanks ## 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 - 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 - [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! - Everyone who contributed music — see [`js/soundtrack.js`](js/soundtrack.js) for a list!
Not associated with or blessed by Chuck Somerville, Niffler, or AOP. Not associated with or blessed by Chuck Sommerville, Niffler, or AOP.
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: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

+208 -47
View File
@@ -10,10 +10,40 @@
<meta name="og:image" content="https://c.eev.ee/lexys-labyrinth/og-preview.png"> <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: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="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, user-scalable=no">
</head> </head>
<body data-mode="splash"> <body data-mode="splash">
<svg id="svg-iconsheet">
<defs>
<!-- Actions -->
<g id="svg-icon-up">
<path d="M0,12 l8,-8 l8,8 z"></path>
</g>
<g id="svg-icon-right">
<use href="#svg-icon-up" transform="rotate(90 8 8)"></use>
</g>
<g id="svg-icon-down">
<use href="#svg-icon-up" transform="rotate(180 8 8)"></use>
</g>
<g id="svg-icon-left">
<use href="#svg-icon-up" transform="rotate(270 8 8)"></use>
</g>
<g id="svg-icon-drop">
<path d="M6,0 h4 v9 h3 l-5,5 h7 v2 h-14 v-2 h7 l-5,-5 h3"></path>
</g>
<g id="svg-icon-cycle">
<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>
</g>
<g id="svg-icon-swap">
<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>
</g>
</defs>
</svg>
<header id="header-main"> <header id="header-main">
<img src="icon.png" alt=""> <img id="header-icon" src="icon.png" alt="">
<h1>Lexy's Labyrinth</h1> <h1>Lexy's Labyrinth</h1>
<p>— a game by <a href="https://eev.ee/">eevee</a></p> <p>— a game by <a href="https://eev.ee/">eevee</a></p>
<nav> <nav>
@@ -25,63 +55,97 @@
<header id="header-pack"> <header id="header-pack">
<h2 id="level-pack-name">Chip's Challenge Level Pack 1</h2> <h2 id="level-pack-name">Chip's Challenge Level Pack 1</h2>
<nav> <nav>
<button id="main-test-pack" type="button">Bulk test</button>
<button id="main-change-pack" type="button">Change pack</button> <button id="main-change-pack" type="button">Change pack</button>
<button id="player-edit" type="button">Edit</button> <button id="player-edit" type="button">Edit</button>
<button id="editor-play" type="button">Test</button> <button id="editor-play" type="button">Play</button>
</nav> </nav>
</header> </header>
<header id="header-level"> <header id="header-level">
<h3 id="level-name">Level 1 — Key Pyramid</h3> <h3 id="level-name">Level 1 — Key Pyramid</h3>
<nav> <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-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> </nav>
</header> </header>
<main id="splash"> <main id="splash">
<div class="drag-overlay"></div>
<header> <header>
<h1><img src="og-preview.png" alt="">Lexy's Labyrinth</h1> <h1><img src="og-preview.png" alt="">Lexy's Labyrinth</h1>
<p>an unapproved Chip's Challenge emulator</p>
</header> </header>
<section id="splash-intro"> <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><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>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>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>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>
<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 -->
</section> </section>
<section id="splash-stock-levels"> <section id="splash-stock-levels">
<h2>Just play something</h2> <h2>Play community levels</h2>
<!-- populated by js --> <!-- populated by js -->
</section> </section>
<section id="splash-upload-levels"> <section id="splash-upload-levels">
<h2>Other levels</h2> <h2>Load other levels</h2>
<p>You can play <code>CHIPS.DAT</code> from the original Microsoft version, any custom levels you have lying around, or perhaps ones you found on the <a href="https://sets.bitbusters.club/">Bit Busters Club set list</a>!</p> <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 explain how to find chips.dat or steam folder --> <!-- TODO zip files! -->
<!-- TODO drag and drop? --> <p>You can also drag and drop files or directories into this window.</p>
<input id="splash-upload" type="file" accept=".dat,.ccl,.c2m,.ccs"> <input id="splash-upload-file" type="file" accept=".dat,.ccl,.c2m,.ccs" multiple>
<button type="button" id="splash-upload-button" class="button-big">Open a local level<!-- TODO: <br>(or drag and drop a file into this window) --></button> <input id="splash-upload-dir" type="file" webkitdirectory>
<p>Supports both the old Microsoft <code>CHIPS.DAT</code> format and the Steam <code>C2M</code> format.</p> <button type="button" id="splash-upload-file-button" class="button-big">Load files</button>
<p>Does <em>not</em> yet support the Steam <code>C2G</code> format, so tragically, the original Steam levels can only be played one at a time. This should be fixed soon!</p> <button type="button" id="splash-upload-dir-button" class="button-big">Load directory</button>
<!-- <p>If you still have the original Microsoft "BOWEP" game lying around, you can play the Chip's Challenge 1 levels by loading <code>CHIPS.DAT</code>.</p>
<p>If you own the Steam versions of <a href="https://store.steampowered.com/app/346850/Chips_Challenge_1/">Chip's Challenge 1</a> (<em>free!</em>) or <a href="https://store.steampowered.com/app/348300/Chips_Challenge_2/">Chip's Challenge 2</a> ($5 last I checked), you can play those too, even on Linux or Mac:</p> <p>If you own the Steam versions of <a href="https://store.steampowered.com/app/346850/Chips_Challenge_1/">Chip's Challenge 1</a> (<em>free!</em>) or <a href="https://store.steampowered.com/app/348300/Chips_Challenge_2/">Chip's Challenge 2</a> ($5 last I checked), you can play those too, even on Linux or Mac:</p>
<ol class="normal-list"> <ol class="normal-list">
<li>Right-click the game in Steam and choose <em>Properties</em>. On the <em>Local Files</em> tab, click <em>Browse local files</em>.</li> <li>Right-click the game in Steam and choose <em>Properties</em>. On the <em>Local Files</em> tab, click <em>Browse local files</em>.</li>
<li>Open the <code>data</code> folder, then <code>games</code>.</li> <li>Open the <code>data</code> folder, then <code>games</code>.</li>
<li>You should see either a <code>cc1</code> or <code>cc2</code> folder. Drag it into this window.</li> <li>You should see either a <code>cc1</code> or <code>cc2</code> folder. Drag it into this window, or load it with the button above.</li>
</ol> </ol>
-->
</section> </section>
<section id="splash-your-levels"> <section id="splash-your-levels">
<h2>Make your own (WIP lol)</h2> <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>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-level" class="button-big">Create a level</button></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> </section>
</main> </main>
<main id="player" hidden> <main id="player" hidden>
<section class="-main-area"> <div id="player-main">
<div class="controls">
<div class="play-controls">
<button class="control-pause" type="button" title="pause">
<svg class="svg-icon" viewBox="0 0 16 16"><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" title="restart">
<svg class="svg-icon" viewBox="0 0 16 16"><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" title="undo">
<svg class="svg-icon" viewBox="0 0 16 16"><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" title="rewind">
<svg class="svg-icon" viewBox="0 0 16 16"><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="actions">
<button class="action-drop" type="button">
<svg class="svg-icon" viewBox="0 0 16 16"><use href="#svg-icon-drop"></use></svg>
drop <span class="keyhint">q</span></button>
<button class="action-cycle" type="button">
<svg class="svg-icon" viewBox="0 0 16 16"><use href="#svg-icon-cycle"></use></svg>
cycle <span class="keyhint">e</span></button>
<button class="action-swap" type="button">
<svg class="svg-icon" viewBox="0 0 16 16"><use href="#svg-icon-swap"></use></svg>
switch <span class="keyhint">c</span></button>
</div>
</div>
<section id="player-game-area">
<div class="level"><!-- level canvas and any overlays go here --></div> <div class="level"><!-- level canvas and any overlays go here --></div>
<div class="overlay-message"> <div class="overlay-message">
<h1 class="-top"></h1> <h1 class="-top"></h1>
@@ -114,30 +178,121 @@
</div> </div>
<audio loop preload="auto"> <audio loop preload="auto">
</div> </div>
<div class="controls">
<div class="play-controls">
<button class="control-pause" type="button">Pause (p)</button>
<button class="control-restart" type="button">Restart</button>
<button class="control-undo" type="button">Undo</button>
<button class="control-rewind" type="button">Rewind (z)</button>
</div> </div>
<div class="demo-controls"> <form id="player-debug">
<button class="demo-play" type="button">View replay</button> <h3>Time</h3>
<button class="demo-step-1" type="button">Step 1 tic</button> <table class="-time-controls">
<button class="demo-step-4" type="button">Step 1 move</button> <tr>
<div class="input"></div> <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>
<div class="-buttons" id="player-debug-time-buttons">
<!-- populated in js -->
</div> </div>
<p>Game speed:
<select name="speed">
<option value="100">100× faster</option>
<option value="50">50× faster</option>
<option value="20">20× faster</option>
<option value="10">10× faster</option>
<option value="5">5× faster</option>
<option value="3">3× faster</option>
<option value="2">2× faster</option>
<option value="3/2">1½× faster</option>
<option value="1" selected>Normal speed</option>
<option value="1/2">2× slower</option>
<option value="1/3">3× slower</option>
<option value="1/4">4× slower</option>
</select>
</p>
<h3>Inventory</h3>
<div class="-inventory">
<!-- populated in js -->
</div> </div>
<!-- TODO debug panel?
- current tic <h3>Replay</h3>
- list of actors, or currently pointed-to actor? <!-- TODO...
<div class="debug"> play back replay
<button>« 4 tics</button> record replay, including altering it from here
<button>« 1 tic</button> stop replay, without restarting the level
<button>1 tic »</button> show progress in %, length in tics + time
<button>4 tics »</button> browse replay? jump to any point? label points???
</div> edit manually?
--> -->
<div class="-replay-columns">
<div id="player-debug-input"></div>
<div class="-replay-status">
<!-- This should be a fixed height and is always showing one of the following -->
<div class="-none">No replay in progress</div>
<div class="-playback">
<progress max="0" value="0"></progress>
<output>100%</output>
<span>0 tics (0:00s)</span>
<button disabled>Relinquish control</button>
</div>
<div class="-recording">Recording...</div>
</div>
</div>
<!-- js inserts a bunch of stuff here -->
<h3>Misc</h3>
<p>Viewport size:
<select id="player-debug-viewport">
<option value="default" selected>Standard</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>
<div class="-buttons" id="player-debug-misc-buttons">
<!-- populated in js -->
</div>
<p>Tip: Middle-click to teleport.</p>
<!-- 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>
</main> </main>
<main id="editor" hidden> <main id="editor" hidden>
<header> <header>
@@ -151,9 +306,15 @@
also deal with levels vs level /packs/ somehow, not sure how that'll work (including downloading them, yeargh?) also deal with levels vs level /packs/ somehow, not sure how that'll work (including downloading them, yeargh?)
--> -->
</header> </header>
<div class="level"><!-- level canvas and any overlays go here --></div> <div class="editor-canvas">
<div class="controls"> <div class="-container">
<button id="editor-share-url" type="button">Share?</button> <!-- level canvas and any overlays go here -->
<!-- the container is to allow them to scroll as a single unit -->
</div>
</div>
<nav class="controls">
<div id="editor-tile">
</div>
<!-- <!--
<p style> <p style>
Tip: Right click to color drop.<br> Tip: Right click to color drop.<br>
@@ -178,7 +339,7 @@
map size map size
</pre> </pre>
--> -->
</div> </nav>
<div class="palette"></div> <div class="palette"></div>
<!-- TODO: <!-- TODO:
controls controls
+55
View File
@@ -4,6 +4,7 @@ export const DIRECTIONS = {
north: { north: {
movement: [0, -1], movement: [0, -1],
bit: 0x01, bit: 0x01,
index: 0,
action: 'up', action: 'up',
left: 'west', left: 'west',
right: 'east', right: 'east',
@@ -12,6 +13,7 @@ export const DIRECTIONS = {
south: { south: {
movement: [0, 1], movement: [0, 1],
bit: 0x04, bit: 0x04,
index: 2,
action: 'down', action: 'down',
left: 'east', left: 'east',
right: 'west', right: 'west',
@@ -20,6 +22,7 @@ export const DIRECTIONS = {
west: { west: {
movement: [-1, 0], movement: [-1, 0],
bit: 0x08, bit: 0x08,
index: 3,
action: 'left', action: 'left',
left: 'south', left: 'south',
right: 'north', right: 'north',
@@ -28,9 +31,61 @@ export const DIRECTIONS = {
east: { east: {
movement: [1, 0], movement: [1, 0],
bit: 0x02, bit: 0x02,
index: 1,
action: 'right', action: 'right',
left: 'north', left: 'north',
right: 'south', right: 'south',
opposite: 'west', opposite: 'west',
}, },
}; };
// Should match the bit ordering above, and CC2's order
export const DIRECTION_ORDER = ['north', 'east', 'south', 'west'];
export const INPUT_BITS = {
drop: 0x01,
down: 0x02,
left: 0x04,
right: 0x08,
up: 0x10,
swap: 0x20,
cycle: 0x40,
// Not real input; used to force advancement for turn-based mode
wait: 0x8000,
};
// 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
};
+148
View File
@@ -0,0 +1,148 @@
import * as util from './util.js';
export class StoredCell extends Array {
}
export class Replay {
constructor(initial_force_floor_direction, blob_seed, inputs = null) {
this.initial_force_floor_direction = initial_force_floor_direction;
this.blob_seed = blob_seed;
this.inputs = inputs ?? new Uint8Array;
this.duration = this.inputs.length;
this.cursor = 0;
}
get(t) {
if (this.duration <= 0) {
return 0;
}
else if (t < this.duration) {
return this.inputs[t];
}
else {
// Last input is implicitly repeated indefinitely
return this.inputs[this.duration - 1];
}
}
set(t, input) {
if (t >= this.inputs.length) {
let new_inputs = new Uint8Array(this.inputs.length + 1024);
for (let i = 0; i < this.inputs.length; i++) {
new_inputs[i] = this.inputs[i];
}
this.inputs = new_inputs;
}
this.inputs[t] = input;
if (t >= this.duration) {
this.duration = t + 1;
}
}
clone() {
let new_inputs = new Uint8Array(this.duration);
for (let i = 0; i < this.duration; i++) {
new_inputs[i] = this.inputs[i];
}
return new this.constructor(this.initial_force_floor_direction, this.blob_seed, new_inputs);
}
}
export class StoredLevel {
constructor(number) {
// 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;
this.time_limit = 0;
this.viewport_size = 9;
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;
// Lazy-loading that allows for checking existence (see methods below)
// TODO this needs a better interface, these get accessed too much atm
this._replay = null;
this._replay_data = null;
this._replay_decoder = null;
this.size_x = 0;
this.size_y = 0;
this.linear_cells = [];
// Maps of button positions to trap/cloner positions, as scalar indexes
// in the linear cell list
// TODO merge these imo
this.has_custom_connections = false;
this.custom_trap_wiring = {};
this.custom_cloner_wiring = {};
// New LL feature: custom camera regions, as lists of {x, y, width, height}
this.camera_regions = [];
}
scalar_to_coords(n) {
return [n % this.size_x, Math.floor(n / this.size_x)];
}
coords_to_scalar(x, y) {
return x + y * this.size_x;
}
check() {
}
get has_replay() {
return this._replay || (this._replay_data && this._replay_decoder);
}
get replay() {
if (! this._replay) {
this._replay = this._replay_decoder(this._replay_data);
}
return this._replay;
}
}
export class StoredPack {
constructor(identifier, level_loader) {
this.identifier = identifier;
this.title = "";
this._level_loader = level_loader;
// 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)
// error: any error received while loading the level
// bytes: Uint8Array of the encoded level data
this.level_metadata = [];
}
// TODO this may or may not work sensibly when correctly following a c2g
load_level(index) {
let meta = this.level_metadata[index];
if (! meta)
throw new util.LLError(`No such level number ${index}`);
if (meta.error)
throw meta.error;
if (meta.stored_level) {
// The editor stores inflated levels at times, so respect that
return meta.stored_level;
}
else {
// Otherwise, attempt to load the level
return this._level_loader(meta);
}
}
}
export const StoredGame = StoredPack;
+1939
View File
File diff suppressed because it is too large. Load diff
-1139
View File
File diff suppressed because it is too large. Load diff
+83 -21
View File
@@ -1,5 +1,6 @@
import * as util from './format-util.js'; import * as format_base from './format-base.js';
import TILE_TYPES from './tiletypes.js'; import TILE_TYPES from './tiletypes.js';
import * as util from './util.js';
const TILE_ENCODING = { const TILE_ENCODING = {
0x00: 'floor', 0x00: 'floor',
@@ -119,18 +120,70 @@ const TILE_ENCODING = {
0x6f: ['player', 'east'], 0x6f: ['player', 'east'],
}; };
function parse_level(buf, number) { function decode_password(bytes, start, len) {
let level = new util.StoredLevel(number); let password = [];
for (let i = 0; i < len; i++) {
password.push(bytes[start + i] ^ 0x99);
}
return String.fromCharCode.apply(null, password);
}
export function parse_level_metadata(bytes) {
let meta = {};
let view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
// Level number; rest of level header is unused
meta.number = view.getUint16(0, true);
// Map layout
// Same structure twice, for the two layers
let p = 8;
for (let l = 0; l < 2; l++) {
let layer_length = view.getUint16(p, true);
p += 2 + layer_length;
}
// Optional metadata fields
let meta_length = view.getUint16(p, true);
p += 2;
let end = p + meta_length;
while (p < end) {
// Common header
let field_type = view.getUint8(p, true);
let field_length = view.getUint8(p + 1, true);
p += 2;
if (field_type === 0x03) {
// Title, including trailing NUL
meta.title = util.string_from_buffer_ascii(bytes, p, field_length - 1);
}
else if (field_type === 0x06) {
// Password, with trailing NUL, and XORed with 0x99 (???)
meta.password = decode_password(bytes, p, field_length - 1);
}
else if (field_type === 0x07) {
// Hint, including trailing NUL, of course
meta.hint = util.string_from_buffer_ascii(bytes, p, field_length - 1);
}
p += field_length;
}
return meta;
}
function parse_level(bytes, number) {
let level = new format_base.StoredLevel(number);
level.has_custom_connections = true;
level.use_ccl_compat = true;
// Map size is always fixed as 32x32 in CC1 // Map size is always fixed as 32x32 in CC1
level.size_x = 32; level.size_x = 32;
level.size_y = 32; level.size_y = 32;
for (let i = 0; i < 1024; i++) { for (let i = 0; i < 1024; i++) {
level.linear_cells.push(new util.StoredCell); level.linear_cells.push(new format_base.StoredCell);
} }
level.use_cc1_boots = true; level.use_cc1_boots = true;
let view = new DataView(buf); let view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
let bytes = new Uint8Array(buf);
// Header // Header
let level_number = view.getUint16(0, true); let level_number = view.getUint16(0, true);
@@ -161,7 +214,7 @@ function parse_level(buf, number) {
// TODO could be more forgiving for goofy levels doing goofy things // TODO could be more forgiving for goofy levels doing goofy things
if (! spec) { if (! spec) {
let [x, y] = level.scalar_to_coords(c); let [x, y] = level.scalar_to_coords(c);
throw new Error(`Invalid tile byte 0x${tile_byte.toString(16)} at (${x}, ${y}) in level ${number}`); throw new Error(`Invalid tile byte 0x${tile_byte.toString(16)} at (${x}, ${y})`);
} }
let name, direction; let name, direction;
@@ -225,11 +278,11 @@ function parse_level(buf, number) {
} }
else if (field_type === 0x03) { else if (field_type === 0x03) {
// Title, including trailing NUL // Title, including trailing NUL
level.title = util.string_from_buffer_ascii(buf.slice(p, p + field_length - 1)); level.title = util.string_from_buffer_ascii(bytes, p, field_length - 1);
} }
else if (field_type === 0x04) { else if (field_type === 0x04) {
// Trap linkages (MSCC only, not in Lynx or CC2) // Trap linkages (MSCC only, not in Lynx or CC2)
let field_view = new DataView(buf.slice(p, p + field_length)); let field_view = new DataView(bytes.buffer, bytes.byteOffset + p, field_length);
let q = 0; let q = 0;
while (q < field_length) { while (q < field_length) {
let button_x = field_view.getUint16(q + 0, true); let button_x = field_view.getUint16(q + 0, true);
@@ -243,7 +296,7 @@ function parse_level(buf, number) {
} }
else if (field_type === 0x05) { else if (field_type === 0x05) {
// Cloner linkages (MSCC only, not in Lynx or CC2) // Cloner linkages (MSCC only, not in Lynx or CC2)
let field_view = new DataView(buf.slice(p, p + field_length)); let field_view = new DataView(bytes.buffer, bytes.byteOffset + p, field_length);
let q = 0; let q = 0;
while (q < field_length) { while (q < field_length) {
let button_x = field_view.getUint16(q + 0, true); let button_x = field_view.getUint16(q + 0, true);
@@ -255,16 +308,12 @@ function parse_level(buf, number) {
} }
} }
else if (field_type === 0x06) { else if (field_type === 0x06) {
// Password, with trailing NUL, and otherwise XORed with 0x99 (?!) // Password, with trailing NUL, and otherwise XORed with 0x99 (???)
let password = []; level.password = decode_password(bytes, p, field_length - 1);
for (let i = 0; i < field_length - 1; i++) {
password.push(view.getUint8(p + i, true) ^ 0x99);
}
level.password = String.fromCharCode.apply(null, password);
} }
else if (field_type === 0x07) { else if (field_type === 0x07) {
// Hint, including trailing NUL, of course // Hint, including trailing NUL, of course
level.hint = util.string_from_buffer_ascii(buf.slice(p, p + field_length - 1)); level.hint = util.string_from_buffer_ascii(bytes, p, field_length - 1);
} }
else if (field_type === 0x08) { else if (field_type === 0x08) {
// Password, but not encoded // Password, but not encoded
@@ -280,8 +329,13 @@ function parse_level(buf, number) {
return level; 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) { export function parse_game(buf) {
let game = new util.StoredGame; let game = new format_base.StoredGame(null, _parse_level_from_stored_meta);
let full_view = new DataView(buf); let full_view = new DataView(buf);
let magic = full_view.getUint32(0, true); let magic = full_view.getUint32(0, true);
@@ -303,11 +357,19 @@ export function parse_game(buf) {
let p = 6; let p = 6;
for (let l = 1; l <= level_count; l++) { for (let l = 1; l <= level_count; l++) {
let length = full_view.getUint16(p, true); let length = full_view.getUint16(p, true);
let level_buf = buf.slice(p + 2, p + 2 + length); let bytes = new Uint8Array(buf, p + 2, length);
p += 2 + length; p += 2 + length;
let level = parse_level(level_buf, l); let meta;
game.levels.push(level); try {
meta = parse_level_metadata(bytes);
}
catch (e) {
meta = {error: e};
}
meta.index = l - 1;
meta.bytes = bytes;
game.level_metadata.push(meta);
} }
return game; return game;
-47
View File
@@ -1,47 +0,0 @@
export function string_from_buffer_ascii(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf));
}
export class StoredCell extends Array {
}
export class StoredLevel {
constructor(number) {
this.number = number; // one-based
this.title = '';
this.password = null;
this.hint = '';
this.chips_required = 0;
this.time_limit = 0;
this.viewport_size = 9;
this.extra_chunks = [];
this.use_cc1_boots = false;
this.size_x = 0;
this.size_y = 0;
this.linear_cells = [];
// Maps of button positions to trap/cloner positions, as scalar indexes
// in the linear cell list
this.custom_trap_wiring = {};
this.custom_cloner_wiring = {};
}
scalar_to_coords(n) {
return [n % this.size_x, Math.floor(n / this.size_x)];
}
coords_to_scalar(x, y) {
return x + y * this.size_x;
}
check() {
}
}
export class StoredGame {
constructor(identifier) {
this.identifier = identifier;
this.levels = [];
}
}
+1613 -565
View File
File diff suppressed because it is too large. Load diff
+140
View File
@@ -0,0 +1,140 @@
import { mk, mk_svg, walk_grid } from './util.js';
// Superclass for the main display modes: the player, the editor, and the splash screen
export class PrimaryView {
constructor(conductor, root) {
this.conductor = conductor;
this.root = root;
this.active = false;
this._done_setup = false;
}
setup() {}
activate() {
this.root.removeAttribute('hidden');
this.active = true;
if (! this._done_setup) {
this.setup();
this._done_setup = true;
}
}
deactivate() {
this.root.setAttribute('hidden', '');
this.active = false;
}
}
// Stackable modal overlay of some kind, usually a dialog
export class Overlay {
constructor(conductor, root) {
this.conductor = conductor;
this.root = root;
// Don't propagate clicks on the root element, so they won't trigger a
// parent overlay's automatic dismissal
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() {
// FIXME ah, but keystrokes can still go to the game, including
// spacebar to begin it if it was waiting. how do i completely disable
// an entire chunk of the page?
if (this.conductor.player.state === 'playing') {
this.conductor.player.set_state('paused');
}
let overlay = mk('div.overlay', this.root);
document.body.append(overlay);
// Remove the overlay when clicking outside the element
overlay.addEventListener('click', ev => {
this.close();
});
return overlay;
}
close() {
this.root.closest('.overlay').remove();
}
}
// 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('form.dialog'));
this.root.append(
this.header = mk('header'),
this.main = mk('section'),
this.footer = mk('footer'),
);
}
set_title(title) {
this.header.textContent = '';
this.header.append(mk('h1', {}, title));
}
add_button(label, onclick) {
let button = mk('button', {type: 'button'}, label);
button.addEventListener('click', onclick);
this.footer.append(button);
}
}
// Yes/no popup dialog
export class ConfirmOverlay extends DialogOverlay {
constructor(conductor, message, what) {
super(conductor);
this.set_title("just checking");
this.main.append(mk('p', {}, message));
let yes = mk('button', {type: 'button'}, "yep");
let no = mk('button', {type: 'button'}, "nope");
yes.addEventListener('click', ev => {
this.close();
what();
});
no.addEventListener('click', ev => {
this.close();
});
this.footer.append(yes, no);
}
}
export function flash_button(button) {
button.classList.add('--button-glow-ok');
window.setTimeout(() => {
button.classList.add('--button-glow');
button.classList.remove('--button-glow-ok');
}, 10);
window.setTimeout(() => {
button.classList.remove('--button-glow');
}, 500);
}
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));
}
+2109
View File
File diff suppressed because it is too large. Load diff
+1288 -856
View File
File diff suppressed because it is too large. Load diff
+87 -18
View File
@@ -1,4 +1,4 @@
import { DIRECTIONS } from './defs.js'; import { DIRECTIONS, DRAW_LAYERS } from './defs.js';
import { mk } from './util.js'; import { mk } from './util.js';
import TILE_TYPES from './tiletypes.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 // to do so, but then we wouldn't make a canvas so it couldn't be
// hooked, yadda yadda // hooked, yadda yadda
if (fixed_size) { if (fixed_size) {
this.viewport_is_fixed = true;
this.viewport_size_x = fixed_size; this.viewport_size_x = fixed_size;
this.viewport_size_y = fixed_size; this.viewport_size_y = fixed_size;
} }
@@ -25,7 +24,10 @@ export class CanvasRenderer {
this.ctx = this.canvas.getContext('2d'); this.ctx = this.canvas.getContext('2d');
this.viewport_x = 0; this.viewport_x = 0;
this.viewport_y = 0; this.viewport_y = 0;
this.viewport_dirty = false;
this.show_actor_bboxes = false;
this.use_rewind_effect = false; this.use_rewind_effect = false;
this.perception = 0; // 0 normal, 1 secret eye, 2 editor
} }
set_level(level) { 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 // 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) { cell_coords_from_event(ev) {
let rect = this.canvas.getBoundingClientRect(); let rect = this.canvas.getBoundingClientRect();
let scale_x = rect.width / this.canvas.width; let scale_x = rect.width / this.canvas.width;
@@ -42,6 +51,15 @@ export class CanvasRenderer {
return [x, y]; return [x, y];
} }
real_cell_coords_from_event(ev) {
let rect = this.canvas.getBoundingClientRect();
let scale_x = rect.width / this.canvas.width;
let scale_y = rect.height / this.canvas.height;
let x = (ev.clientX - rect.x) / scale_x / this.tileset.size_x + this.viewport_x;
let y = (ev.clientY - rect.y) / scale_y / this.tileset.size_y + this.viewport_y;
return [x, y];
}
// Draw to a canvas using tile coordinates // Draw to a canvas using tile coordinates
blit(ctx, sx, sy, dx, dy, w = 1, h = w) { blit(ctx, sx, sy, dx, dy, w = 1, h = w) {
let tw = this.tileset.size_x; let tw = this.tileset.size_x;
@@ -58,6 +76,14 @@ export class CanvasRenderer {
return; 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 tic = (this.level.tic_counter ?? 0) + tic_offset;
let tw = this.tileset.size_x; let tw = this.tileset.size_x;
let th = this.tileset.size_y; let th = this.tileset.size_y;
@@ -76,8 +102,24 @@ export class CanvasRenderer {
[px, py] = [0, 0]; [px, py] = [0, 0];
} }
// Figure out where to start drawing // Figure out where to start drawing
let x0 = Math.max(0, Math.min(this.level.size_x - this.viewport_size_x, px - xmargin)); // TODO support overlapping regions better
let y0 = Math.max(0, Math.min(this.level.size_y - this.viewport_size_y, py - ymargin)); let x0 = px - xmargin;
let y0 = py - ymargin;
// FIXME editor vs player again ugh, which is goofy since none of this is even relevant;
// maybe need to have a separate positioning method
if (this.level.stored_level) {
for (let region of this.level.stored_level.camera_regions) {
if (px >= region.left && px < region.right &&
py >= region.top && py < region.bottom)
{
x0 = Math.max(region.left, Math.min(region.right - this.viewport_size_x, x0));
y0 = Math.max(region.top, Math.min(region.bottom - this.viewport_size_y, y0));
}
}
}
// Always keep us within the map bounds
x0 = Math.max(0, Math.min(this.level.size_x - this.viewport_size_x, x0));
y0 = Math.max(0, Math.min(this.level.size_y - this.viewport_size_y, y0));
// Round to the pixel grid // Round to the pixel grid
x0 = Math.floor(x0 * tw + 0.5) / tw; x0 = Math.floor(x0 * tw + 0.5) / tw;
y0 = Math.floor(y0 * th + 0.5) / th; y0 = Math.floor(y0 * th + 0.5) / th;
@@ -86,12 +128,12 @@ export class CanvasRenderer {
// The viewport might not be aligned to the grid, so split off any fractional part. // The viewport might not be aligned to the grid, so split off any fractional part.
let xf0 = Math.floor(x0); let xf0 = Math.floor(x0);
let yf0 = Math.floor(y0); let yf0 = Math.floor(y0);
// Note that when the viewport is exactly aligned to the grid, we need to draw the cells // We need to draw one cell beyond the viewport, or we'll miss objects partway through
// just outside of it, or we'll miss objects partway through crossing the border // crossing the border moving away from us
if (xf0 === x0 && xf0 > 0) { if (xf0 > 0) {
xf0 -= 1; xf0 -= 1;
} }
if (yf0 === y0 && yf0 > 0) { if (yf0 > 0) {
yf0 -= 1; yf0 -= 1;
} }
// Find where to stop drawing. As with above, if we're aligned to the grid, we need to // Find where to stop drawing. As with above, if we're aligned to the grid, we need to
@@ -100,34 +142,61 @@ export class CanvasRenderer {
let y1 = Math.min(this.level.size_y - 1, Math.ceil(y0 + this.viewport_size_y)); 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 // Draw one layer at a time, so animated objects aren't overdrawn by
// neighboring terrain // neighboring terrain
// XXX layer count hardcoded here // FIXME this is a bit inefficient when there are a lot of rarely-used layers; consider
for (let layer = 0; layer < 4; layer++) { // 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 x = xf0; x <= x1; x++) {
for (let y = yf0; y <= y1; y++) { for (let y = yf0; y <= y1; y++) {
for (let tile of this.level.cells[y][x]) { for (let tile of this.level.cells[y][x]) {
if (tile.type.draw_layer !== layer) if (tile.type.draw_layer !== layer)
continue; continue;
let vx, vy;
if (tile.type.is_actor && if (tile.type.is_actor &&
// FIXME kind of a hack for the editor, which uses bare tile objects // FIXME kind of a hack for the editor, which uses bare tile objects
tile.visual_position) tile.visual_position)
{ {
// Handle smooth scrolling // 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! // Round this to the pixel grid too!
vx = Math.floor(vx * tw + 0.5) / tw; vx = Math.floor(vx * tw + 0.5) / tw;
vy = Math.floor(vy * th + 0.5) / th; 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 { else {
// Non-actors can't move // Non-actors can't move
this.tileset.draw(tile, tic, (sx, sy, dx = 0, dy = 0, w = 1, h = w) => vx = x;
this.blit(this.ctx, sx, sy, x - x0 + dx, y - y0 + dy, w, h)); 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) { if (this.use_rewind_effect) {
@@ -149,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 canvas = mk('canvas', {width: this.tileset.size_x, height: this.tileset.size_y});
let ctx = canvas.getContext('2d'); let ctx = canvas.getContext('2d');
this.tileset.draw_type(name, null, 0, (sx, sy, dx = 0, dy = 0, w = 1, h = w) => 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, sx, sy, dx, dy, w, h)); this.blit(ctx, tx + mx, ty + my, mdx, mdy, mw, mh));
return canvas; return canvas;
} }
} }
+26 -1
View File
@@ -43,7 +43,7 @@ export default [{
title: "conundrum", title: "conundrum",
author: "fluffy", author: "fluffy",
url: 'https://beesbuzz.biz/', url: 'https://beesbuzz.biz/',
beepbox: 'https://www.beepbox.co/#8n31sbk0l00e0rt2mm0a7g0rj07i0r1o3210T1v1L4u01q3d7fay3z6C0c1A5F4B0V1Q0248Pac74E0085T1v1L4u01q1d1f4y4z9C0c1A1F0B0V1Q200ePd593E0787T5v1L4u05q3d0f1y4z0C2c0h0H_--D-quSRIAJJST4v1L4u04q1z6666ji8k8k3jSBKSJJAArriiiiii07JCABrzrrrrrrr00YrkqHrsrrrrjr005zrAqzrjzrrqr1jRjrqGGrrzsrsA099ijrABJJJIAzrrtirqrqjqixzsrAjrqjiqaqqysttAJqjikikrizrHtBJJAzArzrIsRCITKSS099ijrAJS____Qg99habbCAYrDzh00bkzgid18Q4zgid0000000018Nwz55p88Nhnh5t000000ichgR5zkicg004hcx4O4jhkohd14z4isp25PFJvy1wqY58llp2GhKFLh_wFLh-czArdWfRuKrdQ_p13c5FKfZ_tRBPjub_aq_TYtjhZgtBdJlldtlldtlleKGGCKGGCK1wqoiGGiOdI3j1RlkOJBtdZllmG1wqoiGGCK1JFxaEm1STTQMBlldtlldtlJKoHrarqGGqWGGqWGGth7ihT4sChON7a4sIpqPhDkCz9FFOG8YwzOF8Wp8WFOHyfAVARQVB4ughVkAtcAtkVl97Op8Wd8Td6nl9EOaqSqcyIz9UzMbaXbAenShhAPvehhrBRApcVmnlsV5vaGysD9CKsyyLbH8OpOIKGVEGKttV56jdkV55uTnpeldpuptSltdRKpEPpFFOW8YwzOF8Wp8WFOGU0', beepbox: 'https://www.beepbox.co/#8n31sbk0l00e0rt2mm0a7g0rj07i0r1o3210T1v1L4u01q3d7fay3z6C0c1A5F4B0V1Q0248Pac74E0085T1v1L4u01q1d1f4y4z9C0c1A1F0B0V1Q200ePd593E0787T5v1L4u05q3d0f1y4z0C2c0h0H_--D-quSRIAJJST4v1L4u04q1z6666ji8k8k3jSBKSJJAArriiiiii07JCABrzrrrrrrr00YrkqHrsrrrrjr005zrAqzrjzrrqr1jRjrqGGrrzsrsA099ijrABJJJIAzrrtirqrqjqixzsrAjrqjiqaqqysttAJqjikikrizrHtBJJAzArzrIsRCITKSS099ijrAJS____Qg99habbCAYrDzh00b4zgid18Q4zgid0000000018Nwz55p88Nhnh5t000000ichgR5zkicg004hcx4O4jhkohd14z4isp25PFJvy1wqY58llp2GhKFLh_wFLh-czArdWfRuKrdQ_p13c5FKfZ_tRBPjub_aq_TYtjhZgtBdJlldtlldtlleKGGCKGGCK1wqoiGGiOdI3j1RlkOJBtdZllmG1wqoiGGCK1JFxaEm1STTQMBlldtlldtlJKoHrarqGGqWGGqWGGth7ihT4sChON7a4sIpqPhDkCz9FFOG8YwzOF8Wp8WFOHyfAVARQVB4ughVkAtcAtkVl97Op8Wd8Td6nl9EOaqSqcyIz9UzMbaXbAenShhAPvehhrBRApcVmnlsV5vaGysD9CKsyyLbH8OpOIKGVEGKttV56jdkV55uTnpeldpuptSltdRKpEPpFFOW8YwzOF8Wp8WFOGU0',
path: 'music/conundrum.ogg', path: 'music/conundrum.ogg',
}, { }, {
title: "kinda song", title: "kinda song",
@@ -51,4 +51,29 @@ export default [{
twitter: "glitchedpuppet", twitter: "glitchedpuppet",
beepbox: 'https://jummbus.bitbucket.io/#j2N07Unnamedn310s0k0l00e0jt2mm0a7g0jj07i0r1O_U0000o3210T1v0wL0OD0Ou01q1d5f6y0z6C1c0A1F2B5V6Q20a0Pe64bE0171T1v0pL0OD0Ou92q1d4f7y2z1C0c2AbF6B6V9Q0490Pb976E0001T1v0pL0OD0Ou94q1d2f7y2z1C0c2A9F5B5V6Q290dPa883E0011T4v0pL0OD0Ouf0q1z6666ji8k8k3jSBKSJJAArriiiiii07JCABrzrrrrrrr00YrkqHrsrrrrjr005zrAqzrjzrrqr1jRjrqGGrrzsrsA099ijrABJJJIAzrrtirqrqjqixzsrAjrqjiqaqqysttAJqjikikrizrHtBJJAzArzrIsRCITKSS099ijrAJS____Qg99habbCAYrDzh00b4x8Qd000lBu7014x4i4Qd3gQlmoh4ia2cz8OcChA4h4y8w01cPhjl0p27hFCLwATnMkCKChW2ZcngbUxcnikQ7sQO_inQ5WCkXyW9jbZ9vlO_lODhdcDjQNtgJ0Gp7IpAukFczR2FyX2frIzQ4zOGOOfpauP9vainRihQPK4tClqoKMnaAzQQnQQnRknXx7pBnK2OFjSO_oELFAzFIOWa8WF8WpHW3nEdv26LgqZcLQQap7Iu6P9j5R2Q2Q2Q2VeRfbEbWGq2-DinQBZttkHUMRWPn9HFAuwzEe3E8W2ehFyUsNncLQThuCnW2_aGydcngbkO_rZdkPjdcSpvx9jbZ3cRZtcO_lipvFSFkO_lN4YlAjBpdp6hahFyWz5OXbWFbWF8YEmChy3wWiehcK0bWaoEIlChw3JHp5K5E5E5w1sPb5P2f9as0LVds0bkbukQni0JFyQ5c0bokRyXrbxqxr2CyWSOSAzw1qxqxrNrxqxrhtwJgJMJJhvmbibikO8J5JwJEJEJtE', beepbox: 'https://jummbus.bitbucket.io/#j2N07Unnamedn310s0k0l00e0jt2mm0a7g0jj07i0r1O_U0000o3210T1v0wL0OD0Ou01q1d5f6y0z6C1c0A1F2B5V6Q20a0Pe64bE0171T1v0pL0OD0Ou92q1d4f7y2z1C0c2AbF6B6V9Q0490Pb976E0001T1v0pL0OD0Ou94q1d2f7y2z1C0c2A9F5B5V6Q290dPa883E0011T4v0pL0OD0Ouf0q1z6666ji8k8k3jSBKSJJAArriiiiii07JCABrzrrrrrrr00YrkqHrsrrrrjr005zrAqzrjzrrqr1jRjrqGGrrzsrsA099ijrABJJJIAzrrtirqrqjqixzsrAjrqjiqaqqysttAJqjikikrizrHtBJJAzArzrIsRCITKSS099ijrAJS____Qg99habbCAYrDzh00b4x8Qd000lBu7014x4i4Qd3gQlmoh4ia2cz8OcChA4h4y8w01cPhjl0p27hFCLwATnMkCKChW2ZcngbUxcnikQ7sQO_inQ5WCkXyW9jbZ9vlO_lODhdcDjQNtgJ0Gp7IpAukFczR2FyX2frIzQ4zOGOOfpauP9vainRihQPK4tClqoKMnaAzQQnQQnRknXx7pBnK2OFjSO_oELFAzFIOWa8WF8WpHW3nEdv26LgqZcLQQap7Iu6P9j5R2Q2Q2Q2VeRfbEbWGq2-DinQBZttkHUMRWPn9HFAuwzEe3E8W2ehFyUsNncLQThuCnW2_aGydcngbkO_rZdkPjdcSpvx9jbZ3cRZtcO_lipvFSFkO_lN4YlAjBpdp6hahFyWz5OXbWFbWF8YEmChy3wWiehcK0bWaoEIlChw3JHp5K5E5E5w1sPb5P2f9as0LVds0bkbukQni0JFyQ5c0bokRyXrbxqxr2CyWSOSAzw1qxqxrNrxqxrhtwJgJMJJhvmbibikO8J5JwJEJEJtE',
path: 'music/kinda-song.ogg', path: 'music/kinda-song.ogg',
}, {
title: "learning has occurred",
author: "jneen",
twitter: "jneen_",
path: 'music/learning-has-occurred.ogg',
}, {
title: "escape on star road",
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',
}]; }];
+640 -116
View File
File diff suppressed because it is too large. Load diff
+1670 -424
View File
File diff suppressed because it is too large. Load diff
+247 -13
View File
@@ -1,7 +1,13 @@
// Base class for custom errors
export class LLError extends Error {}
// Random choice
export function random_choice(list) { export function random_choice(list) {
return list[Math.floor(Math.random() * list.length)]; return list[Math.floor(Math.random() * list.length)];
} }
// DOM stuff
function _mk(el, children) { function _mk(el, children) {
if (children.length > 0) { if (children.length > 0) {
if (!(children[0] instanceof Node) && children[0] !== undefined && typeof(children[0]) !== "string" && typeof(children[0]) !== "number") { if (!(children[0] instanceof Node) && children[0] !== undefined && typeof(children[0]) !== "string" && typeof(children[0]) !== "number") {
@@ -18,17 +24,92 @@ function _mk(el, children) {
export function mk(tag_selector, ...children) { export function mk(tag_selector, ...children) {
let [tag, ...classes] = tag_selector.split('.'); let [tag, ...classes] = tag_selector.split('.');
let el = document.createElement(tag); let el = document.createElement(tag);
if (classes.length > 0) {
el.classList = classes.join(' '); el.classList = classes.join(' ');
}
return _mk(el, children); return _mk(el, children);
} }
export const SVG_NS = 'http://www.w3.org/2000/svg';
export function mk_svg(tag_selector, ...children) { export function mk_svg(tag_selector, ...children) {
let [tag, ...classes] = tag_selector.split('.'); let [tag, ...classes] = tag_selector.split('.');
let el = document.createElementNS('http://www.w3.org/2000/svg', tag); let el = document.createElementNS(SVG_NS, tag);
if (classes.length > 0) {
el.classList = classes.join(' '); el.classList = classes.join(' ');
}
return _mk(el, children); return _mk(el, children);
} }
export function handle_drop(element, options) {
let dropzone_class = options.dropzone_class ?? null;
let on_drop = options.on_drop;
let require_file = options.require_file ?? false;
let is_valid = ev => {
// TODO this requires files, should make some args for this
if (options.require_file) {
let dt = ev.dataTransfer;
if (! dt || dt.items.length === 0)
return false;
// Only test the first item I guess? If it's a file then they should all be files
if (dt.items[0].kind !== 'file')
return false;
}
return true;
};
let end_drop = () => {
if (dropzone_class !== null) {
element.classList.remove(dropzone_class);
}
};
// TODO should have a filter function for when a drag is valid but i forget which of these
// should have that
element.addEventListener('dragenter', ev => {
if (! is_valid(ev))
return;
ev.stopPropagation();
ev.preventDefault();
if (dropzone_class !== null) {
element.classList.add(dropzone_class);
}
});
element.addEventListener('dragover', ev => {
if (! is_valid(ev))
return;
ev.stopPropagation();
ev.preventDefault();
});
element.addEventListener('dragleave', ev => {
if (ev.relatedTarget && element.contains(ev.relatedTarget))
return;
end_drop();
});
element.addEventListener('drop', ev => {
if (! is_valid(ev))
return;
ev.stopPropagation();
ev.preventDefault();
end_drop();
on_drop(ev);
});
}
export function sleep(t) {
return new Promise(res => {
setTimeout(res, t);
});
}
export function promise_event(element, success_event, failure_event) { export function promise_event(element, success_event, failure_event) {
let resolve, reject; let resolve, reject;
let promise = new Promise((res, rej) => { let promise = new Promise((res, rej) => {
@@ -36,21 +117,21 @@ export function promise_event(element, success_event, failure_event) {
reject = rej; reject = rej;
}); });
let success_handler = e => { let success_handler = ev => {
element.removeEventListener(success_event, success_handler); element.removeEventListener(success_event, success_handler);
if (failure_event) { if (failure_event) {
element.removeEventListener(failure_event, failure_handler); element.removeEventListener(failure_event, failure_handler);
} }
resolve(e); resolve(ev);
}; };
let failure_handler = e => { let failure_handler = ev => {
element.removeEventListener(success_event, success_handler); element.removeEventListener(success_event, success_handler);
if (failure_event) { if (failure_event) {
element.removeEventListener(failure_event, failure_handler); element.removeEventListener(failure_event, failure_handler);
} }
reject(e); reject(ev);
}; };
element.addEventListener(success_event, success_handler); element.addEventListener(success_event, success_handler);
@@ -61,6 +142,7 @@ export function promise_event(element, success_event, failure_event) {
return promise; return promise;
} }
export async function fetch(url) { export async function fetch(url) {
let xhr = new XMLHttpRequest; let xhr = new XMLHttpRequest;
let promise = promise_event(xhr, 'load', 'error'); let promise = promise_event(xhr, 'load', 'error');
@@ -68,11 +150,66 @@ export async function fetch(url) {
xhr.responseType = 'arraybuffer'; xhr.responseType = 'arraybuffer';
xhr.send(); xhr.send();
await promise; await promise;
if (xhr.status !== 200)
throw new Error(`Failed to load ${url} -- ${xhr.status} ${xhr.statusText}`);
return xhr.response; return xhr.response;
} }
export function string_from_buffer_ascii(buf, start = 0, len) {
if (ArrayBuffer.isView(buf)) {
start += buf.byteOffset;
buf = buf.buffer;
}
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;
}
export function b64encode(value) {
if (value instanceof ArrayBuffer || value instanceof Uint8Array) {
value = string_from_buffer_ascii(value);
}
// Make URL-safe and strip trailing padding
return btoa(value).replace(/[+]/g, '-').replace(/[/]/g, '_').replace(/=+$/, '');
}
export function b64decode(data) {
return bytestring_to_buffer(atob(data.replace(/-/g, '+').replace(/_/g, '/')));
}
export function format_duration(seconds, places = 0) {
let mins = Math.floor(seconds / 60);
let secs = seconds % 60;
return `${mins}:${secs < 10 ? '0' : ''}${secs.toFixed(places)}`;
}
export class DelayTimer {
constructor() {
this.active = false;
this._handle = null;
this._bound_alarm = this._alarm.bind(this);
}
set(duration) {
if (this._handle) {
window.clearTimeout(this._handle);
}
this.active = true;
this._handle = window.setTimeout(this._bound_alarm, duration);
}
_alarm() {
this._handle = null;
this.active = false;
}
}
// Cast a line through a grid and yield every cell it touches // 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 // TODO if the ray starts outside the grid (extremely unlikely), we should
// find the point where it ENTERS the grid, otherwise the 'while' // find the point where it ENTERS the grid, otherwise the 'while'
// conditions below will stop immediately // conditions below will stop immediately
@@ -112,9 +249,9 @@ export function* walk_grid(x0, y0, x1, y1) {
step_a = -step_a; step_a = -step_a;
offset_x = 1 - offset_x; offset_x = 1 - offset_x;
} }
// Zero offset means we're on a grid line, so we're actually a full cell else if (offset_x === 0) {
// away from the next grid line // Zero offset means we're on a grid line, so we're a full cell away from the next grid line
if (offset_x === 0) { // (if we're moving forward; if we're moving backward, the next cell really is 0 away)
offset_x = 1; offset_x = 1;
} }
let step_b = 1; let step_b = 1;
@@ -124,15 +261,12 @@ export function* walk_grid(x0, y0, x1, y1) {
step_b = -step_b; step_b = -step_b;
offset_y = 1 - offset_y; offset_y = 1 - offset_y;
} }
if (offset_y === 0) { else if (offset_y === 0) {
offset_y = 1; offset_y = 1;
} }
let err = dy * offset_x - dx * offset_y; 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) { if (dx > dy) {
// Main axis is x/a // Main axis is x/a
while (min_a <= a && a <= max_a && min_b <= b && b <= max_b) { while (min_a <= a && a <= max_a && min_b <= b && b <= max_b) {
@@ -171,3 +305,103 @@ 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
// - a single uploaded zip file
// - a local directory provided via the webkit Entry api
// - HTTP (but only for files we choose ourselves, not arbitrary ones, due to CORS)
// Note that where possible, these classes lowercase all filenames, in keeping with C2G's implicit
// requirement that filenames are case-insensitive :/
class FileSource {
constructor() {}
// Get a file's contents as an ArrayBuffer
async get(path) {}
}
// Files we have had uploaded one at a time (note that each upload becomes its own source)
export class FileFileSource extends FileSource {
constructor(files) {
super();
this.files = {};
for (let file of files) {
this.files[(file.webkitRelativePath ?? file.name).toLowerCase()] = file;
}
}
get(path) {
let file = this.files[path.toLowerCase()];
if (file) {
return file.arrayBuffer();
}
else {
return Promise.reject(new Error(`No such file was provided: ${path}`));
}
}
}
// Regular HTTP fetch
export class HTTPFileSource extends FileSource {
// Should be given a URL object as a root
constructor(root) {
super();
this.root = root;
}
get(path) {
let url = new URL(path, this.root);
return fetch(url);
}
}
// WebKit Entry interface
// XXX this does not appear to work if you drag in a link to a directory but that is probably beyond
// my powers to fix
export class EntryFileSource extends FileSource {
constructor(entries) {
super();
this.files = {};
let file_count = 0;
let read_directory = async (directory_entry, dir_prefix) => {
let reader = directory_entry.createReader();
let all_entries = [];
while (true) {
let entries = await new Promise((res, rej) => reader.readEntries(res, rej));
all_entries.push.apply(all_entries, entries);
if (entries.length === 0)
break;
}
await handle_entries(all_entries, dir_prefix);
};
let handle_entries = (entries, dir_prefix) => {
file_count += entries.length;
if (file_count > 4096)
throw new LLError("Found way too many files; did you drag in the wrong directory?");
let dir_promises = [];
for (let entry of entries) {
if (entry.isDirectory) {
dir_promises.push(read_directory(entry, dir_prefix + entry.name + '/'));
}
else {
this.files[(dir_prefix + entry.name).toLowerCase()] = entry;
}
}
return Promise.all(dir_promises);
};
this._loaded_promise = handle_entries(entries, '');
}
async get(path) {
let entry = this.files[path.toLowerCase()];
if (! entry)
throw new LLError(`No such file in local directory: ${path}`);
let file = await new Promise((res, rej) => entry.file(res, rej));
return await file.arrayBuffer();
}
}
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.
Binary file not shown.
BIN
View File
Binary file not shown.
+777 -76
View File
File diff suppressed because it is too large. Load diff
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.