linefire

package module
v0.0.29 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 1, 2026 License: MIT Imports: 2 Imported by: 0

README

Linefire

[!WARNING] Pre-alpha, work in progress. Not ready for use. This is an early, unfinished project shared in the open while it is built. Expect breakage, missing pieces, and changes that break saves and APIs without notice. Do not depend on it for anything yet.

Linefire attract mode

(attract mode — the game playing itself; preview.webm for a sharper, smaller capture of the same clip)

Linefire is a 2D twin-stick shoot 'em up written in Go with Ebitengine, using a vector look with a CRT glow.

This repository contains the game and its editors:

  • the game (linefire): a 16-stage campaign of hand-authored maps ending in a boss lair; killing the boss opens a one-way portal into The Rift, an endless chain of procedurally generated screens played for score;
  • the unified editor (linefire-edit): a single window that lays out stages (walls, the player start, spawns, zones, portals) and, via a mode switch, edits the vector assets they reference; saving is automatic (Apple-style: on blur, Cmd+S and close);
  • the standalone asset editor (linefire-editor): opens one asset (ship, enemy, projectile, power-up) directly from the command line.

The game keeps the ship centered on screen always pointing up and rotates/translates the world around it; maps are authored top-down in plain 2D world coordinates, and that rotation is a runtime camera concern.

Try it in your browser

Open Linefire →

The game builds to WebAssembly (make wasm / make serve-web) and plays with keyboard/mouse or two thumbs (see Web build below). Desktop browsers run it smoothly; mobile Safari can still exhaust memory during a long session — a fix is waiting on a stable Ebitengine release (we track an alpha).

Data format: Filo

Everything is stored in the Filo language. Filo files are programs, not just data: (def w 64) (def half (/ w 2)) works inside any document, and maps can use geometry generators like (rect x y w h) and (circle cx cy r) that expand at load time.

Extension Document Model package
.lfa asset (ship, enemy, projectile, power-up) asset/
.lfm map/stage level/
.lfc procedural chunk cache procgen/

All I/O lives in filoio/ (parse + emit, with byte-for-byte round-trip tests); the model packages asset/ and level/ are pure structs, free of any serialization concern. The player's local config (audio, best times) is also Filo, at ~/.config/linefire/config.filo.

A minimal map looks like:

(level "map0001"
  (version 2)
  (title "Star Raiders")
  (size 1200 1000)
  (music "music/One_Heart_Remaining.mp3")
  (player-start 600 500 -90)
  (wall "walls"
    (stroke "#80ffff") (width 2) (fill "transparent") (glow 0.8)
    (rect 200 200 320 220))
  (spawn "enemy_1" "enemy" 840 110 90 (kind "enemy"))
  (spawn "boss" "tank" 450 150 -90 (kind "tank") (boss))
  (spawn "exit" "portal" 700 80 0 (kind "portal") (target "map0004"))
  (resolution "cleared" "portal" (target "@rift") (at 450 150))
  (editor (snap) (snap-radius 8) (grid) (grid-size 20) (snap-to-grid)))

(boss) marks an enemy as shielded until every other enemy dies. Resolutions fire once when their condition is met: spawn drops loot, exit warps, portal tears a portal open at a position, win ends the campaign. Portal targets starting with @ are generated at runtime: @bonus (a loot cave), @rift (the endless one-way chain).

Requirements

  • Go 1.26+
  • Ebitengine v2.10.0-alpha.x (downloaded automatically).

CGO_ENABLED=0 is enough on macOS and Windows; no C toolchain is required there. On Linux, Ebitengine needs cgo and the X11/OpenGL/ALSA development headers (see the Ebitengine install guide).

Running the game

The game data (the gameassets/ and music/ trees) is embedded in the binary with go:embed, so a built linefire runs standalone: no files alongside it, from any directory. -dir overrides that with a directory on disk (dev iteration or community content), pointing at a folder laid out like the repository root (gameassets/ beside music/).

go run -trimpath ./cmd/linefire              # run from the embedded game data
go run -trimpath ./cmd/linefire -dir .       # read gameassets/ and music/ from the cwd
go run -trimpath ./cmd/linefire -version     # print the build version and exit

Controls: arrows/WASD turn and thrust (S reverse, ArrowDown brakes), Q/E strafe, Shift afterburner boost, Space / mouse fire, Tab cycles aim modes, G toggles the combat computer's auto-fire, M full-screen automap, F6 mute, F2 render mode, F3 debug HUD, F11 fullscreen, Esc pause menu (with volume, restart and quit).

Touch (two thumbs). Each half of the screen is a floating stick that appears where the finger lands. Left thumb: push where you want to go — the ship turns there and thrusts while deflected. Right thumb: hold to fire the nose gun straight ahead; sweep past a short distance and it becomes the turret stick (the drag direction aims, firing while held). Tap the top-center bars to pause; on the title/game-over screens any tap continues. Keyboard and mouse stay live — the touch layer is additive.

Web build

The game runs in the browser (GitHub Pages): web/ holds the site — index.html (landing + iframe embed) and play.html (the canvas document) — and the Pages workflow builds linefire.wasm + copies wasm_exec.js on every push to trunk. Because all game data is embedded, the wasm module is the whole game (~26 MB, gzipped in transit).

The web build renders on a lighter profile (game/perf_js.go): the offscreen is capped at 1× — a phone's 3× device scale would make every full-screen pass ~9× the pixels and WebGL crawls — and the motion-blur budget is smaller. The browser upscales the frame, which the CRT-glow look absorbs. On a touch device, a THIRD finger (with both thumbs down) toggles the debug readout (FPS/TPS, blur, scale), the touch equivalent of F3.

make wasm        # build web/linefire.wasm + wasm_exec.js locally
make serve-web   # ...and serve web/ at http://localhost:8080 for a browser test

The world is destructible: shots and explosions dig through rock, and digging past a map's edge warps into a fresh, harder procedural screen (with a return portal home; The Rift has none). The HUD's ENEMIES counter leads the readout: at 0 the stage is clear and its exit opens. The full-screen automap (M) draws the tunnels you have carved as well as the charted walls, so a dug passage is not an invisible gap.

Running the editors

go run -trimpath ./cmd/linefire-edit                          # new map
go run -trimpath ./cmd/linefire-edit gameassets/map0001.lfm   # edit a stage
go run -trimpath ./cmd/linefire-edit -assets gameassets x.lfm # explicit palette dir
go run -trimpath ./cmd/linefire-editor gameassets/player.lfa  # one asset directly

Flags come before the file (standard flag ordering). -assets points at the directory scanned for the placeable-asset palette (default: the map file's directory). Saving is automatic (on blur, Cmd+S and close); a new file saves to linefire_level.lfm / linefire_asset.lfa until named via the save panel. Double-clicking an asset in the map editor's list opens it for editing in the same window; a map opens the map editor on it.

Map editor

Two toolbar rows: tools + edit actions on top, view + content actions below. The right panel shows properties, the asset palette (with a category filter that overlays the list) and the selection's fields.

1   select / move a handle (wall vertex, curve control, start, spawn, zone)
2   wall pen: click = corner, click-drag = curve; right-click / double-click /
    Enter finish an OPEN line; click the start vertex to close; Esc cancels
3   set player start
4   place a spawn (its category comes from the palette asset; Tab cycles)
5   add a zone; press 5 again to toggle rect/circle
6   delete node: click a vertex to remove it

Path editing (kutta model). Deleting a vertex CUTS the path open: a closed loop reopens there, an open path splits in two; the editor never heals the gap behind you. Open endpoints always show as red squares: in Select, click one (it rings amber, a rubber band follows the cursor), click another and they weld: the same path closes, two different paths splice into one. A click and a drag on an endpoint are distinguished by a small movement threshold, so endpoints still move normally. Shift+click on an edge inserts a vertex there and grabs it. The wall pen never snaps onto existing vertices (grid only), so drawing near another wall never glues to it.

Reference image. M (or the Ref img… button) loads a raster image (a screenshot of a classic map, a sketch) behind the canvas as a tracing template: Shift+M shows/hides it, -/= scale it, ,/. fade it. It is held in memory only and never saved into the map.

Del/Backspace  delete the selected node, else the one under the cursor
Tab  cycle the palette      [ ]  rotate selected spawn/start (Shift = fine)
G grid   Shift+G grid-snap  P snap-all   B glow   L wall color
N  negative-space preview (playable interior carved black)
I  import SVG (walls + labeled spawn markers)
F5  playtest: save and launch the game straight onto this map
wheel zoom   Space+drag / middle drag pan   arrows pan (Shift = faster)
0 home   F frame content   Cmd+Z / Cmd+Y undo/redo   S / Cmd+S save   Cmd+O open
Alt  hold for free sub-pixel placement (no snapping)

F5 playtests the open map: it saves, then launches go run ./cmd/linefire on that stage (-map <name>), so authoring and trying a screen is one key. The map must be named first, and the editor must be inside the repository (it runs the game from the module root). The game window opens beside the editor.

The map is unbounded: draw as far as you like; size is just the nominal "home" box that 0 frames. Walls reuse the asset layer format, so they render with the same vector + glow pipeline as ships. A closed wall is a solid; open paths render as plain lines (the negative-space preview and the game's flood fill need closed shapes; open endpoints stay flagged in red).

Asset editor

1   select / move a handle (vertex, origin, hardpoint, collision)
2   line / path pen (click = corner, drag = curve, click start = close)
3   set origin / center      4  add weapon hardpoint   5  add thruster hardpoint
6   collision tool; press 6 again to cycle shape (circle / rect / triangle)
7   delete: click a node to remove it

C / V   cycle stroke / fill color     L  color panel
;  '    decrease / increase current layer glow
O   hardpoint panel     U  sounds panel (gion events: fire, destroy, theme...)
G   grid   Shift+G snap-to-grid   P  snap every coordinate to the grid

Ctrl/Cmd+Z undo   Ctrl/Cmd+Shift+Z / Ctrl/Cmd+Y redo   Ctrl/Cmd+S save
wheel zoom   middle drag / Space+drag pan   0 reset view   F frame content

T   cycle transform scope (asset / layer / path)
H / J   flip horizontal / vertical (around the origin)
R   rotate 90°   Shift+R rotate 15°
B   toggle glow   Shift+B cycle variant (stable / pulse / laser)
K   toggle CRT post-process on the preview

Tab / Shift+Tab   next / previous layer     N add layer   Shift+N delete layer
PageUp / PageDown  move layer in draw order  Shift+H toggle layer visibility
arrows  nudge selected handle (Shift = 10 px)
[  ]    rotate selected hardpoint (Shift = 1°)
-  =    layer stroke width
Esc cancel   double-click / Enter / right-click  finish an open path
Backspace / Delete   remove the last point, or the selected node
Alt  free sub-pixel placement    Shift  constrain a segment to 0/45/90°

Layers are drawn in order (later on top), each with its own stroke, fill, glow and optional (hidden) flag. collision shapes are circle / rect / triangle. Sounds are gion recipes declared per asset event (fire, destroy, theme...), synthesized at runtime.

Snapping

Within snap-radius screen pixels of an existing point the editor snaps to it (highlighted yellow). The exception is the map editor's wall pen, grid-only by design. Anywhere else, coordinates land on the integer grid so saved files stay clean; with snap-to-grid on they round to grid-size multiples. Hold Alt/Option for free sub-pixel placement.

Glow

The canvas and preview show the CRT-style glow with a Kage shader: each visible layer's strokes are drawn into an emissive buffer scaled by its glow value, blurred, and added back over the crisp lines (B toggles, Shift+B cycles stable / pulse / laser). A subtle CRT post-process (curvature, scanlines, phosphor mask, aberration) applies to the asset editor's 1:1 preview only (K). This is the same renderer the game uses.

Project layout

cmd/linefire/            game runtime entrypoint
cmd/linefire-edit/       unified map/asset editor entrypoint
cmd/linefire-editor/     standalone asset editor entrypoint
cmd/linefire-svg2map/    SVG -> .lfm walls importer (CLI)
asset/                   asset model + validation (pure structs, no I/O)
level/                   stage model + validation (pure structs, no I/O)
filoio/                  Filo parse/emit for every document (.lfa/.lfm/.lfc, config)
game/                    game runtime (ship, combat, enemies, camera, fog, endless)
editor/                  asset editor (state, input, tools, drawing)
mapeditor/               map editor (state, input, tools, drawing)
editapp/                 unified editor shell hosting both editors
editorkit/               shared camera, dot grid, generic undo history
procgen/                 procedural generation (chunks, bonus caves, edge/Rift rooms)
render/                  shared vector renderer, glow and CRT shaders
sfx/                     gion sound recipes + music synthesis helpers
svgimport/               SVG parser (paths/shapes -> wall polylines)
config/                  player config model (audio, best times)
gameassets/              the shipped campaign: player/enemies/power-ups + map*.lfm
music/                   stage soundtrack MP3s
docs/                    campaign plan and other design docs (Portuguese)

Development

make run          # run the standalone asset editor (GUI)
make run-edit     # run the unified map/asset editor (GUI)
make run-game     # run the game runtime (GUI)
make build        # build cmd/* into bin/
make build-cross  # also build linux/amd64
make release      # standalone game binaries (embedded data) for the desktop matrix
make check        # go fix + vet + gofmt + test (verify flow)
make test         # run unit tests

make release cross-builds linefire for macOS (arm64/amd64) and Windows (amd64) with the data embedded and the version stamped in (linefire -version). Linux needs cgo for Ebitengine (X11/OpenGL), so its release binary is built on a Linux runner rather than cross-compiled.

See CONTRIBUTING.md for the full verify flow and conventions. TODO.md (Portuguese) tracks the roadmap; docs/campaign.md is the living campaign plan.

License

See LICENSE (MIT).

Documentation

Overview

Package linefire embeds the shipped game data so the executable runs standalone, with no external files. The embed directive can only reach files under its own directory, which is why this lives at the module root beside gameassets/ and music/ rather than in cmd/linefire.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Content

func Content() fs.FS

Content is the embedded game data: gameassets/ (player, enemies, power-ups and the map*.lfm campaign) and music/ (the MP3 themes). Names resolve the same way they do on disk — "gameassets/map0001.lfm", "music/One_Heart_Remaining.mp3" — so the game's fs.FS reads are identical whether they hit this bundle or a directory.

Types

This section is empty.

Directories

Path Synopsis
Package asset defines the Linefire vector asset model and its JSON representation.
Package asset defines the Linefire vector asset model and its JSON representation.
cmd
linefire command
Command linefire is the Linefire game runtime.
Command linefire is the Linefire game runtime.
linefire-edit command
Command linefire-edit is the Linefire editor: a single window that edits stage (level) files and, via a mode switch, the vector assets they reference.
Command linefire-edit is the Linefire editor: a single window that edits stage (level) files and, via a mode switch, the vector assets they reference.
linefire-svg2map command
Command linefire-svg2map imports an SVG drawing as a Linefire map's walls: draw the level geometry in Inkscape/Figma/Illustrator, run this, and hand-finish the header and spawns in the emitted .lfm.
Command linefire-svg2map imports an SVG drawing as a Linefire map's walls: draw the level geometry in Inkscape/Figma/Illustrator, run this, and hand-finish the header and spawns in the emitted .lfm.
linefire-ui-demo command
Command linefire-ui-demo is a sandbox for the ui package: a panel with a label, a button and an editable text field, so the immediate-mode toolkit can be exercised on its own before it is wired into the editors.
Command linefire-ui-demo is a sandbox for the ui package: a panel with a label, a button and an editable text field, so the immediate-mode toolkit can be exercised on its own before it is wired into the editors.
Package config is the model for the player's personal settings — today the audio volume and mute plus speedrun best times — kept separate from assets, levels and saves.
Package config is the model for the player's personal settings — today the audio volume and mute plus speedrun best times — kept separate from assets, levels and saves.
Package editapp hosts the Linefire map and asset editors in a single window.
Package editapp hosts the Linefire map and asset editors in a single window.
Package editor implements the minimal Linefire vector editor: an ebiten game with a large drawing canvas on the left and a real-size preview plus a properties panel on the right.
Package editor implements the minimal Linefire vector editor: an ebiten game with a large drawing canvas on the left and a real-size preview plus a properties panel on the right.
Package editorkit holds editor primitives shared by the asset editor and the map editor: a pan/zoom camera, a dot-grid renderer and a generic undo/redo history.
Package editorkit holds editor primitives shared by the asset editor and the map editor: a pan/zoom camera, a dot-grid renderer and a generic undo/redo history.
Package effects is Linefire's particle system: one pool that drives every cosmetic spray in the game — explosions, sparks, exhaust, debris — and the presets that describe them.
Package effects is Linefire's particle system: one pool that drives every cosmetic spray in the game — explosions, sparks, exhaust, debris — and the presets that describe them.
Package filoio reads and writes Linefire's documents in the Filo language.
Package filoio reads and writes Linefire's documents in the Filo language.
Package game is the Linefire runtime: it loads a player asset and a level and runs a minimal ship-in-a-world prototype.
Package game is the Linefire runtime: it loads a player asset and a level and runs a minimal ship-in-a-world prototype.
Package level defines the Linefire stage (phase) format.
Package level defines the Linefire stage (phase) format.
Package loot is the drop table: what a slain ship leaves behind, and how often.
Package loot is the drop table: what a slain ship leaves behind, and how often.
Package mapeditor implements the Linefire stage (phase) editor: a top-down 2D world editor for walls, the player start, enemy/power-up spawns and trigger zones.
Package mapeditor implements the Linefire stage (phase) editor: a top-down 2D world editor for walls, the player start, enemy/power-up spawns and trigger zones.
Package procgen generates the Linefire procedural map as a grid of chunks.
Package procgen generates the Linefire procedural map as a grid of chunks.
Package render draws Linefire assets with the ebiten vector package.
Package render draws Linefire assets with the ebiten vector package.
Package sfx holds Linefire's pure sound-synthesis helpers over gion, shared by the game runtime and the editors so a previewed sound is exactly the played one.
Package sfx holds Linefire's pure sound-synthesis helpers over gion, shared by the game runtime and the editors so a previewed sound is exactly the played one.
Package ship holds the profiles that make one hull fly differently from another: how much it takes, how far it sees, how fast it moves and shoots, and the range it likes to hold.
Package ship holds the profiles that make one hull fly differently from another: how much it takes, how far it sees, how fast it moves and shoots, and the range it likes to hold.
Package svgimport turns an SVG drawing into map wall outlines, so a level's geometry can be drawn in Inkscape/Figma/Illustrator and imported instead of typed by hand.
Package svgimport turns an SVG drawing into map wall outlines, so a level's geometry can be drawn in Inkscape/Figma/Illustrator and imported instead of typed by hand.
Package weapon is Linefire's weapon catalogue: what each weapon does, how often it can be used, what its shots look like and what they cost the target.
Package weapon is Linefire's weapon catalogue: what each weapon does, how often it can be used, what its shots look like and what they cost the target.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL