Skip to content

Dev Log: 2026-05-20

Branch: rc/v0.1.1-dev


Web export

Added a web platform target to build.sh and wired it alongside the existing desktop and Android paths. Running ./build.sh release web produces a zip under dist/ ready to drop onto itch.io. The same all mode that builds dev, demo, and release in sequence also accepts a platform argument, so a full cross-platform build is one command.

itch.io requires SharedArrayBuffer support enabled in the export preset for the Godot 4 web runtime to work. That has to be set manually in the Godot editor (Project > Export > Web > "Enable SharedArrayBuffer"), not via the build script.

Unicode symbol fixes

Once the web build was running, a handful of UI elements were missing icons. The hamburger menu button, the reset-zoom button, and the skip arrow in the cutscene player all showed blank space instead of their symbols. The advance arrow in the dialogue hint also disappeared.

Root cause: Godot 4 web export only ships font glyphs that are explicitly baked into the font resource. It has no OS fallback. Any Unicode codepoint outside Basic Latin that wasn't added to the baked set is invisible. The buttons were using characters like , , , , , , , , and × directly in their text properties.

Rather than baking a larger font, every affected symbol was replaced with a generated PNG icon loaded via Button.icon or TextureRect.texture. The icons were added to the programmatic asset pipeline in tank_tactics_tools/lib/ui_gen.py alongside the existing move/attack/select overlays, then rebuilt via ./shell_scripts/generate.sh ui. Nine new icons were produced:

  • icon_menu.png: hamburger (three horizontal bars)
  • icon_reset_zoom.png: four corner brackets with a center dot
  • icon_minimize.png: single bar
  • icon_advance.png: right-pointing triangle
  • icon_prev.png: left-pointing triangle
  • icon_skip.png: two right-pointing triangles
  • icon_confirm.png: checkmark
  • icon_cancel.png: X cross
  • icon_star.png: five-pointed star

All icons are 16x16 or 24x24 logical pixels at scale=2, consistent with the existing pixel-art style. Icon-only buttons use expand_icon = true so the icon fills the button area rather than left-aligning. Buttons with text alongside an icon leave expand_icon at its default.

Beyond the buttons, the translation CSV had 37 rows with non-Basic-Latin characters scattered across EN, JA, and DE columns. These were fixed in the same pass: ellipsis to ..., arrows to -> or :, multiply sign × to x, minus sign to -, and decorative marks (, , ) stripped entirely. The HELP_BODY entry was the most involved, with bullet points and inline symbol references all rewritten to plain ASCII.

Supporter gate

Chapter 2+ on the web release build is now gated behind a one-time unlock key. The design is simple on purpose: a single reusable key, no server, no account. People who donate on itch.io get access to a download that contains the key.

How it works. A new autoload SupporterGate holds the SHA-256 hash of the plaintext key. When the player presses the "Next Chapter" button after clearing Chapter 1, the gate checks three conditions before allowing the navigation:

  1. Is this a web release build? (OS.get_name() == "Web", not demo, not editor)
  2. Is the target chapter 2 or later?
  3. Has the player already entered the key this save? (GameManager.supporter_unlocked)

If all three are true and the flag is not set, a modal dialog appears with a LineEdit and an inline error label. The key input is stripped of whitespace, uppercased, hashed, and compared against the stored hash. On success, supporter_unlocked = true is written to the save file. The modal closes, a brief confirmation toast appears, and the world map refreshes with Chapter 2 accessible.

All subsequent loads of that save file start with the flag already set, so the prompt never appears again. The flag also survives New Game (same behavior as the Android full_game_owned flag: progress resets, entitlements do not). Save transfer codes include the flag, so a player who exports their save from web and imports it elsewhere carries the unlock with them.

The plaintext key is documented in a local SECRETS.md that is gitignored. Only the SHA-256 hash is in the repository. The itch.io zip attached to the donation reward tier contains a text file with the key.

