No description
  • GDScript 97.8%
  • Shell 2.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
epistemophiliac 0ce4a4c6f1 v0.12.0: restore the v8 turn-survival feel
- Drop star-floor orbit clear (v0.10-v0.11); clear by surviving the
  quota of turns instead (v8 loop).
- Restore v8 defaults: 4 spins/turn, match-4, 3 colours, 2 spawn,
  core HP 8, score 10/frag, per-ring boom-freeze.
- Gentle ramp: colours climb only at orbit 8/14, spawn 2 until 16,
  3 at 16, 4 at 24 (v8 spiked by ~orbit 5).
- Remove v0.11 overflow choke so rounds breathe.
- HUD shows Turns/quota; tests + balance sim green (171/171).
2026-08-10 09:16:04 -04:00
addons/gut Aphelion: orbital survival puzzle roguelite (Godot 4.3) with tests, CI and GDD 2026-07-24 19:35:39 -04:00
docs v0.10.5: hybrid v0.8 balance — danger after gift, paced quotas 2026-07-25 18:01:02 -04:00
jenkins Infinite orbits with orbit-rank scoring, and Jenkins APK pipeline 2026-07-24 20:27:26 -04:00
scripts fix(ci): pin shellcheck 0.11.0 tar.gz (0.10.0 has no gzip bundle) 2026-07-27 02:12:03 -04:00
src v0.12.0: restore the v8 turn-survival feel 2026-08-10 09:16:04 -04:00
test/unit v0.12.0: restore the v8 turn-survival feel 2026-08-10 09:16:04 -04:00
.gitignore Aphelion: orbital survival puzzle roguelite (Godot 4.3) with tests, CI and GDD 2026-07-24 19:35:39 -04:00
.gutconfig.json Aphelion: orbital survival puzzle roguelite (Godot 4.3) with tests, CI and GDD 2026-07-24 19:35:39 -04:00
export_presets.cfg v0.10.5: hybrid v0.8 balance — danger after gift, paced quotas 2026-07-25 18:01:02 -04:00
icon.svg Aphelion: orbital survival puzzle roguelite (Godot 4.3) with tests, CI and GDD 2026-07-24 19:35:39 -04:00
icon.svg.import Aphelion: orbital survival puzzle roguelite (Godot 4.3) with tests, CI and GDD 2026-07-24 19:35:39 -04:00
Jenkinsfile Align Aphelion with Estate style gates without ripping out client BugSink DSN. 2026-07-27 02:02:05 -04:00
project.godot Align Aphelion with Estate style gates without ripping out client BugSink DSN. 2026-07-27 02:02:05 -04:00
README.md v0.12.0: restore the v8 turn-survival feel 2026-08-10 09:16:04 -04:00
VERSION v0.12.0: restore the v8 turn-survival feel 2026-08-10 09:16:04 -04:00

Aphelion

A static-protagonist orbital survival puzzle roguelite for mobile, built in Godot 4.3.

Hold the core. The world orbits you.

You are a dying star at the centre of the screen. Rings of stellar debris orbit around you and spiral inward one step at a time. Spin the orbital tracks to line up three or more matching fragments and fuse them into energy before they crash into your core. Every few orbits you choose a relic that rewrites a rule of the run. A run is fully deterministic from its seed, so no two runs — and no two collapses — play the same.


What makes it niche (unique tropes)

Aphelion borrows the readability of match3 and the rising pressure of fallingblock survival, but twists both:

Common trope Aphelion's twist
The player moves an avatar You never move. You are the fixed core; you rotate the world around yourself.
Matching on a Cartesian swap grid Matching on concentric rotating rings — groups form both angularly (with wraparound) and radially.
Threats approach from one edge Gravity pulls debris inward from every direction at once, toward the single point you must protect.
Roguelite upgrades = cards/stats Relics mutate the spatial and physics rules of the run (match size, gravity cadence, extra actions, a nova pulse).
RNG hidden inside the engine Fully deterministic: a run is a pure function of its seed + inputs, enabling shareable seeds and a complete automated test suite.

