Skip to content

Commit cd3dfae

Browse files
authored
Merge PR #24: Add first-visit mission briefing
Add an accessible starter briefing with persistent first-visit state
2 parents 417f255 + 30cd508 commit cd3dfae

3 files changed

Lines changed: 61 additions & 2 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ An explorable pixel-art micro-world rendered entirely in the browser. Walk throu
66

77
## Controls
88

9+
First-time players receive a short mission briefing. Reopen it at any time with the `?` control.
10+
911
| Action | Input |
1012
| --- | --- |
1113
| Move | `W` `A` `S` `D` or click/tap a destination |
@@ -20,6 +22,9 @@ The current mobile experience supports tap-to-move. A dedicated touch action con
2022
- Eight residents with distinct movement and interaction state machines
2123
- Grid-based movement and click-to-move pathfinding
2224
- A radial scan that reveals character identities
25+
- A three-part signal recovery quest with resident hints and daily challenge codes
26+
- Persistent progress, a discovery journal, replay, and completion sharing
27+
- Optional sound, haptic feedback, touch controls, and reduced-motion support
2328
- No runtime framework, backend, or asset bundle
2429

2530
## Run locally

index.html

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
#share-button{position:absolute;top:max(128px,calc(106px + env(safe-area-inset-top)));right:max(22px,env(safe-area-inset-right));z-index:20;display:none;width:34px;height:34px;border:1px solid #D4A030;border-radius:4px;background:rgba(10,10,10,.82);color:#E8C040;font:18px monospace;cursor:pointer}
3939
#share-button.show{display:block}
4040
#share-button:focus-visible{outline:3px solid #E8E0D0;outline-offset:2px}
41+
#briefing-button{position:absolute;top:max(22px,env(safe-area-inset-top));left:max(22px,env(safe-area-inset-left));z-index:20;width:34px;height:34px;border:1px solid #5A8848;border-radius:4px;background:rgba(10,10,10,.82);color:#A8C088;font:700 16px 'Silkscreen',monospace;cursor:pointer}
42+
#briefing-button:focus-visible,#briefing-start:focus-visible{outline:3px solid #E8E0D0;outline-offset:2px}
4143
#journal-button{position:absolute;top:max(48px,calc(26px + env(safe-area-inset-top)));right:max(22px,env(safe-area-inset-right));z-index:20;width:42px;height:30px;border:1px solid #5A8848;border-radius:4px;background:rgba(10,10,10,.82);color:#A8C088;font:10px 'Silkscreen',monospace;cursor:pointer}
4244
#journal-button:focus-visible,#journal-close:focus-visible{outline:3px solid #E8E0D0;outline-offset:2px}
4345
#sound-button{position:absolute;right:max(20px,env(safe-area-inset-right));bottom:max(20px,env(safe-area-inset-bottom));z-index:20;width:38px;height:34px;border:1px solid #5A8848;border-radius:4px;background:rgba(10,10,10,.82);color:#A8C088;font:18px monospace;cursor:pointer}
@@ -50,6 +52,18 @@
5052
.journal-entry{padding-bottom:7px;border-bottom:1px solid #383030;color:#B0A890}
5153
.journal-entry.found{color:#E8E0D0}
5254
#journal-close{margin-top:16px;padding:8px 12px;border:1px solid #D4A030;border-radius:4px;background:#181820;color:#E8C040;font:11px 'Silkscreen',monospace;cursor:pointer}
55+
#briefing-dialog{width:min(480px,calc(100vw - 32px));padding:0;border:2px solid #5A8848;border-radius:4px;background:#181820;color:#E8E0D0;font-family:'Silkscreen',monospace}
56+
#briefing-dialog::backdrop{background:rgba(10,10,10,.84)}
57+
#briefing-header{padding:16px 18px 12px;border-bottom:1px solid #383030}
58+
#briefing-header span{display:block;margin-bottom:7px;color:#6B9848;font-size:9px}
59+
#briefing-header h2{color:#E8C040;font-size:18px}
60+
#briefing-body{padding:18px}
61+
#briefing-objective{margin-bottom:18px;color:#E8E0D0;font-size:12px;line-height:1.8}
62+
#briefing-controls{display:grid;grid-template-columns:88px 1fr;gap:10px 14px;padding:14px 0;border-top:1px solid #383030;border-bottom:1px solid #383030;font-size:10px;line-height:1.5}
63+
.briefing-input{color:#E8C040}
64+
.briefing-action{color:#B0A890}
65+
#briefing-note{margin-top:14px;color:#8A9870;font-size:9px;line-height:1.7}
66+
#briefing-start{width:100%;margin-top:18px;padding:11px 14px;border:2px solid #D4A030;border-radius:4px;background:#242018;color:#E8C040;font:700 12px 'Silkscreen',monospace;cursor:pointer}
5367
.sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}
5468
@media(max-width:600px){
5569
#ui-overlay{justify-content:flex-start;padding:calc(10px + env(safe-area-inset-top)) 10px 10px}
@@ -61,6 +75,7 @@
6175
#journal-button{top:calc(74px + env(safe-area-inset-top));right:12px}
6276
#quest-reset{top:calc(112px + env(safe-area-inset-top));right:12px}
6377
#share-button{top:calc(152px + env(safe-area-inset-top));right:12px}
78+
#briefing-button{top:calc(50px + env(safe-area-inset-top));left:12px}
6479
#hint{bottom:max(88px,calc(68px + env(safe-area-inset-bottom)));width:70vw;font-size:10px;text-align:center}
6580
#action-button{display:block}
6681
#sound-button{right:max(104px,calc(104px + env(safe-area-inset-right)));bottom:max(28px,calc(28px + env(safe-area-inset-bottom)))}
@@ -91,12 +106,29 @@
91106
<div id="emote"></div>
92107
<div id="status" role="status" aria-live="polite"></div>
93108
<div id="quest" aria-live="polite">SIGNALS 0/3</div>
109+
<button id="briefing-button" type="button" aria-label="Open mission briefing" title="Mission briefing">?</button>
94110
<button id="journal-button" type="button" aria-label="Open signal log">LOG</button>
95111
<button id="quest-reset" type="button" aria-label="Restart signal quest" title="Restart signal quest"></button>
96112
<button id="share-button" type="button" aria-label="Share completion" title="Share completion"></button>
97113
<button id="action-button" type="button" aria-label="Scan or interact">SCAN</button>
98114
<button id="sound-button" type="button" aria-label="Mute sound" title="Toggle sound"></button>
99115
<p class="sr-only" id="game-instructions">Move with WASD, arrow keys, or by selecting a destination. Press Space or Enter, or use the Scan button, to scan nearby residents or interact with the character you face.</p>
116+
<dialog id="briefing-dialog" aria-labelledby="briefing-title">
117+
<div id="briefing-header">
118+
<span>UNIT-01 // DAILY ASSIGNMENT</span>
119+
<h2 id="briefing-title">RESTORE THE LINK</h2>
120+
</div>
121+
<div id="briefing-body">
122+
<p id="briefing-objective">Three signal carriers are dark. Explore the river factory, scan for their locations, and recover all three before the cycle resets.</p>
123+
<div id="briefing-controls" aria-label="Game controls">
124+
<span class="briefing-input">WASD</span><span class="briefing-action">MOVE</span>
125+
<span class="briefing-input">CLICK / TAP</span><span class="briefing-action">WALK TO A LOCATION</span>
126+
<span class="briefing-input">SPACE / SCAN</span><span class="briefing-action">SEARCH OR INTERACT</span>
127+
</div>
128+
<p id="briefing-note">Residents remember what you discover. Progress is saved on this device.</p>
129+
<button id="briefing-start" type="button">BEGIN</button>
130+
</div>
131+
</dialog>
100132
<dialog id="journal-dialog" aria-labelledby="journal-title">
101133
<h2 id="journal-title">SIGNAL LOG</h2>
102134
<div id="journal-list"></div>
@@ -121,6 +153,9 @@ <h2 id="journal-title">SIGNAL LOG</h2>
121153
const actionButton = document.getElementById('action-button');
122154
const questEl = document.getElementById('quest');
123155
const questResetButton = document.getElementById('quest-reset');
156+
const briefingButton = document.getElementById('briefing-button');
157+
const briefingDialog = document.getElementById('briefing-dialog');
158+
const briefingStart = document.getElementById('briefing-start');
124159
const journalButton = document.getElementById('journal-button');
125160
const journalDialog = document.getElementById('journal-dialog');
126161
const journalList = document.getElementById('journal-list');
@@ -1139,6 +1174,19 @@ <h2 id="journal-title">SIGNAL LOG</h2>
11391174
performAction();
11401175
});
11411176
questResetButton.addEventListener('click', () => requestQuestReset());
1177+
const BRIEFING_SEEN_KEY = 'littleandroid.briefingSeen';
1178+
function rememberBriefing() {
1179+
try { window.localStorage.setItem(BRIEFING_SEEN_KEY, '1'); } catch {}
1180+
}
1181+
function openBriefing() {
1182+
if (!briefingDialog.open) briefingDialog.showModal();
1183+
}
1184+
briefingButton.addEventListener('click', openBriefing);
1185+
briefingStart.addEventListener('click', () => briefingDialog.close());
1186+
briefingDialog.addEventListener('close', () => {
1187+
rememberBriefing();
1188+
canvas.focus({ preventScroll: true });
1189+
});
11421190
journalButton.addEventListener('click', () => {
11431191
updateJournal();
11441192
journalDialog.showModal();
@@ -1601,6 +1649,9 @@ <h2 id="journal-title">SIGNAL LOG</h2>
16011649
} else {
16021650
showStatus('OBJECTIVE: LOCATE 3 SIGNALS', 4);
16031651
}
1652+
let hasSeenBriefing = false;
1653+
try { hasSeenBriefing = window.localStorage.getItem(BRIEFING_SEEN_KEY) === '1'; } catch {}
1654+
if (!hasSeenBriefing) openBriefing();
16041655
requestAnimationFrame((t) => { lastTime = t; gameLoop(t); });
16051656
</script>
16061657
</body>

tests/runtime-smoke.test.cjs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ test('runtime initializes and renders a frame', async () => {
4949
'action-button': element(),
5050
quest: element(),
5151
'quest-reset': element(),
52+
'briefing-button': element(),
53+
'briefing-dialog': element(),
54+
'briefing-start': element(),
5255
'journal-button': element(),
5356
'journal-dialog': element(),
5457
'journal-list': element(),
@@ -204,7 +207,7 @@ test('NPC movement rejects a destination reserved earlier in the frame', () => {
204207
getBoundingClientRect: () => ({ left: 0, top: 0 }),
205208
addEventListener() {}, getContext: () => context2d,
206209
});
207-
const elements = { game: element(), coords: element(), hint: element(), emote: element(), status: element(), 'action-button': element(), quest: element(), 'quest-reset': element(), 'journal-button': element(), 'journal-dialog': element(), 'journal-list': element(), 'journal-close': element(), 'sound-button': element(), 'share-button': element() };
210+
const elements = { game: element(), coords: element(), hint: element(), emote: element(), status: element(), 'action-button': element(), quest: element(), 'quest-reset': element(), 'briefing-button': element(), 'briefing-dialog': element(), 'briefing-start': element(), 'journal-button': element(), 'journal-dialog': element(), 'journal-list': element(), 'journal-close': element(), 'sound-button': element(), 'share-button': element() };
208211
const sandbox = {
209212
LittleAndroidLogic, LittleAndroidContent, LittleAndroidProgress, LittleAndroidFeedback, LittleAndroidEngagement, console, Date, Math, setTimeout, clearTimeout,
210213
document: { hidden: false, body: element(), getElementById: id => elements[id] },
@@ -241,7 +244,7 @@ test('interaction waits for a moving NPC to finish its tile step', () => {
241244
getBoundingClientRect: () => ({ left: 0, top: 0 }), addEventListener() {},
242245
getContext: () => context2d,
243246
});
244-
const elements = { game: element(), coords: element(), hint: element(), emote: element(), status: element(), 'action-button': element(), quest: element(), 'quest-reset': element(), 'journal-button': element(), 'journal-dialog': element(), 'journal-list': element(), 'journal-close': element(), 'sound-button': element(), 'share-button': element() };
247+
const elements = { game: element(), coords: element(), hint: element(), emote: element(), status: element(), 'action-button': element(), quest: element(), 'quest-reset': element(), 'briefing-button': element(), 'briefing-dialog': element(), 'briefing-start': element(), 'journal-button': element(), 'journal-dialog': element(), 'journal-list': element(), 'journal-close': element(), 'sound-button': element(), 'share-button': element() };
245248
const sandbox = {
246249
LittleAndroidLogic, LittleAndroidContent, LittleAndroidProgress, LittleAndroidFeedback, LittleAndroidEngagement, console, Date, Math, setTimeout, clearTimeout,
247250
document: { hidden: false, body: element(), getElementById: id => elements[id] },

0 commit comments

Comments
 (0)