A pixel-art farm title screen with mountains, a barn and a night sky

Personal

Stardew Companion

A single-page fan guide: look up any of the 34 villagers' favourite gifts, click around town to learn what each building is for, and play one of four arcade cabinets built into the page. No framework and no build step, styled closely enough that it feels like it belongs in the game.

Open the companion kiara-vong.github.io/stardew · source
RoleEverything
StackHTML, CSS, vanilla JS,
canvas, Web Audio
Year2026
NoteIf you want to know more, let me know!
Context

The wiki is complete and unpleasant

Everything about this game is documented somewhere. The problem is that the documentation is a wiki, and a wiki is optimised for completeness rather than for the question you actually have, which is almost always “what does this one person want”, asked while the game is paused.

So the whole design brief was: one page, no navigation, answer that question in under three seconds.

One page, no navigation. Every section of it, top to bottom, at about one and a half times real speed. It is the only claim in the brief that a picture can actually settle.
Click a building, read what it is for, close it, and you are back where you were. The whole interaction is one layer deep on purpose: this gets used with the game paused, and anything that navigates costs you your place.
Pick a face, read the two lists, and if you already know what is in your bag, type it: the picker filters to that item and tells you which list it lands in. The wiki answers this in four clicks and a lot of scrolling.
How it works

Facts, sprites and code are not the same thing

The gift lists are game data, so a one-time Node script parsed them out of a mirrored fan site rather than my typing 34 characters' worth of preferences by hand. Portraits, item icons and location photos come from the same source and the official wiki, used the way a fan guide typically uses them.

Game code is a different category, and the arcade below draws the line in both directions. Prairie King's shooter logic is an original recreation, because the original is somebody's work in a way a gift list is not. The fishing catch-bar, on the other hand, is a deliberate port of a specific open reference implementation at its own constants, because approximating it did not feel right and matching it exactly was the entire point.

A villager card: a bracketed portrait, a name plate, a birthday, four lines of biography, a gift search and two lists of gifts
  • portrait and name plate

    The game’s own art in a bracketed frame, over a wooden sign. Every label in the game is a sign; nothing here is a default input.

  • season and day

    Worth its own block: a gift on somebody’s birthday is worth eight times the same gift on any other day.

  • four written lines

    The gift lists are parsed game data. The biography is not. It is the one part of the card somebody had to write.

  • search the gifts

    Type what is in your bag rather than reading two lists to find out whether it is in either.

  • loved and liked gifts

    The question the whole page exists to answer, in two lists: what this character loves, and what they merely like. Parsed once from game data rather than typed out thirty-four times.

Five decisions on one card, and the pastiche is the sum of them rather than any one. Read down the list; the ring follows.
01Facts are factsWhat a character likes is not authored; it is looked up. Scraping it once beats retyping it 34 times.
02Sprites are borrowed, and creditedNon-commercial fan use, the same footing as any fan wiki, and named as such.
03Logic is written, or ported on purposeRecreated where it belongs to someone, ported exactly where the reference is open and the fidelity IS the feature.
The arcade

Four cabinets and no game engine

Half of this project is not a guide at all. Four playable cabinets sit on the same page, each a plain canvas element with its own loop, and none of them uses a framework or an engine.

They are also the reason the page does not cost anything while you are reading it. All four pause their render loop through an IntersectionObserver the moment they scroll out of view, and the rhythm game suspends its audio context at the same instant, so closing the modal actually stops the music rather than leaving it playing under everything else.

A runner. Score and lives in the corner, and a best that survives a reload.

Four cabinets, one frame, four tabs. They are alternatives rather than a sequence, and stacking them down the page said the opposite, because you had to scroll past three to reach the fourth. Each is a plain canvas with its own loop and no engine under it.
01Junimo KartA runner. Jump the spikes and the gaps, collect stars, and beat a best score that survives a reload.
02FishingCast, bite and result on canvas, with the catch-bar minigame itself built as a DOM overlay so its physics can run at the source's native 20ms tick rather than once a frame.
03Journey of the Prairie KingA top-down shooter over five waves. Arrows or WASD to move, hold space to fire.
04Junimo JamboreeA four-lane rhythm game at three difficulties, with a song that does not exist as a file.

One source for the song and the chart

Junimo Jamboree has no audio track. A short hand-written motif is arranged into a sixteen-measure structure at load, and that same generated chart drives both the falling notes and the oscillators, so the beatmap and the music are the same object rather than a track with a chart hand-placed on top of it. They cannot drift apart because there is nothing to drift.

Note judgment and the falling-note animation are both timed off the audio context's own clock rather than animation-frame timestamps, which is the difference between a rhythm game that feels tight and one that feels slightly wrong in a way players cannot name. Because the whole chart is known up front, every oscillator is scheduled in one pass at load instead of through the lookahead scheduler that Web Audio sequencing usually needs.

Difficulty changes note density and timing windows. It does not change the song.

One generated chart at the top, with the falling notes and the oscillators drawn under it, both firing on the same sixteen marks
The half of this a screenshot can show is the notes falling. The other half is that the same sixteen marks are what schedules the sound, in one pass at load, so the beatmap and the music cannot drift apart, because there is nothing to drift.
What it taught me

Ports have tick rates

Matching an existing visual language closely is much harder than designing freely, and much better practice. You cannot fall back on taste; you have to measure what is actually there and reproduce it, which is the same skill an audit of a design system needs.

The sharpest version of that lesson was numeric. The first pass at the catch-bar ran its physics once per animation frame, which at 60fps came out about 20% faster than intended, and every constant I then re-tuned by feel took it further from the thing I was trying to match. The fix was not a better constant, it was running the loop at the tick rate the original used.

The other one was structural. The popups rendered off-screen for a day because the page's parallax puts a perspective on the main element, which quietly makes it the containing block for any fixed-position child instead of the viewport. The modals had to live outside it in the DOM. Nothing about the symptom pointed at the cause.

once per animation frame60Hz
every 20ms50Hz
+20%

The fix was not a better constant. It was running the loop at the rate the original used.

The same physics, stepped two ways. Both leave together; the one stepped once per frame arrives a fifth early, and every constant I then re-tuned by feel took it further from the thing I was trying to match.