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 doticon_minimize.png: single baricon_advance.png: right-pointing triangleicon_prev.png: left-pointing triangleicon_skip.png: two right-pointing trianglesicon_confirm.png: checkmarkicon_cancel.png: X crossicon_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:
- Is this a web release build? (
OS.get_name() == "Web", not demo, not editor) - Is the target chapter 2 or later?
- 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.shand 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.shwas exiting with an error code whenever run headlessly due toSCRIPT ERROR:lines from autoloads running as placeholder instances during the--importpass. 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.