Pirate Jam 18
Jan 2026
In Development
Play Crystal's Debut Demo
Unity Engine
Project Manager, Lead Programmer, Lead Game Designer
The player plays as Crystal, a newly hired streamer and follows requests from chat. Requests include interacting with the computer on the screen to complete actions to raise her approval meter. The actions range from minigames, interacting with applications on the computer, to messing with Crystal’s pet Gerbil Kevin.
Introduction
Crystal's Debut was developed for Pirate Jam 18, drawing inspiration from Needy Streamer Overload and a shared passion for VTuber culture. The project blends charming minigames and endearing characters with an undercurrent of corporate pressure and eldritch interference — a deliberate contrast that gives the game its tone.
The goal was to create something mechanically approachable but narratively layered, where every viewer request feels consequential and Crystal's world slowly reveals itself to be stranger than it first appears.
Dialogue & Request System — Yarn Spinner 3
To handle Crystal's chat requests and main dialogue, I integrated Yarn Spinner 3 — a narrative scripting tool built for Unity that allows dialogue to be written in a clean, readable format and driven at runtime without hardcoding conversation logic into C#.
The primary challenge was learning Yarn Spinner's node and command architecture from scratch and designing a system where dialogue could trigger game actions seamlessly. To bridge the gap between narrative and gameplay, I wrote a suite of custom C# commands registered with Yarn Spinner's command dispatcher. These commands allowed Yarn scripts to directly invoke request logic — launching minigames, updating the approval meter, and interacting with in-game objects — without any manual intervention from other systems.
This approach kept all request sequencing and branching inside the Yarn scripts themselves, making it straightforward to add, reorder, or adjust requests without touching the underlying C# codebase.
A key design challenge was distinguishing between Crystal's main dialogue and messages
delivered through her in-game messaging app. Rather than building a separate system,
I extended the existing Yarn Spinner integration by tagging specific lines with
#message directly in the Yarn scripts. A custom dialogue presenter listens
for this tag at runtime and routes those lines into the messaging app UI instead of the
standard dialogue display — keeping all conversation logic in one place while allowing
the presentation layer to vary depending on context.
Wordle Minigame Breakdown
One of the minigames embedded in Crystal's Debut is a fully functional Wordle clone, playable directly on Crystal's in-game computer. The game pulls from two word lists loaded at runtime — a common solutions list and a broader valid words list — and selects a random target word each time the minigame is triggered.
The trickiest part of the implementation was the two-pass letter evaluation system in
OnRowSubmit(). A naive single-pass approach produces incorrect results when
the guess contains duplicate letters — for example, guessing "APPLE" when the answer is
"CRANE" could incorrectly mark both P's as wrong-spot rather than incorrect. To handle
this accurately, the evaluation runs in two passes. The first pass checks for exact
matches, marking any correct letters and removing them from a tracked
remaining string. The second pass then checks the leftover letters for
wrong-spot or incorrect states, consuming each match from remaining as it
goes. This ensures duplicate letters are accounted for precisely and never double-counted.
// Pass 1 — exact matches
for (int i = 0; i < row.tiles.Length; i++)
{
WordleTile tile = row.tiles[i];
if (tile.letter == targetWord[i])
{
tile.SetState(correctState);
remaining = remaining.Remove(i, 1).Insert(i, " ");
}
}
// Pass 2 — wrong spot or incorrect
for (int i = 0; i < row.tiles.Length; i++)
{
WordleTile tile = row.tiles[i];
if (tile.state == correctState) continue;
if (remaining.Contains(tile.letter))
{
tile.SetState(wrongSpotState);
int index = remaining.IndexOf(tile.letter);
remaining = remaining.Remove(index, 1).Insert(index, " ");
}
else
{
tile.SetState(incorrectState);
}
}