diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..8e165ac --- /dev/null +++ b/deploy.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# ===================================================================== +# MORPQUEST — Production Deploy (next door to Beyond Morp) +# ===================================================================== +# Usage: bash deploy.sh [path-to-MRPGI] +# +# 1. Builds the wasm bundle with this game baked in (MRPGI/web/build.sh) +# 2. Rsyncs web/dist to VPS staging +# 3. Replaces /usr/share/nginx/html/morpquest/ inside forum-nginx +# +# Same VPS + container as beyondmorp's deploy.sh: +# ssh humanjing@100.71.119.27 (tailscale), container forum-nginx. +# The game then lives at /morpquest/ beside /beyondmorp/. +# ===================================================================== +set -euo pipefail + +MRPGI="${1:-$HOME/Documents/MRPGI}" +GAME="$(cd "$(dirname "$0")" && pwd)" +VPS="humanjing@100.71.119.27" +CONTAINER="forum-nginx" +REMOTE_WEB_ROOT="/usr/share/nginx/html/morpquest" +STAGING="/tmp/morpquest_deploy" + +# rustup's toolchain has the wasm target; homebrew's rust does not. +RUSTUP_BIN="$HOME/.rustup/toolchains/stable-aarch64-apple-darwin/bin" +[ -d "$RUSTUP_BIN" ] && export PATH="$RUSTUP_BIN:$PATH" + +echo "==> building wasm bundle ($GAME baked in)" +sh "$MRPGI/web/build.sh" "$GAME" + +echo "==> rsync to VPS staging" +ssh "$VPS" "rm -rf $STAGING && mkdir -p $STAGING" +rsync -az --delete "$MRPGI/web/dist/" "$VPS:$STAGING/" + +echo "==> swap into $CONTAINER:$REMOTE_WEB_ROOT" +ssh "$VPS" " + docker exec $CONTAINER rm -rf $REMOTE_WEB_ROOT + docker cp $STAGING $CONTAINER:$REMOTE_WEB_ROOT + docker exec $CONTAINER ls $REMOTE_WEB_ROOT +" +echo "==> deployed. The queue has been waiting since sunrise." diff --git a/music/calm.wav b/music/calm.wav new file mode 100644 index 0000000..d5a9a19 Binary files /dev/null and b/music/calm.wav differ diff --git a/music/eerie.wav b/music/eerie.wav new file mode 100644 index 0000000..710cb4f Binary files /dev/null and b/music/eerie.wav differ diff --git a/music/jolly.wav b/music/jolly.wav new file mode 100644 index 0000000..9f60bc8 Binary files /dev/null and b/music/jolly.wav differ diff --git a/music/spooky.wav b/music/spooky.wav new file mode 100644 index 0000000..ded7ee7 Binary files /dev/null and b/music/spooky.wav differ diff --git a/music/tense.wav b/music/tense.wav new file mode 100644 index 0000000..0c317b8 Binary files /dev/null and b/music/tense.wav differ diff --git a/tools/gen_music.py b/tools/gen_music.py new file mode 100644 index 0000000..83c4673 --- /dev/null +++ b/tools/gen_music.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""8-bit chiptune generator -> music/{calm,eerie,tense,jolly,spooky}.wav + +Pure stdlib (wave/math/struct), deterministic, loop-clean (whole bars, note +envelopes close before the bar line). The engine plays these per room mood at +low volume; anything missing falls back to the built-in synth. +""" +import math, os, struct, wave + +SR = 22050 +OUT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'music') +os.makedirs(OUT, exist_ok=True) + +def note(name): + """'c4' -> Hz ('-' = rest -> 0).""" + if name in ('-', ''): + return 0.0 + names = {'c': 0, 'cs': 1, 'd': 2, 'ds': 3, 'e': 4, 'f': 5, 'fs': 6, + 'g': 7, 'gs': 8, 'a': 9, 'as': 10, 'b': 11} + pitch, octave = name[:-1], int(name[-1]) + return 440.0 * 2 ** ((names[pitch] - 9) / 12 + octave - 4) + +def square(freq, t, duty=0.5): + return 1.0 if (t * freq) % 1.0 < duty else -1.0 + +def tri(freq, t): + p = (t * freq) % 1.0 + return 4 * p - 1 if p < 0.5 else 3 - 4 * p + +_lfsr = 0xACE1 +def _noise_step(): + global _lfsr + bit = ((_lfsr >> 0) ^ (_lfsr >> 2) ^ (_lfsr >> 3) ^ (_lfsr >> 5)) & 1 + _lfsr = (_lfsr >> 1) | (bit << 15) + return (_lfsr & 0xFF) / 127.5 - 1.0 + +def render(length_s, voices): + """voices: list of (events, wave_fn, gain). events: (start_s, dur_s, freq[, duty]).""" + n = int(length_s * SR) + buf = [0.0] * n + for events, fn, gain in voices: + for ev in events: + start, dur, freq = ev[0], ev[1], ev[2] + duty = ev[3] if len(ev) > 3 else 0.5 + if freq == 0: + continue + i0, i1 = int(start * SR), min(int((start + dur) * SR), n) + body = i1 - i0 + atk, rel = int(0.004 * SR), max(1, int(0.05 * SR)) + nz = freq < 0 # negative freq = noise burst + for i in range(body): + t = i / SR + env = min(1.0, i / atk if atk else 1.0, (body - i) / rel) + if nz: + s = _noise_step() + elif fn is square: + s = square(freq, t, duty) + else: + s = fn(freq, t) + buf[i0 + i] += s * env * gain + peak = max(1e-6, max(abs(s) for s in buf)) + scale = 0.82 / peak + return b''.join(struct.pack(' events on a fixed step grid.""" + ev = [] + for i, tok in enumerate(pattern.split()): + f = -1.0 if tok == 'x' else note(tok) + if f: + ev.append((t0 + i * step, step * dur_frac, f, duty)) + return (ev, wave_fn, gain) + +# --- calm: the last calm place before you choose suffering on purpose ------- +# 60 bpm, 4 bars. Cmaj7 / Fmaj7 arps over a soft triangle bass. +B = 1.0 # beat +calm = render(16.0, [ + seq('c3 - - - f2 - - - c3 - - - f2 - g2 -', B, tri, 0.50, 0.95), + seq('e4 g4 b4 g4 a4 c5 e5 c5 e4 g4 b4 g4 a4 c5 d5 b4', B, square, 0.16, 0.55, 0.25), + seq('- - c5 - - - f5 - - - g5 - - - e5 -', B, tri, 0.12, 0.5), +]) +write('calm', calm) + +# --- eerie: the alley knows your name ---------------------------------------- +# Slow tritone drift (A / Eb), long gaps, one distant ping per 4 bars. +eerie = render(16.0, [ + ([(0.0, 7.6, note('a2')), (8.0, 7.6, note('ds3'))], tri, 0.40), + ([(0.0, 7.6, note('a2') * 1.006), (8.0, 7.6, note('ds3') * 1.007)], tri, 0.22), + seq('- - - a4 - - - - - - ds5 - - - - -', B, square, 0.07, 0.3, 0.18), + ([(11.5, 0.12, -1.0)], square, 0.05), +]) +write('eerie', eerie) + +# --- tense: owner energy approaches ------------------------------------------ +# 120 bpm staccato D-minor bass, ticking hat, no resolution on purpose. +S = 0.25 # 16th at 120 +tense = render(12.0, [ + seq(('d2 - d2 - f2 - d2 - d2 - d2 - as1 - c2 - ' * 3), S * 2, square, 0.40, 0.45, 0.5), + seq(('x - - - x - - - x - - - x - x - ' * 6), S, tri, 0.10, 0.15), + seq('- - - - - - - - d4 - - - - - - - - - - - - - - - a3 - - - - - - - ' + '- - - - - - - - f4 - - - - - - - - - - - - - - - e4 - - - - - - -', S * 1.5 / 2, square, 0.09, 0.3, 0.25), +]) +write('tense', tense) + +# --- jolly: the shop floor is a house party at 33rpm ------------------------- +# 118-ish bpm four-on-floor: noise kick+hat, octave square bass, major arp. +K = 0.508 # beat (≈118bpm), 8 bars of 4 = 16.26s +jolly = render(K * 32, [ + seq('x - - - x - - - x - - - x - - - ' * 2, K / 1, tri, 0.55, 0.06), # kick thump + seq('- - x - - - x - - - x - - - x - ' * 2, K / 1, square, 0.08, 0.05), # offbeat hat + seq(('c2 c3 g2 c3 a1 a2 e2 a2 f2 f3 c3 f3 g2 g3 b2 g3 ' * 2), K / 2, square, 0.34, 0.5, 0.5), + seq('e4 g4 c5 g4 a4 c5 e5 c5 f4 a4 c5 a4 g4 b4 d5 g5 ' + 'e4 g4 c5 g4 a4 c5 e5 c5 f4 a4 c5 f5 g4 d5 b4 g4', K / 2, square, 0.13, 0.55, 0.25), +]) +write('jolly', jolly) + +# --- spooky: the back room keeps its own temperature ------------------------- +spooky = render(16.0, [ + ([(0.0, 15.6, note('e2'))], tri, 0.42), + ([(0.0, 15.6, note('e2') * 0.5)], tri, 0.30), + seq('- - - - - g4 - - - - - - fs4 - - -', B, square, 0.06, 0.5, 0.12), + ([(3.0, 1.2, -1.0), (11.0, 1.6, -1.0)], square, 0.03), +]) +write('spooky', spooky) diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..9421a36 --- /dev/null +++ b/web/index.html @@ -0,0 +1,104 @@ + + + + + + MORPQUEST: Record Store Day + + + + +
MORPQUESTqueueing since sunrise…
+ + +
+

MORPQUEST: Record Store Day

+

You're meant to be doing a quick cat-food run. Uncle's list is in the + glovebox. The record shop's front door is a wall of Taylor Swift superfans. + Morp is not cruel; Morp is accurate.

+

Playing

+

←↑↓→ or click to walk · type verbs + + Enter (look, take paperclip, use seatbelt, talk becky…) + · 16 pick a dialogue reply · + Esc leaves a chat · F1 CRT scanlines · + F2 sound.

+

Advice from the walls

+

Elastic. Eyepatch. Gratitude. Door.

+

DELAYS HAPPEN. DON'T PANIC. PANIC IS FOR TOURISTS.

+

There are 250 points, two deaths, and a true ending. Don't read signs + aloud unless you have all the words. Don't eat the kebab. (You'll eat the + kebab.)

+

Made with

+

The MRPGI engine + (a Rust reimagining of Sierra's AGI), backgrounds by nano banana squeezed + into 160×168 EGA, chiptunes by a 60-line Python synth. Adapted from + the Beyond Morp text + adventure next door. Monster Robot Party.

+
+ + + + +