Take a rough swing at phone support
This commit is contained in:
parent
a2e1ed4820
commit
b40805c02e
@ -10,6 +10,7 @@
|
|||||||
<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">
|
||||||
</head>
|
</head>
|
||||||
<body data-mode="splash">
|
<body data-mode="splash">
|
||||||
<header id="header-main">
|
<header id="header-main">
|
||||||
@ -116,10 +117,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="play-controls">
|
<div class="play-controls">
|
||||||
<button class="control-pause" type="button">Pause (p)</button>
|
<button class="control-pause" type="button">Pause <span class="keyhint">(p)</span></button>
|
||||||
<button class="control-restart" type="button">Restart</button>
|
<button class="control-restart" type="button">Restart</button>
|
||||||
<button class="control-undo" type="button">Undo</button>
|
<button class="control-undo" type="button">Undo</button>
|
||||||
<button class="control-rewind" type="button">Rewind (z)</button>
|
<button class="control-rewind" type="button">Rewind <span class="keyhint">(z)</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="demo-controls">
|
<div class="demo-controls">
|
||||||
<button class="demo-play" type="button">View replay</button>
|
<button class="demo-play" type="button">View replay</button>
|
||||||
|
|||||||
147
js/main.js
147
js/main.js
@ -500,6 +500,7 @@ class Player extends PrimaryView {
|
|||||||
let key_target = document.body;
|
let key_target = document.body;
|
||||||
this.previous_input = new Set; // actions that were held last tic
|
this.previous_input = new Set; // actions that were held last tic
|
||||||
this.previous_action = null; // last direction we were moving, if any
|
this.previous_action = null; // last direction we were moving, if any
|
||||||
|
this.using_touch = false; // true if using touch controls
|
||||||
this.current_keys = new Set; // keys that are currently held
|
this.current_keys = new Set; // keys that are currently held
|
||||||
// TODO this could all probably be more rigorous but it's fine for now
|
// TODO this could all probably be more rigorous but it's fine for now
|
||||||
key_target.addEventListener('keydown', ev => {
|
key_target.addEventListener('keydown', ev => {
|
||||||
@ -542,6 +543,7 @@ class Player extends PrimaryView {
|
|||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
|
// TODO for demo compat, this should happen as part of input reading?
|
||||||
if (this.state === 'waiting') {
|
if (this.state === 'waiting') {
|
||||||
this.set_state('playing');
|
this.set_state('playing');
|
||||||
}
|
}
|
||||||
@ -560,13 +562,97 @@ class Player extends PrimaryView {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// Similarly, grab touch events and translate them to directions
|
||||||
|
this.current_touches = {}; // ident => action
|
||||||
|
let touch_target = this.root.querySelector('.-main-area');
|
||||||
|
let collect_touches = ev => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
// If state is anything other than playing/waiting, probably switch to playing, similar
|
||||||
|
// to pressing spacebar
|
||||||
|
if (ev.type === 'touchstart') {
|
||||||
|
if (this.state === 'paused') {
|
||||||
|
this.toggle_pause();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (this.state === 'stopped') {
|
||||||
|
if (this.level.state === 'success') {
|
||||||
|
// Advance to the next level
|
||||||
|
// TODO game ending?
|
||||||
|
// TODO this immediately begins it too, not sure why
|
||||||
|
this.conductor.change_level(this.conductor.level_index + 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Restart
|
||||||
|
this.restart_level();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Figure out where these touches are, relative to the game area
|
||||||
|
// TODO allow starting a level without moving?
|
||||||
|
let rect = touch_target.getBoundingClientRect();
|
||||||
|
for (let touch of ev.changedTouches) {
|
||||||
|
// Normalize touch coordinates to [-1, 1]
|
||||||
|
let rx = (touch.clientX - rect.left) / rect.width * 2 - 1;
|
||||||
|
let ry = (touch.clientY - rect.top) / rect.height * 2 - 1;
|
||||||
|
// Divine a direction from the results
|
||||||
|
let action;
|
||||||
|
if (Math.abs(rx) > Math.abs(ry)) {
|
||||||
|
if (rx < 0) {
|
||||||
|
action = 'left';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
action = 'right';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (ry < 0) {
|
||||||
|
action = 'up';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
action = 'down';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.current_touches[touch.identifier] = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO for demo compat, this should happen as part of input reading?
|
||||||
|
if (this.state === 'waiting') {
|
||||||
|
this.set_state('playing');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
touch_target.addEventListener('touchstart', collect_touches);
|
||||||
|
touch_target.addEventListener('touchmove', collect_touches);
|
||||||
|
let dismiss_touches = ev => {
|
||||||
|
for (let touch of ev.changedTouches) {
|
||||||
|
delete this.current_touches[touch.identifier];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
touch_target.addEventListener('touchend', dismiss_touches);
|
||||||
|
touch_target.addEventListener('touchcancel', dismiss_touches);
|
||||||
|
|
||||||
// When we lose focus, act as though every key was released, and pause the game
|
// When we lose focus, act as though every key was released, and pause the game
|
||||||
window.addEventListener('blur', ev => {
|
window.addEventListener('blur', ev => {
|
||||||
this.current_keys.clear();
|
this.current_keys.clear();
|
||||||
|
this.current_touches = {};
|
||||||
|
|
||||||
if (this.state === 'playing' || this.state === 'rewinding') {
|
if (this.state === 'playing' || this.state === 'rewinding') {
|
||||||
this.set_state('paused');
|
this.autopause();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Same when the window becomes hidden (especially important on phones, where this covers
|
||||||
|
// turning the screen off!)
|
||||||
|
document.addEventListener('visibilitychange', ev => {
|
||||||
|
if (document.visibilityState === 'hidden') {
|
||||||
|
this.current_keys.clear();
|
||||||
|
this.current_touches = {};
|
||||||
|
|
||||||
|
if (this.state === 'playing' || this.state === 'rewinding') {
|
||||||
|
this.autopause();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -688,6 +774,9 @@ class Player extends PrimaryView {
|
|||||||
for (let key of this.current_keys) {
|
for (let key of this.current_keys) {
|
||||||
input.add(this.key_mapping[key]);
|
input.add(this.key_mapping[key]);
|
||||||
}
|
}
|
||||||
|
for (let action of Object.values(this.current_touches)) {
|
||||||
|
input.add(action);
|
||||||
|
}
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -907,6 +996,10 @@ class Player extends PrimaryView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
autopause() {
|
||||||
|
this.set_state('paused');
|
||||||
|
}
|
||||||
|
|
||||||
// waiting: haven't yet pressed a key so the timer isn't going
|
// waiting: haven't yet pressed a key so the timer isn't going
|
||||||
// playing: playing normally
|
// playing: playing normally
|
||||||
// paused: um, paused
|
// paused: um, paused
|
||||||
@ -931,7 +1024,12 @@ class Player extends PrimaryView {
|
|||||||
else if (this.state === 'paused') {
|
else if (this.state === 'paused') {
|
||||||
overlay_reason = 'paused';
|
overlay_reason = 'paused';
|
||||||
overlay_bottom = "/// paused ///";
|
overlay_bottom = "/// paused ///";
|
||||||
overlay_keyhint = "press P to resume";
|
if (this.using_touch) {
|
||||||
|
overlay_keyhint = "tap to resume";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
overlay_keyhint = "press P to resume";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (this.state === 'stopped') {
|
else if (this.state === 'stopped') {
|
||||||
if (this.level.state === 'failure') {
|
if (this.level.state === 'failure') {
|
||||||
@ -939,7 +1037,13 @@ class Player extends PrimaryView {
|
|||||||
overlay_top = "whoops";
|
overlay_top = "whoops";
|
||||||
let obits = OBITUARIES[this.level.fail_reason] ?? OBITUARIES['generic'];
|
let obits = OBITUARIES[this.level.fail_reason] ?? OBITUARIES['generic'];
|
||||||
overlay_bottom = random_choice(obits);
|
overlay_bottom = random_choice(obits);
|
||||||
overlay_keyhint = "press space to try again, or Z to rewind";
|
if (this.using_touch) {
|
||||||
|
// TODO touch gesture to rewind?
|
||||||
|
overlay_keyhint = "tap to try again, or tap undo/rewind above";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
overlay_keyhint = "press space to try again, or Z to rewind";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// We just beat the level! Hey, that's cool.
|
// We just beat the level! Hey, that's cool.
|
||||||
@ -1002,7 +1106,12 @@ class Player extends PrimaryView {
|
|||||||
"alphanumeric!", "nice dynamic typing!",
|
"alphanumeric!", "nice dynamic typing!",
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
overlay_keyhint = "press space to move on";
|
if (this.using_touch) {
|
||||||
|
overlay_keyhint = "tap to move on";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
overlay_keyhint = "press space to move on";
|
||||||
|
}
|
||||||
|
|
||||||
overlay_middle = mk('dl.score-chart',
|
overlay_middle = mk('dl.score-chart',
|
||||||
mk('dt', "base score"),
|
mk('dt', "base score"),
|
||||||
@ -1134,14 +1243,23 @@ class Player extends PrimaryView {
|
|||||||
// Auto-size the game canvas to fit the screen, if possible
|
// Auto-size the game canvas to fit the screen, if possible
|
||||||
adjust_scale() {
|
adjust_scale() {
|
||||||
// TODO make this optional
|
// TODO make this optional
|
||||||
// The base size is the size of the canvas, i.e. the viewport size times the tile size --
|
|
||||||
// but note that horizontally we have 4 extra tiles for the inventory
|
|
||||||
// TODO if there's ever a portrait view for phones, this will need adjusting
|
|
||||||
let base_x = this.conductor.tileset.size_x * (this.renderer.viewport_size_x + 4);
|
|
||||||
let base_y = this.conductor.tileset.size_y * this.renderer.viewport_size_y;
|
|
||||||
// The main UI is centered in a flex item with auto margins, so the
|
|
||||||
// extra space available is the size of those margins
|
|
||||||
let style = window.getComputedStyle(this.root);
|
let style = window.getComputedStyle(this.root);
|
||||||
|
let is_portrait = !! style.getPropertyValue('--is-portrait');
|
||||||
|
// The base size is the size of the canvas, i.e. the viewport size times the tile size --
|
||||||
|
// but note that we have 2x4 extra tiles for the inventory depending on layout
|
||||||
|
// TODO if there's ever a portrait view for phones, this will need adjusting
|
||||||
|
let base_x, base_y;
|
||||||
|
if (is_portrait) {
|
||||||
|
base_x = this.conductor.tileset.size_x * this.renderer.viewport_size_x;
|
||||||
|
base_y = this.conductor.tileset.size_y * (this.renderer.viewport_size_y + 2);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
base_x = this.conductor.tileset.size_x * (this.renderer.viewport_size_x + 4);
|
||||||
|
base_y = this.conductor.tileset.size_y * this.renderer.viewport_size_y;
|
||||||
|
}
|
||||||
|
// The main UI is centered in a flex item with auto margins, so the extra space available is
|
||||||
|
// the size of those margins (which naturally discounts the size of the buttons and music
|
||||||
|
// title and whatnot, so those automatically reserve their own space)
|
||||||
if (style['display'] === 'none') {
|
if (style['display'] === 'none') {
|
||||||
// the computed margins can be 'auto' in this case
|
// the computed margins can be 'auto' in this case
|
||||||
return;
|
return;
|
||||||
@ -1153,9 +1271,10 @@ class Player extends PrimaryView {
|
|||||||
let total_x = extra_x + this.renderer.canvas.offsetWidth + this.inventory_el.offsetWidth;
|
let total_x = extra_x + this.renderer.canvas.offsetWidth + this.inventory_el.offsetWidth;
|
||||||
let total_y = extra_y + this.renderer.canvas.offsetHeight;
|
let total_y = extra_y + this.renderer.canvas.offsetHeight;
|
||||||
let dpr = window.devicePixelRatio || 1.0;
|
let dpr = window.devicePixelRatio || 1.0;
|
||||||
// Divide to find the biggest scale that still fits. But don't
|
// Divide to find the biggest scale that still fits. But don't exceed 90% of the available
|
||||||
// exceed 90% of the available space, or it'll feel cramped.
|
// space, or it'll feel cramped (except on small screens, where being too small HURTS).
|
||||||
let scale = Math.floor(0.9 * dpr * Math.min(total_x / base_x, total_y / base_y));
|
let maxfrac = total_x < 800 ? 1 : 0.9;
|
||||||
|
let scale = Math.floor(maxfrac * dpr * Math.min(total_x / base_x, total_y / base_y));
|
||||||
if (scale <= 1) {
|
if (scale <= 1) {
|
||||||
scale = 1;
|
scale = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
92
style.css
92
style.css
@ -281,7 +281,7 @@ body > header {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
|
|
||||||
margin: 0.25em;
|
padding: 0.5em;
|
||||||
line-height: 1.125;
|
line-height: 1.125;
|
||||||
}
|
}
|
||||||
body > header h1 {
|
body > header h1 {
|
||||||
@ -291,14 +291,13 @@ body > header h2 {
|
|||||||
font-size: 1.33em;
|
font-size: 1.33em;
|
||||||
}
|
}
|
||||||
body > header h3 {
|
body > header h3 {
|
||||||
font-size: 1.25em;
|
font-size: 1.75em;
|
||||||
}
|
}
|
||||||
body > header > nav {
|
body > header > nav {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
margin: 0.25rem 0.5rem;
|
|
||||||
}
|
}
|
||||||
body > header button {
|
body > header button {
|
||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
@ -317,8 +316,25 @@ body[data-mode=player] #editor-play {
|
|||||||
order: 3;
|
order: 3;
|
||||||
color: #606060;
|
color: #606060;
|
||||||
}
|
}
|
||||||
#header-level {
|
|
||||||
font-size: 1.5em;
|
@media (max-width: 800px) {
|
||||||
|
body > header {
|
||||||
|
padding: 0.25em;
|
||||||
|
}
|
||||||
|
/* All these headings are way too big on phones */
|
||||||
|
body > header h1 {
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
body > header h2 {
|
||||||
|
font-size: 1.125em;
|
||||||
|
}
|
||||||
|
body > header h3 {
|
||||||
|
font-size: 1.0625em;
|
||||||
|
}
|
||||||
|
body > header p {
|
||||||
|
/* "a game by eevee" takes up too much space :( */
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**************************************************************************************************/
|
/**************************************************************************************************/
|
||||||
@ -379,6 +395,23 @@ body[data-mode=player] #editor-play {
|
|||||||
grid-area: yours;
|
grid-area: yours;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
#splash {
|
||||||
|
/* Grid layout doesn't fit, just stack everything */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
/* 10% padding is way way too much */
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
/* Shrink logo and title */
|
||||||
|
#splash > header img {
|
||||||
|
width: 48px;
|
||||||
|
}
|
||||||
|
#splash > header h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
button.level-pack-button {
|
button.level-pack-button {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid:
|
grid:
|
||||||
@ -469,7 +502,7 @@ button.level-pack-button p {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
z-index: 1;
|
z-index: 2;
|
||||||
font-size: calc(0.5 * var(--tile-width) * var(--scale));
|
font-size: calc(0.5 * var(--tile-width) * var(--scale));
|
||||||
padding: 2%;
|
padding: 2%;
|
||||||
background: #0009;
|
background: #0009;
|
||||||
@ -552,7 +585,6 @@ dl.score-chart .-sum {
|
|||||||
.bonus output {
|
.bonus output {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
padding: 0.125em;
|
|
||||||
min-width: 2em;
|
min-width: 2em;
|
||||||
min-height: 1em;
|
min-height: 1em;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@ -584,9 +616,11 @@ dl.score-chart .-sum {
|
|||||||
}
|
}
|
||||||
#player .bonus {
|
#player .bonus {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
#player.--bonus-visible .bonus {
|
#player.--bonus-visible .bonus {
|
||||||
visibility: initial;
|
visibility: initial;
|
||||||
|
display: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
@ -644,11 +678,14 @@ dl.score-chart .-sum {
|
|||||||
#player-music {
|
#player-music {
|
||||||
grid-area: music;
|
grid-area: music;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 1em;
|
||||||
|
margin: 0 1em;
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
color: #909090;
|
color: #909090;
|
||||||
}
|
}
|
||||||
#player-music #player-music-left {
|
#player-music #player-music-left {
|
||||||
flex: 1 0 auto;
|
/* allow me to wrap if need be */
|
||||||
|
flex: 1 0 0px;
|
||||||
}
|
}
|
||||||
#player-music #player-music-right {
|
#player-music #player-music-right {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
@ -709,6 +746,45 @@ main.--has-demo .demo-controls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
#player {
|
||||||
|
/* sentinel for js */
|
||||||
|
--is-portrait: 1;
|
||||||
|
/* The play area isn't necessarily the biggest thing any more, and it's ugly when stretched */
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
#player > .-main-area {
|
||||||
|
/* Rearrange the grid to be vertical */
|
||||||
|
grid:
|
||||||
|
"level level"
|
||||||
|
"chips inventory"
|
||||||
|
"time inventory"
|
||||||
|
"bonus inventory"
|
||||||
|
/ min-content min-content
|
||||||
|
;
|
||||||
|
row-gap: 0.5em;
|
||||||
|
column-gap: 1em;
|
||||||
|
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
#player .inventory {
|
||||||
|
/* stick me in the center right */
|
||||||
|
place-self: center end;
|
||||||
|
}
|
||||||
|
#player .message {
|
||||||
|
/* Overlay hints on the inventory area */
|
||||||
|
grid-row: chips / bonus;
|
||||||
|
grid-column: level;
|
||||||
|
z-index: 1;
|
||||||
|
font-size: calc(var(--tile-height) * var(--scale) / 2.5);
|
||||||
|
}
|
||||||
|
#player-music {
|
||||||
|
/* Stack the title/artist on the volume, since they don't fit well side by side */
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**************************************************************************************************/
|
/**************************************************************************************************/
|
||||||
/* Editor */
|
/* Editor */
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user