58 KiB
Pitfalls Research
Domain: Browser-based narrative idle game (cozy + narrative + idle Venn) with 7-Season authored arc, AI-assisted asset pipeline, solo/small-team production Researched: 2026-05-08 Confidence: HIGH for save/persistence/browser pitfalls (well-documented post-mortems and platform docs); HIGH for idle game balance pitfalls (codified knowledge in incremental-game community); MEDIUM-HIGH for narrative-idle pacing (genre is small, so evidence is small-N but specific); MEDIUM for AI-pipeline pitfalls (rapidly evolving practice, fewer post-mortems, but concrete failure modes documented); HIGH for cozy-tone pitfalls (well-discussed in genre criticism); HIGH for solo-scope failure (heavily documented).
Critical Pitfalls
Pitfall 1: The Story Ends but the Idle Loop Doesn't
What goes wrong: The authored 7-Season arc completes in ~30-50 hours of engaged play. The player is now staring at a Roothold counter incrementing forever, with no fragments left to harvest, no characters left to meet, and no reason to keep the tab open. The idle mechanic is now empty calories — Roothold growth that purchases nothing meaningful, with the narrative argument ("what survives is what you understood") feeling hollow because the game itself proves it false (your understanding stopped accumulating). Players churn, reviews go from "moving" to "padded out," and the lineage comparison to A Dark Room and Universal Paperclips (both of which end) becomes a critique rather than a positive association.
Why it happens: Idle games are conventionally engineered for indefinite engagement (months, years). Narrative games are conventionally engineered for a fixed run (10-100 hours). Designers porting idle mechanics into narrative games inherit the "infinite curve" instinct without realizing that A Dark Room and Universal Paperclips succeeded specifically because they end. The seven-Season structure is finite by design, but the temptation to add a "post-game" or "New Game+" or endless prestige tier dilutes the story's claim that persistence is the point.
How to avoid:
- Decide before writing any economy code: does this game end like A Dark Room, or does it rest like a finished album you can replay? The Last Garden's premise ("what persists") strongly suggests "rest" — i.e., Season 7 (Return) reaches a stable, low-activity state where the garden continues but no longer demands attention. Make this an explicit design principle.
- Author the end state first (what does Season 7 Day 14 look like?) before authoring intermediate Seasons. This prevents the trap of designing for engagement-without-end.
- Cap exponential growth so Roothold has a meaningful ceiling tied to the narrative, not infinity. The story argument "Roothold is what you truly understood" implies it is finite — you can't understand infinitely many things.
- Build a "credits roll" / coda experience for players who reach the end. Not an empty grind. Universal Paperclips shows the credits and ends.
Warning signs:
- Spreadsheets with prestige curves that extend past Season 7
- Playtester feedback like "I finished the story, what now?"
- Roothold values that require BigNumber libraries by Season 5 (suggests you've over-scaled)
- Meetings where someone says "endgame content" — there is no endgame, the game ends
Phase to address: Phase 1 (foundations / vertical slice). Before any economy code lands, the design doc must specify the Season 7 end state and the "what after the credits" answer. This is a core-value pitfall — getting this wrong undermines the thematic argument the game exists to make.
Pitfall 2: Players Grind Past Story Beats
What goes wrong: A player accumulates Roothold faster than the story expects, prestiges into Season 4 with currency that lets them rocket to Season 5 in an evening, and never reads the Season 4 fragments. They emerge into the Memory Storm mechanic (Season 4) with no emotional investment because they never met Lura's loss in the slow burn the writer intended. The numbers worked; the story didn't land. Reviews say "I don't get the hype" because they didn't actually play the game the writer wrote.
Why it happens: Idle game economies reward optimization. The moment a fragment becomes a currency multiplier, players will grind for currency multipliers and treat fragments as throughput. A Dark Room avoided this by gating progression behind story events the player couldn't accelerate. Most narrative-idle games don't.
How to avoid:
- Story-gate progression, not just currency-gate it. Season transitions should require both a Roothold threshold and the fragment that introduces the next Season's tonal shift. You cannot Season-transition until you have read the door-opening fragment.
- Pace fragments by reading time, not by harvest count. A fragment that took the writer 20 minutes to write should take ~20 minutes of dwell-time before the next harvestable fragment is available, not 20 seconds. This means harvest cooldowns and fragment-quality tiers, not raw RNG drops.
- Acceleration purchases must accelerate waiting, not skip content. This is already locked in PROJECT.md as a constraint — defend it ruthlessly when economic pressure arises.
- Show, don't measure, optimization. If the UI surfaces "fragments per hour" or "Roothold per click," players will optimize. If the UI surfaces "what Lura said yesterday," they'll inhabit.
Warning signs:
- Speedrun routing emerges in playtester forums in Season 1 alone
- Players ask "what's the most efficient build?" — that's a sign the meaning didn't carry
- Session retention metrics spike on Season transitions (suggesting players are racing to the next tier)
- Discord chat is about numbers, not Lura
Phase to address: Phase 2 (economy + first Season). The first Season's economy is the contract for all subsequent Seasons. Get the gating philosophy right here. If Phase 2 ships with pure currency-gating, Phases 3-9 will inherit that flaw and retrofit will be expensive.
Pitfall 3: Save Format Becomes Untenable Mid-Project
What goes wrong: Six months in, you've shipped Seasons 1-2 to a closed alpha. Players have weeks of progression. Season 3 introduces canopy trees, which require a whole new save shape (per-tree memory state, place-memory unlock flags, vignette completion tracking). You realize your save format was a flat JSON blob with no version field. You can't write a migration because you don't know what version any given save is. You either (a) wipe everyone's save and lose your alpha cohort, (b) ship a save-recovery interview where players manually re-enter where they were, or (c) freeze save format and never add new mechanics, killing the 7-Season arc.
Why it happens: A 7-Season game with mechanic-heavy Seasons (especially 4-7 introducing storms, ecosystems, looms, returns) will require save-shape changes the original architect didn't anticipate. Idle games make this worse than non-idle games because the save is the game — there is no level-restart, the player's Roothold and fragment collection IS the value of having played. Losing it is unrecoverable.
How to avoid:
- Versioned saves from day one. Every save serializes
{version: N, data: {...}}. Never a flat blob. - Migration registry. A function
migrateSave(save) -> savethat walks v1→v2→v3, never skipping versions. Forward-only. Tested. - Save schema lives in code as a typed structure (TypeScript types or Zod schema), and adding a field requires bumping the version and writing a migration. Make this a CI check if possible.
- Ship a "snapshot before migration" backup every time the game loads a save and detects a version bump. Keep the last 3 snapshots. Recovery story exists.
- Persist multiple copies. localStorage + IndexedDB + (eventually) cloud. Different storage layers fail in different ways. Belt-and-suspenders is cheap and a 7-Season idle game cannot afford save loss.
- Call
navigator.storage.persist()explicitly. Without it, browsers will silently evict your save under storage pressure (Chrome's eviction is documented). Request persistence on first save. - Test private-browsing modes. Firefox Private and Brave Shields silently fail IndexedDB writes, so detect and warn the player rather than letting them lose progress on tab close.
Warning signs:
- A new feature requires changing how an existing field is shaped, and there is no obvious place to put a migration
- A bug report says "I had X, now I have Y, no idea what happened"
- The save file is a JSON blob with no version field
- You're tempted to write
if (save.canopy === undefined) save.canopy = {}inline at the read site (this is a migration; it just isn't named one)
Phase to address: Phase 1 (foundations). The save format and migration framework must be first-week infrastructure. Retrofitting versioning after Season 2 has shipped is materially more expensive than building it in. Treat save persistence as load-bearing system, not a checkbox.
Pitfall 4: System Clock Cheating Trivializes the Idle Loop
What goes wrong: A player advances their device clock by 30 days. The offline-progression delta-time math computes 30 days of fragment harvests and dumps them into the inventory. The player has just consumed a month of authored content in 90 seconds. The story's pacing collapsed. Worse, they did this not maliciously but because they were curious what was next. Now they have spoilers and no investment.
Why it happens:
Standard idle-game offline progression is a simple now() - lastSaveTime calculation in client code. It is trivially exploitable. In most idle games this matters mainly for monetization (preventing free progression). In a narrative idle game it matters for the story experience itself — fast-forwarding ruins the work.
How to avoid:
- Cap offline progression. A hard ceiling of (e.g.) 24 hours per offline period prevents both clock cheating and the "I came back after a vacation and harvested everything at once, breaking pacing" problem. This is the single highest-leverage mitigation.
- Use monotonic time for sanity checks. Persist the last-seen timestamp on every save; if
now() < lastSeen, the clock went backwards, so refuse to advance progression and flag the save. - Monotonic in-game tick counter (independent of wall clock) gates story beats. Story unlocks gate on tick count, not real-world time. This way, even successful clock cheats can't unlock the next fragment without the engine actually ticking.
- Lean in to the cheat — make it visible and self-defeating. If a player wants to skip ahead, let them, but show them a fragment that says "you were not here for this; come back when you are." The game's tone supports this in a way that an action game's wouldn't.
Warning signs:
- Reddit / Discord threads about "best way to advance the clock"
- Telemetry showing players going from Season 1 to Season 4 in <1 day of actual elapsed wall time
- Save files with timestamps that don't make physical sense
Phase to address: Phase 2 (economy + offline progression). Build the cap and monotonic check at the same time you build offline progression. They are the same system.
Pitfall 5: AI-Generated Asset Style Drift Across Seasons
What goes wrong: You generate Season 1 plants with a Stable Diffusion LoRA tuned for "warm watercolor, slightly wrong real flora." Six months later you generate Season 4 storm-warped plants with a different prompt strategy and a model checkpoint that has rotated. The styles don't quite match. Then for Season 6 you've migrated to a newer model entirely. You have ~2,000 plant illustrations across 7 Seasons, and they look like they came from three different art teams. The watercolor consistency that the project's premise depends on has fractured. Worse, you can't regenerate the Season 1 assets to match Season 6's style because the original seeds and model checkpoints are gone.
Why it happens: AI image models drift in ways traditional art pipelines don't:
- Models update; same prompt produces different output 6 months later
- Sampler/scheduler defaults change between library versions
- Custom fine-tunes degrade if not re-trained against an authoritative reference set
- "Style drift" is the canonical failure mode of generative art at scale (per Scenario, Layer, and most published asset pipelines)
A 7-Season project will span model generations. This is a near-certainty at current pace of model evolution.
How to avoid:
- Lock the model and pipeline. Pin the exact model checkpoint, sampler, scheduler, library versions. Containerize it. Treat the model as a build dependency, version-controlled like any other.
- Train on a curated reference set. A custom-trained model (Scenario, LoRA, etc.) trained on a small, hand-picked set of "the look" produces more consistent output than prompting a foundation model every time.
- Persist generation provenance. Every asset stores
{model_id, checkpoint_hash, prompt, seed, sampler, params}so any asset can be re-rolled identically months later. - Mandatory human curation gate. Every asset that ships passes through a hand-refinement step where a human paints over, color-corrects, or rejects. AI proposes, humans dispose. The PROJECT.md already commits to this — defend it when schedule pressure arrives.
- Visual regression testing across the full asset library. When you migrate the model, render side-by-side comparisons and reject the migration if Season 1 assets look different from each other.
- Establish a small, locked "north star" reference set (10-20 paintings) hand-selected by the art lead. Every model evaluation compares against this set. Drift is detected by reference, not by feel.
Warning signs:
- "I'll just use the latest version, it's better" (you cannot use the latest version mid-project; it will not match)
- Asset folders without provenance metadata
- Style decisions made on individual assets without comparing to the reference set
- Model API deprecation notices for the model you're depending on (this will happen)
Phase to address: Phase 1 (foundations). The asset pipeline itself is a Phase 1 deliverable, with provenance, locked model, and human curation gate before any production-volume asset generation begins. This is the single biggest schedule-and-quality risk for an AI-assisted multi-year project.
Pitfall 6: 7-Season Scope Eats the Solo Developer Alive
What goes wrong: The project was sold (to self) as "a 7-Season idle game like A Dark Room." Three years in, you've shipped Seasons 1-3, you're working 60-hour weeks, the energy you had for Season 1's writing is gone, Seasons 4-7 are in increasingly thin design docs, and you're about to either (a) ship a half-finished thing that fails the lineage comparison, (b) rewrite Seasons 1-3 because you've grown as a designer and they're now embarrassing, or (c) abandon the project. This is the documented failure mode of multi-year solo indie projects. PROJECT.md commits to full 7 Seasons at v1, which makes this risk acute.
Why it happens:
- Solo devs lack the team feedback loops that catch scope drift
- The vision is sticky and revising it feels like betrayal
- Each Season adds mechanics (Storms, Looms, etc.), so production cost is not flat — it grows
- Three years is longer than most peoples' realistic creative-energy curve for a single project
- The narrative-idle audience expects polish (lineage of Paperclips) which means cuts are visible
How to avoid:
- Ship Season 1 publicly as soon as it stands alone. Not "the demo of a 7-Season game" — Season 1 as a free, complete-feeling experience that earns the budget for Seasons 2-7. PROJECT.md commits to "Full 7 Seasons at v1" — read this carefully: it commits to v1 having 7 Seasons, but does NOT preclude shipping Season 1 first as a free prologue / extended demo. Use this loophole. The lineage games (Dark Room, Paperclips) earned their cult status before any sequel pressure existed.
- Cap Season-introduced mechanics. Hard. Each Season adds at most one new mechanic. If a Season's design adds three (e.g., the bible's Season 5 ecosystem could spawn ten), prune to one and reuse existing systems for the rest.
- Identify "Roothold" features (work that compounds across Seasons) vs. "leaf" features (per-Season work). Front-load Roothold features in Phase 1-2. Leaf features are scope you can cut.
- Pre-write the entire authored content (all fragments, all dialogue) in plain text before serious engineering ramps. If it can't be written, it can't be built. This makes scope visible early and turns "design" into "edit" in later phases.
- Quarterly "kill list" review. Every quarter, kill at least one feature from the requirements list. Not "defer" — kill. This is the only defense against scope creep that works for solo devs (per multiple post-mortems).
- Real production schedule with buffer. Multi-year solo projects routinely run 2-3x longer than plan. Either accept that 7 Seasons is 5-7 calendar years, or scope to fewer Seasons. PROJECT.md commits to multi-year — believe it and plan personal life around it (financial runway, relationships, mental health).
Warning signs:
- Season N is "almost done" for >2 months
- Design doc for Season N+2 hasn't been touched in 6 months but is "still committed"
- You've stopped showing the game to anyone outside the team
- You can't articulate what's not in this game anymore (scope is now everything)
- Rewrite urge for shipped Seasons (this is a sign of personal growth eating velocity)
- Personal life metrics (sleep, relationships, exercise) trending wrong
Phase to address:
Every phase boundary. This is a process pitfall, not a technical one. Build a quarterly review ritual where the kill list is mandatory. Build it into /gsd-complete-milestone.
Pitfall 7: BigNumber Overflow + Floating-Point Drift Late-Game
What goes wrong:
Season 6 introduces ecosystem multipliers that compound on Roothold gain, which itself compounds across prestige cycles. Roothold values cross 1e308 (JavaScript Number maximum) and become Infinity. All comparisons against Infinity succeed, all upgrades become free, the economy collapses. Or — subtler — values around 1e15-1e16 lose integer precision (floating-point), so "you have 9007199254740993 fragments" displays as 9007199254740992 and incrementing it appears to do nothing. Players assume the game is broken.
Why it happens:
JavaScript numbers are IEEE-754 doubles. Safe integer precision ends at 2^53 ≈ 9e15. Numeric values exceeding 1.79e308 become Infinity. Idle games routinely exceed both thresholds. The standard fix is break_eternity.js (or break_infinity.js, or idle-bignum), but switching from native numbers to a BigNumber library after the economy is built is painful — every arithmetic site must change, performance characteristics shift, and serialization changes.
How to avoid:
- Pick a number type day one. If Roothold and fragments could plausibly exceed 1e15 (likely yes for 7 Seasons of idle compounding), use
break_eternity.jsfor those values from the first economy commit. Native JS numbers for things that physically cannot grow large (counts of plant types, etc.). - Wrap in a typed abstraction. Define a
BigQtytype and route all economic math through it. If you later have to switch BigNumber libraries (some go unmaintained), the change site is one file. - Display formatting from the start. Show "1.23 e10" or "12 billion" — never raw scientific notation that confuses players. This is a UX issue, not just a math issue.
- Test with absurd values. Unit tests that simulate 30 prestige cycles of compounding Roothold should pass. If they overflow, you have a balance bug, not just a number-type bug.
- Cap exponents at design level (re: Pitfall 1). If Roothold has a narrative ceiling, BigNumber may not be needed at all. Decide first.
Warning signs:
- Numbers in the UI show as
InfinityorNaN - A counter "stops incrementing" around 9e15
- Save files contain
nullfor values you expected to be numbers - Comparisons in the economy code start behaving non-deterministically late-game
Phase to address:
Phase 2 (economy). The first economy commit defines the number type. Choose break_eternity.js (or a wrapped equivalent) by default — easier to remove than to add.
Pitfall 8: localStorage Eviction and Browser-Update Wipes
What goes wrong: A player spent 80 hours over six months playing The Last Garden. Their Chrome updates overnight, or they hit a "clear browsing data" prompt by accident, or Safari's periodic eviction (a documented WebKit behavior, see WebKit bug 266559) wipes site storage, or they ran out of disk space and Chrome's storage-pressure eviction quietly cleared the site's IndexedDB. They open the game and they're back at Day 1 of Season 1. They refund / leave a one-star review / tell their followers. The story they emotionally invested in is gone — and unlike other genres, idle games are just the persistence; without it they have nothing to return to.
Why it happens: Browser storage is best-effort by default. localStorage and IndexedDB are evictable under storage pressure unless explicitly persisted. Safari has a documented periodic-erase behavior. Private browsing fails silently. Browser updates and CDN changes have cleared storage in past years (see itch.io thread). Most web games treat this as acceptable; for a 7-Season narrative idle it is catastrophic.
How to avoid:
- Call
navigator.storage.persist()explicitly on first save. Bumps the storage from best-effort to persistent on supporting browsers; reduces eviction risk. - Multi-layer persistence. Write to both localStorage and IndexedDB on every save. They fail in different ways; surviving one failure mode is cheap.
- Cloud backup, even minimal. A "back up to email link" or "export save as .json" feature gives the player agency. A free cloud sync (one row per user, ~1KB) is cheap to host and saves the relationship when local storage fails.
- Telemetry / save-loaded count vs. expected. If a returning player loads a fresh save where one shouldn't exist, log it and surface "we noticed your save couldn't be found — do you have a backup?" in-game. Don't silently wipe.
- Detect and warn on private-browsing. "Your browser is in Private mode; saves will not survive this session" is a kindness.
- Periodic "export reminder." After Season transitions, prompt the player to download a save backup. Frame as "let's preserve this."
Warning signs:
- No
navigator.storage.persist()call in the save layer - Single-storage strategy (localStorage only or IndexedDB only)
- No save-export feature
- "Save not found" being treated as "new player" without confirmation
Phase to address: Phase 1 (foundations). Save resilience is part of the save framework. Same phase as Pitfall 3.
Pitfall 9: FOMO/Nag Mechanics Violate Cozy Tone
What goes wrong: Mid-development, someone (a publisher, an investor, a friend who plays Genshin Impact) says "you should add a daily login bonus." Or "limited-time Memory Storms only this week." Or push notifications: "Lura is waiting for you." Or a streak counter that breaks if you skip a day. Each one looks like a reasonable engagement lever in a vacuum. Together they make the game feel like a phone-game obligation rather than a contemplative space. The game's premise ("the act of remembering, slowly") is denied by its own UX.
Why it happens: Live-ops design patterns are deeply embedded in mobile/idle game culture. Most idle game designers have internalized them as defaults. Cozy-game audiences specifically reject them (per genre criticism). The Last Garden lives at the intersection — its mechanics are idle, its tone is cozy. Without a clear "this is anti-pattern for us" doctrine, FOMO patterns will sneak in.
How to avoid:
- Anti-pattern doctrine, written down. A list of mechanics this game does NOT use, with reasons:
- No daily login bonuses (presence is not a debt the game collects)
- No streaks (skipping a day is allowed, even encouraged)
- No limited-time content that disappears (the game's premise is what persists)
- No push notifications about progression (the cello plays whether you listen or not)
- No loss-aversion framing in copy ("you'll lose your X if you don't Y")
- No timers visible in the core UI (the cello, the seasons, those are timers — quiet ones)
- Notification UX: at most one opt-in notification class — Season transitions. Not progression. Not idle reward catch-ups.
- Copy review pass. Every player-facing string is reviewed for FOMO framing. "Don't miss out" and "limited time" are bannable phrases.
- Engagement-without-anxiety as a design principle. Optimize for return on player's own schedule, not return at the schedule the analytics dashboard wants.
- Monetization that doesn't FOMO. PROJECT.md already commits to cosmetic-only + Season acceleration + Keeper's Journal. Defend this. Most pressure to add FOMO comes from monetization anxiety.
Warning signs:
- The team is reading mobile-idle live-ops post-mortems for "engagement" ideas
- "Daily login bonus" or "streak" appears in a meeting and isn't immediately rejected
- Push notification permissions are requested on first session (huge red flag — player hasn't consented to anything yet)
- Copy uses "don't miss" / "limited" / "only X hours left"
- Retention metrics become the primary KPI (vs. "did the story land")
Phase to address: Phase 0 (planning) and continuously. This is mostly a culture-and-doctrine pitfall, not a code pitfall. The doctrine ships before any code does.
Pitfall 10: Authored Content Diverges from Code
What goes wrong: Season 3 introduces canopy trees with place-memory vignettes. The writer drafts the vignettes in Google Docs, the engineer transcribes them into TypeScript files, and over six months the two diverge. The writer fixes a typo in Docs that never makes it to code. The engineer changes a fragment ID and the writer doesn't know. Live-ops fixes go into code, never back into the source-of-truth Docs. By Season 5, you have ~3,000 fragments, no canonical source, and a typo report from a player that nobody can confidently fix because nobody knows which version of which fragment is real.
Why it happens: Solo / small teams default to "code is the source of truth" because that's what ships. Writers naturally work in Docs / Notion / Markdown because that's where writing happens. Without an explicit content pipeline, the two formats drift. This is the canonical failure mode for content-heavy games (especially RPGs and visual novels) and is well-documented as a localization-blocker.
How to avoid:
- Single source of truth, in the repository, in Markdown / YAML / JSON. Writers work in the repo (or in tooling that round-trips to it). Code reads the SOT format at build time. Never copy strings from a Google Doc into a TypeScript file.
- Authored content lives in
/content/(or similar), organized by Season, fragment, character. Engineering content (UI strings, error messages) lives separately. - Externalize all player-visible strings. PROJECT.md's "v1 doesn't ship localized" is fine; it does not mean v1 should hardcode strings. Externalization is a one-time investment that costs ~nothing if done day one and is exponentially expensive to retrofit (per established i18n research).
- Use stable fragment IDs that never change.
season3.canopy.lura_07.vignettenotfragment_274. Renames are forbidden. - Spell-check / proofread CI. A linter runs over the content directory and flags typos. A weekly proofreader pass is part of the production calendar.
- Diff the content separately. Writers can review content PRs without reading code.
Warning signs:
- A fragment exists in two different files
- Strings are inside
.ts/.tsxsource files (UI strings, fragment text, dialogue) - The writer asks "is this version up-to-date?" — that question's existence means it isn't
- Live fixes touch code; the canonical source isn't updated
- Numeric fragment IDs (renumbering will eventually happen and break references)
Phase to address: Phase 1 (foundations). The content pipeline is foundation infrastructure alongside save persistence and asset pipeline. Get it right before Season 1 fragments are written.
Pitfall 11: Web Audio Context Blocked, Cello Never Plays
What goes wrong:
A player opens the game. The watercolor garden fades in. The cello — the signature tonal anchor — should swell. It doesn't. The Web Audio context is in the suspended state because the page hasn't received a user gesture, and the play() promise rejected silently. The player's first impression is not the cello. It's silence. They reload thinking it's broken. The mood is broken before it began.
Why it happens: Browser autoplay policies (Chrome 66+, Safari, Firefox) suspend AudioContext until user gesture. This is well-known in web-audio circles but trips up every team's first ship. The Last Garden's tonal anchor is the cello — getting this wrong is more damaging than for an action game where SFX-on-click works without trouble.
How to avoid:
- Explicit "press to begin" gate. First screen is a hand-painted "Tend the garden" or "Begin" button. The click satisfies the gesture requirement and sets the tone.
- Resume the AudioContext on first gesture explicitly via
context.resume(). Don't assume the browser handles it. - Test on iOS Safari, Mobile Chrome, Mobile Safari low-power mode. Each has slightly different behavior. iOS in low-power mode also throttles
requestAnimationFrame. - Loading screen plays the cello as soon as gesture allows. This is the player's first emotional contact with the game; it must work.
- Detect AudioContext suspension at runtime and surface a "tap to restore audio" affordance if the context gets re-suspended (it can happen on tab focus changes).
Warning signs:
- The cello starts on page load in dev (works without gesture in localhost — false positive)
- Audio code that calls
play()without checking the returned promise - No explicit "begin" gate on first session
- iOS / mobile Safari skipped in QA
Phase to address: Phase 2 or 3 (when audio integration begins). The audio bootstrap is a 1-day fix when planned and a 1-week fire when discovered at launch. Plan it.
Pitfall 12: Background Tab Throttling Breaks Offline Math Boundary
What goes wrong:
The garden is "online progressing" because the player's tab is open in the background. But Chrome 88+ throttles background-tab JS timers heavily (1-minute resolution on the main thread, more aggressive after a minute of background time). The game's progression math, running on a setInterval, ticks at one minute when the player thinks it's ticking continuously. When they switch back, the game discovers the tab is far behind real time and has to reconcile — but the offline-progression code only runs on tab-load, not on tab-resume. The player has lost N minutes of growth they thought they were getting. Or worse: the offline code DOES run on resume, but double-counts the throttled foreground ticks the tab DID get, inflating progression. Either is a bug; both are common.
Why it happens:
- Browser timers in background tabs are aggressively throttled (Chrome documented, Firefox documented)
requestAnimationFramepauses entirely in hidden tabs- Web Workers continue running normally, which surprises many devs
- Most idle-game tutorials use
setIntervalon the main thread — fragile
How to avoid:
- Don't simulate progression with timers. Use a "clock" approach: progression is
f(elapsed_real_time), notf(number_of_ticks_received). On every render, compute current state from last-saved state plus real time elapsed. This is robust against throttling, throttling changes, and tab visibility. - Treat "tab visible" and "tab hidden" identically. No special case. Just elapsed time.
- Page Visibility API for save triggers. Save on
visibilitychangetohidden. Save on close. Save on Season transition. Save defensively. - For animation only, use rAF. rAF pauses in hidden tabs, which is fine — animations don't matter when invisible.
- Web Worker for heavy timed computation if any (e.g., long pre-computation). Workers aren't throttled.
Warning signs:
- The economy tick loop uses
setInterval(tick, 1000)with a per-tick gain calculation - Differences in player state when "I left the tab open in the background" vs. "I closed the tab and came back" — should be identical
- Offline-progression math runs on
loadonly, not onvisibilitychange - Mobile testing shows different progression than desktop testing
Phase to address: Phase 2 (economy + offline progression). Same phase as offline progression itself; the architectural choice happens here.
Pitfall 13: DOM-Heavy Garden Becomes Unrenderable
What goes wrong: Season 5 is when ecosystem planting kicks in. A mature Season-5 garden has 60-300 plants on screen, each with hover affordances, bloom animations, growth-stage transitions, and ambient particle effects. If each plant is a DOM element with React state, you're animating hundreds of layout-affecting nodes per frame. The page hits 5 fps on a Chromebook. The watercolor mood becomes a stutter. iOS Safari runs out of layer memory and tabs crash.
Why it happens: DOM is convenient but slow at scale. An idle game with hundreds of moving sprites needs Canvas / WebGL. Mid-project migration from DOM to Canvas is expensive and architecturally invasive.
How to avoid:
- Pick a renderer that scales. PixiJS (WebGL) and Phaser (Canvas/WebGL) handle hundreds-of-sprites scenes well. Plain DOM/CSS does not. Godot HTML5 export has documented mobile-perf issues (Godot issue 58836) — be cautious.
- Profile early with worst-case scenarios. Simulate Season-5 plant counts in Phase 1's prototype. If it doesn't perform, the renderer choice was wrong.
- DOM for UI; Canvas/WebGL for the garden. The split is fine and conventional. Use DOM where DOM is good (text, layout, accessibility) and Canvas where Canvas is good (sprites, particles).
- Texture atlases, not per-asset image files. Pack plant sprites into atlases. Each per-asset GL texture allocation has overhead.
- Object pools for short-lived effects (particles, drifting petals). Allocating and GC'ing thousands of objects per second causes hitches.
- Memory ceiling test on iOS Safari. Mobile Safari's per-tab memory limits are aggressive (~1GB on recent iPhones, lower on older). Run the largest expected scene on the oldest target device.
Warning signs:
- Frame rate drops as the garden fills
- "Page Unresponsive" warnings on mobile
- DevTools shows hundreds of DOM mutations per frame
- Memory growing without bound during a session
Phase to address: Phase 1 (foundations) — engine and renderer choice. This is bound to engine selection. Pick an engine that handles Season 5+ render loads, validated by prototype.
Pitfall 14: Tonal Failure — "Cozy with Weight" Becomes "Depressing Slog"
What goes wrong: The story is about grief. The premise is loss. Done well, this is Spiritfarer and Gris — players say "I cried, and I'm grateful." Done poorly, this is "I quit at Season 3 because it was too sad and there was no relief." The line is invisible from the inside; you've been writing this for years and your sense of "is this too much" is broken.
Why it happens:
- Grief content benefits from contrast — moments of warmth, humor, stillness — without which it becomes monotone-depressing
- Solo / small-team writing lacks the "this is too dark" feedback the team brings
- The "v1 must read as cozy at first glance and earn its emotional weight gradually" requirement (PROJECT.md constraint) is easy to violate by accident
- Cozy genre players are more sensitive to tonal mis-step than other audiences (per genre criticism)
How to avoid:
- Tonal pacing schedule. For each Season, explicit beats: warm / quiet / heavy / lift. No Season is monotone-grief. Season 1 is especially not allowed to lead with grief (PROJECT.md constraint).
- External readers. Two or three trusted readers outside the team review every Season's fragments before integration. They flag "this is unrelenting" or "I don't have anywhere to breathe here."
- Player check-ins. Some games (e.g., Spiritfarer) include in-game "are you okay?" moments. The Last Garden's contemplative tone supports a similar gesture — a fragment that just says "rest, if you need to."
- Content warnings opt-in. Players who want them, get them. This is cheap to add and the cozy-game audience appreciates it.
- Lura's voice as warmth anchor. PROJECT.md identifies Lura as a named character. Write her as the contrast, not a co-griever. Without warmth, grief monotones.
- Endings matter more than middles. Season 7 (Return) must land warm. The whole point of "the garden persists" is that it's a redemptive persistence, not a pyrrhic one.
Warning signs:
- Reader feedback like "I needed a break and there wasn't one"
- Every fragment in a Season has the same emotional register
- Lura's dialogue reads only as "also sad"
- Writer feedback to the writer (you): "I haven't laughed writing this in three months"
- Quit rates spike at a specific Season
Phase to address: Phase 1 (vertical slice) and at every Season's content review. Tonal calibration must be a milestone gate — no Season ships without external reader sign-off.
Technical Debt Patterns
Shortcuts that seem reasonable but create long-term problems.
| Shortcut | Immediate Benefit | Long-term Cost | When Acceptable |
|---|---|---|---|
| Save format as flat JSON without version field | Ship faster, less ceremony | Save migrations impossible; alpha cohort wipe; users lose months of progress | Never. Day-one cost is ~10 lines. |
Hardcoded fragment text in .ts files |
"Just write code, fastest path" | Localization impossible, content/code drift, no proofread pipeline | Never for player-visible strings. OK for internal log messages. |
| Native JS numbers for economy values | Simpler, faster, no library | Overflow at 1e308, precision loss at 1e15, retrofitting BigNumber requires rewriting all economic math | Acceptable only if economy is provably capped well below 1e15 (rare for idle games) |
setInterval ticker for progression |
"It's just a game loop" | Tab-throttling breaks math; reconciliation bugs; mobile inconsistency | Never for progression math. Use elapsed-time-based clock instead. |
| Single-storage save (localStorage OR IndexedDB) | Simpler | Eviction = total loss; private mode failures; cross-device migration impossible | Acceptable for an arcade-style game; never for a 7-Season idle game. |
Skipping navigator.storage.persist() call |
One less line | Browsers will silently evict your save. Documented Chrome and Safari behavior. | Never |
| Generating AI assets without provenance metadata | Faster iteration | Cannot reproduce style 6 months later; no migration story when models update | Never for production assets. OK for throwaway concept exploration. |
| One mega-LoRA / model for all Seasons (no curation gate) | Lower production cost per asset | Style drift, IP/licensing fog, irreversible if model deprecates | Never for shipped assets. |
Skipping AudioContext.resume() after gesture |
Code reads simpler | First-impression mood broken on every browser supporting autoplay policy | Never |
| Daily login bonus / streak / FOMO mechanic | "Standard idle-game retention" | Violates cozy-tone constraint; alienates target audience; undermines thematic argument | Never in this game. |
| "We'll add localization later" / "scope cut for v1" | Ship Season 1 faster | i18n retrofit cost is exponential. Even if v1 doesn't localize, externalized strings cost ~nothing now and a fortune later. | OK to defer the translation. Not OK to defer externalization. |
Integration Gotchas
| Integration | Common Mistake | Correct Approach |
|---|---|---|
| Web Audio API | Calling play() without user gesture; not handling play() rejected promise |
Explicit "begin" gesture; check AudioContext state; call resume(); handle rejection gracefully |
| localStorage | Treating it as guaranteed persistent | Multi-layer (LS + IDB), call navigator.storage.persist(), ship export-save feature, telemetry on unexpected wipes |
| IndexedDB | Assuming writes succeed; not handling private-browsing failures | Wrap writes in try/catch; detect private mode; warn user; fallback paths |
| Page Visibility API | Not subscribing → lost saves on tab close | Save on visibilitychange to hidden; on beforeunload; on Season transition |
| Image generation API | Pinning latest model, ad-hoc prompts | Pin checkpoint, persist seed/prompt/params with each asset, hand-curation gate, locked reference set |
| Asset pipeline | Generating 1000s without curation | Human-in-the-loop on every shipped asset; reject-rate metrics |
| BigNumber library | Mixing native numbers and BigNumber arithmetic | All economic math through one wrapped abstraction; convert at boundaries only |
| Chrome timer throttling | Background setInterval for progression |
Elapsed-time-based progression model; no main-thread tickers for economy |
Performance Traps
| Trap | Symptoms | Prevention | When It Breaks |
|---|---|---|---|
| DOM nodes per garden plant | FPS drops as garden grows; layout thrashing | Canvas/WebGL renderer (PixiJS/Phaser), object pools | Around 50-100 simultaneously rendered plants |
| Per-asset GL texture (no atlas) | GPU memory grows; mobile crashes | Texture atlases packed at build time | Around 200-500 distinct sprites loaded; sooner on mobile |
setInterval-driven progression |
Inconsistent state vs. elapsed wall time; double-counting after tab resume | Elapsed-time clock; recompute state from saved-time + delta | Triggers any time tab is backgrounded > 1 minute |
| Audio buffer leaks | Audio glitching; memory growth across sessions | Decode and reuse audio buffers; don't re-decode per play | Long sessions; mobile Safari especially |
| Particle effects without pooling | GC stalls (visible hitches); frame drops | Object pools per particle type | Hundreds of particles/sec, common in Season-4 storms |
| Re-rendering whole garden each frame | CPU pegged; battery drain on mobile | Dirty flags, partial rerender, sprite caching | Every frame at 60fps with 50+ plants |
| Synchronous save serialization on big saves | UI freezes during save | Async / Worker-based save; incremental save (diff) | Save sizes >1 MB or after Season 4+ |
| Fragment text held all at once in memory | Slow load; mobile memory pressure | Lazy-load by Season; release prior Seasons' content when not visible | After Season 5, with 1000s of fragments authored |
Security / Integrity Mistakes
Because the project is single-player, primarily client-side, security threats are about cheating and integrity, not multi-user data exposure.
| Mistake | Risk | Prevention |
|---|---|---|
| Trusting client clock for progression | Player can fast-forward 30 days, ruining pacing AND auto-spoiling content | Cap offline progression; monotonic timestamp checks; refuse negative deltas |
| Save file exposed in plain JSON | Trivially editable Roothold values | Acceptable for solo single-player; this is not a competitive game; treat save tampering as the player's choice. Do sign saves if cloud sync exists, to prevent server-side abuse. |
| Server endpoints (if cloud sync added) without rate limiting | DoS, scraped account info | Rate limit per IP and per account; basic auth |
| AI-generated assets shipped without licensing audit | IP/licensing exposure (model trained on copyrighted reference; user-generated content; depending on jurisdiction) | Use models with clear commercial-use terms (e.g., self-trained LoRAs, commercially-licensed services); document provenance; legal review before launch |
| User-uploaded content (if Keeper's Journal supports it post-1.0) | XSS via journal annotation; CSAM/abuse vectors if shared | Sanitize all user-entered HTML; if sharing is added post-1.0, it requires moderation; not a v1 concern |
| Cosmetic monetization without receipt verification | Players claim purchases they didn't make; fraud | Standard payment-platform receipt verification (Stripe / Steam / etc.); replay attack prevention |
UX Pitfalls
| Pitfall | User Impact | Better Approach |
|---|---|---|
| Numbers as the primary UI element | Players treat the game as a spreadsheet, not a story | Foreground the garden, the fragments, the cello. Numbers are present but quiet. |
| "X gained" toast spam | Breaks contemplative mood; mobile-game vibe | At most one "discovery" toast per fragment harvest. Most growth happens silently. |
| No "where am I?" affordance | Returning players forgot the story; bounce | Gentle "what happened last time" recap on session resume — not tutorial-y, in-fiction |
| Tutorial that explains the metaphor | Robs the player of the discovery (the metaphor IS the experience) | Tutorial teaches mechanics only; the meaning emerges. |
| First-session asks for permissions (notifications, audio) | Player hasn't consented; permissions feel demanding | Earn permissions. Cello plays after gesture. Notifications offered later, opt-in, framed in-fiction. |
| No save-export option | Player anxiety about losing 80 hours of play; rightful so given browser storage realities | Ship save export as core feature, not power-user feature. |
| Settings buried | Audio-sensitive players, accessibility-needing players bounce | Audio level, motion-reduction, content warnings — surfaced and accessible from any screen |
| Color/audio shifts without warning (Season transitions) | Photosensitivity / sound-sensitivity issues | Smooth transitions; "reduce motion" respected; volume normalization across Seasons |
| Default "click to harvest" feels rushed | Idle player who isn't there can't engage | Click is optional; idle progression is the floor. Click-rewards exist but never gate. |
| Mobile: tap targets too small, text too small | Cozy audience skews older; text-heavy game punishes small text | Min 44pt touch targets; readable type at default zoom; test on smaller phones |
"Looks Done But Isn't" Checklist
- Save persistence: Often missing — verify saves survive browser update, private mode, storage pressure, app reload. Verify
navigator.storage.persist()is called and returnedtrue. Verify multi-layer write. - Save migration: Often missing — verify a v1 save loads in a v3 build via migrations, not via "lost data, sorry."
- Audio: Often missing — verify cello starts on every tested browser (especially Safari, mobile Safari, mobile Safari low-power). Verify resume on tab focus.
- Offline progression: Often missing — verify cap; verify monotonic clock check; verify identical behavior between "tab open backgrounded" and "tab closed and reopened."
- Asset pipeline: Often missing — verify any asset can be regenerated identically months later from stored provenance. Verify human-curation gate isn't being skipped under deadline pressure.
- Content pipeline: Often missing — verify all player-visible strings are externalized (grep for string literals in JSX). Verify SOT is in repo, not in Docs.
- BigNumber economy: Often missing — verify simulated 30-prestige-cycle test passes without overflow or precision loss.
- End state: Often missing — verify Season 7 final fragment has actually been written and the "what after" experience exists. The credits, the rest state, the coda.
- Tonal balance: Often missing — verify each Season has been read by 2+ external readers and signed off on the warm-quiet-heavy-lift balance.
- Anti-FOMO doctrine: Often missing — verify no daily-login bonus, no streaks, no limited-time content, no nag notifications shipped under "engagement" pressure.
- Performance: Often missing — verify Season-5 plant counts render at 60fps on the oldest target device. Verify no DOM-per-plant pattern.
- Localization-ready: Often missing — verify externalized strings even if v1 isn't translated. Verify no text baked into image assets.
- Accessibility: Often missing — verify reduced-motion respected, text scales, color choices have non-color fallbacks (color blindness), audio captions/text equivalents.
- Mobile testing: Often missing — solo devs default to desktop. Verify iOS Safari, Android Chrome, on actual devices, not just emulator.
- Save export: Often missing — verify player can export and re-import their save. This is the relationship-saving feature when storage fails.
Recovery Strategies
When pitfalls occur despite prevention, how to recover.
| Pitfall | Recovery Cost | Recovery Steps |
|---|---|---|
| Save format unversioned, alpha cohort live | HIGH | Sniff save shape; write best-effort migration with fallback to "interview" UI ("you were on Season X, with Y fragments — does this look right?"); ship versioning permanently going forward; apologize publicly. |
| Save loss (browser eviction) | HIGH if no backup; LOW if export-save was a feature | Recovery: ask player for exported save. Without export feature: a sympathetic email reply, an offer to start them at the right Season, and shipping export-save in the next patch. |
| BigNumber overflow shipped | MEDIUM | Migrate to break_eternity.js; values >1e308 in saves get clamped or recomputed; communicate honestly with affected players. |
| AI asset style drift mid-project | HIGH | Re-train on a curated reference; regenerate worst-offending assets with hand-paint pass; accept that some old assets may need rework. |
| Story finishes too soon (Pitfall 1) | HIGH | Season 7 coda + post-credits "rest" mode added in patch; reframe Roothold as finite; communicate the design intent clearly. |
| Players grinding past story (Pitfall 2) | MEDIUM | Add story-gates to subsequent Seasons; harvest cooldowns; existing players unaffected if applied to new progression only. |
| FOMO mechanic shipped, audience revolts | MEDIUM | Remove. Apologize. Don't try to "balance" it — cozy audiences read partial-removal as gaslighting. |
| Tonal failure (a Season is too dark) | MEDIUM | Edit the Season; ship a content patch; offer affected players a "rebalanced" replay path. Sensitive but manageable. |
| Tab throttling double-counting | MEDIUM | Refactor progression to elapsed-time-based; reconcile saves with detection of impossible deltas. |
| Audio doesn't start on Safari | LOW | Push a patch with explicit resume() and a "begin" gate; shipped within hours. |
| Hardcoded strings, localization required | HIGH (large surface) | Mass extraction with regex + manual review; this is weeks of solo-dev work and is the canonical case for "do it day one." |
| Solo scope death spiral (Pitfall 6) | HIGH | Recovery is a kill list and possibly a reduced-scope v1 (5 Seasons, with 6-7 as a free post-launch update). Far better outcome than abandonment. |
Pitfall-to-Phase Mapping
| Pitfall | Prevention Phase | Verification |
|---|---|---|
| 1: Story ends, idle loop doesn't | Phase 0/1 — design before code | End state exists in design doc; Season 7 written before Season 4; Roothold has finite ceiling |
| 2: Players grind past story | Phase 2 — first-Season economy | First Season ships with story-gating, not pure currency-gating; harvest cooldown by reading time |
| 3: Save format untenable | Phase 1 — foundations | Save has version field; migration framework + tests exist; multi-layer write |
| 4: System clock cheating | Phase 2 — offline progression | Cap on offline progression (24h); monotonic timestamp check; refuse negative deltas |
| 5: AI asset style drift | Phase 1 — asset pipeline | Pinned model; provenance metadata; human-curation gate; locked reference set |
| 6: 7-Season scope eats developer | Every milestone | Quarterly kill list; full content pre-written before engineering ramp; Season 1 ships standalone |
| 7: BigNumber overflow | Phase 2 — economy | break_eternity.js selected; wrapped abstraction; 30-prestige test passes |
| 8: Storage eviction | Phase 1 — foundations | navigator.storage.persist() called; multi-layer; export feature |
| 9: FOMO violates tone | Phase 0 — doctrine; continuous | Anti-pattern doctrine written; copy-review rule; monetization scope locked |
| 10: Content/code divergence | Phase 1 — content pipeline | All strings externalized; content SOT in repo; stable IDs; proofread CI |
| 11: Audio Context blocked | Phase 2/3 — when audio integrates | Begin gesture screen; explicit resume(); iOS Safari tested |
| 12: Tab throttling | Phase 2 — offline progression | Elapsed-time-based progression (no setInterval ticking); Page Visibility hooks |
| 13: DOM-heavy garden unrenderable | Phase 1 — engine choice | PixiJS/Phaser selected; Season-5 prototype runs at 60fps on target hardware |
| 14: Tonal failure | Phase 1 — vertical slice; every Season | External-reader gate; tonal pacing schedule; warmth contrast in every Season |
Sources
- The Math of Idle Games, Part III — Kongregate Blog — prestige-curve mechanics, exponential walls
- The Idle Game Illusion: How Delta-Time Powers Progress — offline-progression math and clock-cheat vulnerability
- Math — the backbone of Idle Games (Medium) — production curves, balance pitfalls
- HTML5 local storage is lost at every update — itch.io — real-world localStorage wipe on browser update
- Why Your IndexedDB Data Keeps Disappearing — DEV.to — Chrome storage-pressure eviction
- WebKit Bug 266559: Safari periodically erasing LocalStorage and IndexedDB — documented Safari periodic eviction
- Uncovering 8% IndexedDB Data Loss After Browser Crashes — DEV.to — IDB loss rates under crash
- break_eternity.js (Patashu) — BigNumber library for incremental games
- Heavy throttling of chained JS timers in Chrome 88 — Chrome for Developers — background-tab timer throttling
- Why do browsers throttle JavaScript timers? — Nolan Lawson — cross-browser throttling behavior
- Web Audio, Autoplay Policy and Games — Chrome for Developers — Web Audio gesture requirement
- MDN: Autoplay guide for media and Web Audio APIs — autoplay policy details
- Pixi.js memory leak in Graphics — GitHub issues 5543, 8189, 11550 — PixiJS production failure modes
- Troubleshooting Phaser Performance and Memory Issues — Mindful Chase — Phaser long-session leaks
- Godot HTML5 mobile performance — Godot issue 58836 — Godot web-export mobile-perf concern
- Scenario — AI Asset Generation — style drift as canonical AI-pipeline failure mode
- How to Create 3D Assets with AI in a Consistent Style — Sloyd — style consistency at scale
- Layer — AI OS for Creative Teams — custom-model curation pipelines
- Why Scope Is the Most Dangerous Enemy of Indie Games — All That's Epic — scope as primary cause of indie failure
- Scope Creep: The Silent Killer of Solo Indie Game Development — Wayline — solo-dev scope failure pattern
- What I Learned About Failing from My 5 Year Indie Game Dev Project — HN discussion — multi-year solo project post-mortem
- Solo Game Dev: Lessons Learned the Hard Way — Romain Mouillard — concrete solo-dev lessons
- How Video Games Abuse The Fear of Missing Out — Game Wisdom — FOMO mechanics and audience trust erosion
- If all is cozy, what isn't? Conceptual problems regarding cozy games — Into the Magic Circle — cozy-genre tonal expectations
- Exploring Themes of Grief and Loss Through Video Games — Game Developer — grief content design considerations
- Game Localization: Complete Guide — better-i18n — localization retrofit costs
- The technical side of localization in video games — Alpha CRC — hardcoded-string failure modes
- How to prevent time cheating in idle games — GameMaker forum — clock-cheat prevention patterns
- Save File Upgrades — Don't Starve Together API — save-version migration pattern reference
Pitfalls research for: The Last Garden — browser-based narrative idle game Researched: 2026-05-08