- GDScript 97.8%
- Shell 2.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
- 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). |
||
| addons/gut | ||
| docs | ||
| jenkins | ||
| scripts | ||
| src | ||
| test/unit | ||
| .gitignore | ||
| .gutconfig.json | ||
| export_presets.cfg | ||
| icon.svg | ||
| icon.svg.import | ||
| Jenkinsfile | ||
| project.godot | ||
| README.md | ||
| VERSION | ||
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 match‑3 and the rising pressure of falling‑block 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 floor’s 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 unit‑testable 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, non‑Mono).
# 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 full‑run 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 sanity‑check 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:
-
Setup Godot — downloads a pinned headless Godot into a workspace cache.
-
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.
-
Balance Simulation — runs the headless balance probe.
-
Export Linux / Export Android APK — install export templates, configure the Android SDK + debug keystore when present, export, and archive artifacts under
build/linux/andbuild/android/aphelion.apk. -
Publish Release — when
PUBLISH_RELEASEis set (default on) and an APK was produced,scripts/ci_publish_release.shattachesaphelion.apkand 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 1–10 / Medium 11–50 / Hard 51–100 / 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^(floor−1)); 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, match‑4, 3 colours / 2 spawn at start, core HP 8, per‑ring boom‑freeze, 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.