[{"data":1,"prerenderedAt":303},["ShallowReactive",2],{"portfolio-general":3},{"variant":4,"profile":29,"experience":56,"projects":137,"skills":193,"writings":253,"generatedAt":302},{"key":5,"label":6,"headline":7,"tagline":8,"heroHeading":9,"heroAccent":10,"heroSub":11,"accent":12,"sectionOrder":13,"featuredTags":20,"projectGroups":21,"projectsHeading":25,"resumeUrl":26,"ctaPrimary":27,"aboutHtml":28},"general","Software & AI","Software Engineer","Products end to end, web, integrations, and AI systems that hold up in production.","I build production web apps, integrations, and AI tools.","AI tools","Real products, shipped end to end. Lately that means AI builders, agent pipelines, and the tooling that keeps them honest in production.","accent-1",[14,15,16,17,18,19],"hero","about","experience","projects","writings","skills",[],[22],{"type":23,"label":24},"app","Selected work","Selected projects","\u002Fresume\u002FChristian-DAlbano-Resume.pdf","View projects","\u003Cp>I build products end to end: the interface, the backend, and lately the AI systems in between. The parts I care about most are the ones that have to survive contact with real users. Dashboards that stay fast. Integrations that fail loudly instead of silently. AI output structured enough to actually ship.\u003C\u002Fp>\n\u003Cp>At Mouseflow I led the analytics dashboard’s migration from jQuery to Vue and ran its integrations platform across Shopify, WordPress, Optimizely, and Salesforce. Solo, I shipped Formship (the agentic orchestration behind a coaching SaaS) and HasteCV (an AI resume builder with its own MCP server). I run my own job search through tools I built, which keeps me honest about what they’re worth.\u003C\u002Fp>\n\u003Cp>This site is part of the work too: a hand-built design system, two identities from one codebase, and a build process where AI implements but never decides.\u003C\u002Fp>\n",{"name":30,"headline":7,"location":31,"email":32,"site":33,"socials":34,"bioHtml":55},"Christian D'Albano","Orlando, FL","chrisdalbano12@gmail.com","chrisdalbano.com",[35,39,43,47,51],{"label":36,"url":37,"icon":38},"GitHub","https:\u002F\u002Fgithub.com\u002Fchrisdalbano","github",{"label":40,"url":41,"icon":42},"LinkedIn","https:\u002F\u002Flinkedin.com\u002Fin\u002Fchrisdalb","linkedin",{"label":44,"url":45,"icon":46},"itch.io","https:\u002F\u002Fchrisdbgames.itch.io","itch",{"label":48,"url":49,"icon":50},"Wavedash","https:\u002F\u002Fwavedash.com\u002Fchrisdalbano12","waves",{"label":52,"url":53,"icon":54},"Email","mailto:chrisdalbano12@gmail.com","mail","\u003Cp>Software engineer in Orlando. I build products end to end (web, integrations, applied AI) and ship small games on the side, art included.\u003C\u002Fp>\n",[57,78,103,119],{"slug":58,"title":59,"org":60,"start":61,"end":62,"location":31,"tags":63,"variants":71,"order":72,"bullets":73},"cencora","Senior Data Operations Engineer","Cencora (Intrinsiq)","2025-07","present",[64,65,66,67,68,69,70],"python","sql-server","azure-databricks","azure-devops","ci-cd","docker","data-quality",[5],1,[74,75,76,77],"Build Python tooling that monitors and manages high-volume data pipelines on Azure Databricks and SQL Server.","Own CI\u002FCD for production deployments through Azure DevOps, adding pre-deploy checks that cut rollout incidents.","Designed the diagnostics and alerting layer for the data workflows that serve practices, providers, and data sources across the network.","Work with engineering and analytics on the consumer-facing Vue surfaces that sit on top of the pipeline.",{"slug":79,"title":7,"org":80,"start":81,"end":62,"location":82,"tags":83,"variants":96,"order":97,"bullets":98},"mouseflow","Mouseflow","2023-08","Remote",[84,85,86,87,88,89,90,91,92,93,94,95],"vue","blazor","javascript","chart-js","web-sdk","integrations","shopify","wordpress","optimizely","salesforce","agentic-ai","mcp",[5],2,[99,100,101,102],"Led the migration of the core analytics dashboard from jQuery to Vue 3 and Blazor, retiring legacy code and speeding up customer-facing views.","Built the dashboard’s data-visualization components in Vue and Chart.js: heatmaps, session-replay surfaces, and funnel views used across customer dashboards.","Owned the integrations platform end to end (Shopify, WordPress, Optimizely, Convert, Salesforce, and the customer JavaScript SDK), taking each connector through discovery, build, QA, and release.","Build and run production agentic-AI workflows with Claude Code, Cursor, MCP servers, and custom agents that the team uses for development and integration work.",{"slug":104,"title":105,"org":106,"start":107,"end":108,"location":82,"tags":109,"variants":113,"order":114,"bullets":115},"aizhak-coffee","Full-Stack Developer (Contract)","Aizhak Coffee","2023-09","2024-01",[84,110,111,69,112],"typescript","firebase","ecommerce",[5],3,[116,117,118],"Built a real-time ecommerce storefront in Vue 3 and TypeScript on Firebase, from cart to dynamic inventory.","Shipped the MVP ordering flow in under six weeks, working directly with design and backend.","Launched it live at \u003Ca href=\"http:\u002F\u002Faizhak.com\">aizhak.com\u003C\u002Fa>.",{"slug":120,"title":121,"org":122,"start":123,"end":81,"location":124,"tags":125,"variants":131,"order":132,"bullets":133},"baptist-health","Junior Frontend Developer","Baptist Health South Florida","2022-06","Miami, FL",[126,110,127,128,129,130],"react","java-spring-boot","accessibility","wcag","healthcare",[5],4,[134,135,136],"Built responsive patient-portal interfaces in React and TypeScript under WCAG accessibility requirements.","Integrated the frontend with Java Spring Boot APIs handling sensitive patient data.","Cut UI load time through code-splitting and bundling.",[138,155,169,181],{"slug":139,"name":140,"blurb":141,"url":142,"repo":143,"type":23,"tags":144,"variants":149,"featured":151,"order":72,"cover":152,"wavedash":143,"engine":143,"jam":143,"media":153,"descriptionHtml":154},"formship","Formship","A SaaS that turns coaching frameworks into structured learning programs. I designed and built the agentic-AI orchestration system behind its quizzes and modular content, solo.","https:\u002F\u002Fformship.io",null,[145,146,94,147,84,64,148],"saas","ai","ai-orchestration","education",[5,150],"game-design",true,"\u002Fshots\u002Fformship.webp",[],"\u003Cp>Formship is a live SaaS that helps coaches turn their frameworks into structured programs their clients can actually work through. I designed and built it end to end, solo, in Vue 3 and Python.\u003C\u002Fp>\n\u003Ch2>Why orchestration instead of one big prompt\u003C\u002Fh2>\n\u003Cp>A coaching program is not a blob of text. It is a sequence: content blocks a learner reads, quizzes that check whether the material landed, checkpoints that track progress. Ask a model to generate all of that in one shot and you get plausible mush, the kind that reads fine and composes terribly.\u003C\u002Fp>\n\u003Cp>So generation runs as a multi-step pipeline instead. Each step in the chain has a narrow job and a structured contract for what it hands to the next one, and the customer-visible features (AI-generated quizzes, modular content) are the last link of that chain, not the whole machine. Decomposing the work this way is what makes the output usable: a quiz that arrives as structured data can be rendered, edited, and scored by the app; a quiz that arrives as prose can only be pasted somewhere.\u003C\u002Fp>\n\u003Ch2>The unit of composition is the block\u003C\u002Fh2>\n\u003Cp>The other load-bearing decision: programs are built from modular content blocks rather than documents. Blocks are what the AI pipeline generates into, what coaches rearrange, and what the analytics layer measures learner progress against. One shape, three jobs. That symmetry was the design goal, because the structure that makes generation reliable is the same structure that makes progress measurable.\u003C\u002Fp>\n\u003Ch2>What it taught me\u003C\u002Fh2>\n\u003Cp>Formship is where I learned the lesson I now apply everywhere, including to the MCP servers I build: AI output is only as composable as the data model you force it into. The model is rarely the hard part. The contract is.\u003C\u002Fp>\n",{"slug":156,"name":157,"blurb":158,"url":159,"repo":143,"type":23,"tags":160,"variants":165,"featured":151,"order":97,"cover":166,"wavedash":143,"engine":143,"jam":143,"media":167,"descriptionHtml":168},"buildvalue","BuildValue","A League of Legends gold-efficiency tool built around the question item designers balance against, what is a stat point actually worth. Scores every item, compares full builds, auto-syncs from Riot's API weekly.","https:\u002F\u002Fbuildvalue.chrisdalbano.com\u002F",[161,162,84,163,64,164,87],"league-of-legends","full-stack","fastapi","data-pipeline",[5,150],"\u002Fshots\u002Fbuildvalue.webp",[],"\u003Cp>A full-stack web app that computes gold efficiency for every League of Legends item and helps players reason about their builds. Gold efficiency is the closest thing item design has to a unit price: every stat gets a gold value derived from basic items, and an item is over- or under-priced against the sum of its parts. BuildValue computes that for the whole item set, scores complete builds, and exposes where the “inefficient” picks are actually buying something the math can’t see (actives, passives, spike timing). Vue 3 and Chart.js on the front, FastAPI and Python on the back, MongoDB Atlas for storage, and a scheduled ETL that pulls from Riot’s DDragon API every week so the data stays current.\u003C\u002Fp>\n",{"slug":170,"name":171,"blurb":172,"url":173,"repo":143,"type":23,"tags":174,"variants":177,"featured":151,"order":97,"cover":178,"wavedash":143,"engine":143,"jam":143,"media":179,"descriptionHtml":180},"hastecv","HasteCV","An AI resume and cover-letter builder with a structured document model, rich-text editing, and an MCP server that lets AI clients read and edit documents directly.","https:\u002F\u002Fhastecv.com",[145,146,95,162,175,84,176],"django","tiptap",[5,150],"\u002Fshots\u002Fhastecv.webp",[],"\u003Cp>HasteCV is my own product: an AI-assisted resume and cover-letter builder, live at \u003Ca href=\"http:\u002F\u002Fhastecv.com\">hastecv.com\u003C\u002Fa>. I built every layer of it solo. Django and Django REST Framework on the API, Celery and Redis for async generation and export jobs, Vue 3 with Tiptap on the front, Docker on Render, with a generation layer that speaks to both Anthropic and OpenAI.\u003C\u002Fp>\n\u003Ch2>The document model came first\u003C\u002Fh2>\n\u003Cp>A resume is two documents wearing one name. It is structured data (experience arrays, education, skills) that templates and exports need, and it is a visible page a person actually reads and edits. Most builders pick one and fake the other. HasteCV keeps both: every document carries structured fields plus a rich-text body, and every id is prefixed by type (cv_, cl_, note_) so any tool can route a document from the id alone, no type parameter required.\u003C\u002Fp>\n\u003Cp>That decision looked like over-engineering until AI clients showed up. Then it became the product.\u003C\u002Fp>\n\u003Ch2>The MCP server\u003C\u002Fh2>\n\u003Cp>HasteCV ships an MCP server that exposes documents to AI clients like Claude Code: list, read, create, update, export, with schema discovery as its own tool so a client learns the exact field shapes instead of guessing at them. A few design calls I would defend in a review:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Errors are values, not exceptions.\u003C\u002Fstrong> A failed call returns a readable error object, because a model that can read the failure can usually recover from it. A raised exception just ends the conversation.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Schema description is a tool.\u003C\u002Fstrong> The client asks the API what a cover letter looks like before writing one. Self-describing beats documented.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Archive over delete.\u003C\u002Fstrong> The destructive path exists but the recoverable one is the default, because an AI client with write access deserves guardrails the same way an intern does.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Arrays replace wholesale.\u003C\u002Fstrong> Read, modify, write back. Partial array patching is where state bugs live.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>Dogfooding, daily\u003C\u002Fh2>\n\u003Cp>I run my own job-application pipeline through this server from Claude Code: tailored CVs pushed in as structured documents, PDFs exported without opening a browser. The thing I learned building it is that making an API legible to a model is a design discipline, not a plumbing task. Small contracts, explicit schemas, recoverable failures. The instincts that make a component system usable by another engineer turn out to make an API usable by an AI, and that overlap is most of why I keep building in this space.\u003C\u002Fp>\n",{"slug":182,"name":106,"blurb":183,"url":184,"repo":143,"type":23,"tags":185,"variants":188,"featured":189,"order":114,"cover":190,"wavedash":143,"engine":143,"jam":143,"media":191,"descriptionHtml":192},"aizhak-ecommerce","A real-time coffee storefront in Vue and Node, shipped as a fast MVP. Cart, inventory, and ordering on Firebase.","https:\u002F\u002Faizhak.com",[112,84,186,111,187],"node","real-time",[5],false,"\u002Fshots\u002Faizhak-ecommerce.webp",[],"\u003Cp>A coffee ecommerce platform I led and shipped as an MVP: Vue frontend, Node services, real-time inventory and orders on Firebase, containerized with Docker. Designed in Figma and built to launch fast.\u003C\u002Fp>\n",[194,203,213,225,236,245],{"label":195,"items":196,"variants":202},"Languages",[197,198,199,200,201],"TypeScript","JavaScript","Python","SQL","GDScript",[5,150],{"label":204,"items":205,"variants":212},"Frontend",[206,207,208,209,210,211],"Vue 3","React","Blazor","Tailwind","Chart.js","Tiptap",[5,150],{"label":214,"items":215,"variants":224},"Backend & Data",[216,217,218,219,220,221,222,223],"Node.js","Django","DRF","Flask","Java Spring Boot","SQL Server","Azure Databricks","Firebase",[5],{"label":226,"items":227,"variants":235},"AI & Agentic",[228,229,230,231,232,233,234],"Claude Code","Claude API","Cursor","MCP","Multi-agent orchestration","Prompt engineering","Structured output",[5,150],{"label":237,"items":238,"variants":244},"Game Dev",[239,240,201,241,242,243],"Phaser 3","Godot 4","Krita","Hand-painted art","Data-driven design",[5,150],{"label":246,"items":247,"variants":252},"Infra & Ops",[248,249,250,251],"Docker","Azure DevOps CI\u002FCD","Git","Render",[5],[254,268,281,291],{"slug":255,"title":256,"dek":257,"date":258,"project":143,"tags":259,"variants":266,"order":72,"readingMinutes":114,"contentHtml":267},"building-this-site","This site is the case study","Three token layers, eleven motion presets, two identities from one codebase, and a two-agent Claude workflow that keeps design authority and implementation honest.","2026-06-11",[260,261,262,263,264,265],"design-systems","nuxt","tokens","motion","ai-tooling","process",[5,150],"\u003Cp>The site you are reading is the most recent thing I shipped, so it should answer the question a portfolio usually dodges: what does this person’s work look like when nobody scoped it for him? I rebuilt \u003Ca href=\"http:\u002F\u002Fchrisdalbano.com\">chrisdalbano.com\u003C\u002Fa> from scratch in about two and a half weeks of nights and weekends, solo, from first scaffold to DNS cutover. Not from a template. The constraint I set on day one: every visual decision has to live in a system, not in a component.\u003C\u002Fp>\n\u003Ch2>Tokens before pixels\u003C\u002Fh2>\n\u003Cp>The styling is built on a three-layer token architecture: primitives (raw oklch values, type sizes, durations), semantic tokens (surface, ink, accent roles), and component tokens that map semantics onto anatomy. Components are only allowed to consume the top layer. There is a binding rule in the repo doctrine: no hex literals in components, ever. Tailwind v4 reads the same tokens through \u003Ccode>@theme\u003C\u002Fcode>, so CSS and utility classes draw from one source.\u003C\u002Fp>\n\u003Cp>That rule is what makes the site’s party trick cheap. There are two versions of this portfolio, a software engineering identity and a game design identity, and they are the same components fed different data and a swapped accent role. Switching variants costs one data attribute. No forked components, no second stylesheet.\u003C\u002Fp>\n\u003Ch2>Motion as vocabulary, not decoration\u003C\u002Fh2>\n\u003Cp>Animation runs through a finite set of eleven named presets (motion-v under the hood): entrances, emphasis, and the hero treatments. Finite is the point. When every animation comes from a shared vocabulary with shared easings and durations, the page feels composed instead of assembled. Every preset carries an explicit reduced-motion branch, and a global guard backs it up, because respecting \u003Ccode>prefers-reduced-motion\u003C\u002Fcode> is table stakes for calling something crafted.\u003C\u002Fp>\n\u003Ch2>Content modeled like a CMS, without the CMS\u003C\u002Fh2>\n\u003Cp>The content is not in the components. It is authored as markdown with typed frontmatter in a private vault, and a build script bakes it into per-variant JSON the site renders from. Each content item declares which identities it belongs to; the pipeline emits one payload per variant. That separation is the same shape as a headless CMS: content modeling, an editorial workflow, and a render layer that never touches the source. It also means publishing a new project or note is a markdown commit, not a deploy decision.\u003C\u002Fp>\n\u003Ch2>The two-agent workflow\u003C\u002Fh2>\n\u003Cp>The part I would defend hardest: how it was built. I run two Claude Code agents with deliberately separated powers. A design-authority agent owns tokens, grid, component anatomy, and motion specs, and writes them to a specs directory. An engineering agent implements those specs in Vue and Tailwind and is forbidden from making design decisions. The spec file is the handoff contract. If a spec is ambiguous, the engineer sends it back rather than improvising.\u003C\u002Fp>\n\u003Cp>This sounds like ceremony for a personal site. It is the opposite. Solo builders drift; the system held the line on every late-night shortcut I was tempted to take, and it is why the site stayed coherent while shipping fast. It is also a working answer to a question I care about professionally: what does AI-assisted craft look like when you architect the collaboration instead of just prompting harder?\u003C\u002Fp>\n\u003Cp>The repo doctrine, token files, and motion presets are all real artifacts, not retrofitted documentation. If you are reading this as a hiring signal: this is the process you would be hiring.\u003C\u002Fp>\n",{"slug":269,"title":270,"dek":271,"date":272,"project":273,"tags":274,"variants":279,"order":72,"readingMinutes":114,"contentHtml":280},"tuning-fear-by-numbers","Tuning fear by numbers","How I Won't Be Abducted ramps difficulty with zero new mechanics: every knob lives in a Godot resource, and the player's kit never changes.","2026-06-09","i-wont-be-abducted",[275,276,277,278],"systems-design","godot","difficulty","data-driven",[5,150],"\u003Cp>Most jam games escalate by addition. Night two adds a new enemy, night three adds a new weapon, and by the deadline you are debugging four mechanics instead of finishing one. For \u003Cem>I Won’t Be Abducted\u003C\u002Fem> I made the opposite bet before writing any code: \u003Cstrong>the player’s kit stays constant for the whole game, and difficulty comes from the world.\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>The boy you control on night three is exactly the boy from night one. Move cooldown 0.18 seconds, kick cooldown 0.4 seconds, three Nerve pips. The dawn draft lets you pick one upgrade between nights, but that is the player’s choice, not the curve. The curve never touches your hands.\u003C\u002Fp>\n\u003Ch2>Where the curve actually lives\u003C\u002Fh2>\n\u003Cp>Every gameplay number sits in a Godot \u003Ccode>Resource\u003C\u002Fcode> (\u003Ccode>.tres\u003C\u002Fcode> file), editable from the inspector without touching a script. Four resource types drive the whole game: \u003Ccode>PlayerConfig\u003C\u002Fcode>, \u003Ccode>EnemyData\u003C\u002Fcode>, \u003Ccode>AugmentData\u003C\u002Fcode>, and the one doing the difficulty work, \u003Ccode>NightConfig\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>duration\u003C\u002Fcode> (how long until dawn)\u003C\u002Fli>\n\u003Cli>\u003Ccode>spawn_interval\u003C\u002Fcode> (seconds between alien drops)\u003C\u002Fli>\n\u003Cli>\u003Ccode>max_on_board\u003C\u002Fcode> (population cap)\u003C\u002Fli>\n\u003Cli>\u003Ccode>enemy_pool\u003C\u002Fcode> (which \u003Ccode>EnemyData\u003C\u002Fcode> resources can spawn, with per-enemy spawn weights)\u003C\u002Fli>\n\u003Cli>\u003Ccode>enemy_speed_scale\u003C\u002Fcode> (a global multiplier on alien step timers)\u003C\u002Fli>\n\u003Cli>\u003Ccode>intensity_ramp\u003C\u002Fcode> (how spawn pressure climbs within a night)\u003C\u002Fli>\n\u003Cli>\u003Ccode>gift_interval\u003C\u002Fcode> \u002F \u003Ccode>gift_amount\u003C\u002Fcode> (Scrap pickups: worth 3, then 4, then 5 across the nights)\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Night one and night three are the same scene, the same code, the same rules. They differ only in those numbers and in which enemies are in the pool. Rebalancing the game means opening a \u003Ccode>.tres\u003C\u002Fcode> and turning a dial: make night one gentler by raising \u003Ccode>spawn_interval\u003C\u002Fcode> and lowering \u003Ccode>max_on_board\u003C\u002Fcode>. In the last hours of a 63-hour jam, that mattered more than any feature. Tuning time is the scarcest resource a jam has, and a curve made of inspector fields is the cheapest curve to iterate.\u003C\u002Fp>\n\u003Ch2>When a number can’t fix it, the rule is wrong\u003C\u002Fh2>\n\u003Cp>Twice during the jam, no knob produced the right feel, and both times the fix was a rule change rather than a bigger number.\u003C\u002Fp>\n\u003Cp>First: aliens only leave the board through real exits (the door and two windows are specific cells, not abstract directions). Players quickly learned to hug corners, where a kick had no exit lane and accomplished nothing. The dials could not fix wasted kicks. The rule could: a kick that slams a chain of aliens into a wall now \u003Cstrong>stuns the whole chain\u003C\u002Fstrong>. Every kick does something. Then, with kicks reliably useful, the population numbers came back down (night two dropped from 5 aliens to 4, night three from 6 to 5).\u003C\u002Fp>\n\u003Cp>Second: the Lobber’s ray-gun dealt direct Nerve damage at range, which on a 15-cell board read as unfair, since there is nowhere honest to hide. Changing its damage number just moved the unfairness around. The rule change: the first ray-gun hit \u003Cstrong>stuns\u003C\u002Fstrong> you, and only a hit while you are already stunned costs a pip. Same enemy, same range, but now the threat is readable and the punishment requires two mistakes.\u003C\u002Fp>\n\u003Cp>That became my working test for the whole project: tune with numbers until the numbers stop helping, then change one rule and go back to numbers.\u003C\u002Fp>\n\u003Ch2>What this buys at jam scale\u003C\u002Fh2>\n\u003Cp>Three concrete wins. Designers (in this case, me at 2 a.m.) balance in the inspector instead of in code. New content is data first: the rare ray-gun alien is mostly a spawn weight (about 15%) in an existing pool, not a scripted wave. And the difficulty story stays legible to the player, because nothing new is ever introduced mid-run. The game gets harder the way a board game gets harder when you add pieces: same rules, turned up.\u003C\u002Fp>\n\u003Cfigure class=\"prose-media\">\n  \u003Cimg src=\"\u002Fshots\u002Fi-wont-be-abducted\u002Fpreview-chars.webp\" alt=\"Character standees from I Won't Be Abducted\" loading=\"lazy\" decoding=\"async\" width=\"1600\" height=\"900\">\n\u003C\u002Ffigure>\n\u003Cp>The full resource schema and the EventBus architecture that goes with it are in the game’s design wiki, which shipped in the repo alongside the game.\u003C\u002Fp>\n",{"slug":282,"title":283,"dek":284,"date":272,"project":273,"tags":285,"variants":289,"order":97,"readingMinutes":132,"contentHtml":290},"i-wont-be-abducted-postmortem","I Won't Be Abducted: a 63-hour postmortem","Shipping a tabletop-defense game solo in one weekend: scope guardrails, a mid-jam economy cut, and hiding the real game underneath the game.",[286,276,287,288],"postmortem","jam","narrative-design",[5,150],"\u003Cp>\u003Cem>I Won’t Be Abducted\u003C\u002Fem> was built in roughly 63 hours for Wavedash Spring Jam 26, theme: \u003Cstrong>Shelter\u003C\u002Fstrong>. Solo on design, code, and direction; hand-drawn characters and props; AI-generated static backgrounds that I edited and finished by hand; Claude Code as a pair on the implementation. It was my first shipped Godot game (my previous jam game, \u003Cem>Spare Knight\u003C\u002Fem>, was Phaser 3), and it went live on \u003Ca href=\"http:\u002F\u002Fitch.io\">itch.io\u003C\u002Fa> and Wavedash.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Spoiler warning: the ending is discussed below, and the ending is the design.\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch2>The pitch\u003C\u002Fh2>\n\u003Cp>A boy refuses to leave his room and defends it from a nightly alien invasion across three nights. Except the aliens are board-game pieces. The giant hands placing them are his own. The final boss is his Dad opening the bedroom door, and beating the monster and the father getting through are the same event. The camera pulls back and it was always his room, his hands, his game.\u003C\u002Fp>\n\u003Cp>The emotional reference was the \u003Cem>Fort\u002FDa\u003C\u002Fem> ritual: a child mastering distress he can’t control by staging it as a game he can. The jam theme asked for shelter. The room was never the shelter. The play was.\u003C\u002Fp>\n\u003Ch2>Scope guardrails: write the “do not build” list first\u003C\u002Fh2>\n\u003Cp>Before building anything I wrote a three-column scope table: must ship, stretch, and \u003Cstrong>do not build\u003C\u002Fstrong>. The third column did the most work. Procedural maps, a real combo system, free-roam pathfinding, and a fourth night were banned on day zero, no matter how much time was left. The rule underneath: the night battle is the game; everything else is a setting on it. The board is 3x5, fifteen cells, and it never apologizes for being small.\u003C\u002Fp>\n\u003Cp>The biggest mid-jam cut was the economy. The original design had a between-nights shop where Scrap bought upgrades. That meant two currencies of tuning debt: pricing every upgrade and pacing every income source, with no time to playtest either. The shop became a free \u003Cstrong>dawn draft\u003C\u002Fstrong> (pick 1 of 3 cards from a pool of 5), and Scrap kept exactly one sink: crafting kickable obstacles mid-fight. Drafts are self-balancing in a way shops are not, since the player only ever compares three options I chose to put in front of them.\u003C\u002Fp>\n\u003Ch2>What worked\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>Data over code.\u003C\u002Fstrong> Every gameplay number lives in a \u003Ccode>.tres\u003C\u002Fcode> resource, so the last night of the jam was spent turning dials instead of editing scripts. I wrote about that system separately in \u003Ca href=\"\u002Fnotes\u002Ftuning-fear-by-numbers\">Tuning fear by numbers\u003C\u002Fa>.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Signals over references.\u003C\u002Fstrong> Gameplay emits events on a global EventBus; UI listens. The HUD, audio, achievements, and the panic shader all subscribe to the same signals the game logic was already firing. Nothing in the gameplay code knows the UI exists.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>The tabletop conceit pays for itself.\u003C\u002Fstrong> Board-game standees don’t need walk cycles. All the motion is code: hops, lunges, knock-backs, screen shake, a desaturation shader that drains color as your Nerve drops. That one constraint moved weeks of animation work off the schedule and made the twist land harder, because the pieces always looked like pieces.\u003C\u002Fp>\n\u003Ch2>What didn’t\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>The roster came in too late.\u003C\u002Fstrong> The Crawler was playable from the first session, but the Lobber, the Rusher, and the Dad fight stayed data-only until the final day. They shipped, but they got hours of playtest instead of days, and it showed in early balance.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Fairness arrived last.\u003C\u002Fstrong> Enemy attacks had no wind-up until the finish pass. Until telegraphs went in (pull back, warning flash, then strike), hits felt arbitrary. A game where you dodge by stepping off a tile is only fair when every hit announces itself. That should have been built with the first enemy, not the last.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>The intro plan was four times too big.\u003C\u002Fstrong> The script called for four voiced narration parts. I recorded one. The cinematic shipped with beat one voiced and captioned, and the rest as timed text over the music bed. Cutting it to that was the right call a day later than it should have been.\u003C\u002Fp>\n\u003Cfigure class=\"prose-media\">\n  \u003Cimg src=\"\u002Fshots\u002Fi-wont-be-abducted\u002Froom-plain.webp\" alt=\"The bedroom board at night\" loading=\"lazy\" decoding=\"async\" width=\"1600\" height=\"872\">\n\u003C\u002Ffigure>\n\u003Ch2>What I’d keep\u003C\u002Fh2>\n\u003Cp>The discipline of the reveal. Every system, the hands, the standees, the game-over art kept deliberately ambiguous, exists to protect one quiet beat at the end. Shipping a complete arc in a weekend came down to knowing which single moment the whole game was for, and cutting toward it.\u003C\u002Fp>\n",{"slug":292,"title":293,"dek":294,"date":272,"project":295,"tags":296,"variants":300,"order":114,"readingMinutes":114,"contentHtml":301},"spare-knight-postmortem","Spare Knight: 13 days, no dice","Designing a tactical roguelike where every attack lands: initiative from speed, commitment instead of RNG, and a hand-painted world made in Krita.","spare-knight",[286,297,287,298,299],"phaser","combat-design","art",[5,150],"\u003Cp>\u003Cem>Spare Knight\u003C\u002Fem> was my first shipped game: a tactical roguelike built in 13 days for Gamedev.js Jam 2026, theme \u003Cstrong>Machines\u003C\u002Fstrong>. I directed it, wrote all the code (Phaser 3 + Vite), and hand-painted every tile, character, and UI element in Krita on a Wacom tablet. Nicholas Drabb joined as collaborator on game design consultation, sound, voice work, and the lore we co-wrote. It shipped open source, live on the jam page and on Wavedash.\u003C\u002Fp>\n\u003Cp>The premise carries the theme: the machines have reached a philosophical consensus that organic life is inefficient. They call it the Absolution Mindset. You are a knight whose firmware is three generations out of date, too old to receive the update, with one human left to protect.\u003C\u002Fp>\n\u003Ch2>The core bet: no attack RNG\u003C\u002Fh2>\n\u003Cp>The first design decision was the one everything else hangs on: \u003Cstrong>attacks never miss\u003C\u002Fstrong>. No hit chance, no damage range. Combat happens on a 6x6 isometric grid with speed-based initiative, and every choice is committed the moment you make it.\u003C\u002Fp>\n\u003Cp>The reasoning: runs are 15 to 20 minutes. In a run that short, a 75%-to-hit roll that misses twice in a row doesn’t read as variance, it reads as theft. XCOM can afford that feeling across a 30-hour campaign; a jam roguelike cannot. So the dice came out, and the tension had to come from somewhere else.\u003C\u002Fp>\n\u003Cp>It moved into execution. Offense and defense run through souls-like timing reactions: multi-wheel strikes and defensive braces where you read an audio chime and commit on the beat. The math of an exchange is fully deterministic; whether you flubbed the timing is on you. Randomness still exists in the game, but it lives in the campaign layer (a Slay-the-Spire-style node map across two acts: cornfield, cathedral, silo, the Herald, then the Prelate), where variance creates replayability instead of resentment.\u003C\u002Fp>\n\u003Cp>The companion decision: \u003Cstrong>defeat is a moment, not a punishment\u003C\u002Fstrong>. Short runs, instant restart, and a tone that treats falling as part of the fiction of being obsolete. Players retry an unfair-feeling game out of spite and a fair-feeling one out of curiosity. Only one of those survives a jam rating page.\u003C\u002Fp>\n\u003Ch2>Painting the whole world\u003C\u002Fh2>\n\u003Cp>The look I wanted was “Bright Ruins”: warm, sunlit, weighty, somewhere between \u003Cem>Don’t Starve\u003C\u002Fem>’s linework and a Ghibli afternoon. Hand-painting an entire isometric set as one person on a deadline taught me the real unit cost of art scope. Two things kept it survivable: the battle takes place on a single screen, so the tile vocabulary stayed small, and painting in Krita with a consistent brush kit meant each new asset inherited the style for free instead of needing a style decision.\u003C\u002Fp>\n\u003Cp>The unexpected benefit: directing art and code in the same head removes a whole category of iteration. When a tile read poorly in-engine, the painter already knew exactly which layer to fix.\u003C\u002Fp>\n\u003Ch2>Working with a collaborator while building\u003C\u002Fh2>\n\u003Cp>Nicholas owned sound, voice, and half the narrative, and consulted on design. The handoff discipline that worked: I kept a single source-of-truth design doc, and his deliverables plugged into systems that already had placeholder hooks (audio events, dialogue beats). What I’d improve: I gave him lore context in bursts instead of up front, which cost us a rewrite on the late-game text.\u003C\u002Fp>\n\u003Ch2>Lessons that carried forward\u003C\u002Fh2>\n\u003Cp>Lock the mechanical core in week one. The timing wheels were the riskiest element, and they were prototyped and felt right before any content was built around them; if they had failed, the game would have become something else early instead of late. Scope the art to the camera, not to ambition. And ship to more than one storefront, because the jam page and Wavedash brought different players and different feedback.\u003C\u002Fp>\n\u003Cp>Two months later I applied all three of those lessons in a 63-hour Godot jam. That one is covered in \u003Ca href=\"\u002Fnotes\u002Fi-wont-be-abducted-postmortem\">its own postmortem\u003C\u002Fa>.\u003C\u002Fp>\n","2026-06-12T01:48:30.298Z",1781228922396]