fuzzyworld

explodorama / files / src / changelog.ts

changelog.ts

Blame: the commit that last changed each line · file · history

bb3823d2 2026-10-31 Pavel Safronov1import type { SceneKind } from './sim/demo.ts'
5607bea2 2026-10-31 Pavel Safronov2
3/**
4 * What changed in each release, for the player (user request, 2026-10-31). Shown in the game as
5 * "what's new since you last played" and, from the menu's version, as the full list. Not the
6 * developer's CHANGELOG.md, which is longer and written for people who read the code.
7 *
8 * Versions are semver, git-tagged by hand when Pavel decides a batch is ready to release. 0.1.0
9 * is the base: nothing before it is listed. Between releases, finished changes collect in
10 * UNRELEASED; when a release is tagged they move into a new entry at the top of RELEASES, and
11 * `version` in package.json goes up to match (test/changelog.test.ts checks that).
12 *
13 * This is separate from SIM_VERSION (sim/record.ts), the rules version that decides whether a
14 * saved run resumes. One release can contain several rules versions.
15 */
16export interface Change {
17 kind: 'added' | 'changed' | 'removed'
18 /** Sprite name, from `assets/kenney/`. */
19 icon: string
20 title: string
21 /** Paragraphs of plain text, as in the almanac. */
22 text: string[]
23 /** The demo scene its entry plays as a looping clip (ui/clip.ts); none for a removal. */
24 clip?: SceneKind
25 /** When it was added, as YYYY-MM-DD. */
26 date: string
27}
28
29export interface Release {
30 /** Semver, x.y.z. */
31 version: string
32 /** YYYY-MM-DD. */
33 date: string
34 /** One line: what this release is. */
35 summary: string
36 changes: Change[]
37}
38
39/** Newest first. */
40export const RELEASES: Release[] = [
805e7a9f 2026-11-03 Pavel Safronov41 {
42 version: '0.2.0',
43 date: '2026-11-02',
44 summary: 'Play with a controller or just the keyboard, with demos for every kind of input; autopilot, oil, and many fixes.',
45 changes: [
46 {
47 kind: 'added',
48 icon: 'star',
49 title: 'Play with a controller',
50 text: [
51 'A gamepad now plays a whole run: the board, the menu, power-up choices, the shop, the almanac and the screens after a death or a level. A square rests on your tile; the d-pad hops it between the tiles you can act on, and the face buttons act there. Hold a button until its ring fills to confirm. Every button on screen shows its controller picture, and "What is that?" works with R1.',
52 'The demo has controller scenes with a virtual controller instead of a finger, and its own buttons work with the d-pad and R1.',
53 ],
54 date: '2026-11-02',
55 },
56 {
57 kind: 'added',
58 icon: 'star',
59 title: 'Play with just the keyboard',
60 text: [
61 'Hold Shift and press an arrow (or W A S D) to show a cursor on the board, hop it between the blue tiles, and act with M, B, V or Enter. Press ? to ask what something is. Alt and arrows move the view, and Home brings it back to you.',
62 'With a controller or the keyboard, a faded blue marks every far tile auto-move can walk to, with the way shown before you confirm; pointing at one with a mouse shows it too.',
63 ],
64 date: '2026-11-02',
65 },
66 {
67 kind: 'added',
68 icon: 'star',
69 title: 'Demos for every kind of input',
70 text: ['A button under the demo switches what it is shown for: touch, mouse, keyboard, or a PlayStation, Xbox, Switch or generic controller. A shuffle button plays the scenes in a random order, every scene has a bar showing how much is left, and only a click, tap, Enter or Esc (or holding the confirm button on a controller) ends the demo.'],
71 date: '2026-11-02',
72 },
73 {
74 kind: 'added',
75 icon: 'coin_gold',
76 title: 'Autopilot',
77 text: [
78 'When the last enemy is gone, the character walks on its own to every coin, charge and gem still lying about, then to the exit, along a safe way. Gems and charges still ask what you want, and every move is a real turn, so gathering can lose the par bonus.',
79 'Stop it with Esc, L1 on a controller, or the Stop button on the note at the top. To do it all yourself, turn off "Autopilot" under Settings in the menu.',
80 ],
81 date: '2026-11-02',
82 },
83 {
84 kind: 'added',
85 icon: 'star',
86 title: 'A fullscreen button',
87 text: ['The game never goes fullscreen by itself. Use the new button next to the menu button, or F11; Esc or F11 leaves it.'],
88 date: '2026-11-02',
89 },
90 {
91 kind: 'changed',
92 icon: 'star',
93 title: 'Smoother play',
94 text: [
95 'Turns now count from 1. A round Cancel button puts away a picked action or a walk you set. The view stays where you put it when you drag the board (reported by Pavel). A gem or charge choice whose window got lost opens again. The screens between levels and after a death are panels like the menu.',
96 ],
97 date: '2026-11-02',
98 },
99 {
100 kind: 'added',
101 icon: 'bomb_active',
102 title: 'Try the next version early',
103 text: ['The menu opens with a box saying whether you are playing the official or the early adopter version, with a button to switch. The early copy is built from the newest work, with its own saves and scores; runs finished there are marked "Early adopter".'],
104 date: '2026-11-01',
105 },
106 {
107 kind: 'changed',
108 icon: 'flag_green_a',
109 title: 'Wall Pass is now Crate Pass',
110 text: ['The power-up lets you walk through crates, not walls, so it has a name that says so. It works exactly as before.'],
111 clip: 'wallPass',
112 date: '2026-11-01',
113 },
114 {
115 kind: 'added',
116 icon: 'bomb',
117 title: 'A note when you pick up a charge',
118 text: ['The first charge of each kind in a run still explains itself. After that, picking one up shows a small note with its name, how many you got and how many you hold.'],
119 date: '2026-11-01',
120 },
121 {
122 kind: 'changed',
123 icon: 'gem_green',
124 title: 'Two gems in one move',
125 text: ['A slide, a run or an oil slide that takes two gems now asks for both choices, one after the other. The second used to be lost.'],
126 date: '2026-10-31',
127 },
128 {
129 kind: 'changed',
130 icon: 'oil',
131 title: 'Oil is slippery',
132 text: [
133 'Step onto oil and you slide straight on over the spill, stopping on its last tile in that line (or short of a bomb or an enemy). You pick up what lies on the way, and flame where you end up burns you.',
134 'Blobs, Seekers, Drifters, Sprinters and Hunters slip too, and their arrows show where the slide ends. Ghosts and Wisps fly over it.',
135 ],
136 clip: 'oilSlide',
137 date: '2026-10-31',
138 },
139 {
140 kind: 'changed',
141 icon: 'bomb',
142 title: 'Kick or walk: you choose',
143 text: ['With both Kick and Bomb Pass, a tap (or arrow key) on a bomb next to you asks whether to kick it or walk onto it. With the Detonator as well, setting it off is a third choice.'],
144 date: '2026-10-31',
145 },
146 {
147 kind: 'changed',
148 icon: 'block_metal',
149 title: 'A tap on a crate walks to it',
150 text: ['Tapping a crate (even from two tiles away, with Run) now walks you to the tile beside it.'],
151 date: '2026-10-31',
152 },
153 {
154 kind: 'changed',
155 icon: 'charge_freeze',
156 title: 'Use a Freeze before swapping charges',
157 text: ['When you pick up a charge while carrying a Freeze, you can now use the Freeze first and then take the new charge, instead of losing one of them.'],
158 date: '2026-10-31',
159 },
160 {
161 kind: 'added',
162 icon: 'rock',
163 title: 'A demo of rubble forming',
164 text: ['The Rubble entry in the Almanac now shows a cracked pillar being broken and the ceiling coming down.'],
165 clip: 'rubble',
166 date: '2026-10-31',
167 },
168 ],
169 },
5607bea2 2026-10-31 Pavel Safronov170 {
171 version: '0.1.0',
172 date: '2026-10-31',
173 summary: 'The first release, and the base later ones are compared with. The Almanac in the menu describes everything in it.',
174 changes: [],
175 },
176]
177
178/** Finished since the latest release, not yet part of one. Moved into RELEASES when one is tagged. */
351348d1 2026-11-04 Pavel Safronov179export const UNRELEASED: Change[] = [
180 {
181 kind: 'changed',
182 icon: 'bomb',
183 title: 'The autopilot keeps out of bomb blasts',
184 text: ['With Bomb Pass, the autopilot could walk through your own ticking bomb to fetch something and be caught in the blast. It now waits for bombs to go off, and walks out of a blast zone first.'],
185 date: '2026-11-03',
186 },
187]
5607bea2 2026-10-31 Pavel Safronov188
189/** The version being played. */
190export const VERSION = RELEASES[0]!.version
191
192/** Parse "x.y.z" into numbers; null if it isn't that. */
193export function parseVersion(v: string): [number, number, number] | null {
194 const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v)
195 return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null
196}
197
198/** Negative if a < b, 0 if equal, positive if a > b. An unparseable version counts as older than any. */
199export function compareVersions(a: string, b: string): number {
200 const pa = parseVersion(a)
201 const pb = parseVersion(b)
202 if (!pa || !pb) return pa ? 1 : pb ? -1 : 0
203 for (let i = 0; i < 3; i++) {
204 const d = pa[i]! - pb[i]!
205 if (d) return d
206 }
207 return 0
208}
209
210/** The releases newer than `last`, newest first; none if `last` is current (or newer, after a rollback). */
211export function releasesSince(last: string, releases: Release[] = RELEASES): Release[] {
212 return releases.filter((r) => compareVersions(r.version, last) > 0)
213}
214
215/**
216 * What to show a player who last played `last` (null: never, or a value that wasn't kept), and
217 * whether to record the current version now. A first visit has nothing to compare with: nothing
218 * to show, record it. Newer releases: show them (recording waits until they are shown). Current:
219 * nothing. Newer than the game (a rollback): nothing, and the newer record stays.
220 */
221export function newsFor(last: string | null, releases: Release[] = RELEASES): { news: Release[]; record: boolean } {
222 if (last === null) return { news: [], record: true }
223 const news = releasesSince(last, releases)
224 return { news, record: !news.length && compareVersions(last, releases[0]!.version) < 0 }
225}