Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5572b3e692 | ||
|
|
b75253a249 | ||
|
|
831a9392e3 | ||
|
|
81f7e7fd64 | ||
|
|
823fe4de37 | ||
|
|
bd4cc10b16 | ||
|
|
ef78085e40 | ||
|
|
422c702777 | ||
|
|
6063ea9fba | ||
|
|
db2a24319d | ||
|
|
b01d50c7e8 | ||
|
|
b2ff27f241 | ||
|
|
235cc79e8b | ||
|
|
5f804dde14 | ||
|
|
e6d7876679 | ||
|
|
a23afe3d90 | ||
|
|
ed58cbac6b | ||
|
|
829184056e | ||
|
|
9d9c5a3988 | ||
|
|
35bbac9c99 | ||
|
|
a44ed295a2 | ||
|
|
90b583df44 | ||
|
|
f521bd6d2d | ||
|
|
4ee56fad01 | ||
|
|
9735ef93e9 | ||
|
|
6d39fd3831 | ||
|
|
35f040c8d7 | ||
|
|
2849260672 | ||
|
|
f462ae3394 | ||
|
|
c4bb1f3df1 | ||
|
|
e754e483ec | ||
|
|
aeac5c285b | ||
|
|
c1ba299e9f | ||
|
|
90b6498ff9 | ||
|
|
30261a106f | ||
|
|
8428572def | ||
|
|
4838bb189b | ||
|
|
54381370c0 | ||
|
|
d981a0a4be | ||
|
|
3b07c78518 | ||
|
|
076aa9133a | ||
|
|
70df85187f | ||
|
|
f7080a2697 | ||
|
|
cb62786470 | ||
|
|
caf4906176 | ||
|
|
564d247657 | ||
|
|
c4dbdcb650 | ||
|
|
30062485ab | ||
|
|
87ac6f94a3 | ||
|
|
7a710ee5dc | ||
|
|
700d3898ab | ||
|
|
8505b132bb | ||
|
|
dfb274e85d | ||
|
|
b97aaa81a9 | ||
|
|
0cd1ea342d | ||
|
|
3c43b8d7cd | ||
|
|
0885bfc9d2 | ||
|
|
411005eaa6 | ||
|
|
89ae9aa4a3 | ||
|
|
fa18238e60 | ||
|
|
c1452e005f | ||
|
|
e51665b612 | ||
|
|
8d197ce479 | ||
|
|
f0680ce0c4 | ||
|
|
72cba627a8 | ||
|
|
aa41336b90 | ||
|
|
ec5d9f7b12 | ||
|
|
0d376e003e | ||
|
|
560a89cfd3 | ||
|
|
e7c9bbe846 | ||
|
|
8d26de6915 | ||
|
|
14061dec0e | ||
|
|
4218657c28 | ||
|
|
dff3081194 | ||
|
|
349af15e05 | ||
|
|
50c81c5c96 | ||
|
|
3a454d77f5 | ||
|
|
ac6e33bb6c | ||
|
|
6c6ce8f344 | ||
|
|
4b6a8e49ae | ||
|
|
5cb29c8f7d | ||
|
|
fb301b3b3e | ||
|
|
ca4eaa86cb | ||
|
|
39d463932b | ||
|
|
e803af2fd2 | ||
|
|
3514f25f2b | ||
|
|
6804169a8a | ||
|
|
37072fa003 | ||
|
|
c8343f1a23 | ||
|
|
81c7f97d72 | ||
|
|
1b6bd68879 | ||
|
|
8ff0bd803a | ||
|
|
350ac08d4d | ||
|
|
84840d2b02 | ||
|
|
e7e02281a2 | ||
|
|
83a1dd23ff | ||
|
|
bf74530aa2 | ||
|
|
131f06ee84 | ||
|
|
49ff0d9723 | ||
|
|
37b44bcca4 | ||
|
|
060dfdc7d7 | ||
|
|
f670224460 | ||
|
|
32b4399683 | ||
|
|
dedaa45d07 | ||
|
|
368e4676fd | ||
|
|
509b3ca3b7 | ||
|
|
0c9a7e3d07 | ||
|
|
a07e10218e | ||
|
|
f1b040f176 | ||
|
|
603a74a751 | ||
|
|
2820c067c5 | ||
|
|
a0e37422b9 | ||
|
|
8c2f71294f | ||
|
|
fedbd200fc | ||
|
|
d03d61516f | ||
|
|
09eb03dad6 | ||
|
|
8073604271 | ||
|
|
edbe32c148 | ||
|
|
9e53aa75a0 | ||
|
|
4388402850 | ||
|
|
e3de4d59c7 | ||
|
|
f7e83342a0 | ||
|
|
be5cc7f97f | ||
|
|
e9d542f438 | ||
|
|
a7c38ae0af | ||
|
|
bb168d7e1e | ||
|
|
e53f00a432 | ||
|
|
a8ce3bca11 | ||
|
|
2e1a87199a | ||
|
|
36b9f2efd7 | ||
|
|
197113c842 | ||
|
|
b20e2cac8c | ||
|
|
5c2fc32546 | ||
|
|
7f8efaa4e0 | ||
|
|
8adb630862 | ||
|
|
4cd0585d0b | ||
|
|
4f5d169d06 | ||
|
|
9b873764fb | ||
|
|
8326b42bc7 | ||
|
|
f99b9826be | ||
|
|
f4363b8fda | ||
|
|
6df0c96d1b | ||
|
|
db3af69ff7 | ||
|
|
dfa0fa4a9e | ||
|
|
325a06395d | ||
|
|
76051870b7 | ||
|
|
432bb881e6 | ||
|
|
8711d87a36 | ||
|
|
ade135514b | ||
|
|
684021f93d | ||
|
|
78bb1f2dc1 | ||
|
|
70d6739465 | ||
|
|
760ca374ce | ||
|
|
c8d80dfc63 | ||
|
|
ccfd5c30ce | ||
|
|
e908434a20 | ||
|
|
5b7273e9d9 | ||
|
|
e6a4e88935 | ||
|
|
d2e900dc3a | ||
|
|
30a145599c | ||
|
|
5c6cd01b39 | ||
|
|
4a0ba44116 | ||
|
|
967520c1bd | ||
|
|
b40805c02e | ||
|
|
a2e1ed4820 | ||
|
|
fb71dd1ff1 | ||
|
|
12066072ec |
No files matched your search
@@ -38,7 +38,9 @@ Give it a try, I guess! [https://c.eev.ee/lexys-labyrinth/](https://c.eev.ee/le
|
||||
|
||||
## Special thanks
|
||||
|
||||
- The incredible nerds who put together the [Chip Wiki](https://wiki.bitbusters.club/) and also reside on the Bit Busters Discord
|
||||
- The incredible nerds who put together the [Chip Wiki](https://wiki.bitbusters.club/) and also reside on the Bit Busters Discord, including:
|
||||
- ruben for documenting the CC2 PRNG
|
||||
- The Architect for documenting the CC2 C2G parser
|
||||
- Everyone who worked on [Chip's Challenge Level Pack 1](https://wiki.bitbusters.club/Chip%27s_Challenge_Level_Pack_1), the default set of levels
|
||||
- [Tile World](https://wiki.bitbusters.club/Tile_World) for being an incredible reference on Lynx mechanics
|
||||
- Everyone who contributed music — see [`js/soundtrack.js`](js/soundtrack.js) for a list!
|
||||
|
||||
|
After Width: | Height: | Size: 508 B |
|
After Width: | Height: | Size: 396 B |
|
After Width: | Height: | Size: 185 B |
@@ -10,10 +10,11 @@
|
||||
<meta name="og:image" content="https://c.eev.ee/lexys-labyrinth/og-preview.png">
|
||||
<meta name="og:title" content="Lexy's Labyrinth">
|
||||
<meta name="og:description" content="A (work in progress) reimplementation of Chip's Challenge 1 and 2, using entirely free assets.">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
</head>
|
||||
<body data-mode="splash">
|
||||
<header id="header-main">
|
||||
<img src="icon.png" alt="">
|
||||
<img id="header-icon" src="icon.png" alt="">
|
||||
<h1>Lexy's Labyrinth</h1>
|
||||
<p>— a game by <a href="https://eev.ee/">eevee</a></p>
|
||||
<nav>
|
||||
@@ -33,51 +34,55 @@
|
||||
<header id="header-level">
|
||||
<h3 id="level-name">Level 1 — Key Pyramid</h3>
|
||||
<nav>
|
||||
<button id="main-prev-level" type="button">←</button>
|
||||
<button id="main-prev-level" type="button">
|
||||
<svg class="svg-icon" viewBox="0 0 16 16" title="previous"><path d="M14,1 2,8 14,14 z"></svg>
|
||||
</button>
|
||||
<button id="main-choose-level" type="button">Level select</button>
|
||||
<button id="main-next-level" type="button">→</button>
|
||||
<button id="main-next-level" type="button">
|
||||
<svg class="svg-icon" viewBox="0 0 16 16" title="next"><path d="M2,1 14,8 2,14 z"></svg>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
<main id="splash">
|
||||
<div class="drag-overlay"></div>
|
||||
<header>
|
||||
<h1><img src="og-preview.png" alt="">Lexy's Labyrinth</h1>
|
||||
<p>an unapproved Chip's Challenge emulator</p>
|
||||
</header>
|
||||
<section id="splash-intro">
|
||||
<p><strong>Welcome</strong> to Lexy's Labyrinth, an open source puzzle game that is curiously similar to — but legally distinct from — the Atari classic <a href="https://en.wikipedia.org/wiki/Chip%27s_Challenge">Chip's Challenge</a>!</p>
|
||||
<p>(This is a Chip's Challenge <em>emulator</em>, designed to be an accessible way to play community-made levels with free assets. It's 99% compatible with Chip's Challenge 1, and support for Chip's Challenge 2 is underway. But you can safely ignore all that and treat this as its own game.)</p>
|
||||
<p>Please note that <em>levels themselves</em> may contain hints or lore referring to a guy named Chip collecting computer chips, even though you are clearly a fox named Lexy collecting hearts. Weird, right? Sorry for any confusion!</p>
|
||||
<p>Pick a level pack to get started! You can also get more technical details or report bugs on <a href="https://github.com/eevee/lexys-labyrinth">GitHub</a>, find out more about Chip's Challenge via the <a href="https://bitbusters.club/">Bit Busters Club</a> fansite, or support this endeavor (and other things I do) via <a href="https://www.patreon.com/eevee">Patreon</a>!</p>
|
||||
<!-- TODO i want to make clear this is a chip's challenge emulator without bogging people down too much about what that means -->
|
||||
<p>Pick a level pack to get started! You can also load and play any levels you've got lying around, or brave the level editor and make one of your own.</p>
|
||||
<p>If you're not familiar with the game, read up on <a href="https://github.com/eevee/lexys-labyrinth/wiki/How-To-Play">how to play</a>. You can also get more technical details or report bugs on <a href="https://github.com/eevee/lexys-labyrinth">GitHub</a>, find out more about Chip's Challenge via the <a href="https://bitbusters.club/">Bit Busters Club</a> fansite, or support this endeavor (and other things I do) on <a href="https://www.patreon.com/eevee">Patreon</a>.</p>
|
||||
</section>
|
||||
|
||||
<section id="splash-stock-levels">
|
||||
<h2>Just play something</h2>
|
||||
<h2>Play community levels</h2>
|
||||
<!-- populated by js -->
|
||||
</section>
|
||||
|
||||
<section id="splash-upload-levels">
|
||||
<h2>Other levels</h2>
|
||||
<p>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>
|
||||
<!-- TODO explain how to find chips.dat or steam folder -->
|
||||
<!-- TODO drag and drop? -->
|
||||
<input id="splash-upload" type="file" accept=".dat,.ccl,.c2m,.ccs">
|
||||
<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>
|
||||
<p>Supports both the old Microsoft <code>CHIPS.DAT</code> format and the Steam <code>C2M</code> format.</p>
|
||||
<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>
|
||||
<!--
|
||||
<h2>Load other levels</h2>
|
||||
<p>Load and play any levels you have on hand, including the original levels. Supports CCL/DAT, C2G, and individual C2Ms (though scores aren't saved for those). Find more on the <a href="https://sets.bitbusters.club/">Bit Busters Club set list</a>.</p>
|
||||
<!-- TODO zip files! -->
|
||||
<p>You can also drag and drop files or directories into this window.</p>
|
||||
<input id="splash-upload-file" type="file" accept=".dat,.ccl,.c2m,.ccs" multiple>
|
||||
<input id="splash-upload-dir" type="file" webkitdirectory>
|
||||
<button type="button" id="splash-upload-file-button" class="button-big">Load files</button>
|
||||
<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>
|
||||
<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>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>
|
||||
-->
|
||||
</section>
|
||||
|
||||
<section id="splash-your-levels">
|
||||
<h2>Make your own (WIP lol)</h2>
|
||||
<p>Please note that the level editor is <strong>extremely</strong> unfinished, and can't even save yet.</p>
|
||||
<p><button type="button" id="splash-create-level" class="button-big">Create a level</button></p>
|
||||
<p>Please note that the level editor is <strong>very</strong> unfinished, and doesn't even have undo yet. It may eat your work! Beware!</p>
|
||||
<p><button type="button" id="splash-create-pack" class="button-big">New pack</button></p>
|
||||
<p><button type="button" id="splash-create-level" class="button-big">New scratch level<br>(won't be saved!)</button></p>
|
||||
</section>
|
||||
</main>
|
||||
<main id="player" hidden>
|
||||
@@ -104,6 +109,88 @@
|
||||
</div>
|
||||
<div class="inventory"></div>
|
||||
</section>
|
||||
<form id="player-debug">
|
||||
<div class="-time">
|
||||
<table class="-time-controls">
|
||||
<tr>
|
||||
<td><button type="button" class="-time-button" data-dt="-1">← 1 tic</button></td>
|
||||
<td id="player-debug-time-tics">0</td>
|
||||
<td>tics</td>
|
||||
<td><button type="button" class="-time-button" data-dt="1">1 tic →</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><button type="button" class="-time-button" data-dt="-4">← 1 move</button></td>
|
||||
<td id="player-debug-time-moves">0</td>
|
||||
<td>moves</td>
|
||||
<td><button type="button" class="-time-button" data-dt="4">1 move →</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><button type="button" class="-time-button" data-dt="-20">← 1 s</button></td>
|
||||
<td id="player-debug-time-secs">0</td>
|
||||
<td>seconds</td>
|
||||
<td><button type="button" class="-time-button" data-dt="20">1 s →</button></td>
|
||||
</tr>
|
||||
</table>
|
||||
<p>Game speed:
|
||||
<select name="speed">
|
||||
<option value="6">6× — 120 tics per second</option>
|
||||
<option value="3">3× — 60 tics per second</option>
|
||||
<option value="2">2× — 40 tics per second</option>
|
||||
<option value="3/2">1½× — 30 tics per second</option>
|
||||
<option value="1" selected>1× — 20 tics per second</option>
|
||||
<option value="1/2">½× — 10 tics per second</option>
|
||||
<option value="1/3">⅓× — 6⅔ tics per second</option>
|
||||
<option value="1/4">¼× — 5 tics per second</option>
|
||||
</select>
|
||||
</p>
|
||||
</div>
|
||||
<div class="input"></div>
|
||||
<div class="-options">
|
||||
<p> Viewport size:
|
||||
<select id="player-debug-viewport">
|
||||
<option value="default" selected>Level default (9 or 10)</option>
|
||||
<option value="12">12 × 12</option>
|
||||
<option value="16">16 × 16</option>
|
||||
<option value="24">24 × 24</option>
|
||||
<option value="32">32 × 32</option>
|
||||
<option value="max">Entire level</option>
|
||||
</select>
|
||||
</p>
|
||||
<ul>
|
||||
<li><label><input type="checkbox" name="show_actor_bboxes"> Show actor bounding boxes</label></li>
|
||||
<li><label><input type="checkbox" name="disable_interpolation"> Disable interpolation</label></li>
|
||||
<!--
|
||||
<li><label><input type="checkbox" disabled> Show actor info</label></li>
|
||||
<li><label><input type="checkbox" disabled> Freeze time for everything else</label></li>
|
||||
<li><label><input type="checkbox" disabled> Player is immortal</label></li>
|
||||
<li><label><input type="checkbox" disabled> Player ignores collision</label></li>
|
||||
<li><label><input type="checkbox" disabled> Player levitates</label></li>
|
||||
-->
|
||||
</ul>
|
||||
<p>(Middle-click to teleport.)</p>
|
||||
</div>
|
||||
<div class="-inventory">
|
||||
<!-- populated in js -->
|
||||
</div>
|
||||
<div class="-buttons">
|
||||
<!-- populated in js -->
|
||||
</div>
|
||||
|
||||
<!-- TODO?
|
||||
- inspect with mouse
|
||||
- list of actors, or currently pointed-to actor?
|
||||
- activate something manually?
|
||||
- click a button ingame?
|
||||
- pan viewport (like editor)
|
||||
- show connections, directions, other editor features
|
||||
- look under anything
|
||||
|
||||
- other game info?
|
||||
- count tiles?
|
||||
- total hearts?
|
||||
- total bonus flags?
|
||||
-->
|
||||
</form>
|
||||
<div id="player-music">
|
||||
<div id="player-music-left">
|
||||
🎵 <a id="player-music-title">title</a> by <a id="player-music-author">author</a>
|
||||
@@ -116,28 +203,39 @@
|
||||
</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>
|
||||
<button class="control-pause" type="button">
|
||||
<svg class="svg-icon" viewBox="0 0 16 16" title="pause"><path d="M2,1 h4 v14 h-4 z M10,1 h4 v14 h-4 z"></svg>
|
||||
<span class="keyhint">p</span></button>
|
||||
<button class="control-restart" type="button">
|
||||
<svg class="svg-icon" viewBox="0 0 16 16" title="restart"><path d="M13,13 A 7,7 270 1,1 13,3 L15,1 15,7 9,7 11,5 A 4,4 270 1,0 11,11 z"></svg>
|
||||
</button>
|
||||
<button class="control-undo" type="button">
|
||||
<svg class="svg-icon" viewBox="0 0 16 16" title="undo"><path d="M6,5 6,2 1,7 6,12 6,9 A 10,10 60 0,1 15,12 A 10,10 90 0,0 6,5"></svg>
|
||||
</button>
|
||||
<button class="control-rewind" type="button">
|
||||
<svg class="svg-icon" viewBox="0 0 16 16" title="rewind"><path d="M1,8 7,2 7,14 z M9,8 15,2 15,14 z"></svg>
|
||||
<span class="keyhint">z</span></button>
|
||||
<label><input class="control-turn-based" type="checkbox"> Turn-based mode</label>
|
||||
</div>
|
||||
<div class="demo-controls">
|
||||
<button class="demo-play" type="button">View replay</button>
|
||||
<button class="demo-step-1" type="button">Step 1 tic</button>
|
||||
<button class="demo-step-4" type="button">Step 1 move</button>
|
||||
<div class="input"></div>
|
||||
<div class="actions">
|
||||
<button class="action-drop" type="button">
|
||||
<svg class="svg-icon" viewBox="0 0 16 16" title="pause"><path d="M6,0 h4 v9 h3 l-5,5 h7 v2 h-14 v-2 h7 l-5,-5 h3"></svg>
|
||||
drop <span class="keyhint">q</span></button>
|
||||
<button class="action-cycle" type="button">
|
||||
<svg class="svg-icon" viewBox="0 0 16 16" title="pause">
|
||||
<path d="M2,3 H11 V1 l4,4 -4,4 V7 H2 Z"></path>
|
||||
<path d="M14,9 H5 V7 l-4,4 4,4 v-2 h9 z"></path>
|
||||
</svg>
|
||||
cycle <span class="keyhint">e</span></button>
|
||||
<button class="action-swap" type="button">
|
||||
<svg class="svg-icon" viewBox="0 0 16 16" title="pause">
|
||||
<path d="m 7,1 h 2 l 1,1 V 6 L 9,7 v 4 L 8,11.5 7,11 V 7 L 6,6 V 2 Z"></path>
|
||||
<path d="M 8,13 13,11 8,9 3,11 Z m 0,2 7,-3 V 11 L 8,8 1,11 v 1 z"></path>
|
||||
<ellipse cx="5.5" cy="11" rx="0.75" ry="0.5"></ellipse>
|
||||
</svg>
|
||||
switch <span class="keyhint">c</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- TODO debug panel?
|
||||
- current tic
|
||||
- list of actors, or currently pointed-to actor?
|
||||
<div class="debug">
|
||||
<button>« 4 tics</button>
|
||||
<button>« 1 tic</button>
|
||||
<button>1 tic »</button>
|
||||
<button>4 tics »</button>
|
||||
</div>
|
||||
-->
|
||||
</main>
|
||||
<main id="editor" hidden>
|
||||
<header>
|
||||
@@ -151,9 +249,15 @@
|
||||
also deal with levels vs level /packs/ somehow, not sure how that'll work (including downloading them, yeargh?)
|
||||
-->
|
||||
</header>
|
||||
<div class="level"><!-- level canvas and any overlays go here --></div>
|
||||
<div class="controls">
|
||||
<button id="editor-share-url" type="button">Share?</button>
|
||||
<div class="editor-canvas">
|
||||
<div class="-container">
|
||||
<!-- level canvas and any overlays go here -->
|
||||
<!-- the container is to allow them to scroll as a single unit -->
|
||||
</div>
|
||||
</div>
|
||||
<nav class="controls">
|
||||
<div id="editor-tile">
|
||||
</div>
|
||||
<!--
|
||||
<p style>
|
||||
Tip: Right click to color drop.<br>
|
||||
@@ -178,7 +282,7 @@
|
||||
map size
|
||||
</pre>
|
||||
-->
|
||||
</div>
|
||||
</nav>
|
||||
<div class="palette"></div>
|
||||
<!-- TODO:
|
||||
controls
|
||||
|
||||
@@ -4,6 +4,7 @@ export const DIRECTIONS = {
|
||||
north: {
|
||||
movement: [0, -1],
|
||||
bit: 0x01,
|
||||
index: 0,
|
||||
action: 'up',
|
||||
left: 'west',
|
||||
right: 'east',
|
||||
@@ -12,6 +13,7 @@ export const DIRECTIONS = {
|
||||
south: {
|
||||
movement: [0, 1],
|
||||
bit: 0x04,
|
||||
index: 2,
|
||||
action: 'down',
|
||||
left: 'east',
|
||||
right: 'west',
|
||||
@@ -20,6 +22,7 @@ export const DIRECTIONS = {
|
||||
west: {
|
||||
movement: [-1, 0],
|
||||
bit: 0x08,
|
||||
index: 3,
|
||||
action: 'left',
|
||||
left: 'south',
|
||||
right: 'north',
|
||||
@@ -28,9 +31,49 @@ export const DIRECTIONS = {
|
||||
east: {
|
||||
movement: [1, 0],
|
||||
bit: 0x02,
|
||||
index: 1,
|
||||
action: 'right',
|
||||
left: 'north',
|
||||
right: 'south',
|
||||
opposite: 'west',
|
||||
},
|
||||
};
|
||||
// Should match the bit ordering above, and CC2's order
|
||||
export const DIRECTION_ORDER = ['north', 'east', 'south', 'west'];
|
||||
|
||||
// TODO cc2 order is: swivel, thinwalls, canopy (and yes you can have them all in the same tile)
|
||||
export const DRAW_LAYERS = {
|
||||
terrain: 0,
|
||||
item: 1,
|
||||
item_mod: 2,
|
||||
actor: 3,
|
||||
overlay: 4,
|
||||
MAX: 5,
|
||||
};
|
||||
|
||||
export const COLLISION = {
|
||||
player1: 0x0001,
|
||||
player2: 0x0002,
|
||||
player: 0x0003,
|
||||
|
||||
block_cc1: 0x0004,
|
||||
block_cc2: 0x0008, // ice + directional
|
||||
|
||||
// Monsters are a little complicated, because some of them have special rules, e.g. fireballs
|
||||
// aren't blocked by fire.
|
||||
// For a monster's MASK, you should use ONLY ONE of these specific monster bits (if
|
||||
// appropriate), OR the generic bit -- DO NOT combine them!
|
||||
monster_generic: 0x0100,
|
||||
fireball: 0x0200,
|
||||
bug: 0x0400,
|
||||
rover: 0x1000,
|
||||
ghost: 0x8000,
|
||||
// For a tile's COLLISION, use one of these bit combinations
|
||||
monster_solid: 0x7f00, // everything but ghost
|
||||
monster_any: 0xff00, // everything including ghost
|
||||
|
||||
// Combo masks used for matching
|
||||
all_but_ghost: 0xffff & ~0x8000,
|
||||
all_but_player: 0xffff & ~0x0003,
|
||||
all: 0xffff,
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import * as util from './util.js';
|
||||
|
||||
export class StoredCell extends Array {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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() {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -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 * as util from './util.js';
|
||||
|
||||
const TILE_ENCODING = {
|
||||
0x00: 'floor',
|
||||
@@ -119,18 +120,70 @@ const TILE_ENCODING = {
|
||||
0x6f: ['player', 'east'],
|
||||
};
|
||||
|
||||
function parse_level(buf, number) {
|
||||
let level = new util.StoredLevel(number);
|
||||
function decode_password(bytes, start, len) {
|
||||
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
|
||||
level.size_x = 32;
|
||||
level.size_y = 32;
|
||||
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;
|
||||
|
||||
let view = new DataView(buf);
|
||||
let bytes = new Uint8Array(buf);
|
||||
let view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
|
||||
// Header
|
||||
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
|
||||
if (! spec) {
|
||||
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;
|
||||
@@ -225,11 +278,11 @@ function parse_level(buf, number) {
|
||||
}
|
||||
else if (field_type === 0x03) {
|
||||
// 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) {
|
||||
// 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;
|
||||
while (q < field_length) {
|
||||
let button_x = field_view.getUint16(q + 0, true);
|
||||
@@ -243,7 +296,7 @@ function parse_level(buf, number) {
|
||||
}
|
||||
else if (field_type === 0x05) {
|
||||
// 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;
|
||||
while (q < field_length) {
|
||||
let button_x = field_view.getUint16(q + 0, true);
|
||||
@@ -255,16 +308,12 @@ function parse_level(buf, number) {
|
||||
}
|
||||
}
|
||||
else if (field_type === 0x06) {
|
||||
// Password, with trailing NUL, and otherwise XORed with 0x99 (?!)
|
||||
let password = [];
|
||||
for (let i = 0; i < field_length - 1; i++) {
|
||||
password.push(view.getUint8(p + i, true) ^ 0x99);
|
||||
}
|
||||
level.password = String.fromCharCode.apply(null, password);
|
||||
// Password, with trailing NUL, and otherwise XORed with 0x99 (???)
|
||||
level.password = decode_password(bytes, p, field_length - 1);
|
||||
}
|
||||
else if (field_type === 0x07) {
|
||||
// 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) {
|
||||
// Password, but not encoded
|
||||
@@ -280,8 +329,13 @@ function parse_level(buf, number) {
|
||||
return level;
|
||||
}
|
||||
|
||||
// This thin wrapper is passed to StoredGame as the parser function
|
||||
function _parse_level_from_stored_meta(meta) {
|
||||
return parse_level(meta.bytes, meta.number);
|
||||
}
|
||||
|
||||
export function parse_game(buf) {
|
||||
let game = new util.StoredGame;
|
||||
let game = new format_base.StoredGame(null, _parse_level_from_stored_meta);
|
||||
|
||||
let full_view = new DataView(buf);
|
||||
let magic = full_view.getUint32(0, true);
|
||||
@@ -303,11 +357,19 @@ export function parse_game(buf) {
|
||||
let p = 6;
|
||||
for (let l = 1; l <= level_count; l++) {
|
||||
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;
|
||||
|
||||
let level = parse_level(level_buf, l);
|
||||
game.levels.push(level);
|
||||
let meta;
|
||||
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;
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
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 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));
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DIRECTIONS } from './defs.js';
|
||||
import { DIRECTIONS, DRAW_LAYERS } from './defs.js';
|
||||
import { mk } from './util.js';
|
||||
import TILE_TYPES from './tiletypes.js';
|
||||
|
||||
@@ -11,7 +11,6 @@ export class CanvasRenderer {
|
||||
// to do so, but then we wouldn't make a canvas so it couldn't be
|
||||
// hooked, yadda yadda
|
||||
if (fixed_size) {
|
||||
this.viewport_is_fixed = true;
|
||||
this.viewport_size_x = fixed_size;
|
||||
this.viewport_size_y = fixed_size;
|
||||
}
|
||||
@@ -25,7 +24,10 @@ export class CanvasRenderer {
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.viewport_x = 0;
|
||||
this.viewport_y = 0;
|
||||
this.viewport_dirty = false;
|
||||
this.show_actor_bboxes = false;
|
||||
this.use_rewind_effect = false;
|
||||
this.perception = 0; // 0 normal, 1 secret eye, 2 editor
|
||||
}
|
||||
|
||||
set_level(level) {
|
||||
@@ -33,6 +35,13 @@ export class CanvasRenderer {
|
||||
// TODO update viewport size... or maybe Game should do that since you might be cheating
|
||||
}
|
||||
|
||||
// Change the viewport size. DOES NOT take effect until the next redraw!
|
||||
set_viewport_size(x, y) {
|
||||
this.viewport_size_x = x;
|
||||
this.viewport_size_y = y;
|
||||
this.viewport_dirty = true;
|
||||
}
|
||||
|
||||
cell_coords_from_event(ev) {
|
||||
let rect = this.canvas.getBoundingClientRect();
|
||||
let scale_x = rect.width / this.canvas.width;
|
||||
@@ -42,6 +51,15 @@ export class CanvasRenderer {
|
||||
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
|
||||
blit(ctx, sx, sy, dx, dy, w = 1, h = w) {
|
||||
let tw = this.tileset.size_x;
|
||||
@@ -58,6 +76,14 @@ export class CanvasRenderer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.viewport_dirty) {
|
||||
this.viewport_dirty = false;
|
||||
this.canvas.setAttribute('width', this.tileset.size_x * this.viewport_size_x);
|
||||
this.canvas.setAttribute('height', this.tileset.size_y * this.viewport_size_y);
|
||||
this.canvas.style.setProperty('--viewport-width', this.viewport_size_x);
|
||||
this.canvas.style.setProperty('--viewport-height', this.viewport_size_y);
|
||||
}
|
||||
|
||||
let tic = (this.level.tic_counter ?? 0) + tic_offset;
|
||||
let tw = this.tileset.size_x;
|
||||
let th = this.tileset.size_y;
|
||||
@@ -76,8 +102,24 @@ export class CanvasRenderer {
|
||||
[px, py] = [0, 0];
|
||||
}
|
||||
// Figure out where to start drawing
|
||||
let x0 = Math.max(0, Math.min(this.level.size_x - this.viewport_size_x, px - xmargin));
|
||||
let y0 = Math.max(0, Math.min(this.level.size_y - this.viewport_size_y, py - ymargin));
|
||||
// TODO support overlapping regions better
|
||||
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
|
||||
x0 = Math.floor(x0 * tw + 0.5) / tw;
|
||||
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.
|
||||
let xf0 = Math.floor(x0);
|
||||
let yf0 = Math.floor(y0);
|
||||
// Note that when the viewport is exactly aligned to the grid, we need to draw the cells
|
||||
// just outside of it, or we'll miss objects partway through crossing the border
|
||||
if (xf0 === x0 && xf0 > 0) {
|
||||
// We need to draw one cell beyond the viewport, or we'll miss objects partway through
|
||||
// crossing the border moving away from us
|
||||
if (xf0 > 0) {
|
||||
xf0 -= 1;
|
||||
}
|
||||
if (yf0 === y0 && yf0 > 0) {
|
||||
if (yf0 > 0) {
|
||||
yf0 -= 1;
|
||||
}
|
||||
// Find where to stop drawing. As with above, if we're aligned to the grid, we need to
|
||||
@@ -100,36 +142,63 @@ export class CanvasRenderer {
|
||||
let y1 = Math.min(this.level.size_y - 1, Math.ceil(y0 + this.viewport_size_y));
|
||||
// Draw one layer at a time, so animated objects aren't overdrawn by
|
||||
// neighboring terrain
|
||||
// XXX layer count hardcoded here
|
||||
for (let layer = 0; layer < 4; layer++) {
|
||||
// FIXME this is a bit inefficient when there are a lot of rarely-used layers; consider
|
||||
// instead drawing everything under actors, then actors, then everything above actors?
|
||||
for (let layer = 0; layer < DRAW_LAYERS.MAX; layer++) {
|
||||
for (let x = xf0; x <= x1; x++) {
|
||||
for (let y = yf0; y <= y1; y++) {
|
||||
for (let tile of this.level.cells[y][x]) {
|
||||
if (tile.type.draw_layer !== layer)
|
||||
continue;
|
||||
|
||||
let vx, vy;
|
||||
if (tile.type.is_actor &&
|
||||
// FIXME kind of a hack for the editor, which uses bare tile objects
|
||||
tile.visual_position)
|
||||
{
|
||||
// Handle smooth scrolling
|
||||
let [vx, vy] = tile.visual_position(tic_offset);
|
||||
[vx, vy] = tile.visual_position(tic_offset);
|
||||
// Round this to the pixel grid too!
|
||||
vx = Math.floor(vx * tw + 0.5) / tw;
|
||||
vy = Math.floor(vy * th + 0.5) / th;
|
||||
this.tileset.draw(tile, tic, (sx, sy, dx = 0, dy = 0, w = 1, h = w) =>
|
||||
this.blit(this.ctx, sx, sy, vx - x0 + dx, vy - y0 + dy, w, h));
|
||||
}
|
||||
else {
|
||||
// Non-actors can't move
|
||||
this.tileset.draw(tile, tic, (sx, sy, dx = 0, dy = 0, w = 1, h = w) =>
|
||||
this.blit(this.ctx, sx, sy, x - x0 + dx, y - y0 + dy, w, h));
|
||||
vx = x;
|
||||
vy = y;
|
||||
}
|
||||
|
||||
// Note that the blit we pass to the tileset has a different signature:
|
||||
// blit(
|
||||
// source_tile_x, source_tile_y,
|
||||
// mask_x = 0, mask_y = 0, mask_width = 1, mask_height = mask_width,
|
||||
// mask_dx = mask_x, mask_dy = mask_y)
|
||||
// This makes it easier to use in the extremely common case of drawing
|
||||
// part of one tile atop another tile, but still aligned to the grid.
|
||||
this.tileset.draw(tile, tic, (tx, ty, mx = 0, my = 0, mw = 1, mh = mw, mdx = mx, mdy = my) =>
|
||||
this.blit(this.ctx,
|
||||
tx + mx, ty + my,
|
||||
vx - x0 + mdx, vy - y0 + mdy,
|
||||
mw, mh));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.show_actor_bboxes && ! this.level.linear_cells) { // FIXME dumb hack so this doesn't happen in editor
|
||||
this.ctx.fillStyle = '#f004';
|
||||
for (let x = xf0; x <= x1; x++) {
|
||||
for (let y = yf0; y <= y1; y++) {
|
||||
let actor = this.level.cells[y][x].get_actor();
|
||||
if (! actor)
|
||||
continue;
|
||||
let [vx, vy] = actor.visual_position(tic_offset);
|
||||
// Don't round to the pixel grid; we want to know if the bbox is misaligned!
|
||||
this.ctx.fillRect((vx - x0) * tw, (vy - y0) * th, 1 * tw, 1 * th);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.use_rewind_effect) {
|
||||
this.draw_rewind_effect(tic);
|
||||
}
|
||||
@@ -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 ctx = canvas.getContext('2d');
|
||||
this.tileset.draw_type(name, null, 0, (sx, sy, dx = 0, dy = 0, w = 1, h = w) =>
|
||||
this.blit(ctx, sx, sy, dx, dy, w, h));
|
||||
this.tileset.draw_type(name, tile, 0, this.perception, (tx, ty, mx = 0, my = 0, mw = 1, mh = mw, mdx = mx, mdy = my) =>
|
||||
this.blit(ctx, tx + mx, ty + my, mdx, mdy, mw, mh));
|
||||
return canvas;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export default [{
|
||||
title: "conundrum",
|
||||
author: "fluffy",
|
||||
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',
|
||||
}, {
|
||||
title: "kinda song",
|
||||
@@ -51,4 +51,29 @@ export default [{
|
||||
twitter: "glitchedpuppet",
|
||||
beepbox: 'https://jummbus.bitbucket.io/#j2N07Unnamedn310s0k0l00e0jt2mm0a7g0jj07i0r1O_U0000o3210T1v0wL0OD0Ou01q1d5f6y0z6C1c0A1F2B5V6Q20a0Pe64bE0171T1v0pL0OD0Ou92q1d4f7y2z1C0c2AbF6B6V9Q0490Pb976E0001T1v0pL0OD0Ou94q1d2f7y2z1C0c2A9F5B5V6Q290dPa883E0011T4v0pL0OD0Ouf0q1z6666ji8k8k3jSBKSJJAArriiiiii07JCABrzrrrrrrr00YrkqHrsrrrrjr005zrAqzrjzrrqr1jRjrqGGrrzsrsA099ijrABJJJIAzrrtirqrqjqixzsrAjrqjiqaqqysttAJqjikikrizrHtBJJAzArzrIsRCITKSS099ijrAJS____Qg99habbCAYrDzh00b4x8Qd000lBu7014x4i4Qd3gQlmoh4ia2cz8OcChA4h4y8w01cPhjl0p27hFCLwATnMkCKChW2ZcngbUxcnikQ7sQO_inQ5WCkXyW9jbZ9vlO_lODhdcDjQNtgJ0Gp7IpAukFczR2FyX2frIzQ4zOGOOfpauP9vainRihQPK4tClqoKMnaAzQQnQQnRknXx7pBnK2OFjSO_oELFAzFIOWa8WF8WpHW3nEdv26LgqZcLQQap7Iu6P9j5R2Q2Q2Q2VeRfbEbWGq2-DinQBZttkHUMRWPn9HFAuwzEe3E8W2ehFyUsNncLQThuCnW2_aGydcngbkO_rZdkPjdcSpvx9jbZ3cRZtcO_lipvFSFkO_lN4YlAjBpdp6hahFyWz5OXbWFbWF8YEmChy3wWiehcK0bWaoEIlChw3JHp5K5E5E5w1sPb5P2f9as0LVds0bkbukQni0JFyQ5c0bokRyXrbxqxr2CyWSOSAzw1qxqxrNrxqxrhtwJgJMJJhvmbibikO8J5JwJEJEJtE',
|
||||
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',
|
||||
}];
|
||||
@@ -3,14 +3,36 @@ import TILE_TYPES from './tiletypes.js';
|
||||
|
||||
// TODO really need to specify this format more concretely, whoof
|
||||
// XXX special kinds of drawing i know this has for a fact:
|
||||
// - letter tiles draw from a block of half-tiles onto the center of the base
|
||||
// - slime and walkers have double-size tiles when moving
|
||||
// - wired tiles are a whole thing
|
||||
// - letter tiles draw from a block (one of two blocks!) of half-tiles onto the center of the base
|
||||
// - force floors are cropped from a double-size tile
|
||||
// - wired tiles are a whole thing (floor)
|
||||
// - thin walls are packed into just two tiles
|
||||
// - rover has a half-tile overlay for its direction?
|
||||
// - railroad tracks overlay a Lot
|
||||
// - directional blocks have arrows in an awkward layout, not 4x4 grid but actually positioned on the edges
|
||||
// - green and purple toggle walls use an overlay
|
||||
// - turtles use an overlay, seem to pick a tile at random every so often
|
||||
// - animations are common, should maybe have configurable timing??
|
||||
// - custom floors and walls /should/ be consolidated into a single tile probably
|
||||
// - thin walls should probably be consolidated?
|
||||
// - traps have a state
|
||||
|
||||
// special features i currently have
|
||||
// - directions for actors, can be used anywhere
|
||||
// - arrows: for directional blocks
|
||||
// - mask: for thin walls (though the idea is useful in many more places)
|
||||
// - wired: for wired tiles
|
||||
// - overlay: for green/purple walls mostly, also some bogus cc1 tiles
|
||||
|
||||
// things that are currently NOT handled
|
||||
// - bomb is supposed to have a fuse
|
||||
// - critters should only animate when moving
|
||||
// - rover animation depends on behavior, also has a quarter-tile overlay for its direction
|
||||
// - slime and walkers have double-size tiles when moving
|
||||
// - logic gates draw over the stuff underneath them
|
||||
// - railroad tracks overlay a Lot
|
||||
// - canopy, at all
|
||||
// - swivel's floor (eugh)
|
||||
// - xray vision
|
||||
// - editor vision
|
||||
export const CC2_TILESET_LAYOUT = {
|
||||
'#wire-width': 1/16,
|
||||
|
||||
@@ -22,8 +44,13 @@ export const CC2_TILESET_LAYOUT = {
|
||||
key_blue: [5, 1],
|
||||
key_yellow: [6, 1],
|
||||
key_green: [7, 1],
|
||||
dirt_block: [8, 1],
|
||||
// xray
|
||||
// FIXME these shouldn't be drawn with a hole when drawn in isolation, gruh
|
||||
dirt_block: {
|
||||
special: 'perception',
|
||||
threshold: 2,
|
||||
hidden: [8, 1],
|
||||
revealed: [9, 1],
|
||||
},
|
||||
ice: [10, 1],
|
||||
ice_se: [11, 1],
|
||||
ice_sw: [12, 1],
|
||||
@@ -35,23 +62,44 @@ export const CC2_TILESET_LAYOUT = {
|
||||
// Wiring!
|
||||
base: [0, 2],
|
||||
wired: [8, 26],
|
||||
wired_cross: [10, 26],
|
||||
is_wired_optional: true,
|
||||
},
|
||||
wall_invisible: [0, 2],
|
||||
wall_appearing: [0, 2],
|
||||
wall: [1, 2],
|
||||
floor_letter: [2, 2],
|
||||
'floor_letter#ascii': {
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
width: 16,
|
||||
height: 1,
|
||||
// FIXME i think these are visible with the secret eye, but my tileset puts text on them, whoops
|
||||
wall_invisible: {
|
||||
special: 'perception',
|
||||
threshold: 2,
|
||||
hidden: [0, 2],
|
||||
revealed: [9, 31],
|
||||
},
|
||||
'floor_letter#arrows': {
|
||||
north: [14, 31],
|
||||
east: [14.5, 31],
|
||||
south: [15, 31],
|
||||
west: [15.5, 31],
|
||||
// FIXME this shouldn't be visible with seeing eye (or should it not spawn at all?)
|
||||
wall_invisible_revealed: [1, 2],
|
||||
wall_appearing: {
|
||||
special: 'perception',
|
||||
threshold: 2,
|
||||
hidden: [0, 2],
|
||||
revealed: [11, 31],
|
||||
},
|
||||
wall: [1, 2],
|
||||
floor_letter: {
|
||||
special: 'letter',
|
||||
base: [2, 2],
|
||||
letter_glyphs: {
|
||||
// Arrows
|
||||
"⬆": [14, 31],
|
||||
"➡": [14.5, 31],
|
||||
"⬇": [15, 31],
|
||||
"⬅": [15.5, 31],
|
||||
},
|
||||
letter_ranges: [{
|
||||
// ASCII text (only up through uppercase)
|
||||
range: [32, 96],
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
w: 0.5,
|
||||
h: 0.5,
|
||||
columns: 32,
|
||||
}],
|
||||
},
|
||||
thief_tools: [3, 2],
|
||||
socket: [4, 2],
|
||||
@@ -62,8 +110,13 @@ export const CC2_TILESET_LAYOUT = {
|
||||
[8, 2],
|
||||
[9, 2],
|
||||
],
|
||||
ice_block: [10, 2],
|
||||
// TODO ice block xray
|
||||
// FIXME these shouldn't be drawn with a hole when drawn in isolation, gruh
|
||||
ice_block: {
|
||||
special: 'perception',
|
||||
threshold: 2,
|
||||
hidden: [10, 2],
|
||||
revealed: [11, 2],
|
||||
},
|
||||
score_1000: [12, 2],
|
||||
score_100: [13, 2],
|
||||
score_10: [14, 2],
|
||||
@@ -73,9 +126,10 @@ export const CC2_TILESET_LAYOUT = {
|
||||
green_chip: [9, 3],
|
||||
chip_extra: [10, 3],
|
||||
chip: [11, 3],
|
||||
// bribe
|
||||
// mercury boot
|
||||
// canopy, xray
|
||||
bribe: [12, 3],
|
||||
speed_boots: [13, 3],
|
||||
canopy: [14, 3],
|
||||
// canopy xray
|
||||
|
||||
// TODO lit
|
||||
dynamite: [0, 4],
|
||||
@@ -92,24 +146,40 @@ export const CC2_TILESET_LAYOUT = {
|
||||
wall_custom_blue: [15, 4],
|
||||
|
||||
explosion: [[0, 5], [1, 5], [2, 5], [3, 5]],
|
||||
splash_slime: [[0, 5], [1, 5], [2, 5], [3, 5]],
|
||||
splash: [[4, 5], [5, 5], [6, 5], [7, 5]],
|
||||
flame_jet_off: [8, 5],
|
||||
flame_jet_on: [[9, 5], [10, 5], [11, 5]],
|
||||
popdown_wall: [12, 5],
|
||||
popdown_floor: [12, 5],
|
||||
popdown_floor: {
|
||||
special: 'perception',
|
||||
threshold: 2,
|
||||
hidden: [12, 5],
|
||||
revealed: [13, 5],
|
||||
},
|
||||
popdown_floor_visible: [13, 5],
|
||||
forbidden: [14, 5],
|
||||
// TODO arrows overlay at [3, 10]
|
||||
directional_block: [15, 5],
|
||||
no_sign: [14, 5],
|
||||
frame_block: {
|
||||
base: [15, 5],
|
||||
arrows: [3, 10],
|
||||
},
|
||||
|
||||
flippers: [0, 6],
|
||||
fire_boots: [1, 6],
|
||||
cleats: [2, 6],
|
||||
suction_boots: [3, 6],
|
||||
hiking_boots: [4, 6],
|
||||
// speed boots...? not boots though
|
||||
lightning_bolt: [5, 6],
|
||||
// weird translucent spiral
|
||||
// weird translucent red
|
||||
// TODO dopps can push but i don't think they have any other visuals
|
||||
doppelganger1: {
|
||||
base: [7, 6],
|
||||
overlay: 'player',
|
||||
},
|
||||
doppelganger2: {
|
||||
base: [7, 6],
|
||||
overlay: 'player2',
|
||||
},
|
||||
button_blue: [8, 6],
|
||||
button_green: [9, 6],
|
||||
button_red: [10, 6],
|
||||
@@ -168,7 +238,13 @@ export const CC2_TILESET_LAYOUT = {
|
||||
],
|
||||
|
||||
fake_wall: [0, 10],
|
||||
fake_floor: [0, 10],
|
||||
// FIXME i think these are visible with the secret eye, but my tileset puts text on them, whoops
|
||||
fake_floor: {
|
||||
special: 'perception',
|
||||
threshold: 2,
|
||||
hidden: [0, 10],
|
||||
revealed: [10, 31],
|
||||
},
|
||||
// Thin walls are built piecemeal from these two tiles; the first is N/S,
|
||||
// the second is E/W
|
||||
thinwall_n: {
|
||||
@@ -197,25 +273,42 @@ export const CC2_TILESET_LAYOUT = {
|
||||
wired: [[4, 10], [5, 10], [6, 10], [7, 10]],
|
||||
},
|
||||
popwall: [8, 10],
|
||||
popwall2: [8, 10],
|
||||
gravel: [9, 10],
|
||||
ball: [[10, 10], [11, 10], [12, 10], [13, 10], [14, 10]],
|
||||
steel: [15, 10],
|
||||
ball: [
|
||||
// appropriately, this animation ping-pongs
|
||||
[10, 10], [11, 10], [12, 10], [13, 10],
|
||||
[14, 10], [13, 10], [12, 10], [11, 10],
|
||||
// FIXME the ball bounces so it specifically needs to play its animation every move; this
|
||||
// defeats the ½x slowdown. it's dumb and means this anim as written doesn't match cc2
|
||||
[10, 10], [11, 10], [12, 10], [13, 10],
|
||||
[14, 10], [13, 10], [12, 10], [11, 10],
|
||||
],
|
||||
steel: {
|
||||
// Wiring!
|
||||
base: [15, 10],
|
||||
wired: [9, 26],
|
||||
wired_cross: [11, 26],
|
||||
is_wired_optional: true,
|
||||
},
|
||||
|
||||
// TODO only animates while moving
|
||||
// TODO should explicitly set the non-moving tile, so we can have the walk tile start with
|
||||
// immediate movement?
|
||||
// TODO this shouldn't run at half speed, it's already designed to be one step, and when teeth
|
||||
// move at half speed it looks clumsy
|
||||
teeth: {
|
||||
// NOTE: CC2 inexplicably dropped north teeth and just uses the south
|
||||
// sprites instead
|
||||
north: [[0, 11], [1, 11], [2, 11]],
|
||||
east: [[3, 11], [4, 11], [5, 11]],
|
||||
south: [[0, 11], [1, 11], [2, 11]],
|
||||
west: [[6, 11], [7, 11], [8, 11]],
|
||||
// NOTE: CC2 inexplicably dropped north teeth and just uses the south sprites instead
|
||||
north: [[1, 11], [0, 11], [1, 11], [2, 11]],
|
||||
east: [[4, 11], [3, 11], [4, 11], [5, 11]],
|
||||
south: [[1, 11], [0, 11], [1, 11], [2, 11]],
|
||||
west: [[7, 11], [6, 11], [7, 11], [8, 11]],
|
||||
},
|
||||
swivel_sw: [9, 11],
|
||||
swivel_nw: [10, 11],
|
||||
swivel_ne: [11, 11],
|
||||
swivel_se: [12, 11],
|
||||
swivel_floor: [13, 11],
|
||||
// TODO some kinda four-edges thing again
|
||||
'#wire-tunnel': [14, 11],
|
||||
stopwatch_penalty: [15, 11],
|
||||
paramecium: {
|
||||
north: [[0, 12], [1, 12], [2, 12]],
|
||||
@@ -231,18 +324,31 @@ export const CC2_TILESET_LAYOUT = {
|
||||
},
|
||||
|
||||
walker: [0, 13],
|
||||
// TODO walker animations span multiple tiles, rgh
|
||||
// FIXME walker animations span multiple tiles
|
||||
helmet: [0, 14],
|
||||
stopwatch_toggle: [14, 14],
|
||||
stopwatch_bonus: [15, 14],
|
||||
|
||||
blob: [0, 15],
|
||||
// TODO blob animations also span multiple tiles
|
||||
// FIXME blob animations span multiple tiles
|
||||
// TODO [0, 16] some kinda red/blue outline
|
||||
floor_mimic: [0, 2], // TODO [14, 16] with xray
|
||||
floor_mimic: {
|
||||
special: 'perception',
|
||||
threshold: 1,
|
||||
hidden: [0, 2],
|
||||
revealed: [14, 16],
|
||||
},
|
||||
// TODO [15, 16] some kinda yellow/black outline
|
||||
|
||||
// timid teeth
|
||||
teeth_timid: {
|
||||
// NOTE: CC2 inexplicably dropped north teeth and just uses the south sprites instead
|
||||
// NOTE: it also skimped on timid teeth frames
|
||||
north: [[1, 17], [0, 17]],
|
||||
east: [[3, 17], [2, 17]],
|
||||
south: [[1, 17], [0, 17]],
|
||||
west: [[5, 17], [4, 17]],
|
||||
},
|
||||
bowling_ball: [6, 17], // TODO also +18 when rolling
|
||||
tank_yellow: {
|
||||
north: [[8, 17], [9, 17]],
|
||||
@@ -251,8 +357,26 @@ export const CC2_TILESET_LAYOUT = {
|
||||
west: [[14, 17], [15, 17]],
|
||||
},
|
||||
|
||||
// TODO rover has layers and moves and stuff
|
||||
rover: [0, 18],
|
||||
// TODO rover has an overlay showing its direction
|
||||
rover: {
|
||||
inert: [0, 18],
|
||||
teeth: [[0, 18], [8, 18]],
|
||||
glider: [
|
||||
// quite fast
|
||||
[0, 18], [1, 18], [2, 18], [3, 18], [4, 18], [5, 18], [6, 18], [7, 18],
|
||||
[0, 18], [1, 18], [2, 18], [3, 18], [4, 18], [5, 18], [6, 18], [7, 18],
|
||||
],
|
||||
bug: [[0, 18], [1, 18], [2, 18], [3, 18], [4, 18], [5, 18], [6, 18], [7, 18]],
|
||||
ball: [[0, 18], [4, 18]],
|
||||
teeth_timid: [[0, 18], [9, 18]],
|
||||
fireball: [
|
||||
// quite fast
|
||||
[7, 18], [6, 18], [5, 18], [4, 18], [3, 18], [2, 18], [1, 18], [0, 18],
|
||||
[7, 18], [6, 18], [5, 18], [4, 18], [3, 18], [2, 18], [1, 18], [0, 18],
|
||||
],
|
||||
paramecium: [[7, 18], [6, 18], [5, 18], [4, 18], [3, 18], [2, 18], [1, 18], [0, 18]],
|
||||
walker: [[8, 18], [9, 18]],
|
||||
},
|
||||
xray_eye: [11, 18],
|
||||
ghost: {
|
||||
north: [12, 18],
|
||||
@@ -288,7 +412,14 @@ export const CC2_TILESET_LAYOUT = {
|
||||
|
||||
force_floor_all: [[0, 21], [1, 21], [2, 21], [3, 21], [4, 21], [5, 21], [6, 21], [7, 21]],
|
||||
// latches
|
||||
// switch
|
||||
light_switch_off: {
|
||||
base: [14, 21],
|
||||
wired: [12, 21],
|
||||
},
|
||||
light_switch_on: {
|
||||
base: [14, 21],
|
||||
wired: [13, 21],
|
||||
},
|
||||
thief_keys: [15, 21],
|
||||
|
||||
player: {
|
||||
@@ -326,8 +457,10 @@ export const CC2_TILESET_LAYOUT = {
|
||||
south: [2, 23],
|
||||
west: [10, 23],
|
||||
},
|
||||
exited: 'normal',
|
||||
// These are frames from the splash/explosion animations
|
||||
drowned: [5, 5],
|
||||
slimed: [5, 5],
|
||||
burned: [1, 5],
|
||||
exploded: [1, 5],
|
||||
failed: [1, 5],
|
||||
@@ -343,7 +476,7 @@ export const CC2_TILESET_LAYOUT = {
|
||||
west: [[6, 24], [7, 24]],
|
||||
},
|
||||
bogus_player_drowned: {
|
||||
overlay: [3, 3], // splash
|
||||
overlay: [5, 5], // splash
|
||||
base: 'water',
|
||||
},
|
||||
bogus_player_burned_fire: {
|
||||
@@ -361,22 +494,100 @@ export const CC2_TILESET_LAYOUT = {
|
||||
[15, 24],
|
||||
],
|
||||
|
||||
logic_gate: {
|
||||
special: 'logic-gate',
|
||||
logic_gate_tiles: {
|
||||
'latch-ccw': {
|
||||
north: [8, 21],
|
||||
east: [9, 21],
|
||||
south: [10, 21],
|
||||
west: [11, 21],
|
||||
},
|
||||
not: {
|
||||
north: [0, 25],
|
||||
east: [1, 25],
|
||||
south: [2, 25],
|
||||
west: [3, 25],
|
||||
},
|
||||
and: {
|
||||
north: [4, 25],
|
||||
east: [5, 25],
|
||||
south: [6, 25],
|
||||
west: [7, 25],
|
||||
},
|
||||
or: {
|
||||
north: [8, 25],
|
||||
east: [9, 25],
|
||||
south: [10, 25],
|
||||
west: [11, 25],
|
||||
},
|
||||
xor: {
|
||||
north: [12, 25],
|
||||
east: [13, 25],
|
||||
south: [14, 25],
|
||||
west: [15, 25],
|
||||
},
|
||||
'latch-cw': {
|
||||
north: [0, 26],
|
||||
east: [1, 26],
|
||||
south: [2, 26],
|
||||
west: [3, 26],
|
||||
},
|
||||
nand: {
|
||||
north: [4, 26],
|
||||
east: [5, 26],
|
||||
south: [6, 26],
|
||||
west: [7, 26],
|
||||
},
|
||||
counter: [14, 26],
|
||||
},
|
||||
},
|
||||
|
||||
'#unpowered': [13, 26],
|
||||
'#powered': [15, 26],
|
||||
|
||||
player2: {
|
||||
moving: {
|
||||
north: [[0, 27], [1, 27], [2, 27], [3, 27], [4, 27], [5, 27], [6, 27], [7, 27]],
|
||||
south: [[0, 28], [1, 28], [2, 28], [3, 28], [4, 28], [5, 28], [6, 28], [7, 28]],
|
||||
west: [[8, 28], [9, 28], [10, 28], [11, 28], [12, 28], [13, 28], [14, 28], [15, 28]],
|
||||
east: [[8, 27], [9, 27], [10, 27], [11, 27], [12, 27], [13, 27], [14, 27], [15, 27]],
|
||||
},
|
||||
normal: {
|
||||
north: [0, 27],
|
||||
south: [0, 28],
|
||||
west: [8, 28],
|
||||
east: [8, 27],
|
||||
},
|
||||
blocked: 'pushing',
|
||||
moving: {
|
||||
north: [[0, 27], [1, 27], [2, 27], [3, 27], [4, 27], [5, 27], [6, 27], [7, 27]],
|
||||
south: [[0, 28], [1, 28], [2, 28], [3, 28], [4, 28], [5, 28], [6, 28], [7, 28]],
|
||||
west: [[8, 28], [9, 28], [10, 28], [11, 28], [12, 28], [13, 28], [14, 28], [15, 28]],
|
||||
east: [[8, 27], [9, 27], [10, 27], [11, 27], [12, 27], [13, 27], [14, 27], [15, 27]],
|
||||
},
|
||||
pushing: {
|
||||
north: [8, 29],
|
||||
east: [9, 29],
|
||||
south: [10, 29],
|
||||
west: [11, 29],
|
||||
},
|
||||
swimming: {
|
||||
north: [[0, 29], [1, 29]],
|
||||
east: [[2, 29], [3, 29]],
|
||||
south: [[4, 29], [5, 29]],
|
||||
west: [[6, 29], [7, 29]],
|
||||
},
|
||||
// The classic CC2 behavior, spinning on ice
|
||||
skating: [[0, 27], [8, 27], [0, 28], [8, 28]],
|
||||
// TODO i don't know what CC2 does
|
||||
forced: {
|
||||
north: [2, 27],
|
||||
east: [10, 27],
|
||||
south: [2, 28],
|
||||
west: [10, 28],
|
||||
},
|
||||
exited: 'normal',
|
||||
// These are frames from the splash/explosion animations
|
||||
drowned: [5, 5],
|
||||
slimed: [5, 5],
|
||||
burned: [1, 5],
|
||||
exploded: [1, 5],
|
||||
failed: [1, 5],
|
||||
},
|
||||
fire: [
|
||||
[12, 29],
|
||||
@@ -386,11 +597,40 @@ export const CC2_TILESET_LAYOUT = {
|
||||
],
|
||||
|
||||
// TODO handle train tracks! this is gonna be complicated.
|
||||
railroad: [9, 10],
|
||||
railroad_sign: [4, 31],
|
||||
railroad: {
|
||||
special: 'railroad',
|
||||
base: [9, 10],
|
||||
railroad_ties: {
|
||||
ne: [0, 30],
|
||||
se: [1, 30],
|
||||
sw: [2, 30],
|
||||
nw: [3, 30],
|
||||
ew: [4, 30],
|
||||
ns: [5, 30],
|
||||
},
|
||||
railroad_switch: [6, 30],
|
||||
railroad_inactive: {
|
||||
ne: [7, 30],
|
||||
se: [8, 30],
|
||||
sw: [9, 30],
|
||||
nw: [10, 30],
|
||||
ew: [11, 30],
|
||||
ns: [12, 30],
|
||||
},
|
||||
railroad_active: {
|
||||
ne: [13, 30],
|
||||
se: [14, 30],
|
||||
sw: [15, 30],
|
||||
nw: [0, 31],
|
||||
ew: [1, 31],
|
||||
ns: [2, 31],
|
||||
},
|
||||
},
|
||||
railroad_sign: [3, 31],
|
||||
dirt: [4, 31],
|
||||
no_player2_sign: [5, 31],
|
||||
no_player1_sign: [6, 31],
|
||||
hook: [7, 31],
|
||||
// misc other stuff
|
||||
};
|
||||
|
||||
@@ -402,6 +642,7 @@ export const TILE_WORLD_TILESET_LAYOUT = {
|
||||
water: [0, 3],
|
||||
fire: [0, 4],
|
||||
wall_invisible: [0, 5],
|
||||
wall_invisible_revealed: [0, 1],
|
||||
thinwall_n: [0, 6],
|
||||
thinwall_w: [0, 7],
|
||||
thinwall_s: [0, 8],
|
||||
@@ -451,6 +692,7 @@ export const TILE_WORLD_TILESET_LAYOUT = {
|
||||
wall_appearing: [2, 12],
|
||||
gravel: [2, 13],
|
||||
popwall: [2, 14],
|
||||
popwall2: [2, 14],
|
||||
hint: [2, 15],
|
||||
|
||||
thinwall_se: [3, 0],
|
||||
@@ -562,8 +804,14 @@ export const TILE_WORLD_TILESET_LAYOUT = {
|
||||
export const LL_TILESET_LAYOUT = Object.assign({}, CC2_TILESET_LAYOUT, {
|
||||
// Completed teeth sprites
|
||||
teeth: Object.assign({}, CC2_TILESET_LAYOUT.teeth, {
|
||||
north: [[0, 32], [1, 32], [2, 32], [1, 32]],
|
||||
north: [[1, 32], [0, 32], [1, 32], [2, 32]],
|
||||
}),
|
||||
teeth_timid: {
|
||||
north: [[7, 32], [6, 32], [7, 32], [8, 32]],
|
||||
east: [[4, 32], [2, 17], [4, 32], [3, 17]],
|
||||
south: [[3, 32], [0, 17], [3, 32], [1, 17]],
|
||||
west: [[5, 32], [4, 17], [5, 32], [5, 17]],
|
||||
},
|
||||
|
||||
// Extra player sprites
|
||||
player: Object.assign({}, CC2_TILESET_LAYOUT.player, {
|
||||
@@ -574,12 +822,25 @@ export const LL_TILESET_LAYOUT = Object.assign({}, CC2_TILESET_LAYOUT, {
|
||||
west: [3, 33],
|
||||
},
|
||||
forced: 'skating',
|
||||
exited: [14, 32],
|
||||
burned: {
|
||||
north: [4, 33],
|
||||
east: [5, 33],
|
||||
south: [6, 33],
|
||||
west: [7, 33],
|
||||
},
|
||||
slimed: [1, 38],
|
||||
}),
|
||||
player2: Object.assign({}, CC2_TILESET_LAYOUT.player2, {
|
||||
// TODO skating
|
||||
exited: [15, 32],
|
||||
burned: {
|
||||
north: [12, 33],
|
||||
east: [13, 33],
|
||||
south: [14, 33],
|
||||
west: [15, 33],
|
||||
},
|
||||
slimed: [1, 38],
|
||||
}),
|
||||
bogus_player_burned_fire: {
|
||||
overlay: [6, 33],
|
||||
@@ -589,6 +850,33 @@ export const LL_TILESET_LAYOUT = Object.assign({}, CC2_TILESET_LAYOUT, {
|
||||
overlay: [6, 33],
|
||||
base: 'floor',
|
||||
},
|
||||
|
||||
// Custom tiles
|
||||
popwall2: [9, 32],
|
||||
gift_bow: [10, 32],
|
||||
circuit_block: {
|
||||
base: [13, 32],
|
||||
wired: [11, 32],
|
||||
wired_cross: [12, 32],
|
||||
},
|
||||
|
||||
// Blob and walker in all four directions
|
||||
blob: {
|
||||
north: [[0, 35], [1, 35], [2, 35], [3, 35], [4, 35], [5, 35], [6, 35], [7, 35]],
|
||||
east: [[8, 35], [9, 35], [10, 35], [11, 35], [12, 35], [13, 35], [14, 35], [15, 35]],
|
||||
south: [[0, 36], [1, 36], [2, 36], [3, 36], [4, 36], [5, 36], [6, 36], [7, 36]],
|
||||
west: [[8, 36], [9, 36], [10, 36], [11, 36], [12, 36], [13, 36], [14, 36], [15, 36]],
|
||||
},
|
||||
walker: {
|
||||
north: [[0, 37], [1, 37], [2, 37], [3, 37]],
|
||||
east: [[4, 37], [5, 37], [6, 37], [7, 37]],
|
||||
// Same animations but played backwards
|
||||
south: [[2, 37], [1, 37], [0, 37], [3, 37]],
|
||||
west: [[6, 37], [5, 37], [4, 37], [7, 37]],
|
||||
},
|
||||
|
||||
// Custom VFX
|
||||
splash_slime: [[0, 38], [1, 38], [2, 38], [3, 38]],
|
||||
});
|
||||
|
||||
export class Tileset {
|
||||
@@ -601,12 +889,163 @@ export class Tileset {
|
||||
}
|
||||
|
||||
draw(tile, tic, blit) {
|
||||
this.draw_type(tile.type.name, tile, tic, blit);
|
||||
// FIXME perception
|
||||
this.draw_type(tile.type.name, tile, tic, 0, blit);
|
||||
}
|
||||
|
||||
// Draw a "standard" drawspec, which is either:
|
||||
// - a single tile: [x, y]
|
||||
// - an animation: [[x0, y0], [x1, y1], ...]
|
||||
// - a directional tile: { north: T, east: T, ... } where T is either of the above
|
||||
_draw_standard(drawspec, tile, tic, blit, mask = []) {
|
||||
// If we have an object, it must be a table of directions
|
||||
let coords = drawspec;
|
||||
if (!(coords instanceof Array)) {
|
||||
coords = coords[(tile && tile.direction) ?? 'south'];
|
||||
}
|
||||
|
||||
// Deal with animation
|
||||
if (coords[0] instanceof Array) {
|
||||
if (tic !== null) {
|
||||
if (tile && tile.animation_speed) {
|
||||
// This tile reports its own animation timing (in tics), so trust that, and just
|
||||
// use the current tic's fraction.
|
||||
// That said: adjusting animation speed complicates this slightly. Consider the
|
||||
// player's walk animation, which takes 4 tics to complete, during which time we
|
||||
// cycle through 8 frames. Playing that at half speed means only half the
|
||||
// animation actually plays, but if the player continues walking, then on the
|
||||
// NEXT four tics, we should play the other half. To make this work, use the
|
||||
// tic as a global timer as well: if the animation started on tics 0-4, play the
|
||||
// first half; if it started on tics 5-8, play the second half. They could get
|
||||
// out of sync if the player hesitates, but no one will notice that, and this
|
||||
// approach minimizes storing extra state.
|
||||
let i = (tile.animation_progress + tic % 1) / tile.animation_speed;
|
||||
// But do NOT do this for explosions or splashes, which have a fixed duration
|
||||
// and only play once
|
||||
if (this.animation_slowdown > 1 && ! tile.type.ttl) {
|
||||
// i ranges from [0, 1), but a slowdown of N means we'll only play the first
|
||||
// 1/N of it before the game ends (or loops) the animation.
|
||||
// So increase by [0..N-1] to get it in some other range, then divide by N
|
||||
// to scale back down to [0, 1)
|
||||
i += Math.floor(tic / tile.animation_speed % this.animation_slowdown);
|
||||
i /= this.animation_slowdown;
|
||||
}
|
||||
coords = coords[Math.floor(i * coords.length)];
|
||||
}
|
||||
else {
|
||||
// This tile animates on a global timer, one cycle every quarter of a second
|
||||
coords = coords[Math.floor(tic / this.animation_slowdown % 5 / 5 * coords.length)];
|
||||
}
|
||||
}
|
||||
else {
|
||||
coords = coords[0];
|
||||
}
|
||||
}
|
||||
|
||||
blit(coords[0], coords[1], ...mask);
|
||||
}
|
||||
|
||||
_draw_letter(drawspec, tile, tic, blit) {
|
||||
this._draw_standard(drawspec.base, tile, tic, blit);
|
||||
|
||||
let glyph = tile ? tile.overlaid_glyph : "?";
|
||||
if (drawspec.letter_glyphs[glyph]) {
|
||||
let [x, y] = drawspec.letter_glyphs[glyph];
|
||||
// XXX size is hardcoded here, but not below, meh
|
||||
blit(x, y, 0, 0, 0.5, 0.5, 0.25, 0.25);
|
||||
}
|
||||
else {
|
||||
// Look for a range
|
||||
let u = glyph.charCodeAt(0);
|
||||
for (let rangedef of drawspec.letter_ranges) {
|
||||
if (rangedef.range[0] <= u && u < rangedef.range[1]) {
|
||||
let t = u - rangedef.range[0];
|
||||
let x = rangedef.x0 + rangedef.w * (t % rangedef.columns);
|
||||
let y = rangedef.y0 + rangedef.h * Math.floor(t / rangedef.columns);
|
||||
blit(x, y, 0, 0, rangedef.w, rangedef.h,
|
||||
(1 - rangedef.w) / 2, (1 - rangedef.h) / 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_draw_logic_gate(drawspec, tile, tic, blit) {
|
||||
// Layer 1: wiring state
|
||||
// Always draw the unpowered wire base
|
||||
let unpowered_coords = this.layout['#unpowered'];
|
||||
let powered_coords = this.layout['#powered'];
|
||||
blit(...unpowered_coords);
|
||||
if (tile && tile.cell) {
|
||||
// What goes on top varies a bit...
|
||||
// FIXME implement for NOT and counter!
|
||||
let r = this.layout['#wire-width'] / 2;
|
||||
if (tile.cell.powered_edges & DIRECTIONS[tile.direction].bit) {
|
||||
// Output (on top)
|
||||
let [x0, y0, x1, y1] = this._rotate(tile.direction, 0.5 - r, 0, 0.5 + r, 0.5);
|
||||
blit(powered_coords[0], powered_coords[1], x0, y0, x1 - x0, y1 - y0);
|
||||
}
|
||||
if (tile.cell.powered_edges & DIRECTIONS[DIRECTIONS[tile.direction].right].bit) {
|
||||
// Right input, which includes the middle
|
||||
let [x0, y0, x1, y1] = this._rotate(tile.direction, 0.5 - r, 0.5 - r, 1, 1);
|
||||
blit(powered_coords[0], powered_coords[1], x0, y0, x1 - x0, y1 - y0);
|
||||
}
|
||||
if (tile.cell.powered_edges & DIRECTIONS[DIRECTIONS[tile.direction].left].bit) {
|
||||
// Left input, which does not include the middle
|
||||
let [x0, y0, x1, y1] = this._rotate(tile.direction, 0, 0.5 - r, 0.5 - r, 1);
|
||||
blit(powered_coords[0], powered_coords[1], x0, y0, x1 - x0, y1 - y0);
|
||||
}
|
||||
}
|
||||
|
||||
// Layer 2: the tile itself
|
||||
this._draw_standard(drawspec.logic_gate_tiles[tile.gate_type], tile, tic, blit);
|
||||
|
||||
// Layer 3: counter number
|
||||
if (tile.gate_type === 'counter') {
|
||||
blit(0, 3, tile.memory * 0.75, 0, 0.75, 1, 0.125, 0);
|
||||
}
|
||||
}
|
||||
|
||||
_draw_railroad(drawspec, tile, tic, blit) {
|
||||
// All railroads have regular gravel underneath
|
||||
// TODO would be nice to disambiguate since it's possible to have nothing visible
|
||||
this._draw_standard(this.layout['gravel'], tile, tic, blit);
|
||||
|
||||
// FIXME what do i draw if there's no tile?
|
||||
let part_order = ['ne', 'se', 'sw', 'nw', 'ew', 'ns'];
|
||||
let visible_parts = [];
|
||||
let topmost_part = null;
|
||||
for (let [i, part] of part_order.entries()) {
|
||||
if (tile && (tile.tracks & (1 << i))) {
|
||||
if (tile.track_switch === i) {
|
||||
topmost_part = part;
|
||||
}
|
||||
visible_parts.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
let has_switch = (tile && tile.track_switch !== null);
|
||||
for (let part of visible_parts) {
|
||||
this._draw_standard(drawspec.railroad_ties[part], tile, tic, blit);
|
||||
}
|
||||
let tracks = has_switch ? drawspec.railroad_inactive : drawspec.railroad_active;
|
||||
for (let part of visible_parts) {
|
||||
if (part !== topmost_part) {
|
||||
this._draw_standard(tracks[part], tile, tic, blit);
|
||||
}
|
||||
}
|
||||
|
||||
if (topmost_part) {
|
||||
this._draw_standard(drawspec.railroad_active[topmost_part], tile, tic, blit);
|
||||
}
|
||||
if (has_switch) {
|
||||
this._draw_standard(drawspec.railroad_switch, tile, tic, blit);
|
||||
}
|
||||
}
|
||||
|
||||
// Draws a tile type, given by name. Passing in a tile is optional, but
|
||||
// without it you'll get defaults.
|
||||
draw_type(name, tile, tic, blit) {
|
||||
draw_type(name, tile, tic, perception, blit) {
|
||||
let drawspec = this.layout[name];
|
||||
if (! drawspec) {
|
||||
console.error(`Don't know how to draw tile type ${name}!`);
|
||||
@@ -615,12 +1054,17 @@ export class Tileset {
|
||||
|
||||
if (drawspec.overlay) {
|
||||
// Goofy overlay thing used for green/purple toggle tiles and
|
||||
// southeast thin walls. Draw the base (a type name), then draw
|
||||
// southeast thin walls. Draw the base (a type name or drawspec), then draw
|
||||
// the overlay (either a type name or a regular draw spec).
|
||||
// TODO chance of infinite recursion here
|
||||
this.draw_type(drawspec.base, tile, tic, blit);
|
||||
if (typeof drawspec.base === 'string') {
|
||||
this.draw_type(drawspec.base, tile, tic, perception, blit);
|
||||
}
|
||||
else {
|
||||
this._draw_standard(drawspec.base, tile, tic, blit);
|
||||
}
|
||||
if (typeof drawspec.overlay === 'string') {
|
||||
this.draw_type(drawspec.overlay, tile, tic, blit);
|
||||
this.draw_type(drawspec.overlay, tile, tic, perception, blit);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
@@ -628,6 +1072,30 @@ export class Tileset {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO shift everything to use this style, this is ridiculous
|
||||
if (drawspec.special) {
|
||||
if (drawspec.special === 'letter') {
|
||||
this._draw_letter(drawspec, tile, tic, blit);
|
||||
return;
|
||||
}
|
||||
else if (drawspec.special === 'perception') {
|
||||
if (perception >= drawspec.threshold) {
|
||||
drawspec = drawspec.revealed;
|
||||
}
|
||||
else {
|
||||
drawspec = drawspec.hidden;
|
||||
}
|
||||
}
|
||||
else if (drawspec.special === 'logic-gate') {
|
||||
this._draw_logic_gate(drawspec, tile, tic, blit);
|
||||
return;
|
||||
}
|
||||
else if (drawspec.special === 'railroad') {
|
||||
this._draw_railroad(drawspec, tile, tic, blit);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let coords = drawspec;
|
||||
if (drawspec.mask) {
|
||||
// Some tiles (OK, just the thin walls) don't actually draw a full
|
||||
@@ -635,31 +1103,51 @@ export class Tileset {
|
||||
coords = drawspec.tile;
|
||||
}
|
||||
else if (drawspec.wired) {
|
||||
if (tile && tile.wire_directions !== undefined && tile.wire_directions !== 0) {
|
||||
// TODO all four is a different thing entirely
|
||||
// Draw the appropriate wire underlay
|
||||
this.draw_type('#unpowered', tile, tic, blit);
|
||||
// This /should/ match CC2's draw order exactly, based on experimentation
|
||||
let wire_radius = this.layout['#wire-width'] / 2;
|
||||
if (tile && tile.wire_directions === 0x0f) {
|
||||
// This is a wired tile with crossing wires, which acts a little differently
|
||||
// Draw the base tile
|
||||
blit(drawspec.base[0], drawspec.base[1]);
|
||||
|
||||
// Draw a masked part of the base tile
|
||||
let wiredir = tile.wire_directions;
|
||||
let wire_radius = this.layout['#wire-width'] / 2;
|
||||
let wire0 = 0.5 - wire_radius;
|
||||
let wire1 = 0.5 + wire_radius;
|
||||
let [bx, by] = drawspec.base;
|
||||
if ((wiredir & DIRECTIONS['north'].bit) === 0) {
|
||||
blit(bx, by, 0, 0, 1, wire0);
|
||||
}
|
||||
if ((wiredir & DIRECTIONS['south'].bit) === 0) {
|
||||
blit(bx, by + wire1, 0, wire1, 1, wire0);
|
||||
}
|
||||
if ((wiredir & DIRECTIONS['west'].bit) === 0) {
|
||||
blit(bx, by, 0, 0, wire0, 1);
|
||||
}
|
||||
if ((wiredir & DIRECTIONS['east'].bit) === 0) {
|
||||
blit(bx + wire1, by, wire1, 0, wire0, 1);
|
||||
}
|
||||
// Draw the two wires as separate rectangles, NS then EW
|
||||
let wire_inset = 0.5 - wire_radius;
|
||||
let wire_coords_ns = this.layout[
|
||||
tile.cell && tile.cell.powered_edges & DIRECTIONS['north'].bit ? '#powered' : '#unpowered'];
|
||||
let wire_coords_ew = this.layout[
|
||||
tile.cell && tile.cell.powered_edges & DIRECTIONS['east'].bit ? '#powered' : '#unpowered'];
|
||||
blit(wire_coords_ns[0], wire_coords_ns[1], wire_inset, 0, wire_radius * 2, 1);
|
||||
blit(wire_coords_ew[0], wire_coords_ew[1], 0, wire_inset, 1, wire_radius * 2);
|
||||
|
||||
// Then draw the wired tile as normal
|
||||
// Draw the cross tile on top
|
||||
coords = drawspec.wired_cross ?? drawspec.wired;
|
||||
}
|
||||
else if (tile && tile.wire_directions) {
|
||||
// Draw the base tile
|
||||
blit(drawspec.base[0], drawspec.base[1]);
|
||||
|
||||
// Draw the wire part as a single rectangle, initially just a small dot in the
|
||||
// center, but extending out to any edge that has a wire present
|
||||
let x0 = 0.5 - wire_radius;
|
||||
let x1 = 0.5 + wire_radius;
|
||||
let y0 = 0.5 - wire_radius;
|
||||
let y1 = 0.5 + wire_radius;
|
||||
if (tile.wire_directions & DIRECTIONS['north'].bit) {
|
||||
y0 = 0;
|
||||
}
|
||||
if (tile.wire_directions & DIRECTIONS['east'].bit) {
|
||||
x1 = 1;
|
||||
}
|
||||
if (tile.wire_directions & DIRECTIONS['south'].bit) {
|
||||
y1 = 1;
|
||||
}
|
||||
if (tile.wire_directions & DIRECTIONS['west'].bit) {
|
||||
x0 = 0;
|
||||
}
|
||||
let wire_coords = this.layout[tile.cell && tile.cell.powered_edges ? '#powered' : '#unpowered'];
|
||||
blit(wire_coords[0], wire_coords[1], x0, y0, x1 - x0, y1 - y0);
|
||||
|
||||
// Then draw the wired tile on top of it all
|
||||
coords = drawspec.wired;
|
||||
}
|
||||
else {
|
||||
@@ -675,6 +1163,10 @@ export class Tileset {
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (drawspec.arrows) {
|
||||
// Directional blocks have a specific overlay, but draw the base first
|
||||
coords = drawspec.base;
|
||||
}
|
||||
else if (drawspec.animate_width) {
|
||||
// Force floors animate their... cutout, I guess?
|
||||
let [x, y] = drawspec.base;
|
||||
@@ -695,7 +1187,7 @@ export class Tileset {
|
||||
}
|
||||
|
||||
// Apply custom per-type visual states
|
||||
if (TILE_TYPES[name].visual_state) {
|
||||
if (TILE_TYPES[name] && TILE_TYPES[name].visual_state) {
|
||||
// Note that these accept null, too, and return a default
|
||||
let state = TILE_TYPES[name].visual_state(tile);
|
||||
// If it's a string, that's an alias for another state
|
||||
@@ -759,41 +1251,70 @@ export class Tileset {
|
||||
// Continue on with masking
|
||||
coords = drawspec.tile;
|
||||
let [x0, y0, w, h] = drawspec.mask;
|
||||
blit(coords[0] + x0, coords[1] + y0, x0, y0, w, h);
|
||||
blit(coords[0], coords[1], x0, y0, w, h);
|
||||
}
|
||||
else {
|
||||
if (!coords) console.error(name, tile);
|
||||
blit(coords[0], coords[1]);
|
||||
}
|
||||
|
||||
// Special behavior for special objects
|
||||
// TODO? hardcode this less?
|
||||
if (name === 'floor_letter') {
|
||||
let n = tile.ascii_code - 32;
|
||||
let scale = 0.5;
|
||||
let sx, sy;
|
||||
if (n < 0) {
|
||||
// Arrows
|
||||
if (n < -4) {
|
||||
// Default to south
|
||||
n = -2;
|
||||
}
|
||||
// Wired tiles may also have tunnels, drawn on top of everything else
|
||||
if (drawspec.wired && tile && tile.wire_tunnel_directions) {
|
||||
let tunnel_coords = this.layout['#wire-tunnel'];
|
||||
let tunnel_width = 6/32;
|
||||
let tunnel_length = 12/32;
|
||||
let tunnel_offset = (1 - tunnel_width) / 2;
|
||||
if (tile.wire_tunnel_directions & DIRECTIONS['north'].bit) {
|
||||
blit(tunnel_coords[0], tunnel_coords[1],
|
||||
tunnel_offset, 0, tunnel_width, tunnel_length);
|
||||
}
|
||||
if (tile.wire_tunnel_directions & DIRECTIONS['south'].bit) {
|
||||
blit(tunnel_coords[0], tunnel_coords[1],
|
||||
tunnel_offset, 1 - tunnel_length, tunnel_width, tunnel_length);
|
||||
}
|
||||
if (tile.wire_tunnel_directions & DIRECTIONS['west'].bit) {
|
||||
blit(tunnel_coords[0], tunnel_coords[1],
|
||||
0, tunnel_offset, tunnel_length, tunnel_width);
|
||||
}
|
||||
if (tile.wire_tunnel_directions & DIRECTIONS['east'].bit) {
|
||||
blit(tunnel_coords[0], tunnel_coords[1],
|
||||
1 - tunnel_length, tunnel_offset, tunnel_length, tunnel_width);
|
||||
}
|
||||
}
|
||||
|
||||
let direction = ['north', 'east', 'south', 'west'][n + 4];
|
||||
[sx, sy] = this.layout['floor_letter#arrows'][direction];
|
||||
// Directional blocks have arrows drawn on top
|
||||
// TODO does cc2 draw even if there are no arrows?
|
||||
if (drawspec.arrows && tile && tile.arrows) {
|
||||
let [x, y] = drawspec.arrows;
|
||||
let x0 = 0.25, y0 = 0.25, x1 = 0.75, y1 = 0.75;
|
||||
if (tile.arrows.has('north')) {
|
||||
y0 = 0;
|
||||
}
|
||||
else {
|
||||
// ASCII text (only up through uppercase)
|
||||
let letter_spec = this.layout['floor_letter#ascii'];
|
||||
if (n > letter_spec.width / scale * letter_spec.height / scale) {
|
||||
n = 0;
|
||||
}
|
||||
let w = letter_spec.width / scale;
|
||||
sx = (letter_spec.x0 + n % w) * scale;
|
||||
sy = (letter_spec.y0 + Math.floor(n / w)) * scale;
|
||||
if (tile.arrows.has('east')) {
|
||||
x1 = 1;
|
||||
}
|
||||
let offset = (1 - scale) / 2;
|
||||
blit(sx, sy, offset, offset, scale, scale);
|
||||
if (tile.arrows.has('south')) {
|
||||
y1 = 1;
|
||||
}
|
||||
if (tile.arrows.has('west')) {
|
||||
x0 = 0;
|
||||
}
|
||||
blit(x, y, x0, y0, x1 - x0, y1 - y0);
|
||||
}
|
||||
}
|
||||
|
||||
_rotate(direction, x0, y0, x1, y1) {
|
||||
if (direction === 'east') {
|
||||
return [1 - y1, x0, 1 - y0, x1];
|
||||
}
|
||||
else if (direction === 'south') {
|
||||
return [1 - x1, 1 - y1, 1 - x0, 1 - y0];
|
||||
}
|
||||
else if (direction === 'west') {
|
||||
return [y0, 1 - x1, y1, 1 - x0];
|
||||
}
|
||||
else {
|
||||
return [x0, y0, x1, y1];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
// Base class for custom errors
|
||||
export class LLError extends Error {}
|
||||
|
||||
// Random choice
|
||||
export function random_choice(list) {
|
||||
return list[Math.floor(Math.random() * list.length)];
|
||||
}
|
||||
|
||||
|
||||
// DOM stuff
|
||||
function _mk(el, children) {
|
||||
if (children.length > 0) {
|
||||
if (!(children[0] instanceof Node) && children[0] !== undefined && typeof(children[0]) !== "string" && typeof(children[0]) !== "number") {
|
||||
@@ -18,17 +24,86 @@ function _mk(el, children) {
|
||||
export function mk(tag_selector, ...children) {
|
||||
let [tag, ...classes] = tag_selector.split('.');
|
||||
let el = document.createElement(tag);
|
||||
el.classList = classes.join(' ');
|
||||
if (classes.length > 0) {
|
||||
el.classList = classes.join(' ');
|
||||
}
|
||||
return _mk(el, children);
|
||||
}
|
||||
|
||||
export const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
export function mk_svg(tag_selector, ...children) {
|
||||
let [tag, ...classes] = tag_selector.split('.');
|
||||
let el = document.createElementNS('http://www.w3.org/2000/svg', tag);
|
||||
el.classList = classes.join(' ');
|
||||
let el = document.createElementNS(SVG_NS, tag);
|
||||
if (classes.length > 0) {
|
||||
el.classList = classes.join(' ');
|
||||
}
|
||||
return _mk(el, children);
|
||||
}
|
||||
|
||||
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 promise_event(element, success_event, failure_event) {
|
||||
let resolve, reject;
|
||||
let promise = new Promise((res, rej) => {
|
||||
@@ -36,21 +111,21 @@ export function promise_event(element, success_event, failure_event) {
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
let success_handler = e => {
|
||||
let success_handler = ev => {
|
||||
element.removeEventListener(success_event, success_handler);
|
||||
if (failure_event) {
|
||||
element.removeEventListener(failure_event, failure_handler);
|
||||
}
|
||||
|
||||
resolve(e);
|
||||
resolve(ev);
|
||||
};
|
||||
let failure_handler = e => {
|
||||
let failure_handler = ev => {
|
||||
element.removeEventListener(success_event, success_handler);
|
||||
if (failure_event) {
|
||||
element.removeEventListener(failure_event, failure_handler);
|
||||
}
|
||||
|
||||
reject(e);
|
||||
reject(ev);
|
||||
};
|
||||
|
||||
element.addEventListener(success_event, success_handler);
|
||||
@@ -61,6 +136,7 @@ export function promise_event(element, success_event, failure_event) {
|
||||
return promise;
|
||||
}
|
||||
|
||||
|
||||
export async function fetch(url) {
|
||||
let xhr = new XMLHttpRequest;
|
||||
let promise = promise_event(xhr, 'load', 'error');
|
||||
@@ -68,11 +144,26 @@ export async function fetch(url) {
|
||||
xhr.responseType = 'arraybuffer';
|
||||
xhr.send();
|
||||
await promise;
|
||||
if (xhr.status !== 200)
|
||||
throw new Error(`Failed to load ${url} -- ${xhr.status} ${xhr.statusText}`);
|
||||
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;
|
||||
}
|
||||
|
||||
// Cast a line through a grid and yield every cell it touches
|
||||
export function* walk_grid(x0, y0, x1, y1) {
|
||||
export function* walk_grid(x0, y0, x1, y1, min_a, min_b, max_a, max_b) {
|
||||
// TODO if the ray starts outside the grid (extremely unlikely), we should
|
||||
// find the point where it ENTERS the grid, otherwise the 'while'
|
||||
// conditions below will stop immediately
|
||||
@@ -112,27 +203,24 @@ export function* walk_grid(x0, y0, x1, y1) {
|
||||
step_a = -step_a;
|
||||
offset_x = 1 - offset_x;
|
||||
}
|
||||
// Zero offset means we're on a grid line, so we're actually a full cell
|
||||
// away from the next grid line
|
||||
if (offset_x === 0) {
|
||||
else if (offset_x === 0) {
|
||||
// Zero offset means we're on a grid line, so we're a full cell away from the next grid line
|
||||
// (if we're moving forward; if we're moving backward, the next cell really is 0 away)
|
||||
offset_x = 1;
|
||||
}
|
||||
let step_b = 1;
|
||||
let offset_y = 1 - (y0 - b);
|
||||
let offset_y = 1 - (y0 - b);
|
||||
if (dy < 0) {
|
||||
dy = -dy;
|
||||
step_b = -step_b;
|
||||
offset_y = 1 - offset_y;
|
||||
}
|
||||
if (offset_y === 0) {
|
||||
else if (offset_y === 0) {
|
||||
offset_y = 1;
|
||||
}
|
||||
|
||||
let err = dy * offset_x - dx * offset_y;
|
||||
|
||||
let min_a = 0, min_b = 0;
|
||||
// TODO get these passed in fool
|
||||
let max_a = 31, max_b = 31;
|
||||
if (dx > dy) {
|
||||
// Main axis is x/a
|
||||
while (min_a <= a && a <= max_a && min_b <= b && b <= max_b) {
|
||||
@@ -171,3 +259,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();
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,10 @@ button {
|
||||
font-family: inherit;
|
||||
color: white;
|
||||
background: var(--button-bg-color);
|
||||
border: none;
|
||||
border: 1px solid hsl(225, 10%, 15%);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px hsl(225, 10%, 33%),
|
||||
0 1px 1px hsl(225, 10%, 10%);
|
||||
border-radius: 0.25em;
|
||||
text-transform: lowercase;
|
||||
cursor: pointer;
|
||||
@@ -45,9 +48,19 @@ button {
|
||||
button:hover {
|
||||
background: var(--button-bg-hover-color);
|
||||
}
|
||||
button:active {
|
||||
box-shadow:
|
||||
inset 0 0 2px 1px hsl(225, 10%, 33%);
|
||||
transform: translateY(1px);
|
||||
/* Need this for the editor's tool help things and i'm not questioning it */
|
||||
z-index: 1;
|
||||
}
|
||||
button:disabled {
|
||||
color: #606060;
|
||||
background: #202020;
|
||||
box-shadow:
|
||||
inset 0 0 2px 1px hsl(225, 0%, 10%),
|
||||
0 1px 0 hsl(225, 10%, 10%);
|
||||
cursor: auto;
|
||||
}
|
||||
button.button-big {
|
||||
@@ -82,6 +95,12 @@ p:first-child {
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
code {
|
||||
color: #c0c0e0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #c0c0c0;
|
||||
@@ -91,10 +110,10 @@ a:visited {
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
a:link {
|
||||
color: hsl(225, 50%, 60%);
|
||||
color: hsl(225, 50%, 75%);
|
||||
}
|
||||
a:visited {
|
||||
color: hsl(300, 50%, 60%);
|
||||
color: hsl(255, 50%, 75%);
|
||||
}
|
||||
a:link:hover,
|
||||
a:visited:hover {
|
||||
@@ -104,6 +123,16 @@ a:active {
|
||||
color: hsl(0, 50%, 60%);
|
||||
}
|
||||
|
||||
svg.svg-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: middle;
|
||||
|
||||
stroke: none;
|
||||
fill: currentColor;
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
|
||||
/* Overlay styling */
|
||||
.overlay {
|
||||
display: flex;
|
||||
@@ -117,6 +146,11 @@ a:active {
|
||||
right: 0;
|
||||
background: #fff4;
|
||||
}
|
||||
.overlay.--transient {
|
||||
align-items: start;
|
||||
justify-content: start;
|
||||
background: none;
|
||||
}
|
||||
.dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -154,6 +188,31 @@ a:active {
|
||||
overflow: auto;
|
||||
padding: 1em;
|
||||
}
|
||||
.dialog pre.error {
|
||||
color: #400000;
|
||||
background: #f0d0d0;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
.dialog a:link {
|
||||
color: hsl(225, 50%, 50%);
|
||||
}
|
||||
.dialog a:visited {
|
||||
color: hsl(255, 50%, 50%);
|
||||
}
|
||||
dl.formgrid {
|
||||
display: grid;
|
||||
grid: auto-flow min-content / 1fr 4fr;
|
||||
align-items: baseline;
|
||||
gap: 1em;
|
||||
margin: 0;
|
||||
}
|
||||
dl.formgrid > dt {
|
||||
grid-column: 1;
|
||||
}
|
||||
dl.formgrid > dd {
|
||||
grid-column: 2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Individual overlays */
|
||||
table.level-browser {
|
||||
@@ -188,6 +247,10 @@ table.level-browser tr.--unvisited {
|
||||
color: #606060;
|
||||
font-style: italic;
|
||||
}
|
||||
table.level-browser tr.--error {
|
||||
color: #600000;
|
||||
font-style: italic;
|
||||
}
|
||||
table.level-browser tbody tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -254,6 +317,13 @@ label.option .option-label {
|
||||
/* TODO */
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.dialog {
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Bits and pieces */
|
||||
img.compat-icon {
|
||||
margin: 0 0.25em 0.125em;
|
||||
@@ -281,7 +351,7 @@ body > header {
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
|
||||
margin: 0.25em;
|
||||
padding: 0.5em;
|
||||
line-height: 1.125;
|
||||
}
|
||||
body > header h1 {
|
||||
@@ -291,14 +361,13 @@ body > header h2 {
|
||||
font-size: 1.33em;
|
||||
}
|
||||
body > header h3 {
|
||||
font-size: 1.25em;
|
||||
font-size: 1.75em;
|
||||
}
|
||||
body > header > nav {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5em;
|
||||
margin: 0.25rem 0.5rem;
|
||||
}
|
||||
body > header button {
|
||||
font-size: 0.75em;
|
||||
@@ -317,8 +386,25 @@ body[data-mode=player] #editor-play {
|
||||
order: 3;
|
||||
color: #606060;
|
||||
}
|
||||
#header-level {
|
||||
font-size: 1.5em;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
body > header {
|
||||
padding: 0.25em;
|
||||
}
|
||||
/* All these headings are way too big on phones */
|
||||
body > header h1 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
body > header h2 {
|
||||
font-size: 1.125em;
|
||||
}
|
||||
body > header h3 {
|
||||
font-size: 1.0625em;
|
||||
}
|
||||
body > header p {
|
||||
/* "a game by eevee" takes up too much space :( */
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/**************************************************************************************************/
|
||||
@@ -334,10 +420,31 @@ body[data-mode=player] #editor-play {
|
||||
;
|
||||
gap: 1em;
|
||||
|
||||
position: relative;
|
||||
padding: 1em 10%;
|
||||
margin: auto;
|
||||
overflow: auto;
|
||||
}
|
||||
#splash > .drag-overlay {
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
font-size: 10vmin;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
background: #fff2;
|
||||
border: 0.25rem dashed white;
|
||||
border-radius: 1rem;
|
||||
text-shadow: 0 1px 5px black;
|
||||
text-align: center;
|
||||
}
|
||||
#splash.--drag-hover > .drag-overlay {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#splash > header {
|
||||
grid-area: header;
|
||||
@@ -351,6 +458,11 @@ body[data-mode=player] #editor-play {
|
||||
#splash > header h1 {
|
||||
font-size: 3em;
|
||||
}
|
||||
#splash > header p {
|
||||
margin: 0;
|
||||
font-style: italic;
|
||||
color: #909090;
|
||||
}
|
||||
#splash h2 {
|
||||
border-bottom: 1px solid #404040;
|
||||
color: #909090;
|
||||
@@ -363,7 +475,7 @@ body[data-mode=player] #editor-play {
|
||||
}
|
||||
#splash > #splash-intro {
|
||||
grid-area: intro;
|
||||
font-size: 18px;
|
||||
font-size: 20px;
|
||||
}
|
||||
#splash > #splash-stock-levels {
|
||||
grid-area: stock;
|
||||
@@ -371,7 +483,8 @@ body[data-mode=player] #editor-play {
|
||||
#splash > #splash-upload-levels {
|
||||
grid-area: upload;
|
||||
}
|
||||
#splash-upload {
|
||||
#splash-upload-file,
|
||||
#splash-upload-dir {
|
||||
/* Hide the file upload control, which is ugly */
|
||||
display: none;
|
||||
}
|
||||
@@ -379,6 +492,23 @@ body[data-mode=player] #editor-play {
|
||||
grid-area: yours;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
#splash {
|
||||
/* Grid layout doesn't fit, just stack everything */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* 10% padding is way way too much */
|
||||
padding: 1em;
|
||||
}
|
||||
/* Shrink logo and title */
|
||||
#splash > header img {
|
||||
width: 48px;
|
||||
}
|
||||
#splash > header h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
button.level-pack-button {
|
||||
display: grid;
|
||||
grid:
|
||||
@@ -434,8 +564,9 @@ button.level-pack-button p {
|
||||
"level bonus" min-content
|
||||
"level message" 1fr
|
||||
"level inventory" min-content
|
||||
"level actions" min-content
|
||||
/* Need explicit min-content to force the hint to wrap */
|
||||
/ min-content min-content
|
||||
/ min-content min-content min-content
|
||||
;
|
||||
column-gap: 2em;
|
||||
row-gap: 0.5em;
|
||||
@@ -469,7 +600,7 @@ button.level-pack-button p {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
font-size: calc(0.5 * var(--tile-width) * var(--scale));
|
||||
padding: 2%;
|
||||
background: #0009;
|
||||
@@ -477,6 +608,10 @@ button.level-pack-button p {
|
||||
text-align: center;
|
||||
text-shadow: 0 2px 1px black;
|
||||
}
|
||||
/* Allow clicking through the overlay in debug mode */
|
||||
#player.--debug .overlay-message {
|
||||
pointer-events: none;
|
||||
}
|
||||
#player .overlay-message p {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -552,7 +687,6 @@ dl.score-chart .-sum {
|
||||
.bonus output {
|
||||
flex: 1;
|
||||
font-size: 2em;
|
||||
padding: 0.125em;
|
||||
min-width: 2em;
|
||||
min-height: 1em;
|
||||
line-height: 1;
|
||||
@@ -584,9 +718,11 @@ dl.score-chart .-sum {
|
||||
}
|
||||
#player .bonus {
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
}
|
||||
#player.--bonus-visible .bonus {
|
||||
visibility: initial;
|
||||
display: initial;
|
||||
}
|
||||
|
||||
.message {
|
||||
@@ -616,7 +752,7 @@ dl.score-chart .-sum {
|
||||
|
||||
background-size: calc(var(--tile-width) * var(--scale)) calc(var(--tile-height) * var(--scale));
|
||||
width: calc(4 * var(--tile-width) * var(--scale));
|
||||
min-height: calc(2 * var(--tile-height) * var(--scale));
|
||||
height: calc(2 * var(--tile-height) * var(--scale));
|
||||
}
|
||||
#player .inventory img {
|
||||
width: calc(var(--tile-width) * var(--scale));
|
||||
@@ -640,15 +776,27 @@ dl.score-chart .-sum {
|
||||
background: #0009;
|
||||
color: white;
|
||||
}
|
||||
#player .actions {
|
||||
grid-area: actions;
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
}
|
||||
#player .actions button {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#player-music {
|
||||
grid-area: music;
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
margin: 0 1em;
|
||||
text-transform: lowercase;
|
||||
color: #909090;
|
||||
}
|
||||
#player-music #player-music-left {
|
||||
flex: 1 0 auto;
|
||||
/* allow me to wrap if need be */
|
||||
flex: 1 0 0px;
|
||||
}
|
||||
#player-music #player-music-right {
|
||||
text-align: right;
|
||||
@@ -660,25 +808,111 @@ dl.score-chart .-sum {
|
||||
#player .controls {
|
||||
grid-area: controls;
|
||||
display: flex;
|
||||
gap: 0.25em;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.play-controls,
|
||||
.demo-controls {
|
||||
#player button {
|
||||
position: relative;
|
||||
}
|
||||
#player button .keyhint {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: -2em;
|
||||
width: 1em;
|
||||
margin: auto;
|
||||
color: #404040;
|
||||
border: 1px solid #202020;
|
||||
border-radius: 0.25em;
|
||||
box-shadow: 0 2px 0 #202020;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
#player button:disabled .keyhint {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.play-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
}
|
||||
.play-controls {
|
||||
align-self: start;
|
||||
}
|
||||
.demo-controls {
|
||||
display: none;
|
||||
flex: 1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
main.--has-demo .demo-controls {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Debug stuff */
|
||||
#player.--debug #player-debug {
|
||||
display: grid;
|
||||
}
|
||||
#player-debug {
|
||||
display: none;
|
||||
grid:
|
||||
"time inventory options"
|
||||
"buttons buttons buttons"
|
||||
/ 2fr 1fr 2fr
|
||||
;
|
||||
gap: 0.25em;
|
||||
padding: 0.5em;
|
||||
background-image: repeating-linear-gradient(135deg,
|
||||
hsl(0, 0%, 4%) 0em,
|
||||
hsl(0, 0%, 4%) 1em,
|
||||
hsl(15, 25%, 8%) 1em,
|
||||
hsl(15, 25%, 8%) 2em);
|
||||
border: 0.25em solid hsla(30, 100%, 50%, 0.5);
|
||||
box-shadow: inset 0 0 0.25em 0.25em #0004;
|
||||
|
||||
}
|
||||
#player-debug > .-time {
|
||||
grid-area: time;
|
||||
}
|
||||
#player-debug > .-time table.-time-controls {
|
||||
}
|
||||
#player-debug > .-time table.-time-controls button {
|
||||
width: 100%;
|
||||
text-align: inherit;
|
||||
}
|
||||
#player-debug > .-time table.-time-controls td:nth-child(1) {
|
||||
/* go backwards button */
|
||||
text-align: left;
|
||||
}
|
||||
#player-debug > .-time table.-time-controls td:nth-child(2) {
|
||||
/* value */
|
||||
width: 6em;
|
||||
text-align: right;
|
||||
}
|
||||
#player-debug > .-time table.-time-controls td:nth-child(3) {
|
||||
/* label */
|
||||
}
|
||||
#player-debug > .-time table.-time-controls td:nth-child(4) {
|
||||
/* go forward button */
|
||||
text-align: right;
|
||||
}
|
||||
#player-debug > .-options {
|
||||
grid-area: options;
|
||||
}
|
||||
#player-debug > .-inventory {
|
||||
grid-area: inventory;
|
||||
display: grid;
|
||||
margin: auto;
|
||||
grid: auto-fill var(--tile-height) / repeat(4, var(--tile-width));
|
||||
gap: 2px;
|
||||
}
|
||||
#player-debug > .-inventory > button {
|
||||
padding: 0;
|
||||
}
|
||||
#player-debug > .-inventory > button > img {
|
||||
display: block;
|
||||
}
|
||||
#player-debug > .-inventory > button.-wide {
|
||||
grid-column: span 4;
|
||||
padding: 0.25em;
|
||||
}
|
||||
#player-debug > .-buttons {
|
||||
grid-area: buttons;
|
||||
display: flex;
|
||||
gap: 0.25em;
|
||||
}
|
||||
.input {
|
||||
display: grid;
|
||||
display: none;
|
||||
@@ -709,6 +943,57 @@ main.--has-demo .demo-controls {
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 800px) {
|
||||
#player {
|
||||
/* sentinel for js */
|
||||
--is-portrait: 1;
|
||||
/* The play area isn't necessarily the biggest thing any more, and it's ugly when stretched */
|
||||
align-items: center;
|
||||
}
|
||||
#player .controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
#player > .-main-area {
|
||||
/* Rearrange the grid to be vertical */
|
||||
grid:
|
||||
"level level"
|
||||
"chips inventory" calc((var(--tile-height) * var(--scale) * 2 - 1em) / 3)
|
||||
"time inventory" calc((var(--tile-height) * var(--scale) * 2 - 1em) / 3)
|
||||
"bonus inventory" calc((var(--tile-height) * var(--scale) * 2 - 1em) / 3)
|
||||
"actions actions" min-content
|
||||
/* FIXME ideally the first column would be 1fr so the hearts/time have space, but that
|
||||
* allows hints to grow to the entire width of the window, which incredibly sucks. i
|
||||
* don't know how to get around this except by giving the grid a fixed width, which i
|
||||
* guess wouldn't be that hard */
|
||||
/ min-content min-content
|
||||
;
|
||||
row-gap: 0.5em;
|
||||
column-gap: 1em;
|
||||
|
||||
padding: 0.5em;
|
||||
}
|
||||
#player .inventory {
|
||||
/* stick me in the center right */
|
||||
place-self: center end;
|
||||
}
|
||||
#player .message {
|
||||
/* Overlay hints on the inventory area */
|
||||
grid-row: chips / bonus;
|
||||
grid-column: level;
|
||||
z-index: 1;
|
||||
font-size: calc(var(--tile-height) * var(--scale) / 2.5);
|
||||
}
|
||||
#player .keyhint {
|
||||
/* Hide key hints, they take up surprisingly a lot of space */
|
||||
display: none;
|
||||
}
|
||||
#player-music {
|
||||
/* Stack the title/artist on the volume, since they don't fit well side by side */
|
||||
font-size: 0.875em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**************************************************************************************************/
|
||||
/* Editor */
|
||||
|
||||
@@ -716,47 +1001,136 @@ main.--has-demo .demo-controls {
|
||||
flex: 1 1 auto;
|
||||
display: grid;
|
||||
grid:
|
||||
"controls level" min-content
|
||||
"palette level" 1fr
|
||||
/ minmax(25%, auto) auto
|
||||
"controls controls" min-content
|
||||
"palette level" 1fr
|
||||
/ min-content 1fr
|
||||
;
|
||||
gap: 0.5em;
|
||||
|
||||
min-height: 0;
|
||||
margin: auto;
|
||||
padding: 1em;
|
||||
margin: auto 1em;
|
||||
}
|
||||
|
||||
#editor .level {
|
||||
#editor .editor-canvas {
|
||||
grid-area: level;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
/* Flex layout to center the canvas and overlay together */
|
||||
display: flex;
|
||||
/* This isn't supported in Chrome (still!!), so use auto margins on the child instead
|
||||
align-items: safe center;
|
||||
justify-content: safe center;
|
||||
*/
|
||||
background: #202020;
|
||||
border: 0.125em solid black;
|
||||
}
|
||||
#editor .editor-canvas .-container {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
/* Give the canvas/overlay a bit of a margin; it has to be a border because, due to some quirk
|
||||
* of overflowing flexboxes I guess, padding and margins won't extend the scroll area on the
|
||||
* right and bottom */
|
||||
/* TODO padding should be half a cell? */
|
||||
border: 2em solid transparent;
|
||||
}
|
||||
#editor .editor-canvas canvas {
|
||||
display: block;
|
||||
width: calc(var(--viewport-width) * var(--tile-width) * var(--scale));
|
||||
--viewport-width: 9;
|
||||
--viewport-height: 9;
|
||||
}
|
||||
/* SVG overlays */
|
||||
#editor svg.level-editor-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
/* FIXME get real size */
|
||||
width: 1024px;
|
||||
height: 1024px;
|
||||
right: 0;
|
||||
/* allow clicks to go through us! */
|
||||
pointer-events: none;
|
||||
}
|
||||
#editor .level-editor-overlay rect.overlay-cxn {
|
||||
|
||||
/* default svg properties */
|
||||
stroke-width: 0.0625;
|
||||
stroke: red;
|
||||
fill: none;
|
||||
}
|
||||
#editor .level-editor-overlay line.overlay-cxn {
|
||||
stroke-width: 0.0625;
|
||||
#editor .level-editor-overlay rect.overlay-cxn {
|
||||
stroke: red;
|
||||
}
|
||||
#editor .level-editor-overlay line.overlay-cxn {
|
||||
stroke: red;
|
||||
}
|
||||
#editor .level-editor-overlay rect.overlay-camera {
|
||||
stroke: #808080;
|
||||
fill: #80808040;
|
||||
pointer-events: auto;
|
||||
}
|
||||
#editor .level-editor-overlay text {
|
||||
/* Each cell is one "pixel", so text needs to be real small */
|
||||
font-size: 1px;
|
||||
}
|
||||
#editor .level-editor-overlay text.overlay-edit-tip {
|
||||
stroke: none;
|
||||
fill: black;
|
||||
}
|
||||
|
||||
#editor .controls {
|
||||
/* TODO with the hint area gone i don't think this needs to be a grid? could just flex */
|
||||
grid-area: controls;
|
||||
display: grid;
|
||||
grid:
|
||||
"tile toolbar layer direction . menu" auto
|
||||
/ auto auto auto auto 1fr auto
|
||||
;
|
||||
align-items: center;
|
||||
column-gap: 1em;
|
||||
}
|
||||
#editor .controls p img {
|
||||
background: url(icons/tool-bg-unselected.png);
|
||||
#editor .controls #editor-tile {
|
||||
grid-area: tile;
|
||||
}
|
||||
#editor .controls #editor-tile canvas {
|
||||
display: block;
|
||||
}
|
||||
#editor .controls #editor-toolbar {
|
||||
grid-area: toolbar;
|
||||
}
|
||||
#editor-toolbar .-help {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
width: max-content;
|
||||
margin-top: -0.25em;
|
||||
margin-left: -0.5em;
|
||||
padding: 0.33em 0.75em;
|
||||
pointer-events: none;
|
||||
|
||||
border: 1px solid black;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.5;
|
||||
text-transform: none;
|
||||
text-align: left;
|
||||
background: hsl(225, 10%, 20%);
|
||||
box-shadow: 0 1px 2px 1px #0004;
|
||||
|
||||
transition-property: margin-top, opacity, visibility;
|
||||
transition-timing-function: ease-out;
|
||||
transition-duration: 0.125s, 0.125s, 0s;
|
||||
transition-delay: 0s, 0s, 0.125s;
|
||||
}
|
||||
#editor-toolbar .-help h3 {
|
||||
font-size: 1.25em;
|
||||
margin-bottom: 0.25rem;
|
||||
border-bottom: 1px solid currentColor;
|
||||
}
|
||||
#editor-toolbar button:hover .-help {
|
||||
opacity: 1;
|
||||
z-index: 2; /* show above any that are in mid-fade */
|
||||
visibility: visible;
|
||||
margin-top: 0.25em;
|
||||
transition-delay: 0.5s;
|
||||
transition-timing-function: ease-in;
|
||||
}
|
||||
#editor .controls .-buttons {
|
||||
grid-area: menu;
|
||||
}
|
||||
.icon-button-set {
|
||||
display: flex;
|
||||
@@ -771,6 +1145,7 @@ main.--has-demo .demo-controls {
|
||||
background: url(icons/tool-bg-unselected.png) no-repeat;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
.icon-button-set button.-selected {
|
||||
background-image: url(icons/tool-bg-selected.png);
|
||||
@@ -778,9 +1153,10 @@ main.--has-demo .demo-controls {
|
||||
.icon-button-set button img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#editor .palette {
|
||||
isolation: isolate;
|
||||
grid-area: palette;
|
||||
min-width: 20vw;
|
||||
padding-right: 0.25em; /* in case of scrollbar */
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -795,12 +1171,172 @@ main.--has-demo .demo-controls {
|
||||
}
|
||||
#editor .palette section {
|
||||
display: grid;
|
||||
grid: auto-flow 32px / repeat(auto-fit, 32px);
|
||||
gap: 3px;
|
||||
margin: 0.33em 4px; /* matches gap */
|
||||
grid: auto-flow 32px / repeat(8, 32px);
|
||||
gap: 4px;
|
||||
}
|
||||
.palette-entry {
|
||||
margin: 0.25em;
|
||||
}
|
||||
.palette-entry:hover {
|
||||
box-shadow: 0 0 0 1px black, 0 0 0 3px hsl(225, 100%, 75%);
|
||||
}
|
||||
.palette-entry.--selected {
|
||||
box-shadow: 0 0 0 1px black, 0 0 0 2px white;
|
||||
z-index: 1;
|
||||
box-shadow: 0 0 0 1px black, 0 0 0 3px white;
|
||||
}
|
||||
|
||||
.editor-level-browser {
|
||||
display: grid;
|
||||
grid: auto-flow auto / repeat(auto-fill, minmax(13em, 1fr)); /* 12em preview wdith + padding */
|
||||
gap: 0.5em;
|
||||
width: 70vw;
|
||||
/* seems to go into the parent's right padding fsr, i guess because the scrollbar is there */
|
||||
margin-right: 1em;
|
||||
list-style: none;
|
||||
}
|
||||
.editor-level-browser li {
|
||||
display: grid;
|
||||
grid:
|
||||
"preview preview"
|
||||
"number title"
|
||||
/ min-content 1fr
|
||||
;
|
||||
gap: 0.25em;
|
||||
padding: 0.5em;
|
||||
}
|
||||
.editor-level-browser li:hover {
|
||||
background: hsl(225, 60%, 85%);
|
||||
}
|
||||
.editor-level-browser li > .-preview {
|
||||
grid-area: preview;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 12em;
|
||||
height: 12em;
|
||||
margin: auto;
|
||||
}
|
||||
.editor-level-browser li > .-preview:empty::before {
|
||||
content: '···';
|
||||
display: block;
|
||||
font-size: 5em;
|
||||
color: #c0c0c0;
|
||||
}
|
||||
.editor-level-browser li > .-preview canvas {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
.editor-level-browser li > .-number {
|
||||
grid-area: number;
|
||||
font-size: 2em;
|
||||
}
|
||||
.editor-level-browser li > .-title {
|
||||
grid-area: title;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* Mini editors for specific tiles with complex properties */
|
||||
/* FIXME should this stuff be on an overlay container class? */
|
||||
form.editor-popup-tile-editor {
|
||||
position: relative;
|
||||
padding: 0.5em;
|
||||
color: black;
|
||||
background: white;
|
||||
border: 1px solid black;
|
||||
box-shadow: 0 2px 4px #0004;
|
||||
|
||||
margin-top: 1em;
|
||||
|
||||
--chevron-offset: 0px;
|
||||
}
|
||||
form.editor-popup-tile-editor.--above {
|
||||
margin-top: -1em;
|
||||
}
|
||||
form.editor-popup-tile-editor h3 {
|
||||
border-bottom: 1px dotted #606060;
|
||||
}
|
||||
form.editor-popup-tile-editor * + h3 {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
/* Use ::before for a chevron pointing at the tile in question */
|
||||
form.editor-popup-tile-editor::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
border: 1em solid transparent;
|
||||
left: var(--chevron-offset);
|
||||
margin-left: -1em;
|
||||
|
||||
top: -1em;
|
||||
border-top: none;
|
||||
border-bottom-color: white;
|
||||
filter: drop-shadow(0 -1px 0 black);
|
||||
}
|
||||
form.editor-popup-tile-editor.--above::before {
|
||||
top: auto;
|
||||
bottom: -1em;
|
||||
border: 1em solid transparent;
|
||||
border-bottom: none;
|
||||
border-top-color: white;
|
||||
filter: drop-shadow(0 1px 0 black);
|
||||
}
|
||||
/* Letter floor tiles, which let you pick the character to use; show them as a grid. Note the
|
||||
* characters are preceded by radio buttons, which we hide for simplicity */
|
||||
ol.editor-letter-tile-picker {
|
||||
display: grid;
|
||||
grid: auto-flow 1.5em / repeat(17, 1.5em);
|
||||
text-align: center;
|
||||
font-family: monospace;
|
||||
}
|
||||
ol.editor-letter-tile-picker label,
|
||||
ol.editor-letter-tile-picker .-glyph {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
ol.editor-letter-tile-picker input[type=radio] {
|
||||
display: none;
|
||||
}
|
||||
ol.editor-letter-tile-picker input[type=radio]:checked + .-glyph {
|
||||
background: hsl(225, 75%, 90%);
|
||||
outline: 2px solid hsl(225, 75%, 80%);
|
||||
}
|
||||
/* Hint tiles accept prose */
|
||||
textarea.editor-hint-tile-text {
|
||||
font-size: 1.5em;
|
||||
width: 20vw;
|
||||
height: 20vh;
|
||||
min-width: 15rem;
|
||||
min-height: 5rem;
|
||||
border: none;
|
||||
font-family: serif;
|
||||
}
|
||||
/* Class for a list that uses hidden inputs with svg icons, shared by directional blocks and
|
||||
* railroad tracks */
|
||||
.editor-tile-editor-svg-parts input {
|
||||
display: none;
|
||||
}
|
||||
.editor-tile-editor-svg-parts svg {
|
||||
display: block;
|
||||
width: 3em;
|
||||
fill: none;
|
||||
stroke: #c0c0c0;
|
||||
stroke-width: 2;
|
||||
}
|
||||
.editor-tile-editor-svg-parts input:checked + svg {
|
||||
stroke: hsl(225, 90%, 50%);
|
||||
}
|
||||
/* Directional blocks have arrows */
|
||||
ol.editor-directional-block-tile-arrows {
|
||||
display: grid;
|
||||
grid: auto-flow 3em / repeat(3, 3em);
|
||||
}
|
||||
/* Railroad tracks are... complicated */
|
||||
ul.editor-railroad-tile-tracks {
|
||||
display: grid;
|
||||
grid: auto-flow 3em / repeat(3, 3em);
|
||||
gap: 0.25em;
|
||||
}
|
||||
ul.editor-railroad-tile-tracks.--switch input:checked + svg {
|
||||
stroke: hsl(15, 90%, 50%);
|
||||
}
|
||||
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 68 KiB |