A hybrid FPS / RTS game built in Godot 4. Play as a ground soldier in first-person while commanding waves of minions and placing towers from a top-down RTS view. Supports single-player and multiplayer (up to 10 players via ENet). Three procedurally generated lanes connect two team bases across a 200Γ200 unit map.
A hybrid FPS/RTS game combining ground-based combat with top-down base management. Features:
- Single-player and multiplayer (up to 10 players via ENet)
- Player roles: Fighter (FPS + RTS) or Supporter (RTS-only)
- Dual-mode gameplay: first-person combat and RTS tower placement
- Procedurally generated terrain with diverse features
- Wave-based minion spawning with escalating difficulty
- Physics-based bullet system with realistic gravity
- Ballistic cannonball tower projectiles with splash damage
- Tower defense mechanics with auto-attack AI
- Team-based currency and resource management
- Dynamic lighting with shootable street lamps
- Fog of war shader overlay
- Time-of-day system affecting lamp behavior and visibility
- Skill system with Fighter (Guardian/DPS/Tank) and Supporter (Arsenal/Logistics/Defense) branches
| Component | Minimum | Recommended |
|---|---|---|
| OS | Linux (X11), Windows 10, macOS 12 | Linux |
| Godot | 4.4+ | 4.6.2 |
| Renderer | Forward+ (Vulkan) | Forward+ (Vulkan) |
| GPU | Vulkan 1.1 capable | Vulkan 1.3+ |
| VRAM | 2 GB | 4 GB+ |
| RAM | 4 GB | 8 GB |
| CPU | Quad-core 2.5 GHz | Any modern multi-core |
| Display | 1280Γ720 | 1920Γ1080 |
Vulkan is required. The project uses the Forward+ rendering backend. OpenGL / Compatibility mode is not supported.
Arch Linux (recommended)
sudo pacman -S godotUbuntu / Debian
# Godot 4.6+ is not in default apt repos β use Flatpak or download directly
flatpak install flathub org.godotengine.Godot
# or download from https://godotengine.org/downloadmacOS
brew install --cask godotWindows Download the standard (non-Mono) 64-bit installer from godotengine.org/download.
Verify your install:
godot --version
# expected: 4.x.x.stable...git clone <repo-url>
cd flanker
make # stop any running instance, launch, show logs| Command | Description |
|---|---|
make |
Stop + relaunch + show logs after 8s (default) |
make run |
Launch + show logs |
make stop |
Kill running instance |
make logs |
Print /tmp/flankers.log |
Logs are written to /tmp/flankers.log. On a successful launch you will see:
Terrain: verts=40401 seed=XXXXXXXXX plateaus=5 peaks=5 secret_paths=6 grass_left=true
DISPLAY=:0 godot --path /path/to/flanker > /tmp/flankers.log 2>&1 &Pre-built binaries are available from the build/ folder in the repository.
-
Download the appropriate zip for your OS:
- Linux: flanker-linux.zip
- Windows: flanker-windows.zip
-
Extract the zip to a folder of your choice.
Linux
Open a terminal in the extracted folder and run:
./flanker-linuxOr double-click the file in your file manager. If prompted, mark as executable.
Windows
Double-click flanker-windows.exe to launch the game.
Note: Vulkan 1.1+ is required. Ensure your GPU drivers are up to date.
| Key / Button | Action |
|---|---|
W A S D |
Move |
| Mouse | Look |
LMB |
Shoot |
RMB (hold) |
Zoom / ADS |
Space |
Jump |
Shift (hold) |
Sprint |
Ctrl (hold) |
Crouch |
Tab |
Switch to RTS mode |
Esc |
Pause menu |
| Key / Button | Action |
|---|---|
W A S D |
Pan camera |
Scroll |
Zoom |
LMB |
Place tower |
Tab |
Switch to FPS mode (Fighter only) |
Supporter role players start in RTS mode and cannot switch to FPS.
Tabis locked for both roles β Fighters stay FPS-only unless they tab switch; Supporters are RTS-only permanently.
- Two teams: Blue (south,
z=+82) and Red (north,z=-82) - Three lanes connect the bases: Left, Mid, Right
- Minions spawn in waves every 30 seconds, escalating in count each wave (cap 6 per lane per team)
- Destroy the enemy base to win
At game start, each player picks a role:
- Fighter β spawns as an FPS soldier. Can switch between first-person combat and RTS tower placement with
Tab - Supporter β RTS-only. No FPS body. Permanently in top-down view, placing towers and managing resources
Only one Supporter per team is allowed. The server enforces this β if two players try to claim Supporter on the same team, the second is rejected and must pick Fighter.
From the Start Menu, choose Host or Join:
- Host β opens a lobby on the specified port (default 8910). Other players join by IP
- Join β connects to a host by IP and port, enters the lobby
- Local Play β single-player, skips networking entirely
In the Lobby, players are auto-assigned to teams (balanced). Everyone hits Ready; the host starts the game. The server broadcasts a seed so all clients generate the identical map.
Up to 10 players supported. Server is authoritative for shot damage, minion state, tower placement, and team points. Respawn timers escalate with each death (5s base + 5s per prior death).
Each run is procedurally seeded β the map is different every game:
- Lanes β flattened dirt paths along cubic BΓ©zier curves, lit by alternating-side hanging street lamps, lined with wooden fence panels
- Mountain bands β rough terrain between
|x|15β80 on each side of the map - Secret paths β 6 narrow mountain trails (3 per side) cutting through the off-lane zones, good for flanking
- Plateaus β 5 elevated flat areas (~6β7 units high) per run, placed in mountain bands. Good sniper nests with sightlines down onto lanes
- Peaks β 5 impassable snow-capped spires (~22 units high) per run. Snow appears above ~13 units. Physically unreachable β jump height is 6 units
- Biomes β one side of the map is grass, the other desert, randomly assigned each run based on seed
- Cover objects β walls and crates scattered across 20 random clearings in off-lane areas
- All shots are physics-based projectiles with realistic gravity drop (
18 m/sΒ²) - Player bullets travel at 280 m/s, minion bullets at 120 m/s
- Tracer color: player = yellow-white, blue minions = blue, red minions = red
- Friendly fire is disabled
- Player has a 1.5s reload delay β a progress bar appears under the crosshair during reload and hides when ready
Towers fire ballistic cannonballs at enemy minions:
- Computed arc: x/z constant velocity, y component overcomes gravity over 2.5 second flight time
- Direct hit damage applies to the first collider struck
- Splash damage β 50% of direct damage, applied to all targets within 3 units of impact
- Friendly fire disabled via the same
_should_damage()pattern as bullets - Impact spawns a GPU particle burst
- Minions detect enemies at 12 units, begin strafing approach
- Stop and fire at 10 units; each has a unique strafe phase so crowds naturally spread out
- Separation steering prevents minions from stacking
- In darkness (no nearby lit lamp), detect range drops to 5 units and shots have a 60% miss chance
- Shoot out a lamp while being chased β minions lose track and bullets go wide
Lanes are lined with hanging street lamps procedurally placed along each curve.
- Time-of-day aware β lamps are on at sunrise, dusk, and night. At noon they stay off (daylight is sufficient)
- Shootable β aim at the bulb and shoot to destroy the light. Only the bulb has a hitbox; shooting the pole does nothing
- Flicker on shoot-out β the bulb flickers rapidly before going dark, simulating the filament dying
- Auto-respawn β shot-out lamps flicker back on after 15 seconds
- Tactical darkness β dark zones reduce minion detection range and accuracy. Use them to break pursuit or set up an ambush
Both edges of every lane are lined with procedural wooden fence panels:
- 20% random gaps β natural breaks in the fence line
- 15% of panels randomly sprout a torch:
OmniLight3D+ GPU particle flame effect - Torches are spaced at least 12 units apart (no torch clusters)
- Fence panels are
StaticBody3Don collision layer 2 β block movement but not bullets
WallPlacer scatters cover across 20 randomly placed clearings in the mountain/off-lane areas:
- Mix of walls (kenney_fantasy-town-kit) and crates (kenney_blaster-kit)
- Clearings avoid lane edges, secret paths, and base zones
- Each piece has a
StaticBody3Dcollision shape on collision layer 2
A full-map shader overlay at y=25 clears visibility circles around allied units:
- Up to 64 visibility sources: player position, allied minion positions, allied tower positions
- Each source has a configurable radius β player sees farther than minions
- Updated every frame via
FogOverlay.update_sources(...)
- Health bars appear above enemies when zoomed in (
RMBhold) - Only visible within 75 units
- Occluded by terrain and geometry β no health bars through hills or walls
On death, the player switches to RTS camera and waits:
- Base respawn time: 5 seconds
- Each subsequent death adds 5 more seconds (escalating penalty)
- In multiplayer, death counts are tracked server-side in
LobbyManager.player_death_counts
The game features a skill system for both player roles:
Fighter Skills:
- Guardian Branch: Field Medic (heal allies), Rally Cry (speed boost), Revive Pulse (full heal)
- DPS Branch: Dash (movement), Rapid Fire (weapon boost), Rocket Barrage (tower targeting)
- Tank Branch: Adrenaline (heal self), Iron Skin (damage shield), Deploy MG (turret placement)
Supporter Skills:
- Arsenal Branch: Build Discount (reduced costs), Turret Overdrive (tower speed boost), Advanced Launcher (unlock missile types)
- Logistics Branch: Fast Respawn (faster return), Ammo Drop (reload allies), Build Anywhere (no setback), Rally (team speed boost)
- Defense Branch: Tower HP (increased tower health), Repair (tower healing), Fortify (barrier durability), Point Surge (team points)
Each role has a unique skill tree with three branches. Skills are earned through leveling up and can be unlocked in the skill tree UI. Active abilities have cooldowns and can be assigned to hotbar slots for quick access.
flanker/
βββ Makefile
βββ project.godot # input actions, autoload singletons, renderer config
βββ assets/
β βββ day_environment.tres
β βββ dusk_environment.tres
β βββ night_environment.tres
β βββ FogOfWar.gdshader
β βββ ui_theme.tres
β βββ weapons/ # weapon preset .tres files
β βββ kenney_blocky-characters/ # GLB character models
β βββ kenney_fantasy-town-kit/ # wall, fence GLB models
β βββ kenney_blaster-kit/ # crate GLB models
β βββ kenney_pirate-kit/ # 3D models (optional)
β βββ kenney_ui-audio/ # UI sound effects
β βββ tower-defense-kit/ # tower models (optional)
βββ scenes/
β βββ Main.tscn # root game scene β all nodes wired here
β βββ StartMenu.tscn # host/join/local-play UI with cinematic camera
β βββ Lobby.tscn # pre-game lobby with team lists and ready checks
β βββ LoadingScreen.tscn # progress bar overlay during scene setup
β βββ RoleSelectDialog.tscn # Fighter / Supporter role picker
β βββ PauseMenu.tscn # Resume / Leave game overlay
β βββ FPSPlayer.tscn
β βββ RemotePlayer.tscn # ghost representation of a remote peer
β βββ Minion.tscn
β βββ Bullet.tscn
β βββ Cannonball.tscn # ballistic tower projectile
β βββ Tower.tscn
β βββ Base.tscn
β βββ WeaponPickup.tscn
βββ scripts/
βββ Main.gd # game manager, mode switching, wave announcements
βββ LaneData.gd # autoload β BΓ©zier curves, waypoints
βββ TeamData.gd # autoload β team points tracking
βββ NetworkManager.gd # autoload β ENet peer management
βββ LobbyManager.gd # autoload β lobby state, RPCs, game start orchestration
βββ GameSync.gd # autoload β in-game player state (health, teams, respawn)
βββ TerrainGenerator.gd # procedural mesh, collision, biomes, peaks, plateaus
βββ LaneVisualizer.gd # dirt ribbon meshes along lanes
βββ LampPlacer.gd # hanging street lamps along lane edges
βββ ShootableLamp.gd # per-lamp flicker + restore logic
βββ FencePlacer.gd # fence panels + torches along lane edges
βββ WallPlacer.gd # walls + crates in random off-lane clearings
βββ TreePlacer.gd # procedural trees along lane edges
βββ FogOverlay.gd # fog of war mesh + shader source management
βββ MenuCamera.gd # cinematic orbiting camera for start menu
βββ FPSController.gd # player movement, shoot, sprint, crouch, zoom, reload
βββ RTSController.gd # top-down camera, tower placement
βββ RemotePlayerManager.gd # creates/removes RemotePlayerGhost nodes
βββ RemotePlayerGhost.gd # lerp position/rotation, drive walk/idle animation
βββ MinionAI.gd # pathfinding, strafing, ranged shooting, separation
βββ MinionSpawner.gd # wave timer, escalating spawn counts
βββ Bullet.gd # projectile physics, raycast collision, friendly fire
βββ Cannonball.gd # ballistic arc, splash damage, impact particles
βββ TowerAI.gd # auto-attack enemy minions in range
βββ Tower.gd # tower setup, state machine
βββ BuildSystem.gd # RTS tower placement logic
βββ Base.gd # base HP, damage, win condition
βββ WeaponData.gd # weapon definitions for pickups
βββ WeaponPickup.gd # weapon pickup interaction
βββ EntityHUD.gd # per-entity health bars (zoom + LOS gated)
βββ RoleSelectDialog.gd # Fighter / Supporter role picker UI
βββ Lobby.gd # lobby screen UI and ready/start logic
βββ LoadingScreen.gd # progress bar overlay
βββ LoadingState.gd # global loading step reporter
βββ PauseMenu.gd # resume / leave game
βββ Minimap.gd # RTS minimap rendering
βββ SkillTree.gd # autoload β skill tree state, RPC, cooldowns, passive bonus queries
βββ SkillDefs.gd # autoload β static skill definitions
βββ FighterSkills.gd # Fighter skill execution
βββ SupporterSkills.gd # Supporter skill execution
- All scene and resource edits are done by hand in
.tscn/.tresfiles β there is no editor GUI workflow - Geometry is generated at runtime in
_ready()β no pre-baked meshes in the repo LaneDataautoload is the single source of truth for all lane positions β never hardcode lane coordinates elsewhere- Multiplayer authority: server is authoritative for shot damage, minion state, tower placement, and team points. Clients send requests; server validates and broadcasts results
- RPC sender ID:
multiplayer.get_remote_sender_id()returns0when the server calls an RPC on itself. Use the_sender_id()helper inLobbyManager(maps0β1) - Seed sync:
LobbyManager.start_gamebroadcasts a non-zero seed to all peers before scene change. Never allow seed=0 βTerrainGeneratorfalls back torandi()causing client/server map divergence - The project was developed against Godot 4.6.2 (system install). No
.NET/ Mono required - No external dependencies beyond Godot 4.6.2 engine
Run make test after every single code change β no exceptions.
make testThis runs the GUT headless suite. The suite must exit with no failing tests (the summary line will read ---- N pending/risky tests ---- with zero failures, or ---- All tests passed! ----). A change that introduces any new failure is not acceptable, even if the feature works at runtime.
Rules:
- Fix the test if the code is correct β with written justification for why the test was wrong.
- Fix the code if the test is correct β do not mark real regressions as
pending(). - Write new tests for every new feature or bug fix before the change is considered complete.
- Never silence a failure by deleting the test or converting it to
pending()without a documented known-bug justification.
Current baseline: 591 passing and 8 pending/risky (all intentional β documented known bugs or no-assert smoke tests). Any run dropping below 591 passing is a regression.
The tests verify data plumbing across all major systems:
| Area | What is tested |
|---|---|
TeamData |
Points add/spend/sync/guards |
LevelSystem |
XP, level-up, carry-over, attribute spend, bonus stats |
GameSync |
Health, damage, death, respawn, teams, ammo |
LobbyManager |
Registration, team balance, role slots, death counts, respawn timers |
TowerBase |
Setup, take_damage, friendly fire, death, XP, fire position, team detection |
MinionBase |
Setup, damage, death, slow debuff, puppet state, waypoints |
ProjectileBase |
Lifetime/expire, gravity, hit callbacks, friendly fire guard |
BuildSystem |
Item costs, spacing formula, team-half guard, placement validation |
Multiplayer RPCs |
Full RPC surface of LobbyManager β registration, shots, transforms, minion sync, tower sync, pings, level/XP |
Position/Visibility Sync |
Player transform broadcast, ghost creation/update, fog overlay sources, minimap fog |
| ENet loopback | Real connection, peer detection, seed broadcast, role accept/reject |
- Visual rendering on a real second Godot client
- GLB assets, animations, or particle effects displaying correctly
- Smooth lerp/interpolation of remote player ghosts on screen
- Audio playback
These are confirmed broken in the current codebase. Tests document them but do not fail on them:
_roles_pendingnever decrements on early disconnect βLobbyManager.gd:236-244- Respawn ignores level bonus HP β
LobbyManager.gd:452-455broadcasts flatPLAYER_MAX_HPinstead ofPLAYER_MAX_HP + bonus request_destroy_treeiscall_remoteβ host-fired bullets never destroy trees on the host side βLobbyManager.gd:694-705broadcast_player_transformdouble-emits on server βremote_player_updatedfires twice on the host βLobbyManager.gd:370-371- Remote players invisible on minimap β
RemotePlayerGhostis in group"remote_players"butMinimaponly queries group"player"βMinimap.gd - Minimap fog ignores allied remote player positions β fog only clears around the local player β
Minimap.gd
(End of file - total 478 lines)