Gate scope. The gate is web-only and release-only. Desktop and Android builds skip it entirely (gate_active() returns false). The Android build has its own separate chapter gate via Google Play IAP (PremiumAccess); the two systems are independent.

Stage controller refactor

stage_controller.gd was getting large. It was split into four focused files:

  • stage_action_log.gd: records the per-action event log used by the result screen and replay system.
  • stage_camera_input.gd: zoom, drag, and viewport input handling.
  • stage_objectives.gd: objective state tracking, banner display, win/loss conditions.
  • stage_vfx.gd: projectiles, hit markers, level-up flashes, and other transient visual effects.

stage_controller.gd itself went from ~960 lines to ~180. No behavior change; purely structural.

Chapter 2 content

All 10 Chapter 2 stages are now drafted and balanced. This included a full balancing pass across every stage after a zero-attack bug was fixed (drones with atk = 0 were interacting incorrectly with the combat formula). The stage editor gained prop terrain categories with validation to catch placement errors earlier.

Two new drone types were introduced: SPOTTER (extends detection range for nearby drones) and REPAIR (restores HP to adjacent drones each tick). Both are mirrored in the Python solver.

Story content for Chapter 2 was rewritten alongside the cutscene editor receiving a polish pass. The alpha character art (Alpha-Katyusha, Alpha-Nadeshiko, Alpha-Maria) is in place. A long-standing naming inconsistency was also fixed: the game was inconsistently using "Drona" and "Dronas" as if the latter were a plural; the character is a singular proper noun and all instances were normalized to "Drona."

NEXUS fixes

The NEXUS AOE was not triggering correctly in some cases. The fix required changes to both stage_controller.gd and the solver's tick.py to align the AOE resolution order. HP and AOE range were also buffed after playtesting showed the Chapter 1 finale was too easy at higher difficulties. Both changes are mirrored in the solver.

XP system: underlevel bonus and overlevel factor review

The xp_overlevel_factor (penalty when fighting below your level) and xp_underlevel_bonus (catch-up bonus when outleveled by a drone) were reviewed against playtest data and tuned. Both values are now exposed in the balance editor and mirrored in the Python solver's progression module. Tests were added for the new parameter paths on both sides.

Solver: Chapter 2 and performance work

The solver gained Chapter 2 checkpoint states (solver/states/ch02/) covering all ten stages, which lets the campaign solver start mid-chapter without replaying from Stage 1. The heuristic was reworked to use k=1 (keep only the single best candidate per layer), which cut search time significantly for the stages that were timing out. A benchmark script (scripts/solver_bench.py) and a dedicated performance test suite were added to catch regressions. The solver was also fixed to respect per-drone default_level values from stage JSON, which was causing it to search at the wrong difficulty.

UX and tooling

  • Auto-select on drone tap: tapping a drone when no unit is selected now automatically selects the unit best positioned to attack it, saving a step.
  • Grid number overlay: a toggle in the debug/cheat menu overlays grid coordinates on each cell, useful during stage design.
  • Stage 9 tutorial text: the tutorial message for Stage 9 was missing and is now set, with the tutorial asset exposed in the stage editor for future edits.
  • Playtest checkpoints for Ch2: the stage editor's playtest launcher can now jump into any Chapter 2 stage with the correct pre-stage unit state loaded from a checkpoint, matching how Ch1 playtesting already worked.
  • Linting: ruff (Python) and gdlint (GDScript) are now run in check.sh and a lint pass was done across the whole codebase.
  • Tool startup tests: a new test suite verifies that the balance editor, stage editor, and cutscene editor all launch without crashing, catching import errors before they reach the build.

Minor

  • import.sh was exiting with an error code whenever run headlessly due to SCRIPT ERROR: lines from autoloads running as placeholder instances during the --import pass. Those are benign; the import succeeds regardless. Fixed by tightening the error grep to ^ERROR: (real Godot engine errors) and explicitly filtering the "resources still in use at exit" shutdown message.