# magicGame — programmable spell arena (Godot 4.7) Open the folder in Godot 4.x and press Play. The game starts in single player; press F1 to host or join a Steam match. Steam must be running and signed in. ## Controls | Key | Normal mode | Spellwriting mode | | --- | --- | --- | | W A S D | move | (ignored, you keep drifting in the direction held when the editor opened) | | Space | jump | types into the editor | | Mouse | look / aim the wand | free cursor; the wand still aims at the point under it | | Left mouse | cast the active spell slot | edit / place the caret | | 1 2 3 4 | pick a spell slot | types digits | | Esc (or `) | open the spell editor | back to normal mode | | Tab / Shift+Tab | – | indent / unindent | | F1 | open the multiplayer panel | open the multiplayer panel | ## Multiplayer Press F1. Sessions run over Steam, so there are no ports or IP addresses: - **Host a game** creates a Steam lobby and hosts inside it. Tick **Public** first to have it listed in the in-game browser; left off, only your Steam friends can see it. - **Invite friends…** opens Steam's own invite dialog. Accepting an invite — from the overlay, the friends list, or a chat link — drops the guest straight into your game, launching it first if it was closed. - **Lobby or Steam id** takes either. Steam packs the account type into the id itself, so the panel works out whether you pasted a lobby or a person and connects the right way. **Copy** hands out the lobby id while you are hosting and your own Steam id otherwise. - **Public games** lists lobbies other people have opened; double-click one to join. Steam never lists the lobby you are already in, so your own game will not appear there. Up to eight players. The player list names everyone by their Steam persona. The host decides all outcomes: health, who got hit, and where a bolt exploded. Bolts fly at a constant speed, so each machine draws them from a few small messages rather than streaming positions. Health is replicated down from the host, so a client cannot heal or damage itself. Leaving a session, or losing the host, drops you back to single player. ### Steam setup `project.godot` initialises Steam with app id **480** — Valve's public Spacewar test app, which any Steam account can use. Swap `steam/initialization/app_data/app_id` for your own app id before shipping. Everything Steam-facing lives in `scripts/net/steam_session.gd`, autoloaded once per process. It initialises the API, pumps `Steam.run_callbacks()` each frame, and owns the lobby. Nothing else talks to the `Steam` singleton directly: that singleton outlives the scene tree, so connections made from shorter-lived nodes fire into freed objects during shutdown. Without Steam the game still runs — the panel says why multiplayer is unavailable and single player is unaffected. Note for anyone copying older GodotSteam snippets: in 4.22 the signature is `Steam.steamInitEx(app_id, embed_callbacks)`, not the older `steamInitEx(retrieve_stats, app_id)`. Passing the old argument order silently initialises the wrong app. ## Health and PvP Everyone has 100 health. **Self-damage is on**: your own fire pools and splashes hurt you, though a bolt will not detonate on the caster as it leaves the wand. Dying, or falling off the edge of the arena, respawns you after 3 seconds at a random spawn point. | Element | Direct hit | Area effect | | --- | --- | --- | | mana | 10 × power | – | | fire | 14 × power | a burning pool: 8 damage per second, 1–4 m wide, lasting 3.8–11 s | | water | 8 × power | a splash: 6 × power damage, shoves crates, puts out fires it reaches | | wind | 4 × power | a gust that throws crates, bolts and players | | heal | none | **restores 25 × power health** to everyone in the splash | `power` is `gather seconds / 2`, clamped to 0.2 … 2.0. A two second charge is a normal bolt and four seconds is the maximum. ## Spell slots Four slots, selected with keys 1-4 or the tabs above the editor. Each keeps its own source and they are saved to `user://spells.cfg` between sessions. The HUD shows which slot the left mouse button will cast. ## Spell language ``` import wand import mana bolt = mana.gatherPoint(wand.location()) bolt.gather(2) bolt.fire(wand.normal) ``` Loops, conditions, elements and world coordinates: ``` import wand import mana import world for i in range(3): bolt = mana.gatherPoint(wand.location()) if i == 0: bolt.infuse(fire) elif i == 1: bolt.infuse(heal) else: bolt.infuse(wind) bolt.gather(1 + i) bolt.fire(world.normal.y * -1) wait(0.5) ``` | Syntax | Meaning | | --- | --- | | `import wand` / `mana` / `world` / `fire` … | acknowledged; everything is always available | | `name = `, `name += `, `name -= ` | store a value | | `if :` / `elif :` / `else:` | conditions; blocks are indented (4 spaces or a tab) | | `for name in range(stop)` / `range(start, stop[, step])` | counted loop | | `while :`, `break`, `continue`, `pass` | loops; a spell running over 200 000 steps without waiting is aborted | | `+ - * / %`, `== != < <= > >=`, `and or not`, `True False` | arithmetic and logic (`/` always gives a decimal) | | `wait(seconds)` | pause the spell (the game keeps running) | | `wand.location()` / `wand.normal` | wand tip position / firing direction, sampled when the line runs | | `world.pos(x, y, z)` | an absolute point in the arena | | `world.normal.x` / `.y` / `.z` | unit direction vectors, so `world.normal.y * -1` is straight down | | `world.up`, `world.down`, `world.zero`, `world.gravity` | handy constants | | `mana.gatherPoint(pos)` | spawn a neutral spell sphere at `pos` | | `bolt.gather(seconds)` | charge; time accumulates. `bolt.gather(wait(s))` is the same | | `bolt.infuse(fire)` / `water` / `wind` / `heal` | give the sphere an element (before firing) | | `bolt.fire(direction)` | launch the sphere | | `bolt.power`, `bolt.element`, `vec.x .y .z` | read-only attributes | | `# comment` | ignored | Errors appear as `Spell Error on line N: ...` and stop only that spell. ## Layout - `main.tscn` — the arena, players container, networking nodes and UI - `scenes/player.tscn` — one player, spawned per peer and named after its peer id - `scripts/game.gd` — modes, input routing, slot selection, casting - `scripts/player.gd` — movement, camera, aiming, health, death and respawn - `scripts/spell_interpreter.gd` — tokenizer, block parser, async executor - `scripts/spell_object.gd`, `scripts/spell_impact.gd` — the bolt and its flash - `scripts/effects/` — `fire_area.gd`, `water_splash.gd`, `wind_gust.gd`, `heal_splash.gd`, `effect_utils.gd` - `scripts/net/` — `steam_session.gd` (Steam API, callbacks and lobbies; autoloaded), `net_manager.gd` (sessions and player spawning), `spell_net.gd` (spell replication), `prop_sync.gd` - `ui/` — `spell_editor.gd`, `hud.gd`, `multiplayer_panel.gd` - `tests/run_tests.gd` — headless checks: `godot --headless --path . -s res://tests/run_tests.gd` The suite covers the Steam path too: it creates a real friends-only lobby, checks the session comes up on a `SteamMultiplayerPeer`, and leaves again. With no Steam client those checks report `SKIP` instead of failing. Replication itself is tested by running two peers in one process over ENet (`NetManager.host_direct()` / `join_direct()`), which a single signed-in Steam account cannot do; nothing in the game uses those two functions.