How it plays

  • The gold ball in the middle is the player. Coloured balls sit on five rings (eight slots each).
  • Spin a ring: tap the right half to spin one way, the left half for the other way. Four spins are available each turn.
  • Same colour that touch go boom. Touch means next to each other on a ring, or one above the other on the same spoke — not diagonal, and not “same colour anywhere.” Mint outlines show which groups will boom when End Turn is pressed.
  • End Turn: booms resolve first. Every ring that boomed holds still for that same step — leftover balls on those rings do not fall in (so a boom on the closest ring can stop leftovers from hitting the heart). Balls on other rings still fall one step closer, then new balls appear on the outside.
  • Heart = life. Stars = score. Power = energy bank (starts at 40, climbs toward 100). Some gifts spend power later via buttons.
  • Clear a level by earning that floors star quota, then pick one gift of three. Gifts have classes (Power, Guard, Spin, Luck, Special) and rarities; some are passives, some are actives with charges, some trigger each turn.
  • Stars scale with level depth: 1 + (level 1) × 0.2.
  • Runs are infinite by default. The run ends when the heart hits zero.
  • Save system: the open run is written to disk after every spin, End Turn, gift pick, and power use (and when the app pauses). Menu → Save & Leave returns to the title without ending the run; Continue restores the exact board, relics, and score. New Game starts fresh and replaces the save. A real death or win clears the continue slot.

Tutorial

First launch offers a how-to; skipping is remembered, and the title screen keeps a How-To Again option.

It is the real game in a preset world (fixed seed and stamped boards), so every lesson shows the same balls. Dual match lessons teach side-by-side and stacked touching. Copy stays in short, kid-simple English. Input is gated to the current lesson. Practice scores do not become high-score records.

The lesson plan (TutorialScript) and state machine (TutorialState) are engine-free and played headless in CI.

Architecture

The project enforces a strict logic / presentation split, which is what makes the whole game unittestable without the engine loop.

src/core/     Pure, engine-agnostic game logic (no scene/UI dependencies)
  AphRNG.gd        Deterministic seeded RNG (shuffle, sample)
  Fragment.gd      A debris piece: colour + mass
  AphConfig.gd     Tunable rule parameters / difficulty state
  Board.gd         Rings, rotation, adjacency, fusion detection, gravity, spawn
  RelicLibrary.gd  Relic definitions and their effects
  RunState.gd      Turn resolution, scoring, progression, win/lose
  Game.gd          Autoload session singleton (active run + high scores)
  TutorialScript.gd  Lesson plan: preset world + step definitions (data only)
  TutorialState.gd   Tutorial flow, input gating, scripted ideal play

src/game/     Presentation (reads state, forwards input — no rules)
  Main.gd / Main.tscn   Root scene, run lifecycle, HUD updates (class_name AphelionMain)
  HudChrome.gd          HUD chrome build + viewport layout (owned controls stay on Main)
  BoardView.gd          Renders the board; turns touches into rotations
  TutorialCoach.gd      Animated coaching drawn over the board
  TutorialUI.gd         Instruction card, progress dots, focus highlighter

test/unit/    GUT test suite (117+ tests)
docs/gdd/     Game Design Document (LaTeX source + compiled PDF)
docs/gdd-template/  The blank Aexora GDD Standard v2.0 template
scripts/      CI helpers + a headless balance-simulation probe
Jenkinsfile   CI pipeline

Because src/core/ has no engine dependencies, the entire rule set runs and is tested headless in well under a second.


Running the game

Requires Godot 4.3 (standard, nonMono).

# Detached, with the class cache rebuilt and output captured for triage:
bash scripts/run_local.sh
# or directly:
godot --path . res://src/game/Main.tscn

scripts/run_local.sh re-imports the project first, because a stale script class cache shows up as "class not found" errors at runtime rather than at import time. Output lands in /tmp/aphelion-run.log.

The project targets portrait phones. Stretch mode is canvas_items with aspect=expand, so the game fills any width/height without letterboxing. The HUD and board reflow from the live viewport size (safe-area insets included): the orbit scales into the middle band, and End Turn / Nova stay in the bottom thumb zone. Desktop window overrides keep a comfortable preview size. Mouse and touch presses share the same handler so desktop playtesting matches device behaviour.


Testing

Tests use GUT (vendored in addons/gut/).

# Run the whole suite headless (what CI runs):
bash scripts/ci_test.sh
# or directly:
godot --headless -s addons/gut/gut_cmdln.gd -gconfig=.gutconfig.json

Coverage spans RNG determinism, ring rotation, adjacency (angular / radial / wraparound), fusion detection and chains, gravity and core impact, spawning, scoring, every relic effect, orbit progression and difficulty scaling, and fullrun determinism and invariants.

test/unit/test_ui_smoke.gd additionally instantiates the real Main.tscn headless and drives it the way a player does: start a run, clear an orbit, take the offered relic, resume play. Rules-only tests cannot catch a runtime fault in the HUD code, and such a fault is severe — a relic panel that fails to open leaves the run in a state where End Turn is disabled and board presses are rejected, which reads as a frozen game. Main._process() also carries a watchdog that reopens the panel and reports to the error tracker if the game ever sits in ORBIT_CLEAR without it.

There is also a headless balance probe that plays runs with a naive bot and reports how far they get — a quick way to sanitycheck tuning:

godot --headless -s scripts/sim_balance.gd

Continuous Integration (Jenkins)

Job: https://jenkins.aexoradao.com/job/Aphelion/ (create/update with bash scripts/setup_jenkins_job.sh).

Jenkinsfile defines the pipeline that maintains the source:

  1. Setup Godot — downloads a pinned headless Godot into a workspace cache.

  2. Unit Tests — imports the project and runs the GUT suite, publishing a JUnit report. This stage is a hard gate: any failing test fails the build.

  3. Balance Simulation — runs the headless balance probe.

  4. Export Linux / Export Android APK — install export templates, configure the Android SDK + debug keystore when present, export, and archive artifacts under build/linux/ and build/android/aphelion.apk.

  5. Publish Release — when PUBLISH_RELEASE is set (default on) and an APK was produced, scripts/ci_publish_release.sh attaches aphelion.apk and the Linux binary to the Forgejo release matching ./VERSION, creating the release if it does not exist and replacing same-named assets on re-runs.

Pushing to main on the Forgejo remote triggers the job. Export presets live in export_presets.cfg; scripts/ci_export.sh installs matching templates automatically and bootstraps a minimal Android SDK when the agent has none.

The current version lives in ./VERSION (single source of truth for the release tag and the Android version/name).

Release names (why each tag)

Tag Name Why it shipped
v0.3 Guided how-to Preset-world tutorial so every lesson is identical and CI-assertable.
v0.3.1 Retry fix Try Again after skipping the how-to started a scored run, not the tutorial again.
v0.5 Logic remake Combined Phase A+B from the remake plan: match clarity (ghosts, dual lessons, fuse preview, kid copy) and classed relic catalog with actives. Plan allowed A as 0.4 and B as 0.5, or one jump after both landed — both landed together, so the tag is 0.5 (not 0.4).
v0.6 Pause & leave Menu / Options: leave a run anytime without dying, resume, jump to how-to, toggle empty-slot dots.
v0.7 Save system Full run save (board, relics, RNG, stats). Menu Save & Leave parks the session; title offers Continue. Autosave after spins / End Turn / gifts / powers; also on app pause. Death clears the continue slot.
v0.7.1 Phone-fit layout (tag only) Band-flow _layout() landed in git, but ./VERSION was still 0.7, so Jenkins re-attached the APK to the v0.7 Forgejo release instead of creating a v0.7.1 page. Superseded by v0.8.
v0.8 Boom-freeze + real publish Rings that boom skip gravity that End Turn (survivors on that track hold still). Includes the phone-fit layout. VERSION / Forgejo release tag aligned so the release page shows the build. GDD updated.
v0.8.1 Match invariants + DSN Pair-booms blocked without Easy Boom (corrupt match_min sanitized). Center boom cannot damage the heart (failsafe + log). BugSink logs logic violations (logic:*) with real release tag. Heavier invariant tests.
v0.8.2 CI + DSN harden Headless tests no longer fire BugSink HTTP (was flaking Jenkins). Watchdog smoke test drives _process directly. Same gameplay fixes as 0.8.1.
v0.8.3 Gift UI + scaling Exhausted active powers leave the thumb row after their last use (with an "all used up" toast). Action bar caps its buttons; overflow/exhausted powers stay usable in a new scrollable Gifts panel. Bottom gift line is a compact tappable summary that stays short at 20+ gifts. New test_gift_ui.gd regressions.
v0.8.4 Readable booms Fixes the "it groups random colours" look: each ready group now glows in its own colour (a single shared mint made adjacent different-coloured groups read as one mixed group), the palette's near-identical steel-blue/teal pair was replaced with distinct blue/green, and every clear — chain waves, post-spawn booms, and clearing actives (Nova/Pulse/…) — now pops with its clear-time colour recorded by the engine (summary.cleared_cells), staggered per wave. New rim spawns flash white.
v0.9 Global boom-freeze + motion Any boom freezes all gravity that End Turn (outer clear protects the heart from unrelated ring-1 debris); spawn still runs. Explosions show connection lines + coloured shockwaves (why it boomed). Balls slide inward on drift; core hits crack/fade instead of vanishing. Gravity moves recorded in the turn summary for replay. Development-team skill added for multi-lane patches.
v0.10 Star floors + fair spawn Clear levels by earning a star quota (not surviving turns). Floors band Easy 110 / Medium 1150 / Hard 51100 / Elite 101+. Baseline match 4; Easy Boom stacks twice (4→3→2) with star pay × match_min/4. Briefly shipped 16 slots / 5 rings (wrong read of “double rings”).
v0.10.1 Rings×2 + spawn/economy hotfix Briefly 10 rings × 8 slots; cheap SpawnPlanner; Easy floor-1 quota 35.
v0.10.2 Classic board depth Back to 5 rings × 8 slots with star floors kept — 10 rings cleared too fast and debris rarely threatened the heart.
v0.10.3 Boom-first gifts + cadence Boom FX finish before the gift overlay. Gifts every 5 floors (not every clear). Stars pay 16/frag; quotas scaled.
v0.10.4 Real difficulty wall Star quota briefly geometric (60 × 1.5^(floor1)); too grindy without board danger.
v0.11 Balance hotfix Per-ring boom freeze (a boom only holds its own ring, so unrelated debris still threatens the core), 3 spins/turn, board-overflow choke (1 core dmg when the rim jams), costed actives (Nova 20 charges; Freeze/Shield now cost power), trade-off relics (Greedy Core, Overdrive), gradual band-pressure ramp.
v0.12 Restore the v8 feel Throws out the star-floor loop (v0.10→v0.11) and brings back turn-survival: clear an orbit by surviving its quota of turns, not by farming stars. Back to 4 spins, match4, 3 colours / 2 spawn at start, core HP 8, perring boomfreeze, and the v8 survival quota (base 6, +2/orbit). The earlier ramp that spiked to 5 colours + max spawn by orbit ~5 was gentled — colours climb only at 8 and 14, spawn stays 2 until 16 then 3 at 24, 4 at 24. Removed the v0.11 overflow choke so rounds breathe. HUD shows Turns/quota instead of Stars.

Game Design Document

The design is documented in full in docs/gdd/aphelion-gdd.pdf (LaTeX source alongside it). It is authored against the Aexora Game & Media Design Document Standard v2.0, whose blank template is included in docs/gdd-template/. The GDD is maintained in this repository and versioned alongside the code — updating it is part of the definition of done for any gameplay change.


Dependencies & licensing

  • Godot Engine 4.3 — MIT.
  • GUT (Godot Unit Test) — MIT, vendored in addons/gut/.

All game code, design, and assets in this repository are original works.