Skip to content

Repository files navigation

Bloom Shooter

A third-person arena wave shooter built in TypeScript on the Bloom engine and compiled with Perry. Art and audio converted from the open-source Unvanquished game (GPLv3).

Built as an end-to-end stress test of the engine — exercising the character controller, physics raycasts, spatial audio, skeletal animation, bloom/vignette post-FX, and the retained-mode scene graph — and as a reference implementation for games that convert Quake-family (.iqe) assets.

Play

perry compile src/main.ts -o main && ./main

Controls:

InputAction
WASDMove (the body turns to face where it moves)
MouseLook
SpaceJump
Left mouseFire (rifle = full-auto, blaster = tap)
Right mouseAim down sights (hold, or toggle — settings)
ShiftSprint (hold, or toggle — settings)
CCrouch (hold)
CtrlDodge
1 / 2 / 3 / 4Switch weapon (rifle / blaster / chaingun / cannon)
QCycle weapon
RReload / restart after death
TabToggle cursor capture
F3Profiler overlay (per-pass CPU/GPU µs)
F5–F9Render-pass debug toggles / path tracing
F12Screenshot (engine-native; see EN-038)
EscPause (menu) — not quit; QUIT is a menu row

On an iPhone

./tools/deploy-ios.sh # build, sign, install, launch
./tools/deploy-ios.sh --console # ...and stream the device log

Runs at ~50 fps on an iPhone 16 Pro, landscape. Touch controls: the left half of the screen is a movement stick that appears under your thumb wherever it lands, the right half is drag-to-look, and FIRE / JUMP / R / GUN sit along the right edge. Holding FIRE still steers, so you can shoot and aim with the same thumb.

The mobile branch drops the screen-space GI, reflections and ambient occlusion — a phone is a tile-based GPU on a battery, and those three are what scale worst — and keeps sun shadows and bloom, which is most of what the arena's look actually rests on.

Requires a paid Apple developer account (Perry's signing flow is App Store Connect API-only). See CLAUDE.md for the provisioning details.

Survive three waves — dretches, mantises, marauders, dragoons, a tyrant, and the advanced marauder and dragoon (seven kinds; the two advanced ones are RANGED, so closing the distance stops being the universal answer). Ammo crates respawn at the four arena corners.

Prerequisites

  • Perry0.5.158+ on $PATH.
  • Bloom engine at ../engine/ relative to this repo, built at commit 0137335 (drop debug-green hardcode in fs_main_3d) or later. The Jolt/libc++ link requirements are committed in the engine repo these days — docs/engine-notes.md records why they exist. On Windows, prefer perry compile src/main.ts -o main --debug-symbols so crash reports symbolize (see CLAUDE.md).
  • Bun for the asset converters and dev scripts.
  • ffmpeg and macOS's sips for audio + texture resizing during asset conversion.

Building assets from Unvanquished sources

The repo ships with converted .glb / .wav / .ogg so you can play immediately. To regenerate from sources:

git clone --recurse-submodules https://github.com/UnvanquishedAssets/UnvanquishedAssets vendor/unvanquished
cd vendor/unvanquished
git submodule update --init --depth 1 \
pkg/res-players_src.dpkdir \
pkg/res-weapons_src.dpkdir \
pkg/res-ambient_src.dpkdir \
pkg/tex-tech_src.dpkdir
cd ../..
bun tools/convert-aliens-anim.ts # 5 skinned + animated alien GLBs
bun tools/convert-arena.ts # textured arena
# (SFX were converted ad-hoc via ffmpeg — see docs/asset-pipeline.md)

See docs/asset-pipeline.md for the full conversion pipeline (IQE skeletal parsing, Y-up conversion quirks, glTF skin layout, texture resizing).

Layout

shooter/
src/ TypeScript game code (Perry-compiled)
main.ts entry + game loop
input.ts WASD / mouse / touch / fire input aggregator
player.ts Jolt character-controller wrapper
world-runtime.ts runtime world loader (world.json -> flat arrays)
assets/ committed runtime assets
models/ .glb (arena + 5 alien classes)
sounds/ .wav SFX + .ogg ambient
worlds/ JSON world format, loaded at runtime (edit with ../editor)
LICENSE.md Unvanquished asset attribution
tools/ Bun scripts for offline asset conversion + diagnostics
convert-aliens-anim.ts IQE → glTF skeletal with skinning + animations
convert-aliens.ts IQE → glTF static (kept as a simpler reference)
convert-arena.ts programmatic textured arena .glb
validate-glb.ts runs gltf-validator against a GLB
inspect-glb.ts human-readable GLB structure dump
dump-nodes.ts one-line-per-node tree dump
diag-skin.ts numerical IBM × rest-world = I check
vendor/ gitignored — Unvanquished source packs cloned here
docs/
asset-pipeline.md IQE/IQM format notes + converter design
engine-notes.md engine-side changes needed to build this game
perry-quirks.md Perry 0.5.158 codegen issues and workarounds
engine-issue-green-gbuffer.md historical post-mortem of the green-screen bug

Key engineering notes

Short version of findings accumulated while building this:

  • IQE is Z-up, glTF is Y-up. Apply X90 rotation to vertex positions + normals and to the root joint's rest pose + every animation keyframe of that root. Child joints inherit through the hierarchy unchanged. IBMs are plain inverses of the resulting world matrices. Conjugating every joint individually (a reasonable-looking alternative) conflicts with bloom's Y-axis yaw that the renderer applies on top of skin matrices.
  • Perry has a running list of codegen bugs we work around — and the list has moved, so treat any specific version number here as history rather than as current advice. Fixed since: reachable throw new Error segfaulting at startup, and JSON.parse arrays reporting no .length. World data is NOT hardcoded any moresrc/world-runtime.ts reads assets/worlds/*.world.json at startup via the engine's loadWorld, there is no src/generated/, and there is no bake step. Still live: object-shorthand returns can corrupt fields; never split()/parseFloat() an FFI string on a per-frame path; some small numeric functions in imported modules silently compile to a constant. The authority is docs/perry-quirks.md — this bullet is a pointer, not a second copy of it.
  • Bloom's deferred pipeline had a debug-green hardcode in fs_main_3d that made all 3D geometry render solid green under Perry-compiled builds on macOS. Fixed upstream in engine commit 0137335. Bisect story in docs/engine-issue-green-gbuffer.md.
  • glTF skinned mesh nodes shouldn't have parent transforms — bloom's skeleton walker in engine/native/shared/src/models.rs:478 starts hierarchy traversal at mat4_identity(), so any armature node above the skeleton root is a no-op. We put mesh + skeleton roots at the scene root and bake the Z-up→Y-up rotation into the root joint's transforms instead.

Licence

GPLv3. Bloom engine is MIT, Perry is Apache-2.0, but the Unvanquished assets this game ships are GPLv3 — so this repo must be too. See LICENSE and assets/LICENSE.md for attribution.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages