[{"data":1,"prerenderedAt":559},["ShallowReactive",2],{"portfolio-general":3,"portfolio-game-design":341},{"variant":4,"profile":31,"experience":58,"projects":139,"skills":197,"writings":257,"generatedAt":340},{"key":5,"label":6,"headline":7,"tagline":8,"heroHeading":9,"heroProject":10,"heroAccent":11,"heroSub":12,"accent":13,"sectionOrder":14,"featuredTags":21,"projectGroups":22,"projectsHeading":26,"resumeUrl":27,"ctaPrimary":28,"availability":29,"aboutHtml":30},"general","Software","Software Engineer","Software engineer in Orlando. Web applications, developer tools, and games.","Hi, I'm Christian.","formship",null,"I'm a software engineer in Orlando. I build web applications, developer tools, and games.","accent-1",[15,16,17,18,19,20],"hero","projects","experience","about","writings","skills",[],[23],{"type":24,"label":25},"app","Selected work","Selected projects","\u002Fresume\u002FChristian-DAlbano-Resume.pdf","Explore projects","Open to software and gameplay engineering roles, Orlando or remote","\u003Cp>My work spans web applications, integrations, and data pipelines. At Mouseflow, I’ve built analytics interfaces and integrations. At Cencora, I build Python tools for production data workflows and maintain their deployment pipelines.\u003C\u002Fp>\n\u003Cp>Outside work, I build my own products and make games. Formship helps coaches put together learning programs, and HasteCV is a resume builder I use myself. Making games gives me room to draw, experiment with gameplay, and see how people respond to what I’ve made.\u003C\u002Fp>\n",{"name":32,"headline":7,"location":33,"email":34,"site":35,"socials":36,"bioHtml":57},"Christian D'Albano","Orlando, FL","chrisdalbano12@gmail.com","chrisdalbano.com",[37,41,45,49,53],{"label":38,"url":39,"icon":40},"GitHub","https:\u002F\u002Fgithub.com\u002Fchrisdalbano","github",{"label":42,"url":43,"icon":44},"LinkedIn","https:\u002F\u002Flinkedin.com\u002Fin\u002Fchrisdalb","linkedin",{"label":46,"url":47,"icon":48},"itch.io","https:\u002F\u002Fchrisdbgames.itch.io","itch",{"label":50,"url":51,"icon":52},"Wavedash","https:\u002F\u002Fwavedash.com\u002Fchrisdalbano12","waves",{"label":54,"url":55,"icon":56},"Email","mailto:chrisdalbano12@gmail.com","mail","\u003Cp>Software engineer in Orlando. I build web applications, developer tools, and games.\u003C\u002Fp>\n",[59,80,105,121],{"slug":60,"title":61,"org":62,"start":63,"end":64,"location":33,"tags":65,"variants":73,"order":74,"bullets":75},"cencora","Senior Data Operations Engineer","Cencora (Intrinsiq)","2025-07","present",[66,67,68,69,70,71,72],"python","sql-server","azure-databricks","azure-devops","ci-cd","docker","data-quality",[5],1,[76,77,78,79],"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":81,"title":7,"org":82,"start":83,"end":64,"location":84,"tags":85,"variants":98,"order":99,"bullets":100},"mouseflow","Mouseflow","2023-08","Remote",[86,87,88,89,90,91,92,93,94,95,96,97],"vue","blazor","javascript","chart-js","web-sdk","integrations","shopify","wordpress","optimizely","salesforce","agentic-ai","mcp",[5],2,[101,102,103,104],"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":106,"title":107,"org":108,"start":109,"end":110,"location":84,"tags":111,"variants":115,"order":116,"bullets":117},"aizhak-coffee","Full-Stack Developer (Contract)","Aizhak Coffee","2023-09","2024-01",[86,112,113,71,114],"typescript","firebase","ecommerce",[5],3,[118,119,120],"Built a real-time ecommerce storefront in Vue 3 and TypeScript on Firebase, from cart to live 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":122,"title":123,"org":124,"start":125,"end":83,"location":126,"tags":127,"variants":133,"order":134,"bullets":135},"baptist-health","Junior Frontend Developer","Baptist Health South Florida","2022-06","Miami, FL",[128,112,129,130,131,132],"react","java-spring-boot","accessibility","wcag","healthcare",[5],4,[136,137,138],"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.",[140,156,171,184],{"slug":10,"name":141,"blurb":142,"contribution":143,"url":144,"repo":11,"type":24,"tags":145,"variants":150,"featured":152,"order":74,"cover":153,"wavedash":11,"engine":11,"jam":11,"media":154,"descriptionHtml":155},"Formship","A platform for coaches to create learning programs, with editable content blocks and AI-generated quizzes.","Product design and full-stack development","https:\u002F\u002Fformship.io",[146,147,96,148,86,66,149],"saas","ai","ai-orchestration","education",[5,151],"game-design",true,"\u002Fshots\u002Fformship.webp",[],"\u003Cp>Formship helps coaches turn their frameworks into structured learning programs. I designed and built the product myself in Vue and Python.\u003C\u002Fp>\n\u003Ch2>Generating content coaches can use\u003C\u002Fh2>\n\u003Cp>Content generation runs in steps, with each step producing structured data for the next. That gives the app quizzes it can render and score, and gives coaches content they can edit before sharing it with learners.\u003C\u002Fp>\n\u003Ch2>Building around content blocks\u003C\u002Fh2>\n\u003Cp>Programs are made from modular blocks. Coaches can rearrange them, the generation pipeline can create them, and the analytics layer tracks learner progress against them. Keeping that structure consistent made it easier to connect authoring, generation, and learning in the same product.\u003C\u002Fp>\n",{"slug":157,"name":158,"blurb":159,"contribution":160,"url":161,"repo":11,"type":24,"tags":162,"variants":167,"featured":152,"order":99,"cover":168,"wavedash":11,"engine":11,"jam":11,"media":169,"descriptionHtml":170},"buildvalue","BuildValue","Compare League of Legends items and full builds by their gold efficiency, using data from Riot's API.","Full-stack development and item-data pipeline","https:\u002F\u002Fbuildvalue.chrisdalbano.com\u002F",[163,164,86,165,66,166,89],"league-of-legends","full-stack","fastapi","data-pipeline",[5,151],"\u002Fshots\u002Fbuildvalue.webp",[],"\u003Cp>BuildValue helps League of Legends players compare the stats they get for their gold. It calculates item efficiency from the prices of basic stats and lets players compare complete builds.\u003C\u002Fp>\n\u003Cp>The numbers are a starting point: active abilities, passives, and the timing of a purchase also affect an item’s value. The interface makes the stat comparison available while leaving room for that judgment.\u003C\u002Fp>\n\u003Cp>I built it with Vue and Chart.js, backed by FastAPI and MongoDB. A scheduled Python pipeline refreshes the item data from Riot’s DDragon API.\u003C\u002Fp>\n",{"slug":172,"name":173,"blurb":174,"contribution":175,"url":176,"repo":11,"type":24,"tags":177,"variants":180,"featured":152,"order":99,"cover":181,"wavedash":11,"engine":11,"jam":11,"media":182,"descriptionHtml":183},"hastecv","HasteCV","A resume and cover-letter builder with rich-text editing, PDF exports, and an MCP server for AI clients.","Product design, full-stack development, and MCP API","https:\u002F\u002Fhastecv.com",[146,147,97,164,178,86,179],"django","tiptap",[5,151],"\u002Fshots\u002Fhastecv.webp",[],"\u003Cp>HasteCV is a resume and cover-letter builder I designed and built myself. It uses Django for the API and Vue with Tiptap for editing. Background jobs handle generation and exports.\u003C\u002Fp>\n\u003Ch2>Documents that people and tools can edit\u003C\u002Fh2>\n\u003Cp>Each document has structured fields for templates and exports, alongside a rich-text body for editing. Through the MCP server, AI clients can read and edit documents or export them as PDFs. It also exposes the document schemas so clients can check the required fields before writing.\u003C\u002Fp>\n\u003Cp>The API returns readable errors, and archiving is the default removal action. Those choices make mistakes easier to understand and recover from.\u003C\u002Fp>\n\u003Ch2>Using it myself\u003C\u002Fh2>\n\u003Cp>I use the server in my own application workflow to prepare documents and export PDFs. That gives me a regular way to test whether the editing and export tools work together as intended.\u003C\u002Fp>\n",{"slug":185,"name":108,"blurb":186,"contribution":187,"url":188,"repo":11,"type":24,"tags":189,"variants":192,"featured":193,"order":116,"cover":194,"wavedash":11,"engine":11,"jam":11,"media":195,"descriptionHtml":196},"aizhak-ecommerce","A coffee storefront with a shopping cart, live inventory, and online ordering.","Storefront development and launch","https:\u002F\u002Faizhak.com",[114,86,190,113,191],"node","real-time",[5],false,"\u002Fshots\u002Faizhak-ecommerce.webp",[],"\u003Cp>I led development of the Aizhak Coffee storefront and shipped its first version. The Vue frontend connects to Node services and Firebase for inventory and orders. I worked from Figma designs to build the shopping and checkout experience.\u003C\u002Fp>\n",[198,207,217,229,240,249],{"label":199,"items":200,"variants":206},"Languages",[201,202,203,204,205],"TypeScript","JavaScript","Python","SQL","GDScript",[5,151],{"label":208,"items":209,"variants":216},"Frontend",[210,211,212,213,214,215],"Vue 3","React","Blazor","Tailwind","Chart.js","Tiptap",[5,151],{"label":218,"items":219,"variants":228},"Backend & Data",[220,221,222,223,224,225,226,227],"Node.js","Django","DRF","Flask","Java Spring Boot","SQL Server","Azure Databricks","Firebase",[5],{"label":230,"items":231,"variants":239},"AI & Agentic",[232,233,234,235,236,237,238],"Claude Code","Claude API","Cursor","MCP","Multi-agent orchestration","Prompt engineering","Structured output",[5,151],{"label":241,"items":242,"variants":248},"Game Dev",[243,244,205,245,246,247],"Phaser 3","Godot 4","Krita","Hand-painted art","Data-driven design",[5,151],{"label":250,"items":251,"variants":256},"Infra & Ops",[252,253,254,255],"Docker","Azure DevOps CI\u002FCD","Git","Render",[5],[258,271,285,296,310,321,329],{"slug":259,"title":260,"dek":261,"date":262,"project":263,"tags":264,"variants":268,"order":269,"readingMinutes":134,"contentHtml":270},"i-wont-be-abducted-after-the-jam","I Won't Be Abducted: fixing what players couldn't read","A broken placement flow, a boss with bad instructions, and an economy that gave away too much. What changed after the jam.","2026-09-07","i-wont-be-abducted",[265,266,151,267],"postmortem","godot","usability",[5,151],0,"\u003Cp>The crafting menu opened. The tool went into your pocket. Then you tried to put it on the board, and nothing happened.\u003C\u002Fp>\n\u003Cp>That was one of the problems in the updates after \u003Cem>I Won’t Be Abducted\u003C\u002Fem> shipped. Others were less obvious: crafting worked but gave you far too much, or the boss had a weakness but did a poor job of showing it. Adding more content had exposed how much the game expected you to figure out while aliens were hitting you.\u003C\u002Fp>\n\u003Cp>The \u003Ca href=\"\u002Fnotes\u002Fi-wont-be-abducted-postmortem?variant=game-design\">original jam postmortem\u003C\u002Fa> covers the weekend build and its cuts. This is about the revisions afterward. I worked with Codex on implementation and debugging, with repeated playtest feedback deciding what needed another pass. The game now runs across four nights, with larger rooms later in the run. Getting those extra systems to make sense took more work than adding them.\u003C\u002Fp>\n\u003Ch2>A passing test, a broken click\u003C\u002Fh2>\n\u003Cp>The placement bug was particularly annoying because the placement logic had been tested. Calling that logic directly worked. Clicking through the actual interface did not.\u003C\u002Fp>\n\u003Cp>Decorative UI layers were swallowing pointer input before it reached the board. The test skipped the exact part that was broken. The regression check had to exercise the pointer event, not just ask the placement function whether a cell was valid.\u003C\u002Fp>\n\u003Cp>There was a separate design problem underneath it. Picking a recipe, finding its pocket, and choosing a cell demanded too much attention during combat. Crafting and placement now pause the fight. Invalid cells leave the tool in your pocket, so a blocked placement does not spend an item or strand you in an unfinished action.\u003C\u002Fp>\n\u003Cfigure class=\"prose-media\">\n  \u003Cimg src=\"\u002Fshots\u002Fi-wont-be-abducted\u002Fworkbench-2026-09.webp\" alt=\"The workbench displays three illustrated recipes with coin costs above the inventory pockets.\" loading=\"lazy\" decoding=\"async\" width=\"1280\" height=\"720\">\n\u003C\u002Ffigure>\n\u003Cp>The workbench needed to answer three things together: what can I make, what does it cost, and where does it go? Coin and item icons help, but they only matter if the interaction underneath them is reliable.\u003C\u002Fp>\n\u003Ch2>The boss needed instructions on the board\u003C\u002Fh2>\n\u003Cp>The final boss went through two opposite problems. Repeated kicks could end the encounter almost immediately. Adding a guard and attack windows stopped that, but then the opening was hard to understand.\u003C\u002Fp>\n\u003Cp>The current cues sit above the boss: \u003Cstrong>WAIT\u003C\u002Fstrong>, \u003Cstrong>DODGE THIS ROW\u003C\u002Fstrong>, and \u003Cstrong>KICK NOW\u003C\u002Fstrong>. The threatened row is marked before the slam. During the opening, free adjacent cells light up with lines pointing toward Dad, and a small countdown shows how long you have. The boss accepts one hit per opening.\u003C\u002Fp>\n\u003Cfigure class=\"prose-media\">\n  \u003Cimg src=\"\u002Fshots\u002Fi-wont-be-abducted\u002Fopening-2026-09.webp\" alt=\"The boss's KICK NOW cue, countdown, and highlighted nearby cells show when and where to attack.\" loading=\"lazy\" decoding=\"async\" width=\"1280\" height=\"720\">\n\u003C\u002Ffigure>\n\u003Cp>The useful change was putting the instruction where you are already looking. A paragraph in a popup cannot help much when you are trying to avoid a row attack and line up a kick.\u003C\u002Fp>\n\u003Ch2>Crafting was practically free\u003C\u002Fh2>\n\u003Cp>Cheap tools, frequent coin drops, and money from tool-assisted kills fed each other. The decoy was especially hard to pass up: distracting enemies bought time to do almost everything else.\u003C\u002Fp>\n\u003Cp>Tools now cost six coins instead of three. Free coin drops are farther apart, and kills caused by bowling tools into enemies no longer refund money. Retrying a night restores its starting wallet and pockets, so failed attempts cannot become a way to accumulate currency.\u003C\u002Fp>\n\u003Cp>The items also needed clearer reasons to exist. The can stuns, the box blocks and can protect a neighboring cell from the boss’s slam, and the decoy draws enemies away but only survives a few bites. Those roles give me something concrete to tune. If distraction still wins every decision, adding another recipe will not solve it.\u003C\u002Fp>\n\u003Ch2>Keep the jokes out of the player’s way\u003C\u002Fh2>\n\u003Cp>Enemy speech bubbles fit a game about a kid turning his bedroom into an alien siege. They also compete with instructions. Only one brief bubble appears at a time, with space between them, and chatter clears before the boss’s attack warning.\u003C\u002Fp>\n\u003Cp>The visual changes follow the same restraint: faint paper grain, soft edges, and a little dust in the lamplight. The new enemy’s headgear needed to look like part of the character rather than a separate icon stuck on top. The loss image now shows a toppled toy. These replacement illustrations used AI assistance with my existing artwork as reference; the original hand-drawn pieces still set the style.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Ending spoilers:\u003C\u002Fstrong> letting Dad in previously dropped you back at the menu too abruptly. There is now a short conversation and credits, giving the joke time to land: the invasion has interrupted lunch, and your defense system is a pencil box. After all that effort keeping him out, the game needed a moment with him in the room.\u003C\u002Fp>\n\u003Ch2>What I still need to watch\u003C\u002Fh2>\n\u003Cp>Automated boss runs help check that the encounter is beatable. They cannot tell me whether someone understands the first opening, notices a coin cost, or finds an enemy’s line funny while dodging it.\u003C\u002Fp>\n\u003Cp>That is the next useful playtest: watch a fresh player craft once and fight Dad without explaining either system. If I have to lean over and tell them what to press, there is still work to do.\u003C\u002Fp>\n\u003Cp>\u003Ca href=\"https:\u002F\u002Fwavedash.com\u002Fgames\u002Fi-won-t-be-abducted\">Play I Won’t Be Abducted on Wavedash\u003C\u002Fa>.\u003C\u002Fp>\n",{"slug":272,"title":273,"dek":274,"date":275,"project":276,"tags":277,"variants":282,"order":74,"readingMinutes":283,"contentHtml":284},"undead-signal-postmortem","Undead Signal: five days of rules before a line of code","How I developed the card rules and tested the balance with a bot running 300 seeded runs per configuration.","2026-07-27","undead-signal",[265,278,279,151,280,281],"unity","jam","balancing","card-game",[5,151],5,"\u003Cp>\u003Cem>Undead Signal\u003C\u002Fem> was built for a Wavedash game jam, theme: \u003Cstrong>Bad Connection\u003C\u002Fstrong>. It’s an anti-poker zombie roguelike, live on Wavedash as a WebGL build with platform achievements, leaderboards, and cloud saves. My fourth shipped game and my second in Unity, after \u003Cem>Spare Knight\u003C\u002Fem> (Phaser 3), \u003Cem>I Won’t Be Abducted\u003C\u002Fem> (Godot), and \u003Cem>ThunderBashers\u003C\u002Fem> (Unity).\u003C\u002Fp>\n\u003Cp>This one was a design-first project on purpose. The previous jams taught me to ship; this jam I wanted to prove I could make the \u003Cem>rules\u003C\u002Fem> the interesting part. So I spent roughly the first five days entirely on rules, in a design vault, before any build existed.\u003C\u002Fp>\n\u003Ch2>The pitch\u003C\u002Fh2>\n\u003Cp>You’re a radio engineer restoring the signal in overrun locations. The dead are networked by frequency: a lone zombie is just a body, but when they align into a lineup, a poker hand, they form a coordinated resonance that overwhelms you at the location’s exit. Each act deals more zombies onto the table; whatever you don’t remove survives to form the horde’s best five-card hand, and its threat hits your health.\u003C\u002Fp>\n\u003Cp>Threat is one equation: the sum of the five zombie ranks times a poker-hand multiplier. That gives you exactly two levers. Drop the sum by killing or shrinking the big ranks, or break the structure by scattering a pair and splitting a straight. Breaking their hand is fixing the Bad Connection is restoring the signal. The theme and the core mechanic are the same sentence, which is the thing I’m proudest of in the whole design.\u003C\u002Fp>\n\u003Ch2>Killing three drafts\u003C\u002Fh2>\n\u003Cp>The honest history is that the first drafts were derivative. Draft one was a straight zombie card battler. Draft two leaned hard into Balatro: build your hand, stack multipliers, score big. On paper it functioned, and on paper it was boring, because Balatro already exists and a worse Balatro with zombies isn’t a game, it’s a reskin.\u003C\u002Fp>\n\u003Cp>The unlock was inverting it. In Balatro \u003Cem>you\u003C\u002Fem> assemble the scoring hand. What if the \u003Cem>enemy\u003C\u002Fem> assembles it, and every verb you have subtracts from their hand instead of adding to yours? Suddenly the power fantasy flips from building a combo to dismantling one, the poker knowledge players already carry becomes threat literacy instead of a scoring manual, and the zombie fiction snaps into place: the horde is the hand.\u003C\u002Fp>\n\u003Cp>There was a third draft between those, a corridor-crawl hybrid, and it died too. I kept every dead draft in an archive folder instead of deleting it, and I’d recommend that to anyone doing solo design work: the graveyard is what proves the final rule set was chosen, not stumbled into. Five days of this felt irresponsible mid-jam. It was the best production decision in the project, because the build phase never had to stop and redesign anything.\u003C\u002Fp>\n\u003Ch2>The rule that gave back\u003C\u002Fh2>\n\u003Cp>My favorite moment in systems design is when one rule starts producing depth you didn’t put there. In Undead Signal, a zombie’s rank is also its health. Hit it for less than its rank and it doesn’t die, it \u003Cem>shrinks\u003C\u002Fem>: a Mutated 9 hit for 3 becomes a 6.\u003C\u002Fp>\n\u003Cp>Which means shrinking can backfire. If there are two 6s already on the table, you just completed three of a kind and \u003Cem>raised\u003C\u002Fem> the threat you were trying to lower. That interaction was never designed. It fell out of “rank is HP” colliding with “poker hands multiply,” and the first time it happened in testing I knew the ruleset was actually alive. We kept it, we tell the player to read the board, and it’s the single most quoted rule when people describe the game back to me.\u003C\u002Fp>\n\u003Ch2>Balancing with a bot\u003C\u002Fh2>\n\u003Cp>The other half of the jam was learning what “balancing on testing” means when you’re solo and the game is a twenty-five minute roguelike run. You cannot feel-test your way to a win rate. So the game got a balance lab: a headless harness where a threat-aware bot plays 300 seeded runs per configuration and prints the funnel, win rate, clear rate per location, how people die.\u003C\u002Fp>\n\u003Cp>The first measurement was humbling. Win rate: 19 percent. And if the bot used the game’s own loot-digging verb naively, it \u003Cem>dropped\u003C\u002Fem> to 5 percent, meaning one of my core verbs was a trap. The numbers found root causes feel never would have: whole runs were passing without a usable weapon appearing before the first exit, and the average first-location exit damage was 60 against 100 health. The deadliest room in the game was the front door, before the player owned a single tool.\u003C\u002Fp>\n\u003Cp>Two dial changes later (gear density up, plus a rule guaranteeing a weapon in the first few cards), the win rate hit 31 percent against a 30 to 40 target, and deaths migrated to the late-game wall where they belong. Then playtesting produced the opposite complaint: hands full at all times, loot stopped mattering. So weapons got durability, failed kills got bite-back, and the lab re-tuned the whole thing to 37 percent with weapons actually cycling. Every one of those numbers is a logged report I can re-run.\u003C\u002Fp>\n\u003Cp>The meta-lesson: the lab also measured the dial I \u003Cem>thought\u003C\u002Fem> I understood. Gear density turned out to be two dials in one, because more gear also means fewer zombies in the deck, and the second effect was the stronger one. I would have tuned that dial wrong by hand forever.\u003C\u002Fp>\n\u003Ch2>Shipping on a platform\u003C\u002Fh2>\n\u003Cp>This was also my first release on Wavedash rather than a plain web upload, and the platform SDK work was its own small discipline: ten achievements, two leaderboards, player identity on the title screen, cloud saves for your settings and lifetime stats. The game had no native score, so the leaderboards run on a derived composite (progress first, kills as the tiebreaker) that had to be designed like any other rule. Launch day itself was a rhythm of building, pushing, and patching against live reports. Five updates went out on day one. That felt closer to live ops than to jam submission, and I want more of it.\u003C\u002Fp>\n\u003Ch2>What I’d keep\u003C\u002Fh2>\n\u003Cp>Spend the design days before the build days; the archive of dead drafts is the receipt. When a rule produces depth you didn’t author, that’s the ruleset telling you it works. And balance is a measurement problem: a bot that plays 300 runs will find the truth about your economy faster than a hundred feel passes, and it leaves you numbers you can defend later.\u003C\u002Fp>\n",{"slug":286,"title":287,"dek":288,"date":289,"project":290,"tags":291,"variants":294,"order":74,"readingMinutes":283,"contentHtml":295},"thunderbashers-postmortem","ThunderBashers: shipping a Unity brawler in five days","Switching to Unity during a jam, developing the spin mechanic, and making the action easier to read.","2026-06-27","thunderbashers",[265,278,279,292,293],"game-feel","ai-design",[5,151],"\u003Cp>\u003Cem>ThunderBashers\u003C\u002Fem> was built in about five days for the Very Serious Juniper Dev Game Jam, theme: \u003Cstrong>Spin To Win\u003C\u002Fstrong>. Solo on design, direction, art, and audio. It went live on \u003Ca href=\"http:\u002F\u002Fitch.io\">itch.io\u003C\u002Fa> as a WebGL build with a Windows fallback. It’s my third shipped jam game and my first in Unity, after \u003Cem>Spare Knight\u003C\u002Fem> (Phaser 3) and \u003Cem>I Won’t Be Abducted\u003C\u002Fem> (Godot).\u003C\u002Fp>\n\u003Ch2>The pitch\u003C\u002Fh2>\n\u003Cp>Robots in a circular pit bash a storm-charged ball at each other. Spinning is your only verb: it strikes the ball, it parries an incoming hit, and it shoves a rival in close. Get touched by the ball while you’re \u003Cem>not\u003C\u002Fem> spinning and you’re eliminated. Last bot standing wins. The theme wasn’t a layer on top of the game: spinning \u003Cem>is\u003C\u002Fem> the offense, the defense, and the win condition.\u003C\u002Fp>\n\u003Cp>I wrapped it in a deadpan corporate framing: the THUNDERBASHERS PRO LEAGUE, a sanctioned robot-combat circuit whose Standards Committee insists the rules are fair. The tone was free points on a jam that rewards “serious,” and it gave every bot a reason to exist.\u003C\u002Fp>\n\u003Cp>The whole game was there in the first napkin sketch: the robots, the spin, the ball. Most of the five days was the distance between this and a build that \u003Cem>felt\u003C\u002Fem> like it.\u003C\u002Fp>\n\u003Cfigure class=\"prose-media\">\n  \u003Cimg src=\"\u002Fshots\u002Fthunderbashers\u002Fconcept-characters.webp\" alt=\"Early concept sketch: the robots, the spin, and the ball\" loading=\"lazy\" decoding=\"async\" width=\"700\" height=\"600\">\n\u003C\u002Ffigure>\n\u003Ch2>The pivot\u003C\u002Fh2>\n\u003Cp>I started the jam in Unreal. Half a day in, it was clear the engine was spending my time instead of saving it: no clean WebGL path, slow iteration, and every hour in the editor was an hour not spent on art. So I pivoted to Unity with about five days left.\u003C\u002Fp>\n\u003Cp>That was an eyes-open trade. It forfeited the “Best Unreal Engine Game” prize. But Unity ships WebGL, which means the game plays instantly in a browser on the itch page (more plays, more ratings), and it opened a “Best Web Game” target instead. The Unreal start isn’t deleted; it’s parked for a hands-on Unreal project later. Switching engines mid-jam sounds reckless, but the real risk was finishing nothing, and the pivot was the move that protected shipping.\u003C\u002Fp>\n\u003Ch2>The mechanic that fought back\u003C\u002Fh2>\n\u003Cp>The core rule, the ball kills you if it catches you not spinning, should have been the easy part. It was the hardest thing in the game. It felt wrong for days, and the temptation was to keep re-tuning the rule.\u003C\u002Fp>\n\u003Cp>The control scheme was simple from the start (move, mouse to aim, space to spin), but “simple to operate” and “feels good to play” turned out to be very different problems.\u003C\u002Fp>\n\u003Cfigure class=\"prose-media\">\n  \u003Cimg src=\"\u002Fshots\u002Fthunderbashers\u002Fconcept-controls.webp\" alt=\"Early controls sketch: move, mouse to aim, space to spin\" loading=\"lazy\" decoding=\"async\" width=\"400\" height=\"400\">\n\u003C\u002Ffigure>\n\u003Cp>When I actually root-caused it instead of symptom-chasing, “the mechanic is broken” turned out to be about five separate problems stacked on top of each other. Any touch killing you made parrying feel like an alien timing-puzzle bolted onto a brawl. Over-correcting to a speed gate made enemies basically never die. The ball kept parking in a dead-zone speed band that read as neither safe nor lethal. One value was gating both a bot’s offense and its defense, so tuning a bot to be beatable also made it stop fighting. And the ball would \u003Cem>look\u003C\u002Fem> red-hot while the kill-check briefly read it as safe after a wall bounce.\u003C\u002Fp>\n\u003Cp>Each “fix that didn’t fix it” was fixing a real bug, just not the one in front of the player that minute.\u003C\u002Fp>\n\u003Ch2>Readability is a mechanic\u003C\u002Fh2>\n\u003Cp>The fix that finally made the core \u003Cem>feel\u003C\u002Fem> right wasn’t a rules change at all. It was a \u003Cstrong>progressive heat-tint\u003C\u002Fstrong> on the ball: cool blue when it’s slow and safe, ramping to red-hot and crackling when it’s fast enough to kill. Suddenly “a slow ball doesn’t hurt you” reads as intuitive instead of buggy, because you can \u003Cem>see\u003C\u002Fem> the threat state. The single biggest feel win in the whole game came from legibility, not from changing what the game does.\u003C\u002Fp>\n\u003Cp>That’s the lesson I’m keeping: when a mechanic feels wrong, it’s often a readability problem wearing a rules problem’s clothes. Fix what the player can see before you change what the game does.\u003C\u002Fp>\n\u003Cfigure class=\"prose-media\">\n  \u003Cimg src=\"\u002Fshots\u002Fthunderbashers\u002Fheat-ball.webp\" alt=\"The thunder-ball glowing red-hot: the danger telegraph\" loading=\"lazy\" decoding=\"async\" width=\"1343\" height=\"635\">\n\u003C\u002Ffigure>\n\u003Ch2>Bots with temperaments\u003C\u002Fh2>\n\u003Cp>It’s a solo game, so the bots \u003Cem>are\u003C\u002Fem> the game. I didn’t want a difficulty slider; I wanted opponents that felt like they had personalities. So every bot is a data profile (reaction time, aim error, spin skill, how far ahead it looks for an incoming ball, how readily it panics), and those profiles fall into five archetypes from a newbie that mistimes and feeds you confidence kills to a veteran you can only beat with wall ricochets and baited parries.\u003C\u002Fp>\n\u003Cp>Two bugs taught me the most. Bots crowded the ball and multi-killed in one pass, looking dumb; the fix was electing a single “committed striker” each frame so they approach one at a time. And splitting offense from defense (always strike, parry by skill) was the unlock that let a bot be tuned beatable without going passive. The campaign then just distributes those archetypes across three arenas to build the curve.\u003C\u002Fp>\n\u003Ch2>What I’d keep\u003C\u002Fh2>\n\u003Cp>Root-cause, don’t symptom-chase. The shipped build is the only build that matters, so build to your real target early. And readability is a mechanic: the most legible version of a rule usually feels like the best-designed one. A small, finished, legible game beats a big unfinished one, every time the clock is running.\n&lt;\u002Fcontent&gt;\u003C\u002Fp>\n",{"slug":297,"title":298,"dek":299,"date":300,"project":11,"tags":301,"variants":308,"order":74,"readingMinutes":116,"contentHtml":309},"building-this-site","This site is the case study","The design tokens and motion system behind the previous version of this portfolio.","2026-06-11",[302,303,304,305,306,307],"design-systems","nuxt","tokens","motion","ai-tooling","process",[5,151],"\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":311,"title":312,"dek":313,"date":314,"project":263,"tags":315,"variants":319,"order":74,"readingMinutes":116,"contentHtml":320},"tuning-fear-by-numbers","Tuning fear by numbers","How I adjusted enemy behavior and difficulty in Godot without changing the player's abilities.","2026-06-09",[316,266,317,318],"systems-design","difficulty","data-driven",[5,151],"\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":322,"title":323,"dek":324,"date":314,"project":263,"tags":325,"variants":327,"order":99,"readingMinutes":134,"contentHtml":328},"i-wont-be-abducted-postmortem","I Won't Be Abducted: a 63-hour postmortem","Building a tabletop-defense game over a weekend, cutting its economy, and setting up the ending.",[265,266,279,326],"narrative-design",[5,151],"\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\u003Cp>\u003Cem>September update: this article records the original jam build. \u003Ca href=\"\u002Fnotes\u002Fi-wont-be-abducted-after-the-jam?variant=game-design\">The follow-up covers the expanded game, crafting fixes, and boss readability\u003C\u002Fa>.\u003C\u002Fem>\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":330,"title":331,"dek":332,"date":314,"project":333,"tags":334,"variants":338,"order":116,"readingMinutes":116,"contentHtml":339},"spare-knight-postmortem","Spare Knight: 13 days, no dice","The combat rules, timing reactions, and hand-painted artwork behind my Phaser roguelike.","spare-knight",[265,335,279,336,337],"phaser","combat-design","art",[5,151],"\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-09-07T06:50:05.888Z",{"variant":342,"profile":361,"experience":368,"projects":369,"skills":513,"writings":526,"generatedAt":340},{"key":151,"label":343,"headline":344,"tagline":345,"heroHeading":9,"heroProject":276,"heroAccent":11,"heroSub":346,"accent":347,"sectionOrder":348,"featuredTags":349,"projectGroups":351,"projectsHeading":356,"resumeUrl":357,"ctaPrimary":358,"availability":359,"aboutHtml":360},"Games","Gameplay Engineer","Gameplay engineer in Orlando. Games made in Unity, Phaser, and Godot.","I'm a software engineer in Orlando who makes games. Here you'll find what I've built and how I approached the gameplay.","accent-2",[15,16,19,18,20],[350],"game-dev",[352,354],{"type":353,"label":343},"game",{"type":24,"label":355},"Tools & AI","Selected games","\u002Fresume\u002FChristian-DAlbano-Game-Design-Resume.pdf","Explore games","Open to gameplay and tools engineering roles, Orlando or remote","\u003Cp>I started making games alongside my software engineering work. I’ve released games in Unity, Phaser, and Godot. I handle the programming and much of the artwork myself. Game jams have given me a reason to try different engines and take an idea through to something people can play.\u003C\u002Fp>\n\u003Cp>I’m interested in how game rules interact and how players learn them. In Undead Signal, that meant building a bot to test the balance. In Spare Knight, it meant revisiting the interface after players found it hard to read. I’m looking for gameplay or tools engineering work where I can keep developing those skills.\u003C\u002Fp>\n",{"name":32,"headline":7,"location":33,"email":34,"site":35,"socials":362,"bioHtml":57},[363,364,365,366,367],{"label":38,"url":39,"icon":40},{"label":42,"url":43,"icon":44},{"label":46,"url":47,"icon":48},{"label":50,"url":51,"icon":52},{"label":54,"url":55,"icon":56},[],[370,374,404,408,412,450,483],{"slug":10,"name":141,"blurb":142,"contribution":143,"url":144,"repo":11,"type":24,"tags":371,"variants":372,"featured":152,"order":74,"cover":153,"wavedash":11,"engine":11,"jam":11,"media":373,"descriptionHtml":155},[146,147,96,148,86,66,149],[5,151],[],{"slug":276,"name":375,"blurb":376,"contribution":377,"url":378,"repo":11,"type":353,"tags":379,"variants":382,"featured":152,"order":74,"cover":383,"wavedash":11,"engine":384,"jam":385,"media":386,"descriptionHtml":403},"Undead Signal","A zombie card roguelike where the horde forms poker hands and you break them apart to survive.","Game design, programming, and artwork","https:\u002F\u002Fwavedash.com\u002Fgames\u002Fundead-signal",[350,278,380,281,381,279,316,280],"webgl","roguelike",[151],"\u002Fshots\u002Fundead-signal.webp","Unity 6","Wavedash game jam",[387,391,394,397,400],{"src":388,"alt":389,"kind":390},"\u002Fshots\u002Fundead-signal\u002Fcover.webp","Undead Signal cover: distressed bone lettering beside three ink-drawn zombies, a field radio and a broadcast tower","image",{"src":392,"alt":393,"kind":390},"\u002Fshots\u002Fundead-signal\u002Fhorde-chase.webp","A five-zombie Chase hand on the table with the threat equation reading sum 19 times 2 equals 38",{"src":395,"alt":396,"kind":390},"\u002Fshots\u002Fundead-signal\u002Fherald-exchange.webp","A Herald and a Skinny flank a survivor Engineer card at the Relay Tower, revolver gear ready below",{"src":398,"alt":399,"kind":390},"\u002Fshots\u002Fundead-signal\u002Fentrance-gear.webp","Act 1 at the Substation: two gear cards revealed on the dark table beside the CRT-styled sidebar",{"src":401,"alt":402,"kind":390},"\u002Fshots\u002Fundead-signal\u002Finspect-skinny.webp","Zoom-inspecting a rank-3 Skinny card while the hint bar explains rank, bite-back, and the Runner rule","\u003Cp>You play a radio engineer restoring the signal in locations overrun by zombies. The horde forms poker hands, and each hand determines the threat you’ll face when you leave. You can lower that threat by removing high cards or breaking the hand’s combinations.\u003C\u002Fp>\n\u003Cp>Searching for loot adds to the horde. Loud weapons draw more cards, equipment can break, and an attack that fails to kill leaves you open to a bite. Those rules make each action a decision about how much risk to take before moving on.\u003C\u002Fp>\n\u003Cp>I designed the rules before building the game in Unity, then tested its balance with an automated bot running 300 seeded runs per configuration. The game is available on Wavedash with achievements, leaderboards, and cloud saves. I also drew the zombie portraits.\u003C\u002Fp>\n",{"slug":157,"name":158,"blurb":159,"contribution":160,"url":161,"repo":11,"type":24,"tags":405,"variants":406,"featured":152,"order":99,"cover":168,"wavedash":11,"engine":11,"jam":11,"media":407,"descriptionHtml":170},[163,164,86,165,66,166,89],[5,151],[],{"slug":172,"name":173,"blurb":174,"contribution":175,"url":176,"repo":11,"type":24,"tags":409,"variants":410,"featured":152,"order":99,"cover":181,"wavedash":11,"engine":11,"jam":11,"media":411,"descriptionHtml":183},[146,147,97,164,178,86,179],[5,151],[],{"slug":290,"name":413,"blurb":414,"contribution":415,"url":416,"repo":11,"type":353,"tags":417,"variants":421,"featured":152,"order":99,"cover":422,"wavedash":11,"engine":384,"jam":423,"media":424,"descriptionHtml":449},"ThunderBashers","An arena brawler where spinning lets you strike and parry a storm-charged ball.","Game design, direction, programming, and the jam's hand-drawn artwork","https:\u002F\u002Fchrisdbgames.itch.io\u002Fthunderbashers",[350,278,380,418,279,419,293,420],"arena-brawler","hand-drawn","2.5d",[151],"\u002Fshots\u002Fthunderbashers.webp","Very Serious Juniper Dev Game Jam",[425,428,431,434,437,440,443,446],{"src":426,"alt":427,"kind":390},"\u002Fshots\u002Fthunderbashers\u002Fcover.webp","ThunderBashers key art: the broadcast-style league logo over a floodlit storm pit",{"src":429,"alt":430,"kind":390},"\u002Fshots\u002Fthunderbashers\u002Ffinal-menu.webp","Title screen: the league lockup, a VERY SERIOUS ROBOT COMBAT LEAGUE strapline, and the storm pit behind the menu",{"src":432,"alt":433,"kind":390},"\u002Fshots\u002Fthunderbashers\u002Ffinal-match.webp","A live match: three robots with league nameplates on a flat-shaded pit floor, the charged ball between them",{"src":435,"alt":436,"kind":390},"\u002Fshots\u002Fthunderbashers\u002Ffinal-dash.webp","Operator mid-dash with a lean and a streak, ability slots for Spin and Dash in the corner",{"src":438,"alt":439,"kind":390},"\u002Fshots\u002Fthunderbashers\u002Ffinal-loading.webp","Loading screen: the empty pit under floodlights with a deadpan league tip",{"src":441,"alt":442,"kind":390},"\u002Fshots\u002Fthunderbashers\u002Ffinal-champion.webp","Champion screen: a spotlit podium and a league certification card",{"src":444,"alt":445,"kind":390},"\u002Fshots\u002Fthunderbashers\u002Farena-diamond.webp","The jam build: hand-drawn robots circle the thunder-ball in The Diamond arena",{"src":447,"alt":448,"kind":390},"\u002Fshots\u002Fthunderbashers\u002Fheat-ball.webp","The jam build: the thunder-ball glowing red-hot, the danger telegraph that signals it's lethal","\u003Cp>Robots compete to be the last one standing in a circular arena. Spin to strike the ball or parry it; get hit while you’re exposed and you’re out.\u003C\u002Fp>\n\u003Cp>I built the game in Unity for a jam and drew the robots and arena by hand. A guided tutorial introduces the controls before a campaign of increasingly difficult arenas. After the jam I directed a staged visual overhaul: a league identity and logo, a UI kit, robots rebuilt as models rendered into the original sprite rig, and a single flat-shaded style for the whole match frame. The \u003Ca href=\"\u002Fnotes\u002Fthunderbashers-visual-overhaul\">overhaul write-up\u003C\u002Fa> covers the order and the reasoning.\u003C\u002Fp>\n\u003Cp>Each opponent has a profile controlling reaction time, aim, and when it panics. Tuning those profiles let me give the bots different personalities and adjust the difficulty without adding new combat rules.\u003C\u002Fp>\n",{"slug":333,"name":451,"blurb":452,"contribution":453,"url":454,"repo":455,"type":353,"tags":456,"variants":460,"featured":152,"order":116,"cover":461,"wavedash":11,"engine":243,"jam":462,"media":463,"descriptionHtml":482},"Spare Knight","A tactical roguelike with predictable attacks, timing-based reactions, and a world I painted by hand.","Game direction, programming, and artwork","https:\u002F\u002Fchrisdbgames.itch.io\u002Fspare-knight","https:\u002F\u002Fgithub.com\u002FChrisdalbano\u002FSpareKnight-webgame",[350,381,457,458,279,335,459],"tactical","isometric","hand-painted",[151],"\u002Fshots\u002Fspare-knight.webp","Gamedev.js Jam 2026",[464,467,470,473,476,479],{"src":465,"alt":466,"kind":390},"\u002Fshots\u002Fspare-knight\u002Fcover.webp","Spare Knight key art: the brush-lettered title logo with its gear over the hand-painted Act 1 countryside, a lone tree on the hill and ruins on the horizon",{"src":468,"alt":469,"kind":390},"\u002Fshots\u002Fspare-knight\u002Fcombat-act1.webp","Act 1 combat arena: hand-painted cornfield battleground on the isometric grid",{"src":471,"alt":472,"kind":390},"\u002Fshots\u002Fspare-knight\u002Fnodemap-act1.webp","Act 1 node map: branching run paths painted across the Bright Ruins countryside",{"src":474,"alt":475,"kind":390},"\u002Fshots\u002Fspare-knight\u002Fcombat-act2.webp","Act 2 combat arena: the cathedral approach",{"src":477,"alt":478,"kind":390},"\u002Fshots\u002Fspare-knight\u002Fcamp.webp","The camp scene between battles, warm and sunlit",{"src":480,"alt":481,"kind":390},"\u002Fshots\u002Fspare-knight\u002Fevent-wren.webp","Event illustration: Wren whispers to the knight","\u003Cp>An outdated knight protects a human in a world where machines have turned against organic life. Combat takes place on an isometric grid, with speed setting the turn order. Attacks have predictable outcomes, while timing-based reactions ask you to listen for a chime and choose when to strike.\u003C\u002Fp>\n\u003Cp>I wrote the game in Phaser and painted the tiles and characters in Krita. A collaborator handled sound and contributed to the narrative and design. After the jam, I rebuilt the interface with shared tooltips and text styles in response to players who found the original hard to read.\u003C\u002Fp>\n",{"slug":263,"name":484,"blurb":485,"contribution":486,"url":487,"repo":11,"type":353,"tags":488,"variants":491,"featured":152,"order":134,"cover":492,"wavedash":493,"engine":494,"jam":495,"media":496,"descriptionHtml":512},"I Won't Be Abducted","Defend a bedroom from an alien invasion in a tabletop game made in Godot.","Game design, programming, and papercraft artwork","https:\u002F\u002Fchrisdbgames.itch.io\u002Fi-wont-be-abducted",[350,266,489,279,419,490,318],"tabletop-defense","narrative",[151],"\u002Fshots\u002Fi-wont-be-abducted.webp","https:\u002F\u002Fwavedash.com\u002Fgames\u002Fi-won-t-be-abducted","Godot 4.6","Wavedash Spring Jam 26",[497,500,503,506,509],{"src":498,"alt":499,"kind":390},"\u002Fshots\u002Fi-wont-be-abducted\u002Fcover.webp","Key art: the boy stands his ground in the bedroom doorway",{"src":501,"alt":502,"kind":390},"\u002Fshots\u002Fi-wont-be-abducted\u002Froom-plain.webp","The bedroom board: a 3x5 tabletop diorama ready for night one",{"src":504,"alt":505,"kind":390},"\u002Fshots\u002Fi-wont-be-abducted\u002Fpreview-chars.webp","Hand-drawn papercraft standees: the boy and the alien pieces",{"src":507,"alt":508,"kind":390},"\u002Fshots\u002Fi-wont-be-abducted\u002Fintro-slide-1.webp","Intro cinematic slide: the farmhouse at dusk",{"src":510,"alt":511,"kind":390},"\u002Fshots\u002Fi-wont-be-abducted\u002Fgame-over.webp","Game-over illustration: a silhouette looms in the doorway","\u003Cp>A boy defends his bedroom from a nightly alien invasion. You charge and kick enemies, craft obstacles, and choose upgrades between nights. The ending pulls the camera back to reveal who’s moving the pieces.\u003C\u002Fp>\n\u003Cp>I designed and programmed the game in Godot and drew the papercraft artwork. Enemy behavior and difficulty are set through Godot resources, which let me adjust the encounters without rewriting the gameplay systems.\u003C\u002Fp>\n\u003Cp>The September update expands the game to four nights and revises crafting, boss cues, and the ending. \u003Ca href=\"\u002Fnotes\u002Fi-wont-be-abducted-after-the-jam?variant=game-design\">Read the follow-up postmortem\u003C\u002Fa> for the problems behind those changes and how I worked with Codex to address them.\u003C\u002Fp>\n",[514,517,520,523],{"label":199,"items":515,"variants":516},[201,202,203,204,205],[5,151],{"label":208,"items":518,"variants":519},[210,211,212,213,214,215],[5,151],{"label":230,"items":521,"variants":522},[232,233,234,235,236,237,238],[5,151],{"label":241,"items":524,"variants":525},[243,244,205,245,246,247],[5,151],[527,530,533,544,547,550,553,556],{"slug":259,"title":260,"dek":261,"date":262,"project":263,"tags":528,"variants":529,"order":269,"readingMinutes":134,"contentHtml":270},[265,266,151,267],[5,151],{"slug":272,"title":273,"dek":274,"date":275,"project":276,"tags":531,"variants":532,"order":74,"readingMinutes":283,"contentHtml":284},[265,278,279,151,280,281],[5,151],{"slug":534,"title":535,"dek":536,"date":537,"project":333,"tags":538,"variants":542,"order":74,"readingMinutes":116,"contentHtml":543},"spare-knight-ui-overhaul","Spare Knight: teaching the UI to explain itself","Responding to player feedback with clearer text and shared tooltips, alongside fixes for progression bugs.","2026-07-01",[539,540,335,541,336],"devlog","ui","readability",[151],"\u003Cp>\u003Cem>Spare Knight\u003C\u002Fem> shipped for Gamedev.js Jam 2026 with combat I was proud of and a UI I was not. The jam comments were kind but clear: the layout was hard to read, and a few players hit bugs that ended their runs. I replied “Game UI is not my forte, hopefully I’ll improve it in next builds” and then went and found out whether that was true. This update is the answer: a full UI overhaul, live now on itch and Wavedash.\u003C\u002Fp>\n\u003Ch2>Start with the font\u003C\u002Fh2>\n\u003Cp>The original build used IM Fell English everywhere. It looked like a medieval manuscript, which was the point, and it read like one too, which was the problem. Ability names, damage numbers, and tooltip text all paid the atmosphere tax. The overhaul swapped body text to Alegreya: still a warm serif that belongs in the world, but designed for reading. That one change did more for the game than any mechanic I tuned all jam.\u003C\u002Fp>\n\u003Ch2>A token system inside a game engine\u003C\u002Fh2>\n\u003Cp>The deeper fix was structural. Text styling was scattered across scenes as ad-hoc Phaser calls, so every fix had to be applied everywhere and drifted anyway. I rebuilt it the way I’d build a web app: a \u003Ccode>textStyle()\u003C\u002Fcode> helper with named type roles, PANEL chrome tokens for panel styling, and an atomic component kit (shared atoms, a real Modal) that every scene consumes. The first bug the migration caught was in the system itself: type role objects that never resolved. Once fixed, restyling all the non-combat scenes was a sweep, not a rewrite.\u003C\u002Fp>\n\u003Cp>If you build design systems for the web, this is the same discipline in a different renderer. Tokens are tokens.\u003C\u002Fp>\n\u003Ch2>Make the mechanics visible\u003C\u002Fh2>\n\u003Cp>The combat rules were always deterministic; the interface just kept that a secret. The readability pass made the game state legible on the grid itself:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Enemies are highlighted directly, with their threat range shown, instead of asking you to decode cell tints.\u003C\u002Fli>\n\u003Cli>Every status effect now renders on-grid, and its tooltip is derived from the actual mechanics data, so the text can’t drift from the rules.\u003C\u002Fli>\n\u003Cli>Ability buttons know why they’re disabled. Out-of-range abilities stay armable, and a reason tooltip tells you what’s wrong instead of just graying out.\u003C\u002Fli>\n\u003Cli>MP costs sit on the buttons as price tags, damage numbers are signed, and hits land with real collision and an impact bounce.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>The lesson repeats from my ThunderBashers postmortem: readability is a mechanic. Deterministic combat only feels fair when the player can see the determinism.\u003C\u002Fp>\n\u003Ch2>The soft-locks\u003C\u002Fh2>\n\u003Cp>Two players reported turns that never ended. Both traced to the reaction system: a timing wheel could wedge in a state that never resolved, and separately, killing the turn announcer tween mid-flight left the turn queue waiting forever. Both were root-caused and fixed, along with a combat input crash from a stale text pool and an invisible-cursor bug that the new painterly cursors replaced outright.\u003C\u002Fp>\n\u003Ch2>What carried\u003C\u002Fh2>\n\u003Cp>The whole overhaul landed as one feature branch, built module by module in seven passes: typography, tooltips, ability bar, statuses, inventory counts, panel chrome, scene migration. Fixes first, then polish (fullscreen mode, cursor sweep, a Salvage and Forge layout rebuild with aligned panels). Player comments picked the target; the token system made hitting it cheap. The next build gets content instead of apologies.\u003C\u002Fp>\n",{"slug":286,"title":287,"dek":288,"date":289,"project":290,"tags":545,"variants":546,"order":74,"readingMinutes":283,"contentHtml":295},[265,278,279,292,293],[5,151],{"slug":297,"title":298,"dek":299,"date":300,"project":11,"tags":548,"variants":549,"order":74,"readingMinutes":116,"contentHtml":309},[302,303,304,305,306,307],[5,151],{"slug":311,"title":312,"dek":313,"date":314,"project":263,"tags":551,"variants":552,"order":74,"readingMinutes":116,"contentHtml":320},[316,266,317,318],[5,151],{"slug":322,"title":323,"dek":324,"date":314,"project":263,"tags":554,"variants":555,"order":99,"readingMinutes":134,"contentHtml":328},[265,266,279,326],[5,151],{"slug":330,"title":331,"dek":332,"date":314,"project":333,"tags":557,"variants":558,"order":116,"readingMinutes":116,"contentHtml":339},[265,335,279,336,337],[5,151],1788763822457]