ElixirHalfLife

Half-Life, played in a terminal, with every line of the engine in Elixir. The game code is FreeHL (ISC licence, built on Nuclide for the FTE engine), compiled QuakeC run on this repository's QuakeC virtual machine. The data comes from your own copy of Half-Life.

mix deps.get
mix half_life --dir "/path/to/Half-Life" --freehl /path/to/freehl
mix half_life --dir "/path/to/Half-Life" --freehl /path/to/freehl --map c1a0 --skill 2

--dir is the Half-Life install, the directory holding valve (Steam's steamapps/common/Half-Life). --freehl is a directory holding FreeHL's release package package_valve.pk3 (from FreeHL's releases page) or its unpacked files. HALF_LIFE_DIR and FREEHL_DIR set them too. The install must hold Half-Life's own valve/halflife.wad and valve/maps/c0a0.bsp; without them the game does not start. No Half-Life data is included.

On a terminal that keeps images (kitty, Ghostty, WezTerm, iTerm2) the picture is drawn at the picture width chosen in the game's options (480 pixels across to begin with) and the terminal scales it to the window, so a small font gives more room without a blurrier picture. --picture (or CAULDRON_PICTURE, or config :cauldron_2d_drafter, picture: "..." in a config/runtime.exs) chooses otherwise, with any of these comma separated: native draws at the game's own size times the largest whole number that fits the window, native:N at N times, size:WxH sets the size itself, png sends each frame as a PNG (about a third smaller, for play over ssh, at more CPU), swap replaces each frame without a blank between, and none draws at the window's cells instead. The default is native:1,swap.

The picture width in the game's options is the width the picture is drawn at under native, so it sets both the sharpness and the cost of a frame: the client program spends about 80 ms a frame on the game code whatever the size, and then more the more pixels it draws, so 320 to 480 pixels across plays at around 8 to 10 frames a second and 960 across at about 4. Under none that width is only a cap on what the window's cells ask for.

How it runs

Two QuakeC programs run, as they would under FTE:

What the engine draws:

Half-Life's fonts, sprite frames, TGA, BMP and WAD pictures serve the HUD.

Keys

Key Action
W S or ↑ ↓ forward, back
A D strafe
← → turn
T G or PgUp PgDn look up, down
C or End look straight ahead
F / V fire / alternate fire
Space / X jump / crouch
E use
R reload
L flashlight
Z walking: each press turns it on or off
Q / Tab previous / next weapon
1–6 weapon slot

The mouse preset adds turning and looking by moving the mouse, firing and alternate firing on the buttons, and weapon changes on the wheel. A controller works as listed in ElixirHalfLife.Client.

What is not here yet

The game itself (ElixirHalfLife.Game, on Cauldron's world at 20 steps a second) only turns the held keys into commands, passes them through the client program's input hook to the server, and places the server's sounds; neither program waits on the other.

Speed

Both programs run as Elixir compiled from their QuakeC (QuakeC.Compiler), compiled the first time and cached. On one Apple M-series machine a physics frame of c0a0, with its NPCs thinking, takes about 7 ms, inside FTE's 13 ms frame; a client frame at 320 by 200 takes 15 to 70 ms, most of it relighting surfaces whose light styles changed.

Starting a level takes about a second for the server program and a little more for the client program, once the map's files are read. Most of the server's second is FreeHL reading its sound definitions: each sound precached reads the definition files line by line in QuakeC, about 150 000 lines a level, as it does under FTE. A level's neighbours are read in the background while it is played, so a level change finds its map, models and sounds already parsed.

Licence

GPL-2.0-or-later, as it runs on quake_server. FreeHL is ISC. Half-Life's data belongs to Valve and is read only from your own copy.