Two levels, three abilities, and a wardrobe of faces
A week ago BallOrcs was a few test steps in an empty room.
In plain words
A week ago BallOrcs was a few test steps in an empty room. Now there are two real levels you can play start to finish: a village that teaches you to move, jump, collect and dash, and a gloomy fire territory where you burn your way through blocked walls until the Fount reignites and the colour comes back. Your orc can dash, flame-dash, and glide. There are checkpoints, a first enemy that flattens you on contact (or squashes if you land on it), a results screen, a level-select hub, and a save file that remembers all of it. We also finished the wardrobe — eyes, mouths, hats, accessories, noses, skins and feet — and taught the game to play on a phone.

Under the hood
Sixteen backlog issues landed. The shape of the week:
- Levels.
level1.tscn(#24) replacedmain.tscnas the main scene — move → jump → collect → speed, in order, ending at the Fount. Its speed crossing is a genuine required gate (a real gap, flanked by full-length boundary walls) so the goal bot's detour logic can't route around it.level2.tscn(#30) is the Fire Fount route: two full-corridorBurnablegates, an oil-slick hazard, an optional speed-pad detour to bonus orbs. Both got art/lighting passes (#29,#33) — L2'slevel2_atmosphere.gdstarts desaturated and warms to full ember vibrance in two steps offBurnable.ignited, so the Flattening's grey literally lifts as you burn. - Three abilities, all GDScript.
Ability::activate()only dispatches to script overrides, so new ability ids stay additive with no GDExtension rebuild. Speed (#19) is a burst — withcooldown_durationexplicitly raised to3.0because a 1.5s burst on a 1.0s default cooldown could re-trigger before the ball ever dropped back to base speed. Fire (#20) is a dash that ignites anything exposing anignite()method (duck-typed seam;#22shipped the other half independently). Flight (#21) is hold-to-glide, driven from_do_tickrather than_do_activate—AbilityHolderonly callsactivate()onis_action_just_pressed, a discrete edge, which is wrong for a hold. - A world that reacts.
Burnable,UpdraftFanandBoostGate(#22), fire hazards — torch chain, flame geyser (#31) — the wind family (#36:wind_current.gdlateral push,wind_gust.gdtelegraphed burst),EnemyV1(#32, patrol + contact damage + Goomba-style stomp), checkpoints, kill volumes and orb pickups. Every gate has a no-button-press path (igniter_torch.gd,speed_pad.gd) because the goal bot can never holdability. - Everything around the game. Main menu with a shared options panel,
contextual tutorial prompts with input-glyph substitution (
#25), level completion + results screen (#26), a versioned forward-compatibleSaveProfile(#40) where unknown fields survive a load→save round-trip and a corruptuser://profile.jsonfalls back to defaults with a warning, and a level-select hub (#41) whose locked rows are non-focusable so pad/keyboard tab order skips them for free. - Phones.
#50added a virtual drag joystick feedingInput.action_press(action, strength)— the same actions the bot injects, so the harness stays device-agnostic — plusTouchScreenButtonnodes for jump/ability. Positions honourDisplayServer.get_display_safe_area(); the whole thing is gated off under--playtestand hidden on non-touch devices.project.godotgainedcanvas_itemsstretch at 1280x720 so the UI scales instead of going microscopic at high DPI. Not verified on a real device — no Android SDK in this sandbox. - The wardrobe finished. Skin (4 full-body material variants — matte,
glossy, cracked-magma, bubbling), nose (3), feet (3) and a starter world-prop
vocabulary (torch, rock cluster, platform block). Feet are the first
slot tied to gameplay:
toggle_stance(C) flips between rolling and a bipedal presentation where the body floats above two independent feet — collision and movement math stay byte-identical, only mesh offset, foot visibility and a step bob change. - Honest gaps. No barber-shop UI yet, so the components can't be
swapped in-game. The skins are same-radius opaque spheres that will
z-fight with
ball_meshuntil a real swap replaces it rather than instancing alongside.