Compare commits
No files matched your search
@@ -38,9 +38,11 @@ Give it a try, I guess! [https://c.eev.ee/lexys-labyrinth/](https://c.eev.ee/le
|
|||||||
|
|
||||||
## Special thanks
|
## Special thanks
|
||||||
|
|
||||||
- The incredible nerds who put together the [Chip Wiki](https://wiki.bitbusters.club/) and also reside on the Bit Busters Discord
|
- The incredible nerds who put together the [Chip Wiki](https://wiki.bitbusters.club/) and also reside on the Bit Busters Discord, including:
|
||||||
|
- ruben for documenting the CC2 PRNG
|
||||||
|
- The Architect for documenting the CC2 C2G parser
|
||||||
- Everyone who worked on [Chip's Challenge Level Pack 1](https://wiki.bitbusters.club/Chip%27s_Challenge_Level_Pack_1), the default set of levels
|
- Everyone who worked on [Chip's Challenge Level Pack 1](https://wiki.bitbusters.club/Chip%27s_Challenge_Level_Pack_1), the default set of levels
|
||||||
- [Tile World](https://wiki.bitbusters.club/Tile_World) for being an incredible reference on Lynx mechanics
|
- [Tile World](https://wiki.bitbusters.club/Tile_World) for being an incredible reference on Lynx mechanics
|
||||||
- Everyone who contributed music — see [`js/soundtrack.js`](js/soundtrack.js) for a list!
|
- Everyone who contributed music — see [`js/soundtrack.js`](js/soundtrack.js) for a list!
|
||||||
|
|
||||||
Not associated with or blessed by Chuck Somerville, Niffler, or AOP.
|
Not associated with or blessed by Chuck Sommerville, Niffler, or AOP.
|
||||||
|
After Width: | Height: | Size: 508 B |
|
After Width: | Height: | Size: 396 B |
|
After Width: | Height: | Size: 185 B |
@@ -10,10 +10,40 @@
|
|||||||
<meta name="og:image" content="https://c.eev.ee/lexys-labyrinth/og-preview.png">
|
<meta name="og:image" content="https://c.eev.ee/lexys-labyrinth/og-preview.png">
|
||||||
<meta name="og:title" content="Lexy's Labyrinth">
|
<meta name="og:title" content="Lexy's Labyrinth">
|
||||||
<meta name="og:description" content="A (work in progress) reimplementation of Chip's Challenge 1 and 2, using entirely free assets.">
|
<meta name="og:description" content="A (work in progress) reimplementation of Chip's Challenge 1 and 2, using entirely free assets.">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||||
</head>
|
</head>
|
||||||
<body data-mode="splash">
|
<body data-mode="splash">
|
||||||
|
<svg id="svg-iconsheet">
|
||||||
|
<defs>
|
||||||
|
<!-- Actions -->
|
||||||
|
<g id="svg-icon-up">
|
||||||
|
<path d="M0,12 l8,-8 l8,8 z"></path>
|
||||||
|
</g>
|
||||||
|
<g id="svg-icon-right">
|
||||||
|
<use href="#svg-icon-up" transform="rotate(90 8 8)"></use>
|
||||||
|
</g>
|
||||||
|
<g id="svg-icon-down">
|
||||||
|
<use href="#svg-icon-up" transform="rotate(180 8 8)"></use>
|
||||||
|
</g>
|
||||||
|
<g id="svg-icon-left">
|
||||||
|
<use href="#svg-icon-up" transform="rotate(270 8 8)"></use>
|
||||||
|
</g>
|
||||||
|
<g id="svg-icon-drop">
|
||||||
|
<path d="M6,0 h4 v9 h3 l-5,5 h7 v2 h-14 v-2 h7 l-5,-5 h3"></path>
|
||||||
|
</g>
|
||||||
|
<g id="svg-icon-cycle">
|
||||||
|
<path d="M2,3 H11 V1 l4,4 -4,4 V7 H2 Z"></path>
|
||||||
|
<path d="M14,9 H5 V7 l-4,4 4,4 v-2 h9 z"></path>
|
||||||
|
</g>
|
||||||
|
<g id="svg-icon-swap">
|
||||||
|
<path d="m 7,1 h 2 l 1,1 V 6 L 9,7 v 4 L 8,11.5 7,11 V 7 L 6,6 V 2 Z"></path>
|
||||||
|
<path d="M 8,13 13,11 8,9 3,11 Z m 0,2 7,-3 V 11 L 8,8 1,11 v 1 z"></path>
|
||||||
|
<ellipse cx="5.5" cy="11" rx="0.75" ry="0.5"></ellipse>
|
||||||
|
</g>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
<header id="header-main">
|
<header id="header-main">
|
||||||
<img src="icon.png" alt="">
|
<img id="header-icon" src="icon.png" alt="">
|
||||||
<h1>Lexy's Labyrinth</h1>
|
<h1>Lexy's Labyrinth</h1>
|
||||||
<p>— a game by <a href="https://eev.ee/">eevee</a></p>
|
<p>— a game by <a href="https://eev.ee/">eevee</a></p>
|
||||||
<nav>
|
<nav>
|
||||||
@@ -25,119 +55,244 @@
|
|||||||
<header id="header-pack">
|
<header id="header-pack">
|
||||||
<h2 id="level-pack-name">Chip's Challenge Level Pack 1</h2>
|
<h2 id="level-pack-name">Chip's Challenge Level Pack 1</h2>
|
||||||
<nav>
|
<nav>
|
||||||
|
<button id="main-test-pack" type="button">Bulk test</button>
|
||||||
<button id="main-change-pack" type="button">Change pack</button>
|
<button id="main-change-pack" type="button">Change pack</button>
|
||||||
<button id="player-edit" type="button">Edit</button>
|
<button id="player-edit" type="button">Edit</button>
|
||||||
<button id="editor-play" type="button">Test</button>
|
<button id="editor-play" type="button">Play</button>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<header id="header-level">
|
<header id="header-level">
|
||||||
<h3 id="level-name">Level 1 — Key Pyramid</h3>
|
<h3 id="level-name">Level 1 — Key Pyramid</h3>
|
||||||
<nav>
|
<nav>
|
||||||
<button id="main-prev-level" type="button">←</button>
|
<button id="main-prev-level" type="button">
|
||||||
|
<svg class="svg-icon" viewBox="0 0 16 16" title="previous"><path d="M14,1 2,8 14,14 z"></svg>
|
||||||
|
</button>
|
||||||
<button id="main-choose-level" type="button">Level select</button>
|
<button id="main-choose-level" type="button">Level select</button>
|
||||||
<button id="main-next-level" type="button">→</button>
|
<button id="main-next-level" type="button">
|
||||||
|
<svg class="svg-icon" viewBox="0 0 16 16" title="next"><path d="M2,1 14,8 2,14 z"></svg>
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main id="splash">
|
<main id="splash">
|
||||||
|
<div class="drag-overlay"></div>
|
||||||
<header>
|
<header>
|
||||||
<h1><img src="og-preview.png" alt="">Lexy's Labyrinth</h1>
|
<h1><img src="og-preview.png" alt="">Lexy's Labyrinth</h1>
|
||||||
|
<p>an unapproved Chip's Challenge emulator</p>
|
||||||
</header>
|
</header>
|
||||||
<section id="splash-intro">
|
<section id="splash-intro">
|
||||||
<p><strong>Welcome</strong> to Lexy's Labyrinth, an open source puzzle game that is curiously similar to — but legally distinct from — the Atari classic <a href="https://en.wikipedia.org/wiki/Chip%27s_Challenge">Chip's Challenge</a>!</p>
|
<p><strong>Welcome</strong> to Lexy's Labyrinth, an open source puzzle game that is curiously similar to — but legally distinct from — the Atari classic <a href="https://en.wikipedia.org/wiki/Chip%27s_Challenge">Chip's Challenge</a>!</p>
|
||||||
<p>(This is a Chip's Challenge <em>emulator</em>, designed to be an accessible way to play community-made levels with free assets. It's 99% compatible with Chip's Challenge 1, and support for Chip's Challenge 2 is underway. But you can safely ignore all that and treat this as its own game.)</p>
|
<p>Pick a level pack to get started! You can also load and play any levels you've got lying around, or brave the level editor and make one of your own.</p>
|
||||||
<p>Please note that <em>levels themselves</em> may contain hints or lore referring to a guy named Chip collecting computer chips, even though you are clearly a fox named Lexy collecting hearts. Weird, right? Sorry for any confusion!</p>
|
<p>If you're not familiar with the game, read up on <a href="https://github.com/eevee/lexys-labyrinth/wiki/How-To-Play">how to play</a>. You can also get more technical details or report bugs on <a href="https://github.com/eevee/lexys-labyrinth">GitHub</a>, find out more about Chip's Challenge via the <a href="https://bitbusters.club/">Bit Busters Club</a> fansite, or support this endeavor (and other things I do) on <a href="https://www.patreon.com/eevee">Patreon</a>.</p>
|
||||||
<p>Pick a level pack to get started! You can also get more technical details or report bugs on <a href="https://github.com/eevee/lexys-labyrinth">GitHub</a>, find out more about Chip's Challenge via the <a href="https://bitbusters.club/">Bit Busters Club</a> fansite, or support this endeavor (and other things I do) via <a href="https://www.patreon.com/eevee">Patreon</a>!</p>
|
|
||||||
<!-- TODO i want to make clear this is a chip's challenge emulator without bogging people down too much about what that means -->
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="splash-stock-levels">
|
<section id="splash-stock-levels">
|
||||||
<h2>Just play something</h2>
|
<h2>Play community levels</h2>
|
||||||
<!-- populated by js -->
|
<!-- populated by js -->
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="splash-upload-levels">
|
<section id="splash-upload-levels">
|
||||||
<h2>Other levels</h2>
|
<h2>Load other levels</h2>
|
||||||
<p>You can play <code>CHIPS.DAT</code> from the original Microsoft version, any custom levels you have lying around, or perhaps ones you found on the <a href="https://sets.bitbusters.club/">Bit Busters Club set list</a>!</p>
|
<p>Load and play any levels you have on hand, including the original levels. Supports CCL/DAT, C2G, and individual C2Ms (though scores aren't saved for those). Find more on the <a href="https://sets.bitbusters.club/">Bit Busters Club set list</a>.</p>
|
||||||
<!-- TODO explain how to find chips.dat or steam folder -->
|
<!-- TODO zip files! -->
|
||||||
<!-- TODO drag and drop? -->
|
<p>You can also drag and drop files or directories into this window.</p>
|
||||||
<input id="splash-upload" type="file" accept=".dat,.ccl,.c2m,.ccs">
|
<input id="splash-upload-file" type="file" accept=".dat,.ccl,.c2m,.ccs" multiple>
|
||||||
<button type="button" id="splash-upload-button" class="button-big">Open a local level<!-- TODO: <br>(or drag and drop a file into this window) --></button>
|
<input id="splash-upload-dir" type="file" webkitdirectory>
|
||||||
<p>Supports both the old Microsoft <code>CHIPS.DAT</code> format and the Steam <code>C2M</code> format.</p>
|
<button type="button" id="splash-upload-file-button" class="button-big">Load files</button>
|
||||||
<p>Does <em>not</em> yet support the Steam <code>C2G</code> format, so tragically, the original Steam levels can only be played one at a time. This should be fixed soon!</p>
|
<button type="button" id="splash-upload-dir-button" class="button-big">Load directory</button>
|
||||||
<!--
|
<p>If you still have the original Microsoft "BOWEP" game lying around, you can play the Chip's Challenge 1 levels by loading <code>CHIPS.DAT</code>.</p>
|
||||||
<p>If you own the Steam versions of <a href="https://store.steampowered.com/app/346850/Chips_Challenge_1/">Chip's Challenge 1</a> (<em>free!</em>) or <a href="https://store.steampowered.com/app/348300/Chips_Challenge_2/">Chip's Challenge 2</a> ($5 last I checked), you can play those too, even on Linux or Mac:</p>
|
<p>If you own the Steam versions of <a href="https://store.steampowered.com/app/346850/Chips_Challenge_1/">Chip's Challenge 1</a> (<em>free!</em>) or <a href="https://store.steampowered.com/app/348300/Chips_Challenge_2/">Chip's Challenge 2</a> ($5 last I checked), you can play those too, even on Linux or Mac:</p>
|
||||||
<ol class="normal-list">
|
<ol class="normal-list">
|
||||||
<li>Right-click the game in Steam and choose <em>Properties</em>. On the <em>Local Files</em> tab, click <em>Browse local files</em>.</li>
|
<li>Right-click the game in Steam and choose <em>Properties</em>. On the <em>Local Files</em> tab, click <em>Browse local files</em>.</li>
|
||||||
<li>Open the <code>data</code> folder, then <code>games</code>.</li>
|
<li>Open the <code>data</code> folder, then <code>games</code>.</li>
|
||||||
<li>You should see either a <code>cc1</code> or <code>cc2</code> folder. Drag it into this window.</li>
|
<li>You should see either a <code>cc1</code> or <code>cc2</code> folder. Drag it into this window, or load it with the button above.</li>
|
||||||
</ol>
|
</ol>
|
||||||
-->
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="splash-your-levels">
|
<section id="splash-your-levels">
|
||||||
<h2>Make your own (WIP lol)</h2>
|
<h2>Make your own (WIP lol)</h2>
|
||||||
<p>Please note that the level editor is <strong>extremely</strong> unfinished, and can't even save yet.</p>
|
<p>Please note that the level editor is <strong>very</strong> unfinished, and doesn't even have undo yet. It may eat your work! Beware!</p>
|
||||||
<p><button type="button" id="splash-create-level" class="button-big">Create a level</button></p>
|
<p><button type="button" id="splash-create-pack" class="button-big">New pack</button></p>
|
||||||
|
<p><button type="button" id="splash-create-level" class="button-big">New scratch level<br>(won't be saved!)</button></p>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<main id="player" hidden>
|
<main id="player" hidden>
|
||||||
<section class="-main-area">
|
<div id="player-main">
|
||||||
<div class="level"><!-- level canvas and any overlays go here --></div>
|
<div class="controls">
|
||||||
<div class="overlay-message">
|
<div class="play-controls">
|
||||||
<h1 class="-top"></h1>
|
<button class="control-pause" type="button" title="pause">
|
||||||
<div class="-middle"></div>
|
<svg class="svg-icon" viewBox="0 0 16 16"><path d="M2,1 h4 v14 h-4 z M10,1 h4 v14 h-4 z"></svg>
|
||||||
<p class="-bottom"></p>
|
<span class="keyhint">p</span></button>
|
||||||
<p class="-keyhint"></p>
|
<button class="control-restart" type="button" title="restart">
|
||||||
|
<svg class="svg-icon" viewBox="0 0 16 16"><path d="M13,13 A 7,7 270 1,1 13,3 L15,1 15,7 9,7 11,5 A 4,4 270 1,0 11,11 z"></svg>
|
||||||
|
</button>
|
||||||
|
<button class="control-undo" type="button" title="undo">
|
||||||
|
<svg class="svg-icon" viewBox="0 0 16 16"><path d="M6,5 6,2 1,7 6,12 6,9 A 10,10 60 0,1 15,12 A 10,10 90 0,0 6,5"></svg>
|
||||||
|
</button>
|
||||||
|
<button class="control-rewind" type="button" title="rewind">
|
||||||
|
<svg class="svg-icon" viewBox="0 0 16 16"><path d="M1,8 7,2 7,14 z M9,8 15,2 15,14 z"></svg>
|
||||||
|
<span class="keyhint">z</span></button>
|
||||||
|
<label><input class="control-turn-based" type="checkbox"> Turn-based mode</label>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="action-drop" type="button">
|
||||||
|
<svg class="svg-icon" viewBox="0 0 16 16"><use href="#svg-icon-drop"></use></svg>
|
||||||
|
drop <span class="keyhint">q</span></button>
|
||||||
|
<button class="action-cycle" type="button">
|
||||||
|
<svg class="svg-icon" viewBox="0 0 16 16"><use href="#svg-icon-cycle"></use></svg>
|
||||||
|
cycle <span class="keyhint">e</span></button>
|
||||||
|
<button class="action-swap" type="button">
|
||||||
|
<svg class="svg-icon" viewBox="0 0 16 16"><use href="#svg-icon-swap"></use></svg>
|
||||||
|
switch <span class="keyhint">c</span></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="message"></div>
|
<section id="player-game-area">
|
||||||
<div class="chips">
|
<div class="level"><!-- level canvas and any overlays go here --></div>
|
||||||
<h3>Hearts</h3>
|
<div class="overlay-message">
|
||||||
<output></output>
|
<h1 class="-top"></h1>
|
||||||
</div>
|
<div class="-middle"></div>
|
||||||
<div class="time">
|
<p class="-bottom"></p>
|
||||||
<h3>Time</h3>
|
<p class="-keyhint"></p>
|
||||||
<output></output>
|
</div>
|
||||||
</div>
|
<div class="message"></div>
|
||||||
<div class="bonus">
|
<div class="chips">
|
||||||
<h3>Bonus</h3>
|
<h3>Hearts</h3>
|
||||||
<output></output>
|
<output></output>
|
||||||
</div>
|
</div>
|
||||||
<div class="inventory"></div>
|
<div class="time">
|
||||||
</section>
|
<h3>Time</h3>
|
||||||
<div id="player-music">
|
<output></output>
|
||||||
<div id="player-music-left">
|
</div>
|
||||||
🎵 <a id="player-music-title">title</a> by <a id="player-music-author">author</a>
|
<div class="bonus">
|
||||||
</div>
|
<h3>Bonus</h3>
|
||||||
<div id="player-music-right">
|
<output></output>
|
||||||
<input id="player-music-volume" type="range" min="0" max="1" step="0.05" value="1">
|
</div>
|
||||||
<input id="player-music-unmute" type="checkbox" checked>
|
<div class="inventory"></div>
|
||||||
</div>
|
</section>
|
||||||
<audio loop preload="auto">
|
<div id="player-music">
|
||||||
</div>
|
<div id="player-music-left">
|
||||||
<div class="controls">
|
🎵 <a id="player-music-title">title</a> by <a id="player-music-author">author</a>
|
||||||
<div class="play-controls">
|
</div>
|
||||||
<button class="control-pause" type="button">Pause (p)</button>
|
<div id="player-music-right">
|
||||||
<button class="control-restart" type="button">Restart</button>
|
<input id="player-music-volume" type="range" min="0" max="1" step="0.05" value="1">
|
||||||
<button class="control-undo" type="button">Undo</button>
|
<input id="player-music-unmute" type="checkbox" checked>
|
||||||
<button class="control-rewind" type="button">Rewind (z)</button>
|
</div>
|
||||||
</div>
|
<audio loop preload="auto">
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- TODO debug panel?
|
<form id="player-debug">
|
||||||
- current tic
|
<h3>Time</h3>
|
||||||
- list of actors, or currently pointed-to actor?
|
<table class="-time-controls">
|
||||||
<div class="debug">
|
<tr>
|
||||||
<button>« 4 tics</button>
|
<td><button type="button" class="-time-button" data-dt="-1">← 1 tic</button></td>
|
||||||
<button>« 1 tic</button>
|
<td id="player-debug-time-tics">0</td>
|
||||||
<button>1 tic »</button>
|
<td>tics</td>
|
||||||
<button>4 tics »</button>
|
<td><button type="button" class="-time-button" data-dt="1">1 tic →</button></td>
|
||||||
</div>
|
</tr>
|
||||||
-->
|
<tr>
|
||||||
|
<td><button type="button" class="-time-button" data-dt="-4">← 1 move</button></td>
|
||||||
|
<td id="player-debug-time-moves">0</td>
|
||||||
|
<td>moves</td>
|
||||||
|
<td><button type="button" class="-time-button" data-dt="4">1 move →</button></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><button type="button" class="-time-button" data-dt="-20">← 1 s</button></td>
|
||||||
|
<td id="player-debug-time-secs">0</td>
|
||||||
|
<td>seconds</td>
|
||||||
|
<td><button type="button" class="-time-button" data-dt="20">1 s →</button></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div class="-buttons" id="player-debug-time-buttons">
|
||||||
|
<!-- populated in js -->
|
||||||
|
</div>
|
||||||
|
<p>Game speed:
|
||||||
|
<select name="speed">
|
||||||
|
<option value="100">100× faster</option>
|
||||||
|
<option value="50">50× faster</option>
|
||||||
|
<option value="20">20× faster</option>
|
||||||
|
<option value="10">10× faster</option>
|
||||||
|
<option value="5">5× faster</option>
|
||||||
|
<option value="3">3× faster</option>
|
||||||
|
<option value="2">2× faster</option>
|
||||||
|
<option value="3/2">1½× faster</option>
|
||||||
|
<option value="1" selected>Normal speed</option>
|
||||||
|
<option value="1/2">2× slower</option>
|
||||||
|
<option value="1/3">3× slower</option>
|
||||||
|
<option value="1/4">4× slower</option>
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Inventory</h3>
|
||||||
|
<div class="-inventory">
|
||||||
|
<!-- populated in js -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Replay</h3>
|
||||||
|
<!-- TODO...
|
||||||
|
play back replay
|
||||||
|
record replay, including altering it from here
|
||||||
|
stop replay, without restarting the level
|
||||||
|
show progress in %, length in tics + time
|
||||||
|
browse replay? jump to any point? label points???
|
||||||
|
edit manually?
|
||||||
|
-->
|
||||||
|
<div class="-replay-columns">
|
||||||
|
<div id="player-debug-input"></div>
|
||||||
|
<div class="-replay-status">
|
||||||
|
<!-- This should be a fixed height and is always showing one of the following -->
|
||||||
|
<div class="-none">No replay in progress</div>
|
||||||
|
<div class="-playback">
|
||||||
|
<progress max="0" value="0"></progress>
|
||||||
|
<output>100%</output>
|
||||||
|
<span>0 tics (0:00s)</span>
|
||||||
|
<button disabled>Relinquish control</button>
|
||||||
|
</div>
|
||||||
|
<div class="-recording">Recording...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- js inserts a bunch of stuff here -->
|
||||||
|
|
||||||
|
<h3>Misc</h3>
|
||||||
|
<p>Viewport size:
|
||||||
|
<select id="player-debug-viewport">
|
||||||
|
<option value="default" selected>Standard</option>
|
||||||
|
<option value="12">12 × 12</option>
|
||||||
|
<option value="16">16 × 16</option>
|
||||||
|
<option value="24">24 × 24</option>
|
||||||
|
<option value="32">32 × 32</option>
|
||||||
|
<option value="max">Entire level</option>
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><label><input type="checkbox" name="show_actor_bboxes"> Show actor bounding boxes</label></li>
|
||||||
|
<li><label><input type="checkbox" name="disable_interpolation"> Disable interpolation</label></li>
|
||||||
|
<!--
|
||||||
|
<li><label><input type="checkbox" disabled> Show actor info</label></li>
|
||||||
|
<li><label><input type="checkbox" disabled> Freeze time for everything else</label></li>
|
||||||
|
<li><label><input type="checkbox" disabled> Player is immortal</label></li>
|
||||||
|
<li><label><input type="checkbox" disabled> Player ignores collision</label></li>
|
||||||
|
<li><label><input type="checkbox" disabled> Player levitates</label></li>
|
||||||
|
-->
|
||||||
|
</ul>
|
||||||
|
<div class="-buttons" id="player-debug-misc-buttons">
|
||||||
|
<!-- populated in js -->
|
||||||
|
</div>
|
||||||
|
<p>Tip: Middle-click to teleport.</p>
|
||||||
|
|
||||||
|
<!-- TODO?
|
||||||
|
- inspect with mouse
|
||||||
|
- list of actors, or currently pointed-to actor?
|
||||||
|
- activate something manually?
|
||||||
|
- click a button ingame?
|
||||||
|
- pan viewport (like editor)
|
||||||
|
- show connections, directions, other editor features
|
||||||
|
- look under anything
|
||||||
|
|
||||||
|
- other game info?
|
||||||
|
- count tiles?
|
||||||
|
- total hearts?
|
||||||
|
- total bonus flags?
|
||||||
|
-->
|
||||||
|
</form>
|
||||||
</main>
|
</main>
|
||||||
<main id="editor" hidden>
|
<main id="editor" hidden>
|
||||||
<header>
|
<header>
|
||||||
@@ -151,9 +306,15 @@
|
|||||||
also deal with levels vs level /packs/ somehow, not sure how that'll work (including downloading them, yeargh?)
|
also deal with levels vs level /packs/ somehow, not sure how that'll work (including downloading them, yeargh?)
|
||||||
-->
|
-->
|
||||||
</header>
|
</header>
|
||||||
<div class="level"><!-- level canvas and any overlays go here --></div>
|
<div class="editor-canvas">
|
||||||
<div class="controls">
|
<div class="-container">
|
||||||
<button id="editor-share-url" type="button">Share?</button>
|
<!-- level canvas and any overlays go here -->
|
||||||
|
<!-- the container is to allow them to scroll as a single unit -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav class="controls">
|
||||||
|
<div id="editor-tile">
|
||||||
|
</div>
|
||||||
<!--
|
<!--
|
||||||
<p style>
|
<p style>
|
||||||
Tip: Right click to color drop.<br>
|
Tip: Right click to color drop.<br>
|
||||||
@@ -178,7 +339,7 @@
|
|||||||
map size
|
map size
|
||||||
</pre>
|
</pre>
|
||||||
-->
|
-->
|
||||||
</div>
|
</nav>
|
||||||
<div class="palette"></div>
|
<div class="palette"></div>
|
||||||
<!-- TODO:
|
<!-- TODO:
|
||||||
controls
|
controls
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export const DIRECTIONS = {
|
|||||||
north: {
|
north: {
|
||||||
movement: [0, -1],
|
movement: [0, -1],
|
||||||
bit: 0x01,
|
bit: 0x01,
|
||||||
|
index: 0,
|
||||||
action: 'up',
|
action: 'up',
|
||||||
left: 'west',
|
left: 'west',
|
||||||
right: 'east',
|
right: 'east',
|
||||||
@@ -12,6 +13,7 @@ export const DIRECTIONS = {
|
|||||||
south: {
|
south: {
|
||||||
movement: [0, 1],
|
movement: [0, 1],
|
||||||
bit: 0x04,
|
bit: 0x04,
|
||||||
|
index: 2,
|
||||||
action: 'down',
|
action: 'down',
|
||||||
left: 'east',
|
left: 'east',
|
||||||
right: 'west',
|
right: 'west',
|
||||||
@@ -20,6 +22,7 @@ export const DIRECTIONS = {
|
|||||||
west: {
|
west: {
|
||||||
movement: [-1, 0],
|
movement: [-1, 0],
|
||||||
bit: 0x08,
|
bit: 0x08,
|
||||||
|
index: 3,
|
||||||
action: 'left',
|
action: 'left',
|
||||||
left: 'south',
|
left: 'south',
|
||||||
right: 'north',
|
right: 'north',
|
||||||
@@ -28,9 +31,61 @@ export const DIRECTIONS = {
|
|||||||
east: {
|
east: {
|
||||||
movement: [1, 0],
|
movement: [1, 0],
|
||||||
bit: 0x02,
|
bit: 0x02,
|
||||||
|
index: 1,
|
||||||
action: 'right',
|
action: 'right',
|
||||||
left: 'north',
|
left: 'north',
|
||||||
right: 'south',
|
right: 'south',
|
||||||
opposite: 'west',
|
opposite: 'west',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
// Should match the bit ordering above, and CC2's order
|
||||||
|
export const DIRECTION_ORDER = ['north', 'east', 'south', 'west'];
|
||||||
|
|
||||||
|
export const INPUT_BITS = {
|
||||||
|
drop: 0x01,
|
||||||
|
down: 0x02,
|
||||||
|
left: 0x04,
|
||||||
|
right: 0x08,
|
||||||
|
up: 0x10,
|
||||||
|
swap: 0x20,
|
||||||
|
cycle: 0x40,
|
||||||
|
// Not real input; used to force advancement for turn-based mode
|
||||||
|
wait: 0x8000,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO cc2 order is: swivel, thinwalls, canopy (and yes you can have them all in the same tile)
|
||||||
|
export const DRAW_LAYERS = {
|
||||||
|
terrain: 0,
|
||||||
|
item: 1,
|
||||||
|
item_mod: 2,
|
||||||
|
actor: 3,
|
||||||
|
overlay: 4,
|
||||||
|
MAX: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const COLLISION = {
|
||||||
|
player1: 0x0001,
|
||||||
|
player2: 0x0002,
|
||||||
|
player: 0x0003,
|
||||||
|
|
||||||
|
block_cc1: 0x0004,
|
||||||
|
block_cc2: 0x0008, // ice + directional
|
||||||
|
|
||||||
|
// Monsters are a little complicated, because some of them have special rules, e.g. fireballs
|
||||||
|
// aren't blocked by fire.
|
||||||
|
// For a monster's MASK, you should use ONLY ONE of these specific monster bits (if
|
||||||
|
// appropriate), OR the generic bit -- DO NOT combine them!
|
||||||
|
monster_generic: 0x0100,
|
||||||
|
fireball: 0x0200,
|
||||||
|
bug: 0x0400,
|
||||||
|
rover: 0x1000,
|
||||||
|
ghost: 0x8000,
|
||||||
|
// For a tile's COLLISION, use one of these bit combinations
|
||||||
|
monster_solid: 0x7f00, // everything but ghost
|
||||||
|
monster_any: 0xff00, // everything including ghost
|
||||||
|
|
||||||
|
// Combo masks used for matching
|
||||||
|
all_but_ghost: 0xffff & ~0x8000,
|
||||||
|
all_but_player: 0xffff & ~0x0003,
|
||||||
|
all: 0xffff,
|
||||||
|
};
|
||||||
@@ -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,148 @@
|
|||||||
|
import * as util from './util.js';
|
||||||
|
|
||||||
|
export class StoredCell extends Array {
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Replay {
|
||||||
|
constructor(initial_force_floor_direction, blob_seed, inputs = null) {
|
||||||
|
this.initial_force_floor_direction = initial_force_floor_direction;
|
||||||
|
this.blob_seed = blob_seed;
|
||||||
|
this.inputs = inputs ?? new Uint8Array;
|
||||||
|
this.duration = this.inputs.length;
|
||||||
|
this.cursor = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(t) {
|
||||||
|
if (this.duration <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
else if (t < this.duration) {
|
||||||
|
return this.inputs[t];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Last input is implicitly repeated indefinitely
|
||||||
|
return this.inputs[this.duration - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set(t, input) {
|
||||||
|
if (t >= this.inputs.length) {
|
||||||
|
let new_inputs = new Uint8Array(this.inputs.length + 1024);
|
||||||
|
for (let i = 0; i < this.inputs.length; i++) {
|
||||||
|
new_inputs[i] = this.inputs[i];
|
||||||
|
}
|
||||||
|
this.inputs = new_inputs;
|
||||||
|
}
|
||||||
|
this.inputs[t] = input;
|
||||||
|
if (t >= this.duration) {
|
||||||
|
this.duration = t + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clone() {
|
||||||
|
let new_inputs = new Uint8Array(this.duration);
|
||||||
|
for (let i = 0; i < this.duration; i++) {
|
||||||
|
new_inputs[i] = this.inputs[i];
|
||||||
|
}
|
||||||
|
return new this.constructor(this.initial_force_floor_direction, this.blob_seed, new_inputs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StoredLevel {
|
||||||
|
constructor(number) {
|
||||||
|
// TODO still not sure this belongs here
|
||||||
|
this.number = number; // one-based
|
||||||
|
this.title = '';
|
||||||
|
this.author = '';
|
||||||
|
this.password = null;
|
||||||
|
this.hint = '';
|
||||||
|
this.chips_required = 0;
|
||||||
|
this.time_limit = 0;
|
||||||
|
this.viewport_size = 9;
|
||||||
|
this.extra_chunks = [];
|
||||||
|
this.use_cc1_boots = false;
|
||||||
|
this.use_ccl_compat = false;
|
||||||
|
// 0 - deterministic (PRNG + simple convolution)
|
||||||
|
// 1 - 4 patterns (default; PRNG + rotating through 0-3)
|
||||||
|
// 2 - extra random (like deterministic, but initial seed is "actually" random)
|
||||||
|
this.blob_behavior = 1;
|
||||||
|
|
||||||
|
// Lazy-loading that allows for checking existence (see methods below)
|
||||||
|
// TODO this needs a better interface, these get accessed too much atm
|
||||||
|
this._replay = null;
|
||||||
|
this._replay_data = null;
|
||||||
|
this._replay_decoder = null;
|
||||||
|
|
||||||
|
this.size_x = 0;
|
||||||
|
this.size_y = 0;
|
||||||
|
this.linear_cells = [];
|
||||||
|
|
||||||
|
// Maps of button positions to trap/cloner positions, as scalar indexes
|
||||||
|
// in the linear cell list
|
||||||
|
// TODO merge these imo
|
||||||
|
this.has_custom_connections = false;
|
||||||
|
this.custom_trap_wiring = {};
|
||||||
|
this.custom_cloner_wiring = {};
|
||||||
|
|
||||||
|
// New LL feature: custom camera regions, as lists of {x, y, width, height}
|
||||||
|
this.camera_regions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
scalar_to_coords(n) {
|
||||||
|
return [n % this.size_x, Math.floor(n / this.size_x)];
|
||||||
|
}
|
||||||
|
|
||||||
|
coords_to_scalar(x, y) {
|
||||||
|
return x + y * this.size_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
check() {
|
||||||
|
}
|
||||||
|
|
||||||
|
get has_replay() {
|
||||||
|
return this._replay || (this._replay_data && this._replay_decoder);
|
||||||
|
}
|
||||||
|
|
||||||
|
get replay() {
|
||||||
|
if (! this._replay) {
|
||||||
|
this._replay = this._replay_decoder(this._replay_data);
|
||||||
|
}
|
||||||
|
return this._replay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StoredPack {
|
||||||
|
constructor(identifier, level_loader) {
|
||||||
|
this.identifier = identifier;
|
||||||
|
this.title = "";
|
||||||
|
this._level_loader = level_loader;
|
||||||
|
|
||||||
|
// Simple objects containing keys that are usually:
|
||||||
|
// title: level title
|
||||||
|
// index: level index, used internally only
|
||||||
|
// number: level number (may not match index due to C2G shenanigans)
|
||||||
|
// error: any error received while loading the level
|
||||||
|
// bytes: Uint8Array of the encoded level data
|
||||||
|
this.level_metadata = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO this may or may not work sensibly when correctly following a c2g
|
||||||
|
load_level(index) {
|
||||||
|
let meta = this.level_metadata[index];
|
||||||
|
if (! meta)
|
||||||
|
throw new util.LLError(`No such level number ${index}`);
|
||||||
|
if (meta.error)
|
||||||
|
throw meta.error;
|
||||||
|
|
||||||
|
if (meta.stored_level) {
|
||||||
|
// The editor stores inflated levels at times, so respect that
|
||||||
|
return meta.stored_level;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Otherwise, attempt to load the level
|
||||||
|
return this._level_loader(meta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StoredGame = StoredPack;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as util from './format-util.js';
|
import * as format_base from './format-base.js';
|
||||||
import TILE_TYPES from './tiletypes.js';
|
import TILE_TYPES from './tiletypes.js';
|
||||||
|
import * as util from './util.js';
|
||||||
|
|
||||||
const TILE_ENCODING = {
|
const TILE_ENCODING = {
|
||||||
0x00: 'floor',
|
0x00: 'floor',
|
||||||
@@ -119,18 +120,70 @@ const TILE_ENCODING = {
|
|||||||
0x6f: ['player', 'east'],
|
0x6f: ['player', 'east'],
|
||||||
};
|
};
|
||||||
|
|
||||||
function parse_level(buf, number) {
|
function decode_password(bytes, start, len) {
|
||||||
let level = new util.StoredLevel(number);
|
let password = [];
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
password.push(bytes[start + i] ^ 0x99);
|
||||||
|
}
|
||||||
|
return String.fromCharCode.apply(null, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parse_level_metadata(bytes) {
|
||||||
|
let meta = {};
|
||||||
|
|
||||||
|
let view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||||
|
|
||||||
|
// Level number; rest of level header is unused
|
||||||
|
meta.number = view.getUint16(0, true);
|
||||||
|
|
||||||
|
// Map layout
|
||||||
|
// Same structure twice, for the two layers
|
||||||
|
let p = 8;
|
||||||
|
for (let l = 0; l < 2; l++) {
|
||||||
|
let layer_length = view.getUint16(p, true);
|
||||||
|
p += 2 + layer_length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional metadata fields
|
||||||
|
let meta_length = view.getUint16(p, true);
|
||||||
|
p += 2;
|
||||||
|
let end = p + meta_length;
|
||||||
|
while (p < end) {
|
||||||
|
// Common header
|
||||||
|
let field_type = view.getUint8(p, true);
|
||||||
|
let field_length = view.getUint8(p + 1, true);
|
||||||
|
p += 2;
|
||||||
|
if (field_type === 0x03) {
|
||||||
|
// Title, including trailing NUL
|
||||||
|
meta.title = util.string_from_buffer_ascii(bytes, p, field_length - 1);
|
||||||
|
}
|
||||||
|
else if (field_type === 0x06) {
|
||||||
|
// Password, with trailing NUL, and XORed with 0x99 (???)
|
||||||
|
meta.password = decode_password(bytes, p, field_length - 1);
|
||||||
|
}
|
||||||
|
else if (field_type === 0x07) {
|
||||||
|
// Hint, including trailing NUL, of course
|
||||||
|
meta.hint = util.string_from_buffer_ascii(bytes, p, field_length - 1);
|
||||||
|
}
|
||||||
|
p += field_length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse_level(bytes, number) {
|
||||||
|
let level = new format_base.StoredLevel(number);
|
||||||
|
level.has_custom_connections = true;
|
||||||
|
level.use_ccl_compat = true;
|
||||||
// Map size is always fixed as 32x32 in CC1
|
// Map size is always fixed as 32x32 in CC1
|
||||||
level.size_x = 32;
|
level.size_x = 32;
|
||||||
level.size_y = 32;
|
level.size_y = 32;
|
||||||
for (let i = 0; i < 1024; i++) {
|
for (let i = 0; i < 1024; i++) {
|
||||||
level.linear_cells.push(new util.StoredCell);
|
level.linear_cells.push(new format_base.StoredCell);
|
||||||
}
|
}
|
||||||
level.use_cc1_boots = true;
|
level.use_cc1_boots = true;
|
||||||
|
|
||||||
let view = new DataView(buf);
|
let view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||||
let bytes = new Uint8Array(buf);
|
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
let level_number = view.getUint16(0, true);
|
let level_number = view.getUint16(0, true);
|
||||||
@@ -161,7 +214,7 @@ function parse_level(buf, number) {
|
|||||||
// TODO could be more forgiving for goofy levels doing goofy things
|
// TODO could be more forgiving for goofy levels doing goofy things
|
||||||
if (! spec) {
|
if (! spec) {
|
||||||
let [x, y] = level.scalar_to_coords(c);
|
let [x, y] = level.scalar_to_coords(c);
|
||||||
throw new Error(`Invalid tile byte 0x${tile_byte.toString(16)} at (${x}, ${y}) in level ${number}`);
|
throw new Error(`Invalid tile byte 0x${tile_byte.toString(16)} at (${x}, ${y})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let name, direction;
|
let name, direction;
|
||||||
@@ -225,11 +278,11 @@ function parse_level(buf, number) {
|
|||||||
}
|
}
|
||||||
else if (field_type === 0x03) {
|
else if (field_type === 0x03) {
|
||||||
// Title, including trailing NUL
|
// Title, including trailing NUL
|
||||||
level.title = util.string_from_buffer_ascii(buf.slice(p, p + field_length - 1));
|
level.title = util.string_from_buffer_ascii(bytes, p, field_length - 1);
|
||||||
}
|
}
|
||||||
else if (field_type === 0x04) {
|
else if (field_type === 0x04) {
|
||||||
// Trap linkages (MSCC only, not in Lynx or CC2)
|
// Trap linkages (MSCC only, not in Lynx or CC2)
|
||||||
let field_view = new DataView(buf.slice(p, p + field_length));
|
let field_view = new DataView(bytes.buffer, bytes.byteOffset + p, field_length);
|
||||||
let q = 0;
|
let q = 0;
|
||||||
while (q < field_length) {
|
while (q < field_length) {
|
||||||
let button_x = field_view.getUint16(q + 0, true);
|
let button_x = field_view.getUint16(q + 0, true);
|
||||||
@@ -243,7 +296,7 @@ function parse_level(buf, number) {
|
|||||||
}
|
}
|
||||||
else if (field_type === 0x05) {
|
else if (field_type === 0x05) {
|
||||||
// Cloner linkages (MSCC only, not in Lynx or CC2)
|
// Cloner linkages (MSCC only, not in Lynx or CC2)
|
||||||
let field_view = new DataView(buf.slice(p, p + field_length));
|
let field_view = new DataView(bytes.buffer, bytes.byteOffset + p, field_length);
|
||||||
let q = 0;
|
let q = 0;
|
||||||
while (q < field_length) {
|
while (q < field_length) {
|
||||||
let button_x = field_view.getUint16(q + 0, true);
|
let button_x = field_view.getUint16(q + 0, true);
|
||||||
@@ -255,16 +308,12 @@ function parse_level(buf, number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (field_type === 0x06) {
|
else if (field_type === 0x06) {
|
||||||
// Password, with trailing NUL, and otherwise XORed with 0x99 (?!)
|
// Password, with trailing NUL, and otherwise XORed with 0x99 (???)
|
||||||
let password = [];
|
level.password = decode_password(bytes, p, field_length - 1);
|
||||||
for (let i = 0; i < field_length - 1; i++) {
|
|
||||||
password.push(view.getUint8(p + i, true) ^ 0x99);
|
|
||||||
}
|
|
||||||
level.password = String.fromCharCode.apply(null, password);
|
|
||||||
}
|
}
|
||||||
else if (field_type === 0x07) {
|
else if (field_type === 0x07) {
|
||||||
// Hint, including trailing NUL, of course
|
// Hint, including trailing NUL, of course
|
||||||
level.hint = util.string_from_buffer_ascii(buf.slice(p, p + field_length - 1));
|
level.hint = util.string_from_buffer_ascii(bytes, p, field_length - 1);
|
||||||
}
|
}
|
||||||
else if (field_type === 0x08) {
|
else if (field_type === 0x08) {
|
||||||
// Password, but not encoded
|
// Password, but not encoded
|
||||||
@@ -280,8 +329,13 @@ function parse_level(buf, number) {
|
|||||||
return level;
|
return level;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This thin wrapper is passed to StoredGame as the parser function
|
||||||
|
function _parse_level_from_stored_meta(meta) {
|
||||||
|
return parse_level(meta.bytes, meta.number);
|
||||||
|
}
|
||||||
|
|
||||||
export function parse_game(buf) {
|
export function parse_game(buf) {
|
||||||
let game = new util.StoredGame;
|
let game = new format_base.StoredGame(null, _parse_level_from_stored_meta);
|
||||||
|
|
||||||
let full_view = new DataView(buf);
|
let full_view = new DataView(buf);
|
||||||
let magic = full_view.getUint32(0, true);
|
let magic = full_view.getUint32(0, true);
|
||||||
@@ -303,11 +357,19 @@ export function parse_game(buf) {
|
|||||||
let p = 6;
|
let p = 6;
|
||||||
for (let l = 1; l <= level_count; l++) {
|
for (let l = 1; l <= level_count; l++) {
|
||||||
let length = full_view.getUint16(p, true);
|
let length = full_view.getUint16(p, true);
|
||||||
let level_buf = buf.slice(p + 2, p + 2 + length);
|
let bytes = new Uint8Array(buf, p + 2, length);
|
||||||
p += 2 + length;
|
p += 2 + length;
|
||||||
|
|
||||||
let level = parse_level(level_buf, l);
|
let meta;
|
||||||
game.levels.push(level);
|
try {
|
||||||
|
meta = parse_level_metadata(bytes);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
meta = {error: e};
|
||||||
|
}
|
||||||
|
meta.index = l - 1;
|
||||||
|
meta.bytes = bytes;
|
||||||
|
game.level_metadata.push(meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
return game;
|
return game;
|
||||||
|
|||||||
@@ -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,140 @@
|
|||||||
|
import { mk, mk_svg, walk_grid } from './util.js';
|
||||||
|
|
||||||
|
// Superclass for the main display modes: the player, the editor, and the splash screen
|
||||||
|
export class PrimaryView {
|
||||||
|
constructor(conductor, root) {
|
||||||
|
this.conductor = conductor;
|
||||||
|
this.root = root;
|
||||||
|
this.active = false;
|
||||||
|
this._done_setup = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setup() {}
|
||||||
|
|
||||||
|
activate() {
|
||||||
|
this.root.removeAttribute('hidden');
|
||||||
|
this.active = true;
|
||||||
|
if (! this._done_setup) {
|
||||||
|
this.setup();
|
||||||
|
this._done_setup = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivate() {
|
||||||
|
this.root.setAttribute('hidden', '');
|
||||||
|
this.active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stackable modal overlay of some kind, usually a dialog
|
||||||
|
export class Overlay {
|
||||||
|
constructor(conductor, root) {
|
||||||
|
this.conductor = conductor;
|
||||||
|
this.root = root;
|
||||||
|
|
||||||
|
// Don't propagate clicks on the root element, so they won't trigger a
|
||||||
|
// parent overlay's automatic dismissal
|
||||||
|
this.root.addEventListener('click', ev => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
});
|
||||||
|
// Block any window-level key handlers from firing when we type
|
||||||
|
this.root.addEventListener('keydown', ev => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
// FIXME ah, but keystrokes can still go to the game, including
|
||||||
|
// spacebar to begin it if it was waiting. how do i completely disable
|
||||||
|
// an entire chunk of the page?
|
||||||
|
if (this.conductor.player.state === 'playing') {
|
||||||
|
this.conductor.player.set_state('paused');
|
||||||
|
}
|
||||||
|
|
||||||
|
let overlay = mk('div.overlay', this.root);
|
||||||
|
document.body.append(overlay);
|
||||||
|
|
||||||
|
// Remove the overlay when clicking outside the element
|
||||||
|
overlay.addEventListener('click', ev => {
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.root.closest('.overlay').remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overlay styled like a popup of some sort
|
||||||
|
export class TransientOverlay extends Overlay {
|
||||||
|
open() {
|
||||||
|
// TODO i don't like how vaguely arbitrary this feels.
|
||||||
|
let overlay = super.open();
|
||||||
|
overlay.classList.add('--transient');
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overlay styled like a dialog box
|
||||||
|
export class DialogOverlay extends Overlay {
|
||||||
|
constructor(conductor) {
|
||||||
|
super(conductor, mk('form.dialog'));
|
||||||
|
|
||||||
|
this.root.append(
|
||||||
|
this.header = mk('header'),
|
||||||
|
this.main = mk('section'),
|
||||||
|
this.footer = mk('footer'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
set_title(title) {
|
||||||
|
this.header.textContent = '';
|
||||||
|
this.header.append(mk('h1', {}, title));
|
||||||
|
}
|
||||||
|
|
||||||
|
add_button(label, onclick) {
|
||||||
|
let button = mk('button', {type: 'button'}, label);
|
||||||
|
button.addEventListener('click', onclick);
|
||||||
|
this.footer.append(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yes/no popup dialog
|
||||||
|
export class ConfirmOverlay extends DialogOverlay {
|
||||||
|
constructor(conductor, message, what) {
|
||||||
|
super(conductor);
|
||||||
|
this.set_title("just checking");
|
||||||
|
this.main.append(mk('p', {}, message));
|
||||||
|
let yes = mk('button', {type: 'button'}, "yep");
|
||||||
|
let no = mk('button', {type: 'button'}, "nope");
|
||||||
|
yes.addEventListener('click', ev => {
|
||||||
|
this.close();
|
||||||
|
what();
|
||||||
|
});
|
||||||
|
no.addEventListener('click', ev => {
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
|
this.footer.append(yes, no);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flash_button(button) {
|
||||||
|
button.classList.add('--button-glow-ok');
|
||||||
|
window.setTimeout(() => {
|
||||||
|
button.classList.add('--button-glow');
|
||||||
|
button.classList.remove('--button-glow-ok');
|
||||||
|
}, 10);
|
||||||
|
window.setTimeout(() => {
|
||||||
|
button.classList.remove('--button-glow');
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function load_json_from_storage(key) {
|
||||||
|
return JSON.parse(window.localStorage.getItem(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function save_json_to_storage(key, value) {
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(value));
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DIRECTIONS } from './defs.js';
|
import { DIRECTIONS, DRAW_LAYERS } from './defs.js';
|
||||||
import { mk } from './util.js';
|
import { mk } from './util.js';
|
||||||
import TILE_TYPES from './tiletypes.js';
|
import TILE_TYPES from './tiletypes.js';
|
||||||
|
|
||||||
@@ -11,7 +11,6 @@ export class CanvasRenderer {
|
|||||||
// to do so, but then we wouldn't make a canvas so it couldn't be
|
// to do so, but then we wouldn't make a canvas so it couldn't be
|
||||||
// hooked, yadda yadda
|
// hooked, yadda yadda
|
||||||
if (fixed_size) {
|
if (fixed_size) {
|
||||||
this.viewport_is_fixed = true;
|
|
||||||
this.viewport_size_x = fixed_size;
|
this.viewport_size_x = fixed_size;
|
||||||
this.viewport_size_y = fixed_size;
|
this.viewport_size_y = fixed_size;
|
||||||
}
|
}
|
||||||
@@ -25,7 +24,10 @@ export class CanvasRenderer {
|
|||||||
this.ctx = this.canvas.getContext('2d');
|
this.ctx = this.canvas.getContext('2d');
|
||||||
this.viewport_x = 0;
|
this.viewport_x = 0;
|
||||||
this.viewport_y = 0;
|
this.viewport_y = 0;
|
||||||
|
this.viewport_dirty = false;
|
||||||
|
this.show_actor_bboxes = false;
|
||||||
this.use_rewind_effect = false;
|
this.use_rewind_effect = false;
|
||||||
|
this.perception = 0; // 0 normal, 1 secret eye, 2 editor
|
||||||
}
|
}
|
||||||
|
|
||||||
set_level(level) {
|
set_level(level) {
|
||||||
@@ -33,6 +35,13 @@ export class CanvasRenderer {
|
|||||||
// TODO update viewport size... or maybe Game should do that since you might be cheating
|
// TODO update viewport size... or maybe Game should do that since you might be cheating
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Change the viewport size. DOES NOT take effect until the next redraw!
|
||||||
|
set_viewport_size(x, y) {
|
||||||
|
this.viewport_size_x = x;
|
||||||
|
this.viewport_size_y = y;
|
||||||
|
this.viewport_dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
cell_coords_from_event(ev) {
|
cell_coords_from_event(ev) {
|
||||||
let rect = this.canvas.getBoundingClientRect();
|
let rect = this.canvas.getBoundingClientRect();
|
||||||
let scale_x = rect.width / this.canvas.width;
|
let scale_x = rect.width / this.canvas.width;
|
||||||
@@ -42,6 +51,15 @@ export class CanvasRenderer {
|
|||||||
return [x, y];
|
return [x, y];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
real_cell_coords_from_event(ev) {
|
||||||
|
let rect = this.canvas.getBoundingClientRect();
|
||||||
|
let scale_x = rect.width / this.canvas.width;
|
||||||
|
let scale_y = rect.height / this.canvas.height;
|
||||||
|
let x = (ev.clientX - rect.x) / scale_x / this.tileset.size_x + this.viewport_x;
|
||||||
|
let y = (ev.clientY - rect.y) / scale_y / this.tileset.size_y + this.viewport_y;
|
||||||
|
return [x, y];
|
||||||
|
}
|
||||||
|
|
||||||
// Draw to a canvas using tile coordinates
|
// Draw to a canvas using tile coordinates
|
||||||
blit(ctx, sx, sy, dx, dy, w = 1, h = w) {
|
blit(ctx, sx, sy, dx, dy, w = 1, h = w) {
|
||||||
let tw = this.tileset.size_x;
|
let tw = this.tileset.size_x;
|
||||||
@@ -58,6 +76,14 @@ export class CanvasRenderer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.viewport_dirty) {
|
||||||
|
this.viewport_dirty = false;
|
||||||
|
this.canvas.setAttribute('width', this.tileset.size_x * this.viewport_size_x);
|
||||||
|
this.canvas.setAttribute('height', this.tileset.size_y * this.viewport_size_y);
|
||||||
|
this.canvas.style.setProperty('--viewport-width', this.viewport_size_x);
|
||||||
|
this.canvas.style.setProperty('--viewport-height', this.viewport_size_y);
|
||||||
|
}
|
||||||
|
|
||||||
let tic = (this.level.tic_counter ?? 0) + tic_offset;
|
let tic = (this.level.tic_counter ?? 0) + tic_offset;
|
||||||
let tw = this.tileset.size_x;
|
let tw = this.tileset.size_x;
|
||||||
let th = this.tileset.size_y;
|
let th = this.tileset.size_y;
|
||||||
@@ -76,8 +102,24 @@ export class CanvasRenderer {
|
|||||||
[px, py] = [0, 0];
|
[px, py] = [0, 0];
|
||||||
}
|
}
|
||||||
// Figure out where to start drawing
|
// Figure out where to start drawing
|
||||||
let x0 = Math.max(0, Math.min(this.level.size_x - this.viewport_size_x, px - xmargin));
|
// TODO support overlapping regions better
|
||||||
let y0 = Math.max(0, Math.min(this.level.size_y - this.viewport_size_y, py - ymargin));
|
let x0 = px - xmargin;
|
||||||
|
let y0 = py - ymargin;
|
||||||
|
// FIXME editor vs player again ugh, which is goofy since none of this is even relevant;
|
||||||
|
// maybe need to have a separate positioning method
|
||||||
|
if (this.level.stored_level) {
|
||||||
|
for (let region of this.level.stored_level.camera_regions) {
|
||||||
|
if (px >= region.left && px < region.right &&
|
||||||
|
py >= region.top && py < region.bottom)
|
||||||
|
{
|
||||||
|
x0 = Math.max(region.left, Math.min(region.right - this.viewport_size_x, x0));
|
||||||
|
y0 = Math.max(region.top, Math.min(region.bottom - this.viewport_size_y, y0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Always keep us within the map bounds
|
||||||
|
x0 = Math.max(0, Math.min(this.level.size_x - this.viewport_size_x, x0));
|
||||||
|
y0 = Math.max(0, Math.min(this.level.size_y - this.viewport_size_y, y0));
|
||||||
// Round to the pixel grid
|
// Round to the pixel grid
|
||||||
x0 = Math.floor(x0 * tw + 0.5) / tw;
|
x0 = Math.floor(x0 * tw + 0.5) / tw;
|
||||||
y0 = Math.floor(y0 * th + 0.5) / th;
|
y0 = Math.floor(y0 * th + 0.5) / th;
|
||||||
@@ -86,12 +128,12 @@ export class CanvasRenderer {
|
|||||||
// The viewport might not be aligned to the grid, so split off any fractional part.
|
// The viewport might not be aligned to the grid, so split off any fractional part.
|
||||||
let xf0 = Math.floor(x0);
|
let xf0 = Math.floor(x0);
|
||||||
let yf0 = Math.floor(y0);
|
let yf0 = Math.floor(y0);
|
||||||
// Note that when the viewport is exactly aligned to the grid, we need to draw the cells
|
// We need to draw one cell beyond the viewport, or we'll miss objects partway through
|
||||||
// just outside of it, or we'll miss objects partway through crossing the border
|
// crossing the border moving away from us
|
||||||
if (xf0 === x0 && xf0 > 0) {
|
if (xf0 > 0) {
|
||||||
xf0 -= 1;
|
xf0 -= 1;
|
||||||
}
|
}
|
||||||
if (yf0 === y0 && yf0 > 0) {
|
if (yf0 > 0) {
|
||||||
yf0 -= 1;
|
yf0 -= 1;
|
||||||
}
|
}
|
||||||
// Find where to stop drawing. As with above, if we're aligned to the grid, we need to
|
// Find where to stop drawing. As with above, if we're aligned to the grid, we need to
|
||||||
@@ -100,36 +142,63 @@ export class CanvasRenderer {
|
|||||||
let y1 = Math.min(this.level.size_y - 1, Math.ceil(y0 + this.viewport_size_y));
|
let y1 = Math.min(this.level.size_y - 1, Math.ceil(y0 + this.viewport_size_y));
|
||||||
// Draw one layer at a time, so animated objects aren't overdrawn by
|
// Draw one layer at a time, so animated objects aren't overdrawn by
|
||||||
// neighboring terrain
|
// neighboring terrain
|
||||||
// XXX layer count hardcoded here
|
// FIXME this is a bit inefficient when there are a lot of rarely-used layers; consider
|
||||||
for (let layer = 0; layer < 4; layer++) {
|
// instead drawing everything under actors, then actors, then everything above actors?
|
||||||
|
for (let layer = 0; layer < DRAW_LAYERS.MAX; layer++) {
|
||||||
for (let x = xf0; x <= x1; x++) {
|
for (let x = xf0; x <= x1; x++) {
|
||||||
for (let y = yf0; y <= y1; y++) {
|
for (let y = yf0; y <= y1; y++) {
|
||||||
for (let tile of this.level.cells[y][x]) {
|
for (let tile of this.level.cells[y][x]) {
|
||||||
if (tile.type.draw_layer !== layer)
|
if (tile.type.draw_layer !== layer)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
let vx, vy;
|
||||||
if (tile.type.is_actor &&
|
if (tile.type.is_actor &&
|
||||||
// FIXME kind of a hack for the editor, which uses bare tile objects
|
// FIXME kind of a hack for the editor, which uses bare tile objects
|
||||||
tile.visual_position)
|
tile.visual_position)
|
||||||
{
|
{
|
||||||
// Handle smooth scrolling
|
// Handle smooth scrolling
|
||||||
let [vx, vy] = tile.visual_position(tic_offset);
|
[vx, vy] = tile.visual_position(tic_offset);
|
||||||
// Round this to the pixel grid too!
|
// Round this to the pixel grid too!
|
||||||
vx = Math.floor(vx * tw + 0.5) / tw;
|
vx = Math.floor(vx * tw + 0.5) / tw;
|
||||||
vy = Math.floor(vy * th + 0.5) / th;
|
vy = Math.floor(vy * th + 0.5) / th;
|
||||||
this.tileset.draw(tile, tic, (sx, sy, dx = 0, dy = 0, w = 1, h = w) =>
|
|
||||||
this.blit(this.ctx, sx, sy, vx - x0 + dx, vy - y0 + dy, w, h));
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Non-actors can't move
|
// Non-actors can't move
|
||||||
this.tileset.draw(tile, tic, (sx, sy, dx = 0, dy = 0, w = 1, h = w) =>
|
vx = x;
|
||||||
this.blit(this.ctx, sx, sy, x - x0 + dx, y - y0 + dy, w, h));
|
vy = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note that the blit we pass to the tileset has a different signature:
|
||||||
|
// blit(
|
||||||
|
// source_tile_x, source_tile_y,
|
||||||
|
// mask_x = 0, mask_y = 0, mask_width = 1, mask_height = mask_width,
|
||||||
|
// mask_dx = mask_x, mask_dy = mask_y)
|
||||||
|
// This makes it easier to use in the extremely common case of drawing
|
||||||
|
// part of one tile atop another tile, but still aligned to the grid.
|
||||||
|
this.tileset.draw(tile, tic, (tx, ty, mx = 0, my = 0, mw = 1, mh = mw, mdx = mx, mdy = my) =>
|
||||||
|
this.blit(this.ctx,
|
||||||
|
tx + mx, ty + my,
|
||||||
|
vx - x0 + mdx, vy - y0 + mdy,
|
||||||
|
mw, mh));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.show_actor_bboxes && ! this.level.linear_cells) { // FIXME dumb hack so this doesn't happen in editor
|
||||||
|
this.ctx.fillStyle = '#f004';
|
||||||
|
for (let x = xf0; x <= x1; x++) {
|
||||||
|
for (let y = yf0; y <= y1; y++) {
|
||||||
|
let actor = this.level.cells[y][x].get_actor();
|
||||||
|
if (! actor)
|
||||||
|
continue;
|
||||||
|
let [vx, vy] = actor.visual_position(tic_offset);
|
||||||
|
// Don't round to the pixel grid; we want to know if the bbox is misaligned!
|
||||||
|
this.ctx.fillRect((vx - x0) * tw, (vy - y0) * th, 1 * tw, 1 * th);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.use_rewind_effect) {
|
if (this.use_rewind_effect) {
|
||||||
this.draw_rewind_effect(tic);
|
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 canvas = mk('canvas', {width: this.tileset.size_x, height: this.tileset.size_y});
|
||||||
let ctx = canvas.getContext('2d');
|
let ctx = canvas.getContext('2d');
|
||||||
this.tileset.draw_type(name, null, 0, (sx, sy, dx = 0, dy = 0, w = 1, h = w) =>
|
this.tileset.draw_type(name, tile, 0, this.perception, (tx, ty, mx = 0, my = 0, mw = 1, mh = mw, mdx = mx, mdy = my) =>
|
||||||
this.blit(ctx, sx, sy, dx, dy, w, h));
|
this.blit(ctx, tx + mx, ty + my, mdx, mdy, mw, mh));
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default [{
|
|||||||
title: "conundrum",
|
title: "conundrum",
|
||||||
author: "fluffy",
|
author: "fluffy",
|
||||||
url: 'https://beesbuzz.biz/',
|
url: 'https://beesbuzz.biz/',
|
||||||
beepbox: 'https://www.beepbox.co/#8n31sbk0l00e0rt2mm0a7g0rj07i0r1o3210T1v1L4u01q3d7fay3z6C0c1A5F4B0V1Q0248Pac74E0085T1v1L4u01q1d1f4y4z9C0c1A1F0B0V1Q200ePd593E0787T5v1L4u05q3d0f1y4z0C2c0h0H_--D-quSRIAJJST4v1L4u04q1z6666ji8k8k3jSBKSJJAArriiiiii07JCABrzrrrrrrr00YrkqHrsrrrrjr005zrAqzrjzrrqr1jRjrqGGrrzsrsA099ijrABJJJIAzrrtirqrqjqixzsrAjrqjiqaqqysttAJqjikikrizrHtBJJAzArzrIsRCITKSS099ijrAJS____Qg99habbCAYrDzh00bkzgid18Q4zgid0000000018Nwz55p88Nhnh5t000000ichgR5zkicg004hcx4O4jhkohd14z4isp25PFJvy1wqY58llp2GhKFLh_wFLh-czArdWfRuKrdQ_p13c5FKfZ_tRBPjub_aq_TYtjhZgtBdJlldtlldtlleKGGCKGGCK1wqoiGGiOdI3j1RlkOJBtdZllmG1wqoiGGCK1JFxaEm1STTQMBlldtlldtlJKoHrarqGGqWGGqWGGth7ihT4sChON7a4sIpqPhDkCz9FFOG8YwzOF8Wp8WFOHyfAVARQVB4ughVkAtcAtkVl97Op8Wd8Td6nl9EOaqSqcyIz9UzMbaXbAenShhAPvehhrBRApcVmnlsV5vaGysD9CKsyyLbH8OpOIKGVEGKttV56jdkV55uTnpeldpuptSltdRKpEPpFFOW8YwzOF8Wp8WFOGU0',
|
beepbox: 'https://www.beepbox.co/#8n31sbk0l00e0rt2mm0a7g0rj07i0r1o3210T1v1L4u01q3d7fay3z6C0c1A5F4B0V1Q0248Pac74E0085T1v1L4u01q1d1f4y4z9C0c1A1F0B0V1Q200ePd593E0787T5v1L4u05q3d0f1y4z0C2c0h0H_--D-quSRIAJJST4v1L4u04q1z6666ji8k8k3jSBKSJJAArriiiiii07JCABrzrrrrrrr00YrkqHrsrrrrjr005zrAqzrjzrrqr1jRjrqGGrrzsrsA099ijrABJJJIAzrrtirqrqjqixzsrAjrqjiqaqqysttAJqjikikrizrHtBJJAzArzrIsRCITKSS099ijrAJS____Qg99habbCAYrDzh00b4zgid18Q4zgid0000000018Nwz55p88Nhnh5t000000ichgR5zkicg004hcx4O4jhkohd14z4isp25PFJvy1wqY58llp2GhKFLh_wFLh-czArdWfRuKrdQ_p13c5FKfZ_tRBPjub_aq_TYtjhZgtBdJlldtlldtlleKGGCKGGCK1wqoiGGiOdI3j1RlkOJBtdZllmG1wqoiGGCK1JFxaEm1STTQMBlldtlldtlJKoHrarqGGqWGGqWGGth7ihT4sChON7a4sIpqPhDkCz9FFOG8YwzOF8Wp8WFOHyfAVARQVB4ughVkAtcAtkVl97Op8Wd8Td6nl9EOaqSqcyIz9UzMbaXbAenShhAPvehhrBRApcVmnlsV5vaGysD9CKsyyLbH8OpOIKGVEGKttV56jdkV55uTnpeldpuptSltdRKpEPpFFOW8YwzOF8Wp8WFOGU0',
|
||||||
path: 'music/conundrum.ogg',
|
path: 'music/conundrum.ogg',
|
||||||
}, {
|
}, {
|
||||||
title: "kinda song",
|
title: "kinda song",
|
||||||
@@ -51,4 +51,29 @@ export default [{
|
|||||||
twitter: "glitchedpuppet",
|
twitter: "glitchedpuppet",
|
||||||
beepbox: 'https://jummbus.bitbucket.io/#j2N07Unnamedn310s0k0l00e0jt2mm0a7g0jj07i0r1O_U0000o3210T1v0wL0OD0Ou01q1d5f6y0z6C1c0A1F2B5V6Q20a0Pe64bE0171T1v0pL0OD0Ou92q1d4f7y2z1C0c2AbF6B6V9Q0490Pb976E0001T1v0pL0OD0Ou94q1d2f7y2z1C0c2A9F5B5V6Q290dPa883E0011T4v0pL0OD0Ouf0q1z6666ji8k8k3jSBKSJJAArriiiiii07JCABrzrrrrrrr00YrkqHrsrrrrjr005zrAqzrjzrrqr1jRjrqGGrrzsrsA099ijrABJJJIAzrrtirqrqjqixzsrAjrqjiqaqqysttAJqjikikrizrHtBJJAzArzrIsRCITKSS099ijrAJS____Qg99habbCAYrDzh00b4x8Qd000lBu7014x4i4Qd3gQlmoh4ia2cz8OcChA4h4y8w01cPhjl0p27hFCLwATnMkCKChW2ZcngbUxcnikQ7sQO_inQ5WCkXyW9jbZ9vlO_lODhdcDjQNtgJ0Gp7IpAukFczR2FyX2frIzQ4zOGOOfpauP9vainRihQPK4tClqoKMnaAzQQnQQnRknXx7pBnK2OFjSO_oELFAzFIOWa8WF8WpHW3nEdv26LgqZcLQQap7Iu6P9j5R2Q2Q2Q2VeRfbEbWGq2-DinQBZttkHUMRWPn9HFAuwzEe3E8W2ehFyUsNncLQThuCnW2_aGydcngbkO_rZdkPjdcSpvx9jbZ3cRZtcO_lipvFSFkO_lN4YlAjBpdp6hahFyWz5OXbWFbWF8YEmChy3wWiehcK0bWaoEIlChw3JHp5K5E5E5w1sPb5P2f9as0LVds0bkbukQni0JFyQ5c0bokRyXrbxqxr2CyWSOSAzw1qxqxrNrxqxrhtwJgJMJJhvmbibikO8J5JwJEJEJtE',
|
beepbox: 'https://jummbus.bitbucket.io/#j2N07Unnamedn310s0k0l00e0jt2mm0a7g0jj07i0r1O_U0000o3210T1v0wL0OD0Ou01q1d5f6y0z6C1c0A1F2B5V6Q20a0Pe64bE0171T1v0pL0OD0Ou92q1d4f7y2z1C0c2AbF6B6V9Q0490Pb976E0001T1v0pL0OD0Ou94q1d2f7y2z1C0c2A9F5B5V6Q290dPa883E0011T4v0pL0OD0Ouf0q1z6666ji8k8k3jSBKSJJAArriiiiii07JCABrzrrrrrrr00YrkqHrsrrrrjr005zrAqzrjzrrqr1jRjrqGGrrzsrsA099ijrABJJJIAzrrtirqrqjqixzsrAjrqjiqaqqysttAJqjikikrizrHtBJJAzArzrIsRCITKSS099ijrAJS____Qg99habbCAYrDzh00b4x8Qd000lBu7014x4i4Qd3gQlmoh4ia2cz8OcChA4h4y8w01cPhjl0p27hFCLwATnMkCKChW2ZcngbUxcnikQ7sQO_inQ5WCkXyW9jbZ9vlO_lODhdcDjQNtgJ0Gp7IpAukFczR2FyX2frIzQ4zOGOOfpauP9vainRihQPK4tClqoKMnaAzQQnQQnRknXx7pBnK2OFjSO_oELFAzFIOWa8WF8WpHW3nEdv26LgqZcLQQap7Iu6P9j5R2Q2Q2Q2VeRfbEbWGq2-DinQBZttkHUMRWPn9HFAuwzEe3E8W2ehFyUsNncLQThuCnW2_aGydcngbkO_rZdkPjdcSpvx9jbZ3cRZtcO_lipvFSFkO_lN4YlAjBpdp6hahFyWz5OXbWFbWF8YEmChy3wWiehcK0bWaoEIlChw3JHp5K5E5E5w1sPb5P2f9as0LVds0bkbukQni0JFyQ5c0bokRyXrbxqxr2CyWSOSAzw1qxqxrNrxqxrhtwJgJMJJhvmbibikO8J5JwJEJEJtE',
|
||||||
path: 'music/kinda-song.ogg',
|
path: 'music/kinda-song.ogg',
|
||||||
|
}, {
|
||||||
|
title: "learning has occurred",
|
||||||
|
author: "jneen",
|
||||||
|
twitter: "jneen_",
|
||||||
|
path: 'music/learning-has-occurred.ogg',
|
||||||
|
}, {
|
||||||
|
title: "escape on star road",
|
||||||
|
author: "jneen",
|
||||||
|
twitter: "jneen_",
|
||||||
|
path: 'music/escape-on-star-road.ogg',
|
||||||
|
}, {
|
||||||
|
title: "inner orbit",
|
||||||
|
author: "notchris",
|
||||||
|
twitter: "chrismcgrane",
|
||||||
|
path: 'music/inner-orbit.ogg',
|
||||||
|
}, {
|
||||||
|
title: "canopy",
|
||||||
|
author: "notchris",
|
||||||
|
twitter: "chrismcgrane",
|
||||||
|
path: 'music/canopy.ogg',
|
||||||
|
}, {
|
||||||
|
title: "asteroid prairie",
|
||||||
|
author: "notchris",
|
||||||
|
twitter: "chrismcgrane",
|
||||||
|
path: 'music/asteroid-prairie.ogg',
|
||||||
}];
|
}];
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
|
// Base class for custom errors
|
||||||
|
export class LLError extends Error {}
|
||||||
|
|
||||||
|
// Random choice
|
||||||
export function random_choice(list) {
|
export function random_choice(list) {
|
||||||
return list[Math.floor(Math.random() * list.length)];
|
return list[Math.floor(Math.random() * list.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// DOM stuff
|
||||||
function _mk(el, children) {
|
function _mk(el, children) {
|
||||||
if (children.length > 0) {
|
if (children.length > 0) {
|
||||||
if (!(children[0] instanceof Node) && children[0] !== undefined && typeof(children[0]) !== "string" && typeof(children[0]) !== "number") {
|
if (!(children[0] instanceof Node) && children[0] !== undefined && typeof(children[0]) !== "string" && typeof(children[0]) !== "number") {
|
||||||
@@ -18,17 +24,92 @@ function _mk(el, children) {
|
|||||||
export function mk(tag_selector, ...children) {
|
export function mk(tag_selector, ...children) {
|
||||||
let [tag, ...classes] = tag_selector.split('.');
|
let [tag, ...classes] = tag_selector.split('.');
|
||||||
let el = document.createElement(tag);
|
let el = document.createElement(tag);
|
||||||
el.classList = classes.join(' ');
|
if (classes.length > 0) {
|
||||||
|
el.classList = classes.join(' ');
|
||||||
|
}
|
||||||
return _mk(el, children);
|
return _mk(el, children);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||||
export function mk_svg(tag_selector, ...children) {
|
export function mk_svg(tag_selector, ...children) {
|
||||||
let [tag, ...classes] = tag_selector.split('.');
|
let [tag, ...classes] = tag_selector.split('.');
|
||||||
let el = document.createElementNS('http://www.w3.org/2000/svg', tag);
|
let el = document.createElementNS(SVG_NS, tag);
|
||||||
el.classList = classes.join(' ');
|
if (classes.length > 0) {
|
||||||
|
el.classList = classes.join(' ');
|
||||||
|
}
|
||||||
return _mk(el, children);
|
return _mk(el, children);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handle_drop(element, options) {
|
||||||
|
let dropzone_class = options.dropzone_class ?? null;
|
||||||
|
let on_drop = options.on_drop;
|
||||||
|
|
||||||
|
let require_file = options.require_file ?? false;
|
||||||
|
let is_valid = ev => {
|
||||||
|
// TODO this requires files, should make some args for this
|
||||||
|
if (options.require_file) {
|
||||||
|
let dt = ev.dataTransfer;
|
||||||
|
if (! dt || dt.items.length === 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Only test the first item I guess? If it's a file then they should all be files
|
||||||
|
if (dt.items[0].kind !== 'file')
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
let end_drop = () => {
|
||||||
|
if (dropzone_class !== null) {
|
||||||
|
element.classList.remove(dropzone_class);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO should have a filter function for when a drag is valid but i forget which of these
|
||||||
|
// should have that
|
||||||
|
element.addEventListener('dragenter', ev => {
|
||||||
|
if (! is_valid(ev))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
if (dropzone_class !== null) {
|
||||||
|
element.classList.add(dropzone_class);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
element.addEventListener('dragover', ev => {
|
||||||
|
if (! is_valid(ev))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
});
|
||||||
|
element.addEventListener('dragleave', ev => {
|
||||||
|
if (ev.relatedTarget && element.contains(ev.relatedTarget))
|
||||||
|
return;
|
||||||
|
|
||||||
|
end_drop();
|
||||||
|
});
|
||||||
|
element.addEventListener('drop', ev => {
|
||||||
|
if (! is_valid(ev))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
end_drop();
|
||||||
|
on_drop(ev);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sleep(t) {
|
||||||
|
return new Promise(res => {
|
||||||
|
setTimeout(res, t);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function promise_event(element, success_event, failure_event) {
|
export function promise_event(element, success_event, failure_event) {
|
||||||
let resolve, reject;
|
let resolve, reject;
|
||||||
let promise = new Promise((res, rej) => {
|
let promise = new Promise((res, rej) => {
|
||||||
@@ -36,21 +117,21 @@ export function promise_event(element, success_event, failure_event) {
|
|||||||
reject = rej;
|
reject = rej;
|
||||||
});
|
});
|
||||||
|
|
||||||
let success_handler = e => {
|
let success_handler = ev => {
|
||||||
element.removeEventListener(success_event, success_handler);
|
element.removeEventListener(success_event, success_handler);
|
||||||
if (failure_event) {
|
if (failure_event) {
|
||||||
element.removeEventListener(failure_event, failure_handler);
|
element.removeEventListener(failure_event, failure_handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(e);
|
resolve(ev);
|
||||||
};
|
};
|
||||||
let failure_handler = e => {
|
let failure_handler = ev => {
|
||||||
element.removeEventListener(success_event, success_handler);
|
element.removeEventListener(success_event, success_handler);
|
||||||
if (failure_event) {
|
if (failure_event) {
|
||||||
element.removeEventListener(failure_event, failure_handler);
|
element.removeEventListener(failure_event, failure_handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
reject(e);
|
reject(ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
element.addEventListener(success_event, success_handler);
|
element.addEventListener(success_event, success_handler);
|
||||||
@@ -61,6 +142,7 @@ export function promise_event(element, success_event, failure_event) {
|
|||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function fetch(url) {
|
export async function fetch(url) {
|
||||||
let xhr = new XMLHttpRequest;
|
let xhr = new XMLHttpRequest;
|
||||||
let promise = promise_event(xhr, 'load', 'error');
|
let promise = promise_event(xhr, 'load', 'error');
|
||||||
@@ -68,11 +150,66 @@ export async function fetch(url) {
|
|||||||
xhr.responseType = 'arraybuffer';
|
xhr.responseType = 'arraybuffer';
|
||||||
xhr.send();
|
xhr.send();
|
||||||
await promise;
|
await promise;
|
||||||
|
if (xhr.status !== 200)
|
||||||
|
throw new Error(`Failed to load ${url} -- ${xhr.status} ${xhr.statusText}`);
|
||||||
return xhr.response;
|
return xhr.response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function string_from_buffer_ascii(buf, start = 0, len) {
|
||||||
|
if (ArrayBuffer.isView(buf)) {
|
||||||
|
start += buf.byteOffset;
|
||||||
|
buf = buf.buffer;
|
||||||
|
}
|
||||||
|
return String.fromCharCode.apply(null, new Uint8Array(buf, start, len));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts a string to a buffer, using NO ENCODING, assuming single-byte characters
|
||||||
|
export function bytestring_to_buffer(bytestring) {
|
||||||
|
return Uint8Array.from(bytestring, c => c.charCodeAt(0)).buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function b64encode(value) {
|
||||||
|
if (value instanceof ArrayBuffer || value instanceof Uint8Array) {
|
||||||
|
value = string_from_buffer_ascii(value);
|
||||||
|
}
|
||||||
|
// Make URL-safe and strip trailing padding
|
||||||
|
return btoa(value).replace(/[+]/g, '-').replace(/[/]/g, '_').replace(/=+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function b64decode(data) {
|
||||||
|
return bytestring_to_buffer(atob(data.replace(/-/g, '+').replace(/_/g, '/')));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function format_duration(seconds, places = 0) {
|
||||||
|
let mins = Math.floor(seconds / 60);
|
||||||
|
let secs = seconds % 60;
|
||||||
|
return `${mins}:${secs < 10 ? '0' : ''}${secs.toFixed(places)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DelayTimer {
|
||||||
|
constructor() {
|
||||||
|
this.active = false;
|
||||||
|
this._handle = null;
|
||||||
|
this._bound_alarm = this._alarm.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
set(duration) {
|
||||||
|
if (this._handle) {
|
||||||
|
window.clearTimeout(this._handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.active = true;
|
||||||
|
this._handle = window.setTimeout(this._bound_alarm, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
_alarm() {
|
||||||
|
this._handle = null;
|
||||||
|
this.active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cast a line through a grid and yield every cell it touches
|
// Cast a line through a grid and yield every cell it touches
|
||||||
export function* walk_grid(x0, y0, x1, y1) {
|
export function* walk_grid(x0, y0, x1, y1, min_a, min_b, max_a, max_b) {
|
||||||
// TODO if the ray starts outside the grid (extremely unlikely), we should
|
// TODO if the ray starts outside the grid (extremely unlikely), we should
|
||||||
// find the point where it ENTERS the grid, otherwise the 'while'
|
// find the point where it ENTERS the grid, otherwise the 'while'
|
||||||
// conditions below will stop immediately
|
// conditions below will stop immediately
|
||||||
@@ -112,27 +249,24 @@ export function* walk_grid(x0, y0, x1, y1) {
|
|||||||
step_a = -step_a;
|
step_a = -step_a;
|
||||||
offset_x = 1 - offset_x;
|
offset_x = 1 - offset_x;
|
||||||
}
|
}
|
||||||
// Zero offset means we're on a grid line, so we're actually a full cell
|
else if (offset_x === 0) {
|
||||||
// away from the next grid line
|
// Zero offset means we're on a grid line, so we're a full cell away from the next grid line
|
||||||
if (offset_x === 0) {
|
// (if we're moving forward; if we're moving backward, the next cell really is 0 away)
|
||||||
offset_x = 1;
|
offset_x = 1;
|
||||||
}
|
}
|
||||||
let step_b = 1;
|
let step_b = 1;
|
||||||
let offset_y = 1 - (y0 - b);
|
let offset_y = 1 - (y0 - b);
|
||||||
if (dy < 0) {
|
if (dy < 0) {
|
||||||
dy = -dy;
|
dy = -dy;
|
||||||
step_b = -step_b;
|
step_b = -step_b;
|
||||||
offset_y = 1 - offset_y;
|
offset_y = 1 - offset_y;
|
||||||
}
|
}
|
||||||
if (offset_y === 0) {
|
else if (offset_y === 0) {
|
||||||
offset_y = 1;
|
offset_y = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let err = dy * offset_x - dx * offset_y;
|
let err = dy * offset_x - dx * offset_y;
|
||||||
|
|
||||||
let min_a = 0, min_b = 0;
|
|
||||||
// TODO get these passed in fool
|
|
||||||
let max_a = 31, max_b = 31;
|
|
||||||
if (dx > dy) {
|
if (dx > dy) {
|
||||||
// Main axis is x/a
|
// Main axis is x/a
|
||||||
while (min_a <= a && a <= max_a && min_b <= b && b <= max_b) {
|
while (min_a <= a && a <= max_a && min_b <= b && b <= max_b) {
|
||||||
@@ -171,3 +305,103 @@ export function* walk_grid(x0, y0, x1, y1) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
window.walk_grid = walk_grid;
|
||||||
|
// console.table(Array.from(walk_grid(13, 27.133854389190674, 12.90625, 27.227604389190674)))
|
||||||
|
|
||||||
|
// Root class to indirect over where we might get files from
|
||||||
|
// - a pool of uploaded in-memory files
|
||||||
|
// - a single uploaded zip file
|
||||||
|
// - a local directory provided via the webkit Entry api
|
||||||
|
// - HTTP (but only for files we choose ourselves, not arbitrary ones, due to CORS)
|
||||||
|
// Note that where possible, these classes lowercase all filenames, in keeping with C2G's implicit
|
||||||
|
// requirement that filenames are case-insensitive :/
|
||||||
|
class FileSource {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
// Get a file's contents as an ArrayBuffer
|
||||||
|
async get(path) {}
|
||||||
|
}
|
||||||
|
// Files we have had uploaded one at a time (note that each upload becomes its own source)
|
||||||
|
export class FileFileSource extends FileSource {
|
||||||
|
constructor(files) {
|
||||||
|
super();
|
||||||
|
this.files = {};
|
||||||
|
for (let file of files) {
|
||||||
|
this.files[(file.webkitRelativePath ?? file.name).toLowerCase()] = file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(path) {
|
||||||
|
let file = this.files[path.toLowerCase()];
|
||||||
|
if (file) {
|
||||||
|
return file.arrayBuffer();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return Promise.reject(new Error(`No such file was provided: ${path}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Regular HTTP fetch
|
||||||
|
export class HTTPFileSource extends FileSource {
|
||||||
|
// Should be given a URL object as a root
|
||||||
|
constructor(root) {
|
||||||
|
super();
|
||||||
|
this.root = root;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(path) {
|
||||||
|
let url = new URL(path, this.root);
|
||||||
|
return fetch(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// WebKit Entry interface
|
||||||
|
// XXX this does not appear to work if you drag in a link to a directory but that is probably beyond
|
||||||
|
// my powers to fix
|
||||||
|
export class EntryFileSource extends FileSource {
|
||||||
|
constructor(entries) {
|
||||||
|
super();
|
||||||
|
this.files = {};
|
||||||
|
let file_count = 0;
|
||||||
|
|
||||||
|
let read_directory = async (directory_entry, dir_prefix) => {
|
||||||
|
let reader = directory_entry.createReader();
|
||||||
|
let all_entries = [];
|
||||||
|
while (true) {
|
||||||
|
let entries = await new Promise((res, rej) => reader.readEntries(res, rej));
|
||||||
|
all_entries.push.apply(all_entries, entries);
|
||||||
|
if (entries.length === 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await handle_entries(all_entries, dir_prefix);
|
||||||
|
};
|
||||||
|
let handle_entries = (entries, dir_prefix) => {
|
||||||
|
file_count += entries.length;
|
||||||
|
if (file_count > 4096)
|
||||||
|
throw new LLError("Found way too many files; did you drag in the wrong directory?");
|
||||||
|
|
||||||
|
let dir_promises = [];
|
||||||
|
for (let entry of entries) {
|
||||||
|
if (entry.isDirectory) {
|
||||||
|
dir_promises.push(read_directory(entry, dir_prefix + entry.name + '/'));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.files[(dir_prefix + entry.name).toLowerCase()] = entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(dir_promises);
|
||||||
|
};
|
||||||
|
|
||||||
|
this._loaded_promise = handle_entries(entries, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(path) {
|
||||||
|
let entry = this.files[path.toLowerCase()];
|
||||||
|
if (! entry)
|
||||||
|
throw new LLError(`No such file in local directory: ${path}`);
|
||||||
|
|
||||||
|
let file = await new Promise((res, rej) => entry.file(res, rej));
|
||||||
|
return await file.arrayBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 |