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: 185 B |
@@ -10,11 +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">
|
<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>
|
||||||
@@ -26,40 +55,44 @@
|
|||||||
<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>
|
<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>Load and play any levels you have on hand — both the original levels and any custom ones, perhaps ones you found on the <a href="https://sets.bitbusters.club/">Bit Busters Club set list</a>. Supports CCL/DAT, C2G, and individual C2Ms (though scores aren't saved for those).</p>
|
<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! -->
|
<!-- TODO zip files! -->
|
||||||
<p>You can also drag and drop files or directories into this window.</p>
|
<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-file" type="file" accept=".dat,.ccl,.c2m,.ccs" multiple>
|
||||||
@@ -77,68 +110,189 @@
|
|||||||
|
|
||||||
<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 <span class="keyhint">(p)</span></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 <span class="keyhint">(z)</span></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>
|
||||||
@@ -152,14 +306,14 @@
|
|||||||
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">
|
||||||
<nav class="controls">
|
<div class="-container">
|
||||||
<div id="editor-toolbar">
|
<!-- level canvas and any overlays go here -->
|
||||||
<!-- tools go here -->
|
<!-- the container is to allow them to scroll as a single unit -->
|
||||||
</div>
|
</div>
|
||||||
<button id="editor-share-url" type="button">Share?</button>
|
</div>
|
||||||
<div id="editor-tool-help">
|
<nav class="controls">
|
||||||
<strong>Pencil</strong> — <span>Select a tile and draw with the left mouse button. Erase with the right mouse button.</span>
|
<div id="editor-tile">
|
||||||
</div>
|
</div>
|
||||||
<!--
|
<!--
|
||||||
<p style>
|
<p style>
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -3,11 +3,57 @@ import * as util from './util.js';
|
|||||||
export class StoredCell extends Array {
|
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 {
|
export class StoredLevel {
|
||||||
constructor(number) {
|
constructor(number) {
|
||||||
// TODO still not sure this belongs here
|
// TODO still not sure this belongs here
|
||||||
this.number = number; // one-based
|
this.number = number; // one-based
|
||||||
this.title = '';
|
this.title = '';
|
||||||
|
this.author = '';
|
||||||
this.password = null;
|
this.password = null;
|
||||||
this.hint = '';
|
this.hint = '';
|
||||||
this.chips_required = 0;
|
this.chips_required = 0;
|
||||||
@@ -16,6 +62,16 @@ export class StoredLevel {
|
|||||||
this.extra_chunks = [];
|
this.extra_chunks = [];
|
||||||
this.use_cc1_boots = false;
|
this.use_cc1_boots = false;
|
||||||
this.use_ccl_compat = 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_x = 0;
|
||||||
this.size_y = 0;
|
this.size_y = 0;
|
||||||
@@ -42,14 +98,26 @@ export class StoredLevel {
|
|||||||
|
|
||||||
check() {
|
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 StoredGame {
|
export class StoredPack {
|
||||||
constructor(identifier, level_loader) {
|
constructor(identifier, level_loader) {
|
||||||
this.identifier = identifier;
|
this.identifier = identifier;
|
||||||
|
this.title = "";
|
||||||
this._level_loader = level_loader;
|
this._level_loader = level_loader;
|
||||||
|
|
||||||
// Simple objects containing keys:
|
// Simple objects containing keys that are usually:
|
||||||
// title: level title
|
// title: level title
|
||||||
// index: level index, used internally only
|
// index: level index, used internally only
|
||||||
// number: level number (may not match index due to C2G shenanigans)
|
// number: level number (may not match index due to C2G shenanigans)
|
||||||
@@ -72,7 +140,9 @@ export class StoredGame {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Otherwise, attempt to load the level
|
// Otherwise, attempt to load the level
|
||||||
return this._level_loader(meta.bytes, meta.number);
|
return this._level_loader(meta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const StoredGame = StoredPack;
|
||||||
@@ -1,68 +1,100 @@
|
|||||||
import { DIRECTIONS } from './defs.js';
|
import { DIRECTIONS, DIRECTION_ORDER } from './defs.js';
|
||||||
import * as format_base from './format-base.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';
|
import * as util from './util.js';
|
||||||
|
|
||||||
const CC2_DEMO_INPUT_MASK = {
|
export function decode_replay(bytes) {
|
||||||
drop: 0x01,
|
if (bytes instanceof ArrayBuffer) {
|
||||||
down: 0x02,
|
bytes = new Uint8Array(bytes);
|
||||||
left: 0x04,
|
|
||||||
right: 0x08,
|
|
||||||
up: 0x10,
|
|
||||||
swap: 0x20,
|
|
||||||
cycle: 0x40,
|
|
||||||
};
|
|
||||||
|
|
||||||
class CC2Demo {
|
|
||||||
constructor(bytes) {
|
|
||||||
this.bytes = bytes;
|
|
||||||
|
|
||||||
// byte 0 is unknown, always 0?
|
|
||||||
// Force floor seed can apparently be anything; my best guess, based on the Desert Oasis
|
|
||||||
// replay, is that it's just incremented and allowed to overflow, so taking it mod 4 gives
|
|
||||||
// the correct starting direction
|
|
||||||
this.initial_force_floor_direction = ['north', 'east', 'south', 'west'][this.bytes[1] % 4];
|
|
||||||
this.blob_seed = this.bytes[2];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*[Symbol.iterator]() {
|
// byte 0 is unknown, always 0?
|
||||||
let l = this.bytes.length;
|
// Force floor seed can apparently be anything; my best guess, based on the Desert Oasis
|
||||||
if (l % 2 === 0) {
|
// replay, is that it's just incremented and allowed to overflow, so taking it mod 4 gives
|
||||||
l--;
|
// the correct starting direction
|
||||||
|
let initial_force_floor_direction = DIRECTION_ORDER[bytes[1] % 4];
|
||||||
|
let blob_seed = bytes[2];
|
||||||
|
|
||||||
|
// Add up the total length
|
||||||
|
let duration = 0;
|
||||||
|
let l = bytes.length ?? bytes.byteLength;
|
||||||
|
if (l % 2 === 0) {
|
||||||
|
l--;
|
||||||
|
}
|
||||||
|
for (let p = 3; p < l; p += 2) {
|
||||||
|
let delay = bytes[p];
|
||||||
|
if (delay === 0xff)
|
||||||
|
break;
|
||||||
|
duration += delay;
|
||||||
|
}
|
||||||
|
duration = Math.floor(duration / 3) + 1; // leave room for final input
|
||||||
|
|
||||||
|
// Inflate the replay into an array of byte-per-tic
|
||||||
|
let inputs = new Uint8Array(duration);
|
||||||
|
let i = 0;
|
||||||
|
let t = 0;
|
||||||
|
let input = 0;
|
||||||
|
for (let p = 3; p < l; p += 2) {
|
||||||
|
// The first byte measures how long the /previous/ input remains
|
||||||
|
// valid, so yield that first. Note that this is measured in 60Hz
|
||||||
|
// frames, so we need to convert to 20Hz tics by subtracting 3
|
||||||
|
// frames at a time.
|
||||||
|
let delay = bytes[p];
|
||||||
|
if (delay === 0xff)
|
||||||
|
break;
|
||||||
|
|
||||||
|
t += delay;
|
||||||
|
while (t >= 3) {
|
||||||
|
t -= 3;
|
||||||
|
inputs[i] = input;
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
let input = new Set;
|
|
||||||
let t = 0;
|
|
||||||
for (let p = 3; p < l; p += 2) {
|
|
||||||
// The first byte measures how long the /previous/ input remains
|
|
||||||
// valid, so yield that first. Note that this is measured in 60Hz
|
|
||||||
// frames, so we need to convert to 20Hz tics by subtracting 3
|
|
||||||
// frames at a time.
|
|
||||||
let delay = this.bytes[p];
|
|
||||||
if (delay === 0xff)
|
|
||||||
break;
|
|
||||||
|
|
||||||
t += delay;
|
input = bytes[p + 1];
|
||||||
while (t >= 3) {
|
let is_player_2 = ((input & 0x80) !== 0);
|
||||||
t -= 3;
|
// TODO handle player 2
|
||||||
yield input;
|
if (is_player_2)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
inputs[i] = input;
|
||||||
|
|
||||||
|
return new format_base.Replay(initial_force_floor_direction, blob_seed, inputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encode_replay(replay, stored_level = null) {
|
||||||
|
let out = new Uint8Array(1024);
|
||||||
|
out[0] = 0;
|
||||||
|
out[1] = DIRECTIONS[replay.initial_force_floor_direction].index;
|
||||||
|
out[2] = replay.blob_seed;
|
||||||
|
let p = 3;
|
||||||
|
let prev_input = null;
|
||||||
|
let count = 0;
|
||||||
|
for (let i = 0; i < replay.duration; i++) {
|
||||||
|
if (p >= out.length - 4) {
|
||||||
|
let new_out = new Uint8Array(Math.floor(out.length * 1.5));
|
||||||
|
for (let j = 0; j < out.length; j++) {
|
||||||
|
new_out[j] = out[j];
|
||||||
}
|
}
|
||||||
|
out = new_out;
|
||||||
|
}
|
||||||
|
|
||||||
let input_mask = this.bytes[p + 1];
|
let input = replay.inputs[i];
|
||||||
let is_player_2 = ((input_mask & 0x80) !== 0);
|
if (input !== prev_input || count >= 252) {
|
||||||
// TODO handle player 2
|
out[p] = count;
|
||||||
if (is_player_2)
|
out[p + 1] = input;
|
||||||
continue;
|
p += 2;
|
||||||
|
count = 3;
|
||||||
for (let [action, bit] of Object.entries(CC2_DEMO_INPUT_MASK)) {
|
prev_input = input;
|
||||||
if ((input_mask & bit) === 0) {
|
}
|
||||||
input.delete(action);
|
else {
|
||||||
}
|
count += 3;
|
||||||
else {
|
|
||||||
input.add(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
out[p] = 0xff;
|
||||||
|
p += 1;
|
||||||
|
out = out.subarray(0, p);
|
||||||
|
// TODO stick it on the level if given?
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -80,7 +112,7 @@ let modifier_wire = {
|
|||||||
let arg_direction = {
|
let arg_direction = {
|
||||||
size: 1,
|
size: 1,
|
||||||
decode(tile, dirbyte) {
|
decode(tile, dirbyte) {
|
||||||
let direction = ['north', 'east', 'south', 'west'][dirbyte & 0x03];
|
let direction = DIRECTION_ORDER[dirbyte & 0x03];
|
||||||
tile.direction = direction;
|
tile.direction = direction;
|
||||||
},
|
},
|
||||||
encode(tile) {
|
encode(tile) {
|
||||||
@@ -323,7 +355,10 @@ const TILE_ENCODING = {
|
|||||||
name: 'bomb',
|
name: 'bomb',
|
||||||
has_next: true,
|
has_next: true,
|
||||||
},
|
},
|
||||||
//0x41: Open trap (unused in main levels) :
|
0x41: {
|
||||||
|
// FIXME cc2lp1 uses this, i don't know what it actually does
|
||||||
|
error: "Open trap is not yet implemented!",
|
||||||
|
},
|
||||||
0x42: {
|
0x42: {
|
||||||
name: 'trap',
|
name: 'trap',
|
||||||
},
|
},
|
||||||
@@ -344,18 +379,21 @@ const TILE_ENCODING = {
|
|||||||
0x47: {
|
0x47: {
|
||||||
name: 'button_gray',
|
name: 'button_gray',
|
||||||
},
|
},
|
||||||
// FIXME swivel floors... argh...
|
|
||||||
0x48: {
|
0x48: {
|
||||||
name: 'swivel_sw',
|
name: 'swivel_sw',
|
||||||
|
dummy_terrain: 'swivel_floor',
|
||||||
},
|
},
|
||||||
0x49: {
|
0x49: {
|
||||||
name: 'swivel_nw',
|
name: 'swivel_nw',
|
||||||
|
dummy_terrain: 'swivel_floor',
|
||||||
},
|
},
|
||||||
0x4a: {
|
0x4a: {
|
||||||
name: 'swivel_ne',
|
name: 'swivel_ne',
|
||||||
|
dummy_terrain: 'swivel_floor',
|
||||||
},
|
},
|
||||||
0x4b: {
|
0x4b: {
|
||||||
name: 'swivel_se',
|
name: 'swivel_se',
|
||||||
|
dummy_terrain: 'swivel_floor',
|
||||||
},
|
},
|
||||||
0x4c: {
|
0x4c: {
|
||||||
name: 'stopwatch_bonus',
|
name: 'stopwatch_bonus',
|
||||||
@@ -372,12 +410,30 @@ const TILE_ENCODING = {
|
|||||||
0x4f: {
|
0x4f: {
|
||||||
name: 'railroad',
|
name: 'railroad',
|
||||||
modifier: {
|
modifier: {
|
||||||
|
_parts: ['ne', 'se', 'sw', 'ne', 'ew', 'ns'],
|
||||||
decode(tile, mask) {
|
decode(tile, mask) {
|
||||||
// TODO railroad props
|
// Leave the track parts alone as a bitmask; the type has a list of them
|
||||||
|
tile.tracks = mask & 0x3f;
|
||||||
|
// Check for a switch, which is a bit number in the above mask
|
||||||
|
if (mask & 0x40) {
|
||||||
|
tile.track_switch = (mask >> 8) & 0x0f;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tile.track_switch = null;
|
||||||
|
}
|
||||||
|
// Initial actor facing is in the highest nybble
|
||||||
|
tile.entered_direction = DIRECTION_ORDER[(mask >> 12) & 0x03];
|
||||||
},
|
},
|
||||||
encode(tile) {
|
encode(tile) {
|
||||||
// TODO
|
let ret = tile.tracks & 0x3f;
|
||||||
return 0;
|
if (tile.track_switch !== null) {
|
||||||
|
ret |= 0x40;
|
||||||
|
ret |= tile.track_switch << 8;
|
||||||
|
}
|
||||||
|
if (tile.entered_direction) {
|
||||||
|
ret |= DIRECTION_ORDER.indexOf(tile.entered_direction) << 12;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -402,10 +458,9 @@ const TILE_ENCODING = {
|
|||||||
name: 'teeth_timid',
|
name: 'teeth_timid',
|
||||||
has_next: true,
|
has_next: true,
|
||||||
extra_args: [arg_direction],
|
extra_args: [arg_direction],
|
||||||
error: "Timid chomper is not yet implemented, sorry!",
|
|
||||||
},
|
},
|
||||||
0x58: {
|
0x58: {
|
||||||
// TODO??? unused in main levels -- name: 'doppelganger2',
|
// TODO??? unused in main levels -- name: '',
|
||||||
has_next: true,
|
has_next: true,
|
||||||
extra_args: [arg_direction],
|
extra_args: [arg_direction],
|
||||||
error: "Explosion animation is not implemented, sorry!",
|
error: "Explosion animation is not implemented, sorry!",
|
||||||
@@ -421,9 +476,60 @@ const TILE_ENCODING = {
|
|||||||
name: 'no_player1_sign',
|
name: 'no_player1_sign',
|
||||||
},
|
},
|
||||||
0x5c: {
|
0x5c: {
|
||||||
// TODO (modifier chooses logic gate) name: 'doppelganger2',
|
name: 'logic_gate',
|
||||||
// TODO modifier: ...
|
modifier: {
|
||||||
error: "Logic gates are not yet implemented, sorry!",
|
decode(tile, modifier) {
|
||||||
|
if (modifier >= 0x1e && modifier <= 0x27) {
|
||||||
|
// Counter, which can't be rotated
|
||||||
|
tile.direction = 'north';
|
||||||
|
tile.gate_type = 'counter';
|
||||||
|
tile.memory = modifier - 0x1e;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tile.direction = DIRECTION_ORDER[modifier & 0x03];
|
||||||
|
let type = modifier >> 2;
|
||||||
|
if (type < 6) {
|
||||||
|
tile.gate_type = ['not', 'and', 'or', 'xor', 'latch-cw', 'nand'][type];
|
||||||
|
}
|
||||||
|
else if (type === 16) {
|
||||||
|
tile.gate_type = 'latch-ccw';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tile.gate_type = 'bogus';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
encode(tile) {
|
||||||
|
let direction_offset = DIRECTIONS[tile.direction].index;
|
||||||
|
if (tile.gate_type === 'not') {
|
||||||
|
return 0 + direction_offset;
|
||||||
|
}
|
||||||
|
else if (tile.gate_type === 'and') {
|
||||||
|
return 4 + direction_offset;
|
||||||
|
}
|
||||||
|
else if (tile.gate_type === 'or') {
|
||||||
|
return 8 + direction_offset;
|
||||||
|
}
|
||||||
|
else if (tile.gate_type === 'xor') {
|
||||||
|
return 12 + direction_offset;
|
||||||
|
}
|
||||||
|
else if (tile.gate_type === 'latch-cw') {
|
||||||
|
return 16 + direction_offset;
|
||||||
|
}
|
||||||
|
else if (tile.gate_type === 'nand') {
|
||||||
|
return 20 + direction_offset;
|
||||||
|
}
|
||||||
|
else if (tile.gate_type === 'counter') {
|
||||||
|
return 30 + tile.memory;
|
||||||
|
}
|
||||||
|
else if (tile.gate_type === 'latch-ccw') {
|
||||||
|
return 64 + direction_offset;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 0xff;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
0x5e: {
|
0x5e: {
|
||||||
name: 'button_pink',
|
name: 'button_pink',
|
||||||
@@ -441,7 +547,6 @@ const TILE_ENCODING = {
|
|||||||
0x62: {
|
0x62: {
|
||||||
name: 'lightning_bolt',
|
name: 'lightning_bolt',
|
||||||
has_next: true,
|
has_next: true,
|
||||||
error: "The lightning bolt is not yet implemented, sorry!",
|
|
||||||
},
|
},
|
||||||
0x63: {
|
0x63: {
|
||||||
name: 'tank_yellow',
|
name: 'tank_yellow',
|
||||||
@@ -455,13 +560,11 @@ const TILE_ENCODING = {
|
|||||||
name: 'doppelganger1',
|
name: 'doppelganger1',
|
||||||
has_next: true,
|
has_next: true,
|
||||||
extra_args: [arg_direction],
|
extra_args: [arg_direction],
|
||||||
error: "Doppelganger Lexy is not yet implemented, sorry!",
|
|
||||||
},
|
},
|
||||||
0x66: {
|
0x66: {
|
||||||
name: 'doppelganger2',
|
name: 'doppelganger2',
|
||||||
has_next: true,
|
has_next: true,
|
||||||
extra_args: [arg_direction],
|
extra_args: [arg_direction],
|
||||||
error: "Doppelganger Cerise is not yet implemented, sorry!",
|
|
||||||
},
|
},
|
||||||
0x68: {
|
0x68: {
|
||||||
name: 'bowling_ball',
|
name: 'bowling_ball',
|
||||||
@@ -495,18 +598,33 @@ const TILE_ENCODING = {
|
|||||||
name: 'floor_letter',
|
name: 'floor_letter',
|
||||||
modifier: {
|
modifier: {
|
||||||
decode(tile, ascii_code) {
|
decode(tile, ascii_code) {
|
||||||
tile.ascii_code = ascii_code;
|
if (ascii_code < 28 || ascii_code >= 96) {
|
||||||
|
// Invalid
|
||||||
|
tile.overlaid_glyph = "?";
|
||||||
|
}
|
||||||
|
else if (ascii_code < 32) {
|
||||||
|
// Arrows are stored goofily
|
||||||
|
tile.overlaid_glyph = ["⬆", "➡", "⬇", "⬅"][ascii_code - 28];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tile.overlaid_glyph = String.fromCharCode(ascii_code);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
encode(tile) {
|
encode(tile) {
|
||||||
return tile.ascii_code;
|
let arrow_index = ["⬆", "➡", "⬇", "⬅"].indexOf(tile.overlaid_glyph);
|
||||||
|
if (arrow_index >= 0) {
|
||||||
|
return arrow_index + 28;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tile.overlaid_glyph.charCodeAt(0);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
0x72: {
|
0x72: {
|
||||||
name: 'purple_wall',
|
name: 'purple_floor',
|
||||||
},
|
},
|
||||||
0x73: {
|
0x73: {
|
||||||
name: 'purple_floor',
|
name: 'purple_wall',
|
||||||
},
|
},
|
||||||
0x76: {
|
0x76: {
|
||||||
name: '#mod8',
|
name: '#mod8',
|
||||||
@@ -536,7 +654,7 @@ const TILE_ENCODING = {
|
|||||||
name: 'popdown_floor',
|
name: 'popdown_floor',
|
||||||
},
|
},
|
||||||
0x7f: {
|
0x7f: {
|
||||||
name: 'forbidden',
|
name: 'no_sign',
|
||||||
has_next: true,
|
has_next: true,
|
||||||
},
|
},
|
||||||
0x80: {
|
0x80: {
|
||||||
@@ -544,7 +662,8 @@ const TILE_ENCODING = {
|
|||||||
has_next: true,
|
has_next: true,
|
||||||
},
|
},
|
||||||
0x81: {
|
0x81: {
|
||||||
name: 'directional_block',
|
name: 'frame_block',
|
||||||
|
has_next: true,
|
||||||
extra_args: [
|
extra_args: [
|
||||||
arg_direction,
|
arg_direction,
|
||||||
{
|
{
|
||||||
@@ -559,12 +678,14 @@ const TILE_ENCODING = {
|
|||||||
tile.arrows = arrows;
|
tile.arrows = arrows;
|
||||||
},
|
},
|
||||||
encode(tile) {
|
encode(tile) {
|
||||||
// TODO
|
let bits = 0;
|
||||||
return 0;
|
for (let direction of tile.arrows) {
|
||||||
|
bits |= DIRECTIONS[direction].bit;
|
||||||
|
}
|
||||||
|
return bits;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
has_next: true,
|
|
||||||
},
|
},
|
||||||
0x82: {
|
0x82: {
|
||||||
name: 'floor_mimic',
|
name: 'floor_mimic',
|
||||||
@@ -585,11 +706,11 @@ const TILE_ENCODING = {
|
|||||||
},
|
},
|
||||||
0x88: {
|
0x88: {
|
||||||
name: 'light_switch_off',
|
name: 'light_switch_off',
|
||||||
error: "The light switch is not yet implemented, sorry!",
|
modifier: modifier_wire,
|
||||||
},
|
},
|
||||||
0x89: {
|
0x89: {
|
||||||
name: 'light_switch_on',
|
name: 'light_switch_on',
|
||||||
error: "The light switch is not yet implemented, sorry!",
|
modifier: modifier_wire,
|
||||||
},
|
},
|
||||||
0x8a: {
|
0x8a: {
|
||||||
name: 'thief_keys',
|
name: 'thief_keys',
|
||||||
@@ -613,17 +734,26 @@ const TILE_ENCODING = {
|
|||||||
0x8f: {
|
0x8f: {
|
||||||
name: 'bribe',
|
name: 'bribe',
|
||||||
has_next: true,
|
has_next: true,
|
||||||
error: "The bribe is not yet implemented, sorry!",
|
|
||||||
},
|
},
|
||||||
0x90: {
|
0x90: {
|
||||||
name: 'speed_boots',
|
name: 'speed_boots',
|
||||||
has_next: true,
|
has_next: true,
|
||||||
error: "The speed boots are not yet implemented, sorry!",
|
|
||||||
},
|
},
|
||||||
0x91: {
|
0x92: {
|
||||||
name: 'hook',
|
name: 'hook',
|
||||||
has_next: true,
|
has_next: true,
|
||||||
error: "The hook is not yet implemented, sorry!",
|
},
|
||||||
|
|
||||||
|
// LL-specific tiles
|
||||||
|
0xe0: {
|
||||||
|
name: 'gift_bow',
|
||||||
|
has_next: true,
|
||||||
|
},
|
||||||
|
0xe1: {
|
||||||
|
name: 'circuit_block',
|
||||||
|
has_next: true,
|
||||||
|
modifier: modifier_wire,
|
||||||
|
extra_args: [arg_direction],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const REVERSE_TILE_ENCODING = {};
|
const REVERSE_TILE_ENCODING = {};
|
||||||
@@ -776,11 +906,10 @@ export function parse_level(buf, number = 1) {
|
|||||||
level.hint = str;
|
level.hint = str;
|
||||||
}
|
}
|
||||||
else if (type === 'NOTE') {
|
else if (type === 'NOTE') {
|
||||||
// Author's comments... but might also include multiple hints
|
// Author's comments... but might also include multiple hints for levels with
|
||||||
// for levels with multiple hint tiles, delineated by [CLUE].
|
// multiple hint tiles, delineated by [CLUE] (anywhere in the line (!)).
|
||||||
// For my purposes, extra hints are associated with the
|
// LL treats extra hints as tile properties, so store them for later
|
||||||
// individual tiles, so we'll map those later
|
[level.comment, ...extra_hints] = str.split(/\n?^.*\[CLUE\].*$\n?/mg);
|
||||||
[level.comment, ...extra_hints] = str.split(/^\[CLUE\]$/mg);
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -793,6 +922,7 @@ export function parse_level(buf, number = 1) {
|
|||||||
level.time_limit = view.getUint16(0, true);
|
level.time_limit = view.getUint16(0, true);
|
||||||
|
|
||||||
// TODO 0 - 10x10, 1 - 9x9, 2 - split, otherwise unknown which needs handling
|
// TODO 0 - 10x10, 1 - 9x9, 2 - split, otherwise unknown which needs handling
|
||||||
|
// FIXME does this default to 0 if no OPTN block is present?
|
||||||
let viewport = view.getUint8(2, true);
|
let viewport = view.getUint8(2, true);
|
||||||
if (viewport === 0) {
|
if (viewport === 0) {
|
||||||
level.viewport_size = 10;
|
level.viewport_size = 10;
|
||||||
@@ -835,7 +965,7 @@ export function parse_level(buf, number = 1) {
|
|||||||
|
|
||||||
if (view.byteLength <= 24)
|
if (view.byteLength <= 24)
|
||||||
continue;
|
continue;
|
||||||
//level.blob_behavior = view.getUint8(24, true);
|
level.blob_behavior = view.getUint8(24, true);
|
||||||
}
|
}
|
||||||
else if (type === 'MAP ' || type === 'PACK') {
|
else if (type === 'MAP ' || type === 'PACK') {
|
||||||
if (type === 'PACK') {
|
if (type === 'PACK') {
|
||||||
@@ -894,7 +1024,6 @@ export function parse_level(buf, number = 1) {
|
|||||||
let name = spec.name;
|
let name = spec.name;
|
||||||
|
|
||||||
// Make a tile template, possibly dealing with some special cases
|
// Make a tile template, possibly dealing with some special cases
|
||||||
// FIXME restore this
|
|
||||||
if (name === '#thinwall/canopy') {
|
if (name === '#thinwall/canopy') {
|
||||||
// Thin walls and the canopy are combined into a single byte for some
|
// Thin walls and the canopy are combined into a single byte for some
|
||||||
// reason; split them apart here. Which ones we get is determined by a
|
// reason; split them apart here. Which ones we get is determined by a
|
||||||
@@ -924,7 +1053,7 @@ export function parse_level(buf, number = 1) {
|
|||||||
}
|
}
|
||||||
else if (name instanceof Array) {
|
else if (name instanceof Array) {
|
||||||
// Custom floors and walls are one of several options, chosen by modifier
|
// Custom floors and walls are one of several options, chosen by modifier
|
||||||
name = name[modifier];
|
name = name[modifier % name.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
let type = TILE_TYPES[name];
|
let type = TILE_TYPES[name];
|
||||||
@@ -935,6 +1064,13 @@ export function parse_level(buf, number = 1) {
|
|||||||
spec.modifier.decode(tile, modifier);
|
spec.modifier.decode(tile, modifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Swivels come with their own specific flooring
|
||||||
|
// TODO this should go on the bottom
|
||||||
|
// TODO we should sort and also only allow one thing per layer
|
||||||
|
if (spec.dummy_terrain) {
|
||||||
|
cell.push({type: TILE_TYPES[spec.dummy_terrain]});
|
||||||
|
}
|
||||||
|
|
||||||
if (type.is_required_chip) {
|
if (type.is_required_chip) {
|
||||||
level.chips_required++;
|
level.chips_required++;
|
||||||
}
|
}
|
||||||
@@ -966,9 +1102,11 @@ export function parse_level(buf, number = 1) {
|
|||||||
else if (type === 'REPL' || type === 'PRPL') {
|
else if (type === 'REPL' || type === 'PRPL') {
|
||||||
// "Replay", i.e. demo solution
|
// "Replay", i.e. demo solution
|
||||||
if (type === 'PRPL') {
|
if (type === 'PRPL') {
|
||||||
|
// TODO is even this necessary though
|
||||||
bytes = decompress(bytes);
|
bytes = decompress(bytes);
|
||||||
}
|
}
|
||||||
level.demo = new CC2Demo(bytes);
|
level._replay_data = bytes;
|
||||||
|
level._replay_decoder = decode_replay;
|
||||||
}
|
}
|
||||||
else if (type === 'RDNY') {
|
else if (type === 'RDNY') {
|
||||||
}
|
}
|
||||||
@@ -996,18 +1134,24 @@ export function parse_level(buf, number = 1) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Connect extra hints
|
// Connect extra hints
|
||||||
let h = 0;
|
for (let [i, tile] of hint_tiles.entries()) {
|
||||||
for (let tile of hint_tiles) {
|
if (i < extra_hints.length) {
|
||||||
if (h > extra_hints.length)
|
tile.hint_text = extra_hints[i];
|
||||||
break;
|
}
|
||||||
|
else {
|
||||||
tile.specific_hint = extra_hints[h];
|
// Fall back to regular hint
|
||||||
h++;
|
tile.hint_text = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Write 1, 2, or 4 bytes to a DataView
|
// Write 1, 2, or 4 bytes to a DataView
|
||||||
function write_n_bytes(view, start, n, value) {
|
function write_n_bytes(view, start, n, value) {
|
||||||
@@ -1065,10 +1209,18 @@ function compress(buf) {
|
|||||||
// If we found a match that's worth copying (i.e. shorter than just writing a data block),
|
// If we found a match that's worth copying (i.e. shorter than just writing a data block),
|
||||||
// then do so
|
// then do so
|
||||||
let do_copy = (best_length > 3);
|
let do_copy = (best_length > 3);
|
||||||
|
|
||||||
|
// If we're not copying, add this byte to a pending data block /now/, so the next block can
|
||||||
|
// catch it if it happens to be the last byte
|
||||||
|
if (! do_copy) {
|
||||||
|
pending_data_length += 1;
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
|
||||||
// Write out any pending data block if necessary -- i.e. if we're about to write a copy
|
// Write out any pending data block if necessary -- i.e. if we're about to write a copy
|
||||||
// block, if we're at the max size of a data block, or if this is the end of the data
|
// block, if we're at the max size of a data block, or if this is the end of the data
|
||||||
if (pending_data_length > 0 &&
|
if (pending_data_length > 0 &&
|
||||||
(do_copy || pending_data_length === 127 || p === buf.byteLength - 1))
|
(do_copy || pending_data_length === 127 || p >= buf.byteLength))
|
||||||
{
|
{
|
||||||
outbytes[q] = pending_data_length;
|
outbytes[q] = pending_data_length;
|
||||||
q++;
|
q++;
|
||||||
@@ -1079,6 +1231,7 @@ function compress(buf) {
|
|||||||
pending_data_length = 0;
|
pending_data_length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finally, do a copy
|
||||||
if (do_copy) {
|
if (do_copy) {
|
||||||
outbytes[q] = 0x80 + best_length;
|
outbytes[q] = 0x80 + best_length;
|
||||||
outbytes[q + 1] = p - best_start;
|
outbytes[q + 1] = p - best_start;
|
||||||
@@ -1086,19 +1239,13 @@ function compress(buf) {
|
|||||||
// Update p, noting that we might've done a copy into the future
|
// Update p, noting that we might've done a copy into the future
|
||||||
p += best_length;
|
p += best_length;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
// Otherwise, add this to a pending data block
|
|
||||||
pending_data_length += 1;
|
|
||||||
p++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we ever exceed the uncompressed length, don't even bother
|
// If we ever exceed the uncompressed length, don't even bother
|
||||||
if (q > buf.byteLength) {
|
if (q > buf.byteLength) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// FIXME don't love this slice
|
return outbytes.subarray(0, q);
|
||||||
return outbytes.buffer.slice(0, q);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class C2M {
|
class C2M {
|
||||||
@@ -1156,6 +1303,26 @@ export function synthesize_level(stored_level) {
|
|||||||
let c2m = new C2M;
|
let c2m = new C2M;
|
||||||
c2m.add_section('CC2M', '133');
|
c2m.add_section('CC2M', '133');
|
||||||
|
|
||||||
|
if (stored_level.title) {
|
||||||
|
c2m.add_section('TITL', stored_level.title);
|
||||||
|
}
|
||||||
|
if (stored_level.author) {
|
||||||
|
c2m.add_section('AUTH', stored_level.author);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options block
|
||||||
|
let options = new Uint8Array(3);
|
||||||
|
new DataView(options.buffer).setUint16(0, stored_level.time_limit, true);
|
||||||
|
if (stored_level.viewport_size === 10) {
|
||||||
|
options[2] = 0;
|
||||||
|
}
|
||||||
|
else if (stored_level.viewport_size === 9) {
|
||||||
|
options[2] = 1;
|
||||||
|
}
|
||||||
|
// TODO split
|
||||||
|
// TODO for size purposes, omit the block entirely if all options are defaults?
|
||||||
|
c2m.add_section('OPTN', options);
|
||||||
|
|
||||||
// Store camera regions
|
// Store camera regions
|
||||||
// TODO LL feature, should be distinguished somehow
|
// TODO LL feature, should be distinguished somehow
|
||||||
if (stored_level.camera_regions.length > 0) {
|
if (stored_level.camera_regions.length > 0) {
|
||||||
@@ -1171,18 +1338,47 @@ export function synthesize_level(stored_level) {
|
|||||||
c2m.add_section('LXCM', bytes.buffer);
|
c2m.add_section('LXCM', bytes.buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME well this will not do
|
let map_bytes = new Uint8Array(1024);
|
||||||
let map_bytes = new Uint8Array(4096);
|
|
||||||
let map_view = new DataView(map_bytes.buffer);
|
let map_view = new DataView(map_bytes.buffer);
|
||||||
map_bytes[0] = stored_level.size_x;
|
map_bytes[0] = stored_level.size_x;
|
||||||
map_bytes[1] = stored_level.size_y;
|
map_bytes[1] = stored_level.size_y;
|
||||||
|
let hints = [];
|
||||||
let p = 2;
|
let p = 2;
|
||||||
for (let cell of stored_level.linear_cells) {
|
for (let cell of stored_level.linear_cells) {
|
||||||
|
// If we're in danger of running out of room in our buffer, add another kilobyte and copy
|
||||||
|
if (p >= map_bytes.length - 64) {
|
||||||
|
let new_bytes = new Uint8Array(map_bytes.length + 1024);
|
||||||
|
for (let i = 0; i < map_bytes.length; i++) {
|
||||||
|
new_bytes[i] = map_bytes[i];
|
||||||
|
}
|
||||||
|
map_bytes = new_bytes;
|
||||||
|
map_view = new DataView(map_bytes.buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO complain if duplicates on a layer
|
||||||
|
let dummy_terrain_tile = null;
|
||||||
for (let i = cell.length - 1; i >= 0; i--) {
|
for (let i = cell.length - 1; i >= 0; i--) {
|
||||||
let tile = cell[i];
|
let tile = cell[i];
|
||||||
// FIXME does not yet support canopy or thin walls >:S
|
// FIXME does not yet support canopy or thin walls >:S
|
||||||
let spec = REVERSE_TILE_ENCODING[tile.type.name];
|
let spec = REVERSE_TILE_ENCODING[tile.type.name];
|
||||||
|
|
||||||
|
// Handle the swivel, a tile that draws as an overlay but is stored like terrain. In a
|
||||||
|
// level, it has two parts: the swivel itself, and a dummy swivel_floor terrain which is
|
||||||
|
// unencodable. To encode that, we notice when we hit a swivel (which happens first),
|
||||||
|
// save it until we reach the terrain layer, and then sub it in instead.
|
||||||
|
// TODO if i follow in tyler's footsteps and give swivel its own layer then i'll need to
|
||||||
|
// complicate this somewhat
|
||||||
|
if (tile.type.draw_layer === 0 && dummy_terrain_tile) {
|
||||||
|
tile = dummy_terrain_tile;
|
||||||
|
spec = REVERSE_TILE_ENCODING[tile.type.name];
|
||||||
|
}
|
||||||
|
else if (spec.dummy_terrain) {
|
||||||
|
// This is a swivel, a tile that draws as an overlay but is stored like terrain, so
|
||||||
|
// wait until we hit terrain and store it then
|
||||||
|
dummy_terrain_tile = tile;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (spec.modifier) {
|
if (spec.modifier) {
|
||||||
let mod = spec.modifier.encode(tile);
|
let mod = spec.modifier.encode(tile);
|
||||||
if (mod === 0) {
|
if (mod === 0) {
|
||||||
@@ -1219,18 +1415,47 @@ export function synthesize_level(stored_level) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tile.type.name === 'hint') {
|
||||||
|
hints.push(tile.hint_text);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO assert that the bottom tile has no next, and all the others do
|
// TODO assert that the bottom tile has no next, and all the others do
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
map_bytes = map_bytes.subarray(0, p);
|
||||||
|
|
||||||
// FIXME ack, ArrayBuffer.slice makes a copy actually! and i use it a lot in this file i think!!
|
// Collect hints first so we can put them in the comment field
|
||||||
let map_buf = map_bytes.buffer.slice(0, p);
|
// FIXME this does not respect global hint, but then, neither does the editor.
|
||||||
let compressed_map = compress(map_buf);
|
hints = hints.map(hint => hint ?? '');
|
||||||
|
hints.push('');
|
||||||
|
hints.unshift('');
|
||||||
|
c2m.add_section('NOTE', hints.join('\n[CLUE]\n'));
|
||||||
|
|
||||||
|
let compressed_map = compress(map_bytes);
|
||||||
if (compressed_map) {
|
if (compressed_map) {
|
||||||
c2m.add_section('PACK', compressed_map);
|
c2m.add_section('PACK', compressed_map);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
c2m.add_section('MAP ', map_buf);
|
c2m.add_section('MAP ', map_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the replay, if any
|
||||||
|
if (stored_level.has_replay) {
|
||||||
|
let replay_bytes;
|
||||||
|
if (stored_level._replay_data && stored_level._replay_decoder === decode_replay) {
|
||||||
|
replay_bytes = stored_level._replay_data;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
replay_bytes = encode_replay(stored_level.replay, stored_level);
|
||||||
|
}
|
||||||
|
|
||||||
|
let compressed_replay = compress(replay_bytes);
|
||||||
|
if (compressed_replay) {
|
||||||
|
c2m.add_section('PRPL', compressed_replay);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
c2m.add_section('REPL', replay_bytes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c2m.add_section('END ', '');
|
c2m.add_section('END ', '');
|
||||||
@@ -1265,7 +1490,7 @@ const TOKENIZE_RX = RegExp(
|
|||||||
// 2: Comments are preceded by ; or // for some reason and run to the end of the line
|
// 2: Comments are preceded by ; or // for some reason and run to the end of the line
|
||||||
'|(?:;|//)(.*)' +
|
'|(?:;|//)(.*)' +
|
||||||
// 3: Strings are double-quoted (only!) and contain no escapes
|
// 3: Strings are double-quoted (only!) and contain no escapes
|
||||||
'|"([^"]+?)"' +
|
'|"([^"]*?)"' +
|
||||||
// 4: Labels are indicated by a #, including when used with 'goto'
|
// 4: Labels are indicated by a #, including when used with 'goto'
|
||||||
// (the exact set of allowed characters is unclear and i'm fudging it here)
|
// (the exact set of allowed characters is unclear and i'm fudging it here)
|
||||||
'|#(\\w+)' +
|
'|#(\\w+)' +
|
||||||
@@ -1621,7 +1846,7 @@ const MAX_SIMULTANEOUS_REQUESTS = 5;
|
|||||||
let resolve;
|
let resolve;
|
||||||
let promise = new Promise((res, rej) => { resolve = res });
|
let promise = new Promise((res, rej) => { resolve = res });
|
||||||
|
|
||||||
let game = new format_base.StoredGame(undefined, parse_level);
|
let game = new format_base.StoredGame(undefined, _parse_level_from_stored_meta);
|
||||||
let parser;
|
let parser;
|
||||||
let active_map_fetches = new Set;
|
let active_map_fetches = new Set;
|
||||||
let pending_map_fetches = [];
|
let pending_map_fetches = [];
|
||||||
@@ -1692,13 +1917,12 @@ const MAX_SIMULTANEOUS_REQUESTS = 5;
|
|||||||
resolve(game);
|
resolve(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(game);
|
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Individual levels don't make sense on their own, but we can wrap them in a dummy one-level game
|
// Individual levels don't make sense on their own, but we can wrap them in a dummy one-level game
|
||||||
export function wrap_individual_level(buf) {
|
export function wrap_individual_level(buf) {
|
||||||
let game = new format_base.StoredGame(undefined, parse_level);
|
let game = new format_base.StoredGame(undefined, _parse_level_from_stored_meta);
|
||||||
let meta = {
|
let meta = {
|
||||||
index: 0,
|
index: 0,
|
||||||
number: 1,
|
number: 1,
|
||||||
|
|||||||
@@ -329,8 +329,13 @@ function parse_level(bytes, 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 format_base.StoredGame(null, parse_level);
|
let game = new format_base.StoredGame(null, _parse_level_from_stored_meta);
|
||||||
|
|
||||||
let full_view = new DataView(buf);
|
let full_view = new DataView(buf);
|
||||||
let magic = full_view.getUint32(0, true);
|
let magic = full_view.getUint32(0, true);
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ export class Overlay {
|
|||||||
this.root.addEventListener('click', ev => {
|
this.root.addEventListener('click', ev => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
});
|
});
|
||||||
|
// Block any window-level key handlers from firing when we type
|
||||||
|
this.root.addEventListener('keydown', ev => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
open() {
|
open() {
|
||||||
@@ -54,6 +58,8 @@ export class Overlay {
|
|||||||
overlay.addEventListener('click', ev => {
|
overlay.addEventListener('click', ev => {
|
||||||
this.close();
|
this.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
@@ -61,10 +67,20 @@ export class Overlay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Overlay styled like a popup of some sort
|
||||||
|
export class TransientOverlay extends Overlay {
|
||||||
|
open() {
|
||||||
|
// TODO i don't like how vaguely arbitrary this feels.
|
||||||
|
let overlay = super.open();
|
||||||
|
overlay.classList.add('--transient');
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Overlay styled like a dialog box
|
// Overlay styled like a dialog box
|
||||||
export class DialogOverlay extends Overlay {
|
export class DialogOverlay extends Overlay {
|
||||||
constructor(conductor) {
|
constructor(conductor) {
|
||||||
super(conductor, mk('div.dialog'));
|
super(conductor, mk('form.dialog'));
|
||||||
|
|
||||||
this.root.append(
|
this.root.append(
|
||||||
this.header = mk('header'),
|
this.header = mk('header'),
|
||||||
@@ -103,3 +119,22 @@ export class ConfirmOverlay extends DialogOverlay {
|
|||||||
this.footer.append(yes, no);
|
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;
|
||||||
@@ -67,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;
|
||||||
@@ -125,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);
|
||||||
}
|
}
|
||||||
@@ -174,11 +218,11 @@ export class CanvasRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
create_tile_type_canvas(name) {
|
create_tile_type_canvas(name, tile = null) {
|
||||||
let canvas = mk('canvas', {width: this.tileset.size_x, height: this.tileset.size_y});
|
let 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,4 +61,19 @@ export default [{
|
|||||||
author: "jneen",
|
author: "jneen",
|
||||||
twitter: "jneen_",
|
twitter: "jneen_",
|
||||||
path: 'music/escape-on-star-road.ogg',
|
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
|
// TODO really need to specify this format more concretely, whoof
|
||||||
// XXX special kinds of drawing i know this has for a fact:
|
// 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
|
// - letter tiles draw from a block (one of two blocks!) of half-tiles onto the center of the base
|
||||||
// - slime and walkers have double-size tiles when moving
|
// - force floors are cropped from a double-size tile
|
||||||
// - wired tiles are a whole thing
|
// - wired tiles are a whole thing (floor)
|
||||||
// - thin walls are packed into just two tiles
|
// - 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
|
// - 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
|
// - 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 = {
|
export const CC2_TILESET_LAYOUT = {
|
||||||
'#wire-width': 1/16,
|
'#wire-width': 1/16,
|
||||||
|
|
||||||
@@ -22,8 +44,13 @@ export const CC2_TILESET_LAYOUT = {
|
|||||||
key_blue: [5, 1],
|
key_blue: [5, 1],
|
||||||
key_yellow: [6, 1],
|
key_yellow: [6, 1],
|
||||||
key_green: [7, 1],
|
key_green: [7, 1],
|
||||||
dirt_block: [8, 1],
|
// FIXME these shouldn't be drawn with a hole when drawn in isolation, gruh
|
||||||
// xray
|
dirt_block: {
|
||||||
|
special: 'perception',
|
||||||
|
threshold: 2,
|
||||||
|
hidden: [8, 1],
|
||||||
|
revealed: [9, 1],
|
||||||
|
},
|
||||||
ice: [10, 1],
|
ice: [10, 1],
|
||||||
ice_se: [11, 1],
|
ice_se: [11, 1],
|
||||||
ice_sw: [12, 1],
|
ice_sw: [12, 1],
|
||||||
@@ -35,23 +62,44 @@ export const CC2_TILESET_LAYOUT = {
|
|||||||
// Wiring!
|
// Wiring!
|
||||||
base: [0, 2],
|
base: [0, 2],
|
||||||
wired: [8, 26],
|
wired: [8, 26],
|
||||||
|
wired_cross: [10, 26],
|
||||||
is_wired_optional: true,
|
is_wired_optional: true,
|
||||||
},
|
},
|
||||||
wall_invisible: [0, 2],
|
// FIXME i think these are visible with the secret eye, but my tileset puts text on them, whoops
|
||||||
wall_appearing: [0, 2],
|
wall_invisible: {
|
||||||
wall: [1, 2],
|
special: 'perception',
|
||||||
floor_letter: [2, 2],
|
threshold: 2,
|
||||||
'floor_letter#ascii': {
|
hidden: [0, 2],
|
||||||
x0: 0,
|
revealed: [9, 31],
|
||||||
y0: 0,
|
|
||||||
width: 16,
|
|
||||||
height: 1,
|
|
||||||
},
|
},
|
||||||
'floor_letter#arrows': {
|
// FIXME this shouldn't be visible with seeing eye (or should it not spawn at all?)
|
||||||
north: [14, 31],
|
wall_invisible_revealed: [1, 2],
|
||||||
east: [14.5, 31],
|
wall_appearing: {
|
||||||
south: [15, 31],
|
special: 'perception',
|
||||||
west: [15.5, 31],
|
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],
|
thief_tools: [3, 2],
|
||||||
socket: [4, 2],
|
socket: [4, 2],
|
||||||
@@ -62,8 +110,13 @@ export const CC2_TILESET_LAYOUT = {
|
|||||||
[8, 2],
|
[8, 2],
|
||||||
[9, 2],
|
[9, 2],
|
||||||
],
|
],
|
||||||
ice_block: [10, 2],
|
// FIXME these shouldn't be drawn with a hole when drawn in isolation, gruh
|
||||||
// TODO ice block xray
|
ice_block: {
|
||||||
|
special: 'perception',
|
||||||
|
threshold: 2,
|
||||||
|
hidden: [10, 2],
|
||||||
|
revealed: [11, 2],
|
||||||
|
},
|
||||||
score_1000: [12, 2],
|
score_1000: [12, 2],
|
||||||
score_100: [13, 2],
|
score_100: [13, 2],
|
||||||
score_10: [14, 2],
|
score_10: [14, 2],
|
||||||
@@ -73,12 +126,14 @@ export const CC2_TILESET_LAYOUT = {
|
|||||||
green_chip: [9, 3],
|
green_chip: [9, 3],
|
||||||
chip_extra: [10, 3],
|
chip_extra: [10, 3],
|
||||||
chip: [11, 3],
|
chip: [11, 3],
|
||||||
// bribe
|
bribe: [12, 3],
|
||||||
// mercury boot
|
speed_boots: [13, 3],
|
||||||
// canopy, xray
|
canopy: [14, 3],
|
||||||
|
// canopy xray
|
||||||
|
|
||||||
// TODO lit
|
|
||||||
dynamite: [0, 4],
|
dynamite: [0, 4],
|
||||||
|
// FIXME lit frames
|
||||||
|
dynamite_lit: [0, 4],
|
||||||
bomb: [5, 4],
|
bomb: [5, 4],
|
||||||
green_bomb: [6, 4],
|
green_bomb: [6, 4],
|
||||||
// TODO bomb fuse tile, ugh
|
// TODO bomb fuse tile, ugh
|
||||||
@@ -92,24 +147,40 @@ export const CC2_TILESET_LAYOUT = {
|
|||||||
wall_custom_blue: [15, 4],
|
wall_custom_blue: [15, 4],
|
||||||
|
|
||||||
explosion: [[0, 5], [1, 5], [2, 5], [3, 5]],
|
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]],
|
splash: [[4, 5], [5, 5], [6, 5], [7, 5]],
|
||||||
flame_jet_off: [8, 5],
|
flame_jet_off: [8, 5],
|
||||||
flame_jet_on: [[9, 5], [10, 5], [11, 5]],
|
flame_jet_on: [[9, 5], [10, 5], [11, 5]],
|
||||||
popdown_wall: [12, 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],
|
popdown_floor_visible: [13, 5],
|
||||||
forbidden: [14, 5],
|
no_sign: [14, 5],
|
||||||
// TODO arrows overlay at [3, 10]
|
frame_block: {
|
||||||
directional_block: [15, 5],
|
base: [15, 5],
|
||||||
|
arrows: [3, 10],
|
||||||
|
},
|
||||||
|
|
||||||
flippers: [0, 6],
|
flippers: [0, 6],
|
||||||
fire_boots: [1, 6],
|
fire_boots: [1, 6],
|
||||||
cleats: [2, 6],
|
cleats: [2, 6],
|
||||||
suction_boots: [3, 6],
|
suction_boots: [3, 6],
|
||||||
hiking_boots: [4, 6],
|
hiking_boots: [4, 6],
|
||||||
// speed boots...? not boots though
|
lightning_bolt: [5, 6],
|
||||||
// weird translucent spiral
|
// 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_blue: [8, 6],
|
||||||
button_green: [9, 6],
|
button_green: [9, 6],
|
||||||
button_red: [10, 6],
|
button_red: [10, 6],
|
||||||
@@ -168,7 +239,13 @@ export const CC2_TILESET_LAYOUT = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
fake_wall: [0, 10],
|
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,
|
// Thin walls are built piecemeal from these two tiles; the first is N/S,
|
||||||
// the second is E/W
|
// the second is E/W
|
||||||
thinwall_n: {
|
thinwall_n: {
|
||||||
@@ -199,22 +276,33 @@ export const CC2_TILESET_LAYOUT = {
|
|||||||
popwall: [8, 10],
|
popwall: [8, 10],
|
||||||
popwall2: [8, 10],
|
popwall2: [8, 10],
|
||||||
gravel: [9, 10],
|
gravel: [9, 10],
|
||||||
ball: [[10, 10], [11, 10], [12, 10], [13, 10], [14, 10]],
|
ball: [
|
||||||
|
// appropriately, this animation ping-pongs
|
||||||
|
[10, 10], [11, 10], [12, 10], [13, 10],
|
||||||
|
[14, 10], [13, 10], [12, 10], [11, 10],
|
||||||
|
// FIXME the ball bounces so it specifically needs to play its animation every move; this
|
||||||
|
// defeats the ½x slowdown. it's dumb and means this anim as written doesn't match cc2
|
||||||
|
[10, 10], [11, 10], [12, 10], [13, 10],
|
||||||
|
[14, 10], [13, 10], [12, 10], [11, 10],
|
||||||
|
],
|
||||||
steel: {
|
steel: {
|
||||||
// Wiring!
|
// Wiring!
|
||||||
base: [15, 10],
|
base: [15, 10],
|
||||||
wired: [9, 26],
|
wired: [9, 26],
|
||||||
|
wired_cross: [11, 26],
|
||||||
is_wired_optional: true,
|
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: {
|
teeth: {
|
||||||
// NOTE: CC2 inexplicably dropped north teeth and just uses the south
|
// NOTE: CC2 inexplicably dropped north teeth and just uses the south sprites instead
|
||||||
// sprites instead
|
north: [[1, 11], [0, 11], [1, 11], [2, 11]],
|
||||||
north: [[0, 11], [1, 11], [2, 11]],
|
east: [[4, 11], [3, 11], [4, 11], [5, 11]],
|
||||||
east: [[3, 11], [4, 11], [5, 11]],
|
south: [[1, 11], [0, 11], [1, 11], [2, 11]],
|
||||||
south: [[0, 11], [1, 11], [2, 11]],
|
west: [[7, 11], [6, 11], [7, 11], [8, 11]],
|
||||||
west: [[6, 11], [7, 11], [8, 11]],
|
|
||||||
},
|
},
|
||||||
swivel_sw: [9, 11],
|
swivel_sw: [9, 11],
|
||||||
swivel_nw: [10, 11],
|
swivel_nw: [10, 11],
|
||||||
@@ -237,19 +325,33 @@ export const CC2_TILESET_LAYOUT = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
walker: [0, 13],
|
walker: [0, 13],
|
||||||
// TODO walker animations span multiple tiles, rgh
|
// FIXME walker animations span multiple tiles
|
||||||
helmet: [0, 14],
|
helmet: [0, 14],
|
||||||
stopwatch_toggle: [14, 14],
|
stopwatch_toggle: [14, 14],
|
||||||
stopwatch_bonus: [15, 14],
|
stopwatch_bonus: [15, 14],
|
||||||
|
|
||||||
blob: [0, 15],
|
blob: [0, 15],
|
||||||
// TODO blob animations also span multiple tiles
|
// FIXME blob animations span multiple tiles
|
||||||
// TODO [0, 16] some kinda red/blue outline
|
// 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
|
// TODO [15, 16] some kinda yellow/black outline
|
||||||
|
|
||||||
// timid teeth
|
// timid teeth
|
||||||
bowling_ball: [6, 17], // TODO also +18 when rolling
|
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],
|
||||||
|
rolling_ball: [[6, 17], [7, 17]],
|
||||||
tank_yellow: {
|
tank_yellow: {
|
||||||
north: [[8, 17], [9, 17]],
|
north: [[8, 17], [9, 17]],
|
||||||
east: [[10, 17], [11, 17]],
|
east: [[10, 17], [11, 17]],
|
||||||
@@ -257,8 +359,26 @@ export const CC2_TILESET_LAYOUT = {
|
|||||||
west: [[14, 17], [15, 17]],
|
west: [[14, 17], [15, 17]],
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO rover has layers and moves and stuff
|
// TODO rover has an overlay showing its direction
|
||||||
rover: [0, 18],
|
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],
|
xray_eye: [11, 18],
|
||||||
ghost: {
|
ghost: {
|
||||||
north: [12, 18],
|
north: [12, 18],
|
||||||
@@ -294,7 +414,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]],
|
force_floor_all: [[0, 21], [1, 21], [2, 21], [3, 21], [4, 21], [5, 21], [6, 21], [7, 21]],
|
||||||
// latches
|
// latches
|
||||||
// switch
|
light_switch_off: {
|
||||||
|
base: [14, 21],
|
||||||
|
wired: [12, 21],
|
||||||
|
},
|
||||||
|
light_switch_on: {
|
||||||
|
base: [14, 21],
|
||||||
|
wired: [13, 21],
|
||||||
|
},
|
||||||
thief_keys: [15, 21],
|
thief_keys: [15, 21],
|
||||||
|
|
||||||
player: {
|
player: {
|
||||||
@@ -332,8 +459,10 @@ export const CC2_TILESET_LAYOUT = {
|
|||||||
south: [2, 23],
|
south: [2, 23],
|
||||||
west: [10, 23],
|
west: [10, 23],
|
||||||
},
|
},
|
||||||
|
exited: 'normal',
|
||||||
// These are frames from the splash/explosion animations
|
// These are frames from the splash/explosion animations
|
||||||
drowned: [5, 5],
|
drowned: [5, 5],
|
||||||
|
slimed: [5, 5],
|
||||||
burned: [1, 5],
|
burned: [1, 5],
|
||||||
exploded: [1, 5],
|
exploded: [1, 5],
|
||||||
failed: [1, 5],
|
failed: [1, 5],
|
||||||
@@ -367,6 +496,55 @@ export const CC2_TILESET_LAYOUT = {
|
|||||||
[15, 24],
|
[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],
|
'#unpowered': [13, 26],
|
||||||
'#powered': [15, 26],
|
'#powered': [15, 26],
|
||||||
|
|
||||||
@@ -405,8 +583,10 @@ export const CC2_TILESET_LAYOUT = {
|
|||||||
south: [2, 28],
|
south: [2, 28],
|
||||||
west: [10, 28],
|
west: [10, 28],
|
||||||
},
|
},
|
||||||
|
exited: 'normal',
|
||||||
// These are frames from the splash/explosion animations
|
// These are frames from the splash/explosion animations
|
||||||
drowned: [5, 5],
|
drowned: [5, 5],
|
||||||
|
slimed: [5, 5],
|
||||||
burned: [1, 5],
|
burned: [1, 5],
|
||||||
exploded: [1, 5],
|
exploded: [1, 5],
|
||||||
failed: [1, 5],
|
failed: [1, 5],
|
||||||
@@ -419,11 +599,40 @@ export const CC2_TILESET_LAYOUT = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
// TODO handle train tracks! this is gonna be complicated.
|
// TODO handle train tracks! this is gonna be complicated.
|
||||||
railroad: [9, 10],
|
railroad: {
|
||||||
railroad_sign: [4, 31],
|
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],
|
dirt: [4, 31],
|
||||||
no_player2_sign: [5, 31],
|
no_player2_sign: [5, 31],
|
||||||
no_player1_sign: [6, 31],
|
no_player1_sign: [6, 31],
|
||||||
|
hook: [7, 31],
|
||||||
// misc other stuff
|
// misc other stuff
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -435,6 +644,7 @@ export const TILE_WORLD_TILESET_LAYOUT = {
|
|||||||
water: [0, 3],
|
water: [0, 3],
|
||||||
fire: [0, 4],
|
fire: [0, 4],
|
||||||
wall_invisible: [0, 5],
|
wall_invisible: [0, 5],
|
||||||
|
wall_invisible_revealed: [0, 1],
|
||||||
thinwall_n: [0, 6],
|
thinwall_n: [0, 6],
|
||||||
thinwall_w: [0, 7],
|
thinwall_w: [0, 7],
|
||||||
thinwall_s: [0, 8],
|
thinwall_s: [0, 8],
|
||||||
@@ -596,9 +806,14 @@ export const TILE_WORLD_TILESET_LAYOUT = {
|
|||||||
export const LL_TILESET_LAYOUT = Object.assign({}, CC2_TILESET_LAYOUT, {
|
export const LL_TILESET_LAYOUT = Object.assign({}, CC2_TILESET_LAYOUT, {
|
||||||
// Completed teeth sprites
|
// Completed teeth sprites
|
||||||
teeth: Object.assign({}, CC2_TILESET_LAYOUT.teeth, {
|
teeth: Object.assign({}, CC2_TILESET_LAYOUT.teeth, {
|
||||||
north: [[0, 32], [1, 32], [2, 32], [1, 32]],
|
north: [[1, 32], [0, 32], [1, 32], [2, 32]],
|
||||||
}),
|
}),
|
||||||
popwall2: [9, 32],
|
teeth_timid: {
|
||||||
|
north: [[7, 32], [6, 32], [7, 32], [8, 32]],
|
||||||
|
east: [[4, 32], [2, 17], [4, 32], [3, 17]],
|
||||||
|
south: [[3, 32], [0, 17], [3, 32], [1, 17]],
|
||||||
|
west: [[5, 32], [4, 17], [5, 32], [5, 17]],
|
||||||
|
},
|
||||||
|
|
||||||
// Extra player sprites
|
// Extra player sprites
|
||||||
player: Object.assign({}, CC2_TILESET_LAYOUT.player, {
|
player: Object.assign({}, CC2_TILESET_LAYOUT.player, {
|
||||||
@@ -609,14 +824,26 @@ export const LL_TILESET_LAYOUT = Object.assign({}, CC2_TILESET_LAYOUT, {
|
|||||||
west: [3, 33],
|
west: [3, 33],
|
||||||
},
|
},
|
||||||
forced: 'skating',
|
forced: 'skating',
|
||||||
|
exited: [14, 32],
|
||||||
burned: {
|
burned: {
|
||||||
north: [4, 33],
|
north: [4, 33],
|
||||||
east: [5, 33],
|
east: [5, 33],
|
||||||
south: [6, 33],
|
south: [6, 33],
|
||||||
west: [7, 33],
|
west: [7, 33],
|
||||||
},
|
},
|
||||||
|
slimed: [1, 38],
|
||||||
|
}),
|
||||||
|
player2: Object.assign({}, CC2_TILESET_LAYOUT.player2, {
|
||||||
|
// TODO skating
|
||||||
|
exited: [15, 32],
|
||||||
|
burned: {
|
||||||
|
north: [12, 33],
|
||||||
|
east: [13, 33],
|
||||||
|
south: [14, 33],
|
||||||
|
west: [15, 33],
|
||||||
|
},
|
||||||
|
slimed: [1, 38],
|
||||||
}),
|
}),
|
||||||
// TODO player2 equivalents
|
|
||||||
bogus_player_burned_fire: {
|
bogus_player_burned_fire: {
|
||||||
overlay: [6, 33],
|
overlay: [6, 33],
|
||||||
base: 'fire',
|
base: 'fire',
|
||||||
@@ -625,6 +852,33 @@ export const LL_TILESET_LAYOUT = Object.assign({}, CC2_TILESET_LAYOUT, {
|
|||||||
overlay: [6, 33],
|
overlay: [6, 33],
|
||||||
base: 'floor',
|
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 {
|
export class Tileset {
|
||||||
@@ -637,12 +891,163 @@ export class Tileset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
draw(tile, tic, blit) {
|
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.movement_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.movement_speed - tile.movement_cooldown) + tic % 1) / tile.movement_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.movement_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
|
// Draws a tile type, given by name. Passing in a tile is optional, but
|
||||||
// without it you'll get defaults.
|
// without it you'll get defaults.
|
||||||
draw_type(name, tile, tic, blit) {
|
draw_type(name, tile, tic, perception, blit) {
|
||||||
let drawspec = this.layout[name];
|
let drawspec = this.layout[name];
|
||||||
if (! drawspec) {
|
if (! drawspec) {
|
||||||
console.error(`Don't know how to draw tile type ${name}!`);
|
console.error(`Don't know how to draw tile type ${name}!`);
|
||||||
@@ -651,12 +1056,17 @@ export class Tileset {
|
|||||||
|
|
||||||
if (drawspec.overlay) {
|
if (drawspec.overlay) {
|
||||||
// Goofy overlay thing used for green/purple toggle tiles and
|
// 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).
|
// the overlay (either a type name or a regular draw spec).
|
||||||
// TODO chance of infinite recursion here
|
// 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') {
|
if (typeof drawspec.overlay === 'string') {
|
||||||
this.draw_type(drawspec.overlay, tile, tic, blit);
|
this.draw_type(drawspec.overlay, tile, tic, perception, blit);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -664,6 +1074,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;
|
let coords = drawspec;
|
||||||
if (drawspec.mask) {
|
if (drawspec.mask) {
|
||||||
// Some tiles (OK, just the thin walls) don't actually draw a full
|
// Some tiles (OK, just the thin walls) don't actually draw a full
|
||||||
@@ -671,31 +1105,52 @@ export class Tileset {
|
|||||||
coords = drawspec.tile;
|
coords = drawspec.tile;
|
||||||
}
|
}
|
||||||
else if (drawspec.wired) {
|
else if (drawspec.wired) {
|
||||||
if (tile && tile.wire_directions) {
|
// This /should/ match CC2's draw order exactly, based on experimentation
|
||||||
// TODO all four is a different thing entirely with two separate parts, ugh
|
let wire_radius = this.layout['#wire-width'] / 2;
|
||||||
// Draw the appropriate wire underlay
|
if (tile && tile.wire_directions === 0x0f) {
|
||||||
this.draw_type(tile.cell.is_powered ? '#powered' : '#unpowered', tile, tic, blit);
|
// 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
|
// Draw the two wires as separate rectangles, NS then EW
|
||||||
let wiredir = tile.wire_directions;
|
let wire_inset = 0.5 - wire_radius;
|
||||||
let wire_radius = this.layout['#wire-width'] / 2;
|
let wire_coords_ns = this.layout[
|
||||||
let wire0 = 0.5 - wire_radius;
|
tile.cell && tile.cell.powered_edges & DIRECTIONS['north'].bit ? '#powered' : '#unpowered'];
|
||||||
let wire1 = 0.5 + wire_radius;
|
let wire_coords_ew = this.layout[
|
||||||
let [bx, by] = drawspec.base;
|
tile.cell && tile.cell.powered_edges & DIRECTIONS['east'].bit ? '#powered' : '#unpowered'];
|
||||||
if ((wiredir & DIRECTIONS['north'].bit) === 0) {
|
blit(wire_coords_ns[0], wire_coords_ns[1], wire_inset, 0, wire_radius * 2, 1);
|
||||||
blit(bx, by, 0, 0, 1, wire0);
|
blit(wire_coords_ew[0], wire_coords_ew[1], 0, wire_inset, 1, wire_radius * 2);
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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]);
|
||||||
|
|
||||||
|
// FIXME some tiles don't have active wiring on all sides...
|
||||||
|
// 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;
|
coords = drawspec.wired;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -711,6 +1166,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) {
|
else if (drawspec.animate_width) {
|
||||||
// Force floors animate their... cutout, I guess?
|
// Force floors animate their... cutout, I guess?
|
||||||
let [x, y] = drawspec.base;
|
let [x, y] = drawspec.base;
|
||||||
@@ -756,7 +1215,7 @@ export class Tileset {
|
|||||||
// Deal with animation
|
// Deal with animation
|
||||||
if (coords[0] instanceof Array) {
|
if (coords[0] instanceof Array) {
|
||||||
if (tic !== null) {
|
if (tic !== null) {
|
||||||
if (tile && tile.animation_speed) {
|
if (tile && tile.movement_speed) {
|
||||||
// This tile reports its own animation timing (in tics), so trust that, and just
|
// This tile reports its own animation timing (in tics), so trust that, and just
|
||||||
// use the current tic's fraction.
|
// use the current tic's fraction.
|
||||||
// That said: adjusting animation speed complicates this slightly. Consider the
|
// That said: adjusting animation speed complicates this slightly. Consider the
|
||||||
@@ -768,7 +1227,7 @@ export class Tileset {
|
|||||||
// first half; if it started on tics 5-8, play the second half. They could get
|
// 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
|
// out of sync if the player hesitates, but no one will notice that, and this
|
||||||
// approach minimizes storing extra state.
|
// approach minimizes storing extra state.
|
||||||
let i = (tile.animation_progress + tic % 1) / tile.animation_speed;
|
let i = ((tile.movement_speed - tile.movement_cooldown) + tic % 1) / tile.movement_speed;
|
||||||
// But do NOT do this for explosions or splashes, which have a fixed duration
|
// But do NOT do this for explosions or splashes, which have a fixed duration
|
||||||
// and only play once
|
// and only play once
|
||||||
if (this.animation_slowdown > 1 && ! tile.type.ttl) {
|
if (this.animation_slowdown > 1 && ! tile.type.ttl) {
|
||||||
@@ -776,7 +1235,7 @@ export class Tileset {
|
|||||||
// 1/N of it before the game ends (or loops) the animation.
|
// 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
|
// So increase by [0..N-1] to get it in some other range, then divide by N
|
||||||
// to scale back down to [0, 1)
|
// to scale back down to [0, 1)
|
||||||
i += Math.floor(tic / tile.animation_speed % this.animation_slowdown);
|
i += Math.floor(tic / tile.movement_speed % this.animation_slowdown);
|
||||||
i /= this.animation_slowdown;
|
i /= this.animation_slowdown;
|
||||||
}
|
}
|
||||||
coords = coords[Math.floor(i * coords.length)];
|
coords = coords[Math.floor(i * coords.length)];
|
||||||
@@ -795,7 +1254,7 @@ export class Tileset {
|
|||||||
// Continue on with masking
|
// Continue on with masking
|
||||||
coords = drawspec.tile;
|
coords = drawspec.tile;
|
||||||
let [x0, y0, w, h] = drawspec.mask;
|
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 {
|
else {
|
||||||
if (!coords) console.error(name, tile);
|
if (!coords) console.error(name, tile);
|
||||||
@@ -809,51 +1268,56 @@ export class Tileset {
|
|||||||
let tunnel_length = 12/32;
|
let tunnel_length = 12/32;
|
||||||
let tunnel_offset = (1 - tunnel_width) / 2;
|
let tunnel_offset = (1 - tunnel_width) / 2;
|
||||||
if (tile.wire_tunnel_directions & DIRECTIONS['north'].bit) {
|
if (tile.wire_tunnel_directions & DIRECTIONS['north'].bit) {
|
||||||
blit(tunnel_coords[0] + tunnel_offset, tunnel_coords[1],
|
blit(tunnel_coords[0], tunnel_coords[1],
|
||||||
tunnel_offset, 0, tunnel_width, tunnel_length);
|
tunnel_offset, 0, tunnel_width, tunnel_length);
|
||||||
}
|
}
|
||||||
if (tile.wire_tunnel_directions & DIRECTIONS['south'].bit) {
|
if (tile.wire_tunnel_directions & DIRECTIONS['south'].bit) {
|
||||||
blit(tunnel_coords[0] + tunnel_offset, tunnel_coords[1] + 1 - tunnel_length,
|
blit(tunnel_coords[0], tunnel_coords[1],
|
||||||
tunnel_offset, 1 - tunnel_length, tunnel_width, tunnel_length);
|
tunnel_offset, 1 - tunnel_length, tunnel_width, tunnel_length);
|
||||||
}
|
}
|
||||||
if (tile.wire_tunnel_directions & DIRECTIONS['west'].bit) {
|
if (tile.wire_tunnel_directions & DIRECTIONS['west'].bit) {
|
||||||
blit(tunnel_coords[0], tunnel_coords[1] + tunnel_offset,
|
blit(tunnel_coords[0], tunnel_coords[1],
|
||||||
0, tunnel_offset, tunnel_length, tunnel_width);
|
0, tunnel_offset, tunnel_length, tunnel_width);
|
||||||
}
|
}
|
||||||
if (tile.wire_tunnel_directions & DIRECTIONS['east'].bit) {
|
if (tile.wire_tunnel_directions & DIRECTIONS['east'].bit) {
|
||||||
blit(tunnel_coords[0] + 1 - tunnel_length, tunnel_coords[1] + tunnel_offset,
|
blit(tunnel_coords[0], tunnel_coords[1],
|
||||||
1 - tunnel_length, tunnel_offset, tunnel_length, tunnel_width);
|
1 - tunnel_length, tunnel_offset, tunnel_length, tunnel_width);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special behavior for special objects
|
// Directional blocks have arrows drawn on top
|
||||||
// TODO? hardcode this less?
|
// TODO does cc2 draw even if there are no arrows?
|
||||||
if (name === 'floor_letter') {
|
if (drawspec.arrows && tile && tile.arrows) {
|
||||||
let n = tile.ascii_code - 32;
|
let [x, y] = drawspec.arrows;
|
||||||
let scale = 0.5;
|
let x0 = 0.25, y0 = 0.25, x1 = 0.75, y1 = 0.75;
|
||||||
let sx, sy;
|
if (tile.arrows.has('north')) {
|
||||||
if (n < 0) {
|
y0 = 0;
|
||||||
// Arrows
|
}
|
||||||
if (n < -4) {
|
if (tile.arrows.has('east')) {
|
||||||
// Default to south
|
x1 = 1;
|
||||||
n = -2;
|
}
|
||||||
}
|
if (tile.arrows.has('south')) {
|
||||||
|
y1 = 1;
|
||||||
|
}
|
||||||
|
if (tile.arrows.has('west')) {
|
||||||
|
x0 = 0;
|
||||||
|
}
|
||||||
|
blit(x, y, x0, y0, x1 - x0, y1 - y0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let direction = ['north', 'east', 'south', 'west'][n + 4];
|
_rotate(direction, x0, y0, x1, y1) {
|
||||||
[sx, sy] = this.layout['floor_letter#arrows'][direction];
|
if (direction === 'east') {
|
||||||
}
|
return [1 - y1, x0, 1 - y0, x1];
|
||||||
else {
|
}
|
||||||
// ASCII text (only up through uppercase)
|
else if (direction === 'south') {
|
||||||
let letter_spec = this.layout['floor_letter#ascii'];
|
return [1 - x1, 1 - y1, 1 - x0, 1 - y0];
|
||||||
if (n > letter_spec.width / scale * letter_spec.height / scale) {
|
}
|
||||||
n = 0;
|
else if (direction === 'west') {
|
||||||
}
|
return [y0, 1 - x1, y1, 1 - x0];
|
||||||
let w = letter_spec.width / scale;
|
}
|
||||||
sx = (letter_spec.x0 + n % w) * scale;
|
else {
|
||||||
sy = (letter_spec.y0 + Math.floor(n / w)) * scale;
|
return [x0, y0, x1, y1];
|
||||||
}
|
|
||||||
let offset = (1 - scale) / 2;
|
|
||||||
blit(sx, sy, offset, offset, scale, scale);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,14 +24,19 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +104,12 @@ export function handle_drop(element, options) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) => {
|
||||||
@@ -152,8 +163,53 @@ export function string_from_buffer_ascii(buf, start = 0, len) {
|
|||||||
return String.fromCharCode.apply(null, new Uint8Array(buf, start, len));
|
return String.fromCharCode.apply(null, new Uint8Array(buf, start, len));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Converts a string to a buffer, using NO ENCODING, assuming single-byte characters
|
||||||
|
export function bytestring_to_buffer(bytestring) {
|
||||||
|
return Uint8Array.from(bytestring, c => c.charCodeAt(0)).buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -193,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) {
|
||||||
@@ -252,6 +305,8 @@ export function* walk_grid(x0, y0, x1, y1) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
window.walk_grid = walk_grid;
|
||||||
|
// console.table(Array.from(walk_grid(13, 27.133854389190674, 12.90625, 27.227604389190674)))
|
||||||
|
|
||||||
// Root class to indirect over where we might get files from
|
// Root class to indirect over where we might get files from
|
||||||
// - a pool of uploaded in-memory files
|
// - a pool of uploaded in-memory files
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ button {
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
color: white;
|
color: white;
|
||||||
background: var(--button-bg-color);
|
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;
|
border-radius: 0.25em;
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -45,9 +48,19 @@ button {
|
|||||||
button:hover {
|
button:hover {
|
||||||
background: var(--button-bg-hover-color);
|
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 {
|
button:disabled {
|
||||||
color: #606060;
|
color: #606060;
|
||||||
background: #202020;
|
background: #202020;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 2px 1px hsl(225, 0%, 10%),
|
||||||
|
0 1px 0 hsl(225, 10%, 10%);
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
}
|
}
|
||||||
button.button-big {
|
button.button-big {
|
||||||
@@ -56,6 +69,12 @@ button.button-big {
|
|||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
}
|
}
|
||||||
|
button.--button-glow-ok {
|
||||||
|
background: hsl(225, 100%, 50%);
|
||||||
|
}
|
||||||
|
button.--button-glow {
|
||||||
|
transition: background-color 0.5s ease-out;
|
||||||
|
}
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -97,10 +116,10 @@ a:visited {
|
|||||||
text-decoration: underline dotted;
|
text-decoration: underline dotted;
|
||||||
}
|
}
|
||||||
a:link {
|
a:link {
|
||||||
color: hsl(225, 50%, 60%);
|
color: hsl(225, 50%, 75%);
|
||||||
}
|
}
|
||||||
a:visited {
|
a:visited {
|
||||||
color: hsl(300, 50%, 60%);
|
color: hsl(255, 50%, 75%);
|
||||||
}
|
}
|
||||||
a:link:hover,
|
a:link:hover,
|
||||||
a:visited:hover {
|
a:visited:hover {
|
||||||
@@ -110,6 +129,20 @@ a:active {
|
|||||||
color: hsl(0, 50%, 60%);
|
color: hsl(0, 50%, 60%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svg#svg-iconsheet {
|
||||||
|
/* This is a collection of SVG icons to be re-<use>d */
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
svg.svg-icon {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
stroke: none;
|
||||||
|
fill: currentColor;
|
||||||
|
fill-rule: evenodd;
|
||||||
|
}
|
||||||
|
|
||||||
/* Overlay styling */
|
/* Overlay styling */
|
||||||
.overlay {
|
.overlay {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -123,13 +156,18 @@ a:active {
|
|||||||
right: 0;
|
right: 0;
|
||||||
background: #fff4;
|
background: #fff4;
|
||||||
}
|
}
|
||||||
|
.overlay.--transient {
|
||||||
|
align-items: start;
|
||||||
|
justify-content: start;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
.dialog {
|
.dialog {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
min-width: 33%;
|
min-width: 33vw;
|
||||||
max-width: 75%;
|
max-width: 75vw;
|
||||||
max-height: 75%;
|
max-height: 75vh;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
color: black;
|
color: black;
|
||||||
background: #f4f4f4;
|
background: #f4f4f4;
|
||||||
@@ -157,6 +195,7 @@ a:active {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.dialog > section {
|
.dialog > section {
|
||||||
|
flex: auto;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
}
|
}
|
||||||
@@ -165,6 +204,26 @@ a:active {
|
|||||||
background: #f0d0d0;
|
background: #f0d0d0;
|
||||||
padding: 0.5em 1em;
|
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 */
|
/* Individual overlays */
|
||||||
table.level-browser {
|
table.level-browser {
|
||||||
@@ -254,7 +313,6 @@ label.option {
|
|||||||
}
|
}
|
||||||
label.option:hover {
|
label.option:hover {
|
||||||
outline: 2px solid #d0d0d0;
|
outline: 2px solid #d0d0d0;
|
||||||
outline-radius: 2px;
|
|
||||||
}
|
}
|
||||||
label.option .option-label {
|
label.option .option-label {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -269,6 +327,13 @@ label.option .option-label {
|
|||||||
/* TODO */
|
/* TODO */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.dialog {
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Bits and pieces */
|
/* Bits and pieces */
|
||||||
img.compat-icon {
|
img.compat-icon {
|
||||||
margin: 0 0.25em 0.125em;
|
margin: 0 0.25em 0.125em;
|
||||||
@@ -403,6 +468,11 @@ body[data-mode=player] #editor-play {
|
|||||||
#splash > header h1 {
|
#splash > header h1 {
|
||||||
font-size: 3em;
|
font-size: 3em;
|
||||||
}
|
}
|
||||||
|
#splash > header p {
|
||||||
|
margin: 0;
|
||||||
|
font-style: italic;
|
||||||
|
color: #909090;
|
||||||
|
}
|
||||||
#splash h2 {
|
#splash h2 {
|
||||||
border-bottom: 1px solid #404040;
|
border-bottom: 1px solid #404040;
|
||||||
color: #909090;
|
color: #909090;
|
||||||
@@ -415,7 +485,7 @@ body[data-mode=player] #editor-play {
|
|||||||
}
|
}
|
||||||
#splash > #splash-intro {
|
#splash > #splash-intro {
|
||||||
grid-area: intro;
|
grid-area: intro;
|
||||||
font-size: 18px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
#splash > #splash-stock-levels {
|
#splash > #splash-stock-levels {
|
||||||
grid-area: stock;
|
grid-area: stock;
|
||||||
@@ -476,16 +546,83 @@ button.level-pack-button p {
|
|||||||
color: #c0c0c0;
|
color: #c0c0c0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* "Bulk test" button, only available in debug mode */
|
||||||
|
#main-test-pack {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
body.--debug #main-test-pack {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
.packtest-dialog {
|
||||||
|
width: 75vw;
|
||||||
|
height: 75vh;
|
||||||
|
}
|
||||||
|
ol.packtest-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
height: 1em;
|
||||||
|
border: 1px solid #606060;
|
||||||
|
}
|
||||||
|
ol.packtest-summary > li {
|
||||||
|
/* Give a meaty flex-basis; the dialog has a max-width so it won't blow out, and these will
|
||||||
|
* simply shrink if necessary */
|
||||||
|
flex: 1 1 1em;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
.packtest-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
.packtest-row > p {
|
||||||
|
flex: 9;
|
||||||
|
margin: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.packtest-row > button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.packtest-results {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
.packtest-results > li {
|
||||||
|
padding: 0.25em;
|
||||||
|
margin: 0.25em 0;
|
||||||
|
}
|
||||||
|
.packtest-results > li > canvas {
|
||||||
|
display: block;
|
||||||
|
margin: 0.5em auto;
|
||||||
|
}
|
||||||
|
ol.packtest-colorcoded > li[data-status=no-replay] {
|
||||||
|
background: hsl(0, 0%, 25%);
|
||||||
|
}
|
||||||
|
ol.packtest-colorcoded > li[data-status=running] {
|
||||||
|
background: hsl(30, 100%, 75%);
|
||||||
|
}
|
||||||
|
ol.packtest-colorcoded > li[data-status=success] {
|
||||||
|
background: hsl(120, 60%, 75%);
|
||||||
|
}
|
||||||
|
ol.packtest-colorcoded > li[data-status=failure] {
|
||||||
|
background: hsl(0, 60%, 60%);
|
||||||
|
}
|
||||||
|
ol.packtest-colorcoded > li[data-status=short] {
|
||||||
|
background: hsl(330, 60%, 75%);
|
||||||
|
}
|
||||||
|
ol.packtest-colorcoded > li[data-status=error] {
|
||||||
|
background: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**************************************************************************************************/
|
/**************************************************************************************************/
|
||||||
/* Player */
|
/* Player */
|
||||||
|
|
||||||
#player {
|
#player {
|
||||||
flex: 0;
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
justify-content: stretch;
|
|
||||||
gap: 0.5em;
|
|
||||||
margin: auto; /* center in both directions baby */
|
|
||||||
|
|
||||||
image-rendering: crisp-edges;
|
image-rendering: crisp-edges;
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
@@ -494,8 +631,20 @@ button.level-pack-button p {
|
|||||||
--tile-height: 32px;
|
--tile-height: 32px;
|
||||||
--scale: 1;
|
--scale: 1;
|
||||||
}
|
}
|
||||||
#player > .-main-area {
|
#player-main {
|
||||||
|
/* This element basically just exists so that #player can have relative positioning and the
|
||||||
|
* debug panel can use that to sit against the right edge; absolute positioning excludes
|
||||||
|
* margins, so if it were positioned as a child of THIS element, it would be stuffed into the
|
||||||
|
* game area (oops!) */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
gap: 0.5em;
|
||||||
|
margin: auto; /* center in both directions baby */
|
||||||
|
}
|
||||||
|
#player-game-area {
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
|
align-self: center; /* don't stretch if the buttons or music blow out somehow */
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
grid:
|
grid:
|
||||||
@@ -547,6 +696,10 @@ button.level-pack-button p {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
text-shadow: 0 2px 1px black;
|
text-shadow: 0 2px 1px black;
|
||||||
}
|
}
|
||||||
|
/* Allow clicking through the overlay in debug mode */
|
||||||
|
body.--debug .overlay-message {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
#player .overlay-message p {
|
#player .overlay-message p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@@ -687,7 +840,7 @@ dl.score-chart .-sum {
|
|||||||
|
|
||||||
background-size: calc(var(--tile-width) * var(--scale)) calc(var(--tile-height) * var(--scale));
|
background-size: calc(var(--tile-width) * var(--scale)) calc(var(--tile-height) * var(--scale));
|
||||||
width: calc(4 * var(--tile-width) * 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 {
|
#player .inventory img {
|
||||||
width: calc(var(--tile-width) * var(--scale));
|
width: calc(var(--tile-width) * var(--scale));
|
||||||
@@ -711,6 +864,14 @@ dl.score-chart .-sum {
|
|||||||
background: #0009;
|
background: #0009;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
#player .actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
#player .actions button {
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
#player-music {
|
#player-music {
|
||||||
grid-area: music;
|
grid-area: music;
|
||||||
@@ -734,52 +895,217 @@ dl.score-chart .-sum {
|
|||||||
#player .controls {
|
#player .controls {
|
||||||
grid-area: controls;
|
grid-area: controls;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 0.25em;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
.play-controls,
|
#player button {
|
||||||
.demo-controls {
|
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;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 0.25em;
|
gap: 0.25em;
|
||||||
}
|
}
|
||||||
.play-controls {
|
.play-controls {
|
||||||
align-self: start;
|
align-self: start;
|
||||||
}
|
}
|
||||||
.demo-controls {
|
|
||||||
display: none;
|
|
||||||
flex: 1;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
main.--has-demo .demo-controls {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Debug stuff */
|
/* Debug stuff */
|
||||||
.input {
|
body.--debug #player-debug {
|
||||||
display: grid;
|
display: flex;
|
||||||
|
}
|
||||||
|
#player-debug {
|
||||||
|
flex: none;
|
||||||
|
align-self: center;
|
||||||
display: none;
|
display: none;
|
||||||
|
/* FIXME this blows out the height instead of scrolling and i cannot figure out how to make it
|
||||||
|
* stop, so here is an arbitrary max height on it which i extremely hate */
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 75vh;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: start;
|
||||||
|
gap: 0.25em;
|
||||||
|
padding: 0.5em;
|
||||||
|
background-image: repeating-linear-gradient(135deg,
|
||||||
|
hsl(0, 0%, 4%) 0px,
|
||||||
|
hsl(0, 0%, 4%) 16px,
|
||||||
|
hsl(30, 25%, 8%) 16px,
|
||||||
|
hsl(30, 25%, 8%) 32px);
|
||||||
|
border: 0.25em solid hsl(30, 75%, 50%);
|
||||||
|
border-right-width: 0;
|
||||||
|
box-shadow: inset 0 0 0.25em 0.25em #0004;
|
||||||
|
}
|
||||||
|
#player-debug > h3 {
|
||||||
|
margin: -0.25em -0.25em 0.25em;
|
||||||
|
padding: 0 0.5em;
|
||||||
|
background: hsl(30, 60%, 20%);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
#player-debug > * + h3 {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
#player-debug hr {
|
||||||
|
border: none;
|
||||||
|
margin: 0.25em 10%;
|
||||||
|
border-bottom: 1px solid #404040;
|
||||||
|
}
|
||||||
|
#player-debug table.-time-controls {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
#player-debug table.-time-controls button {
|
||||||
|
width: 100%;
|
||||||
|
text-align: inherit;
|
||||||
|
}
|
||||||
|
#player-debug table.-time-controls td:nth-child(1) {
|
||||||
|
/* go backwards button */
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
#player-debug table.-time-controls td:nth-child(2) {
|
||||||
|
/* value */
|
||||||
|
width: 4em;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
#player-debug table.-time-controls td:nth-child(3) {
|
||||||
|
/* label */
|
||||||
|
}
|
||||||
|
#player-debug table.-time-controls td:nth-child(4) {
|
||||||
|
/* go forward button */
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
#player-debug .-inventory {
|
||||||
|
display: grid;
|
||||||
|
margin: 0 auto;
|
||||||
|
grid: auto-fill var(--tile-height) / repeat(8, 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 8;
|
||||||
|
padding: 0.25em;
|
||||||
|
}
|
||||||
|
#player-debug .-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.25em;
|
||||||
|
}
|
||||||
|
#player-debug .-buttons button {
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-debug .-replay-columns {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
#player-debug .-replay-columns .-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
#player-debug-input {
|
||||||
|
flex: none;
|
||||||
|
display: grid;
|
||||||
grid:
|
grid:
|
||||||
"drop up cycle" 1.5em
|
"drop up cycle" 1em
|
||||||
"left swap right" 1.5em
|
"left . right" 1em
|
||||||
". down . " 1.5em
|
". down swap" 1em
|
||||||
/ 1.5em 1.5em 1.5em
|
/ 1em 1em 1em
|
||||||
;
|
;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
}
|
}
|
||||||
.input-action {
|
#player-debug-input > svg {
|
||||||
padding: 0.25em;
|
fill: #404040;
|
||||||
line-height: 1;
|
|
||||||
color: #fff4;
|
|
||||||
background: #202020;
|
|
||||||
}
|
}
|
||||||
.input-action[data-action=up] { grid-area: up; }
|
#player-debug-input > svg.--held {
|
||||||
.input-action[data-action=down] { grid-area: down; }
|
fill: white;
|
||||||
.input-action[data-action=left] { grid-area: left; }
|
}
|
||||||
.input-action[data-action=right] { grid-area: right; }
|
#player-debug > .-replay-available {
|
||||||
.input-action[data-action=swap] { grid-area: swap; }
|
display: flex;
|
||||||
.input-action[data-action=cycle] { grid-area: cycle; }
|
gap: 0.5em;
|
||||||
.input-action[data-action=drop] { grid-area: drop; }
|
}
|
||||||
.input-action.--pressed {
|
#player-debug > .-replay-available > h4 {
|
||||||
color: white;
|
flex: 1 0 0;
|
||||||
background: hsl(225, 75%, 25%);
|
}
|
||||||
|
#player-debug > .-replay-available > p {
|
||||||
|
flex: 2 0 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#player-debug .-replay-status {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 4em;
|
||||||
|
}
|
||||||
|
#player-debug .-replay-status > .-none {
|
||||||
|
}
|
||||||
|
#player.--replay-playback #player-debug .-replay-status > .-none,
|
||||||
|
#player.--replay-recording #player-debug .-replay-status > .-none {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#player-debug .-replay-status > .-playback {
|
||||||
|
flex: 1;
|
||||||
|
display: none;
|
||||||
|
grid:
|
||||||
|
"duration percent" 1em
|
||||||
|
"progress progress" 0.5em
|
||||||
|
"button button" 2em
|
||||||
|
/ 3fr 1fr
|
||||||
|
;
|
||||||
|
gap: 0.25em;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
#player.--replay-playback #player-debug .-replay-status > .-playback {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
#player-debug .-replay-status > .-playback > progress {
|
||||||
|
grid-area: progress;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
#player-debug .-replay-status > .-playback > output {
|
||||||
|
grid-area: percent;
|
||||||
|
min-width: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
#player-debug .-replay-status > .-playback > span {
|
||||||
|
grid-area: duration;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
#player-debug .-replay-status > .-playback > button {
|
||||||
|
grid-area: button;
|
||||||
|
}
|
||||||
|
#player-debug .-replay-status > .-recording {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
#player.--replay-recording #player-debug .-replay-status > .-recording {
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -790,13 +1116,20 @@ main.--has-demo .demo-controls {
|
|||||||
/* The play area isn't necessarily the biggest thing any more, and it's ugly when stretched */
|
/* The play area isn't necessarily the biggest thing any more, and it's ugly when stretched */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
#player > .-main-area {
|
#player .controls {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
#player-game-area {
|
||||||
/* Rearrange the grid to be vertical */
|
/* Rearrange the grid to be vertical */
|
||||||
grid:
|
grid:
|
||||||
"level level"
|
"level level"
|
||||||
"chips inventory"
|
"chips inventory" calc((var(--tile-height) * var(--scale) * 2 - 1em) / 3)
|
||||||
"time inventory"
|
"time inventory" calc((var(--tile-height) * var(--scale) * 2 - 1em) / 3)
|
||||||
"bonus inventory"
|
"bonus inventory" calc((var(--tile-height) * var(--scale) * 2 - 1em) / 3)
|
||||||
|
/* 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
|
/ min-content min-content
|
||||||
;
|
;
|
||||||
row-gap: 0.5em;
|
row-gap: 0.5em;
|
||||||
@@ -815,6 +1148,10 @@ main.--has-demo .demo-controls {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
font-size: calc(var(--tile-height) * var(--scale) / 2.5);
|
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 {
|
#player-music {
|
||||||
/* Stack the title/artist on the volume, since they don't fit well side by side */
|
/* Stack the title/artist on the volume, since they don't fit well side by side */
|
||||||
font-size: 0.875em;
|
font-size: 0.875em;
|
||||||
@@ -831,32 +1168,48 @@ main.--has-demo .demo-controls {
|
|||||||
grid:
|
grid:
|
||||||
"controls controls" min-content
|
"controls controls" min-content
|
||||||
"palette level" 1fr
|
"palette level" 1fr
|
||||||
/ minmax(25%, auto) 1fr
|
/ min-content 1fr
|
||||||
;
|
;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
|
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
margin: auto;
|
margin: auto 1em;
|
||||||
padding: 1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#editor .level {
|
#editor .editor-canvas {
|
||||||
grid-area: level;
|
grid-area: level;
|
||||||
position: relative;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
/* Padding and background make it easier to tell when we're at the edge of the map */
|
/* Flex layout to center the canvas and overlay together */
|
||||||
/* TODO padding should be half a cell, and svg should respect it too */
|
display: flex;
|
||||||
/* padding: 1em; */
|
/* 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;
|
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 */
|
/* SVG overlays */
|
||||||
#editor svg.level-editor-overlay {
|
#editor svg.level-editor-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
/* FIXME get real size */
|
right: 0;
|
||||||
width: 1024px;
|
|
||||||
height: 1024px;
|
|
||||||
/* allow clicks to go through us! */
|
/* allow clicks to go through us! */
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
@@ -885,10 +1238,64 @@ main.--has-demo .demo-controls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#editor .controls {
|
#editor .controls {
|
||||||
|
/* TODO with the hint area gone i don't think this needs to be a grid? could just flex */
|
||||||
grid-area: controls;
|
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 {
|
#editor .controls #editor-tile {
|
||||||
background: url(icons/tool-bg-unselected.png);
|
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 {
|
.icon-button-set {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -903,6 +1310,7 @@ main.--has-demo .demo-controls {
|
|||||||
background: url(icons/tool-bg-unselected.png) no-repeat;
|
background: url(icons/tool-bg-unselected.png) no-repeat;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
.icon-button-set button.-selected {
|
.icon-button-set button.-selected {
|
||||||
background-image: url(icons/tool-bg-selected.png);
|
background-image: url(icons/tool-bg-selected.png);
|
||||||
@@ -910,9 +1318,10 @@ main.--has-demo .demo-controls {
|
|||||||
.icon-button-set button img {
|
.icon-button-set button img {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
#editor .palette {
|
#editor .palette {
|
||||||
|
isolation: isolate;
|
||||||
grid-area: palette;
|
grid-area: palette;
|
||||||
min-width: 20vw;
|
|
||||||
padding-right: 0.25em; /* in case of scrollbar */
|
padding-right: 0.25em; /* in case of scrollbar */
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
@@ -927,12 +1336,172 @@ main.--has-demo .demo-controls {
|
|||||||
}
|
}
|
||||||
#editor .palette section {
|
#editor .palette section {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid: auto-flow 32px / repeat(auto-fit, 32px);
|
margin: 0.33em 4px; /* matches gap */
|
||||||
gap: 3px;
|
grid: auto-flow 32px / repeat(8, 32px);
|
||||||
|
gap: 4px;
|
||||||
}
|
}
|
||||||
.palette-entry {
|
.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 {
|
.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 |