diff --git a/js/main.js b/js/main.js
index 8c865e2..ada6025 100644
--- a/js/main.js
+++ b/js/main.js
@@ -500,6 +500,7 @@ class Player extends PrimaryView {
let key_target = document.body;
this.previous_input = new Set; // actions that were held last tic
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
// TODO this could all probably be more rigorous but it's fine for now
key_target.addEventListener('keydown', ev => {
@@ -542,6 +543,7 @@ class Player extends PrimaryView {
ev.stopPropagation();
ev.preventDefault();
+ // TODO for demo compat, this should happen as part of input reading?
if (this.state === 'waiting') {
this.set_state('playing');
}
@@ -560,13 +562,97 @@ class Player extends PrimaryView {
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
window.addEventListener('blur', ev => {
this.current_keys.clear();
+ this.current_touches = {};
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) {
input.add(this.key_mapping[key]);
}
+ for (let action of Object.values(this.current_touches)) {
+ input.add(action);
+ }
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
// playing: playing normally
// paused: um, paused
@@ -931,7 +1024,12 @@ class Player extends PrimaryView {
else if (this.state === 'paused') {
overlay_reason = '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') {
if (this.level.state === 'failure') {
@@ -939,7 +1037,13 @@ class Player extends PrimaryView {
overlay_top = "whoops";
let obits = OBITUARIES[this.level.fail_reason] ?? OBITUARIES['generic'];
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 {
// We just beat the level! Hey, that's cool.
@@ -1002,7 +1106,12 @@ class Player extends PrimaryView {
"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',
mk('dt', "base score"),
@@ -1134,14 +1243,23 @@ class Player extends PrimaryView {
// Auto-size the game canvas to fit the screen, if possible
adjust_scale() {
// 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 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') {
// the computed margins can be 'auto' in this case
return;
@@ -1153,9 +1271,10 @@ class Player extends PrimaryView {
let total_x = extra_x + this.renderer.canvas.offsetWidth + this.inventory_el.offsetWidth;
let total_y = extra_y + this.renderer.canvas.offsetHeight;
let dpr = window.devicePixelRatio || 1.0;
- // Divide to find the biggest scale that still fits. But don't
- // exceed 90% of the available space, or it'll feel cramped.
- let scale = Math.floor(0.9 * dpr * Math.min(total_x / base_x, total_y / base_y));
+ // Divide to find the biggest scale that still fits. But don't exceed 90% of the available
+ // space, or it'll feel cramped (except on small screens, where being too small HURTS).
+ 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) {
scale = 1;
}
diff --git a/style.css b/style.css
index 2d59920..89c3a00 100644
--- a/style.css
+++ b/style.css
@@ -281,7 +281,7 @@ body > header {
align-items: center;
gap: 0.5em;
- margin: 0.25em;
+ padding: 0.5em;
line-height: 1.125;
}
body > header h1 {
@@ -291,14 +291,13 @@ body > header h2 {
font-size: 1.33em;
}
body > header h3 {
- font-size: 1.25em;
+ font-size: 1.75em;
}
body > header > nav {
flex: 1;
display: flex;
justify-content: flex-end;
gap: 0.5em;
- margin: 0.25rem 0.5rem;
}
body > header button {
font-size: 0.75em;
@@ -317,8 +316,25 @@ body[data-mode=player] #editor-play {
order: 3;
color: #606060;
}
-#header-level {
- font-size: 1.5em;
+
+@media (max-width: 800px) {
+ body > header {
+ padding: 0.25em;
+ }
+ /* All these headings are way too big on phones */
+ body > header h1 {
+ font-size: 1.25em;
+ }
+ body > header h2 {
+ font-size: 1.125em;
+ }
+ body > header h3 {
+ font-size: 1.0625em;
+ }
+ body > header p {
+ /* "a game by eevee" takes up too much space :( */
+ display: none;
+ }
}
/**************************************************************************************************/
@@ -379,6 +395,23 @@ body[data-mode=player] #editor-play {
grid-area: yours;
}
+@media (max-width: 800px) {
+ #splash {
+ /* Grid layout doesn't fit, just stack everything */
+ display: flex;
+ flex-direction: column;
+ /* 10% padding is way way too much */
+ padding: 1em;
+ }
+ /* Shrink logo and title */
+ #splash > header img {
+ width: 48px;
+ }
+ #splash > header h1 {
+ font-size: 2em;
+ }
+}
+
button.level-pack-button {
display: grid;
grid:
@@ -469,7 +502,7 @@ button.level-pack-button p {
justify-content: center;
align-items: center;
- z-index: 1;
+ z-index: 2;
font-size: calc(0.5 * var(--tile-width) * var(--scale));
padding: 2%;
background: #0009;
@@ -552,7 +585,6 @@ dl.score-chart .-sum {
.bonus output {
flex: 1;
font-size: 2em;
- padding: 0.125em;
min-width: 2em;
min-height: 1em;
line-height: 1;
@@ -584,9 +616,11 @@ dl.score-chart .-sum {
}
#player .bonus {
visibility: hidden;
+ display: none;
}
#player.--bonus-visible .bonus {
visibility: initial;
+ display: initial;
}
.message {
@@ -644,11 +678,14 @@ dl.score-chart .-sum {
#player-music {
grid-area: music;
display: flex;
+ gap: 1em;
+ margin: 0 1em;
text-transform: lowercase;
color: #909090;
}
#player-music #player-music-left {
- flex: 1 0 auto;
+ /* allow me to wrap if need be */
+ flex: 1 0 0px;
}
#player-music #player-music-right {
text-align: right;
@@ -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 */