Aistea Kingdoms – building a browser-based 3D strategy game with Vanilla JS and Three.js
A game I built – and what's inside
Aistea Kingdoms is a browser-based 3D strategy game: build your castle, repel siege waves, climb the leaderboard. No app store, no installation – plays directly in the browser, including on mobile. The tech behind it is surprisingly lean.
No framework – Vanilla JS with Vite
The entire game runs on plain, modular JavaScript (ES modules) – no React, no Vue, no Angular. That keeps load times short and gives me full control over every interaction. Vite handles the build as a multi-page setup: four independent entry points – landing page, game, profile page, and admin area – each with its own HTML entry.
No framework overhead, no abstraction layers to learn first. What's in the code is what runs in the browser – directly and transparently.
Three.js – a 3D world in the browser
The visual core is Three.js. It renders the castle, landscape, troops, and battle effects directly in the browser via WebGL – no plugin, no app installation required. The style is intentionally low-poly: it keeps the scene performant enough for the mobile web without sacrificing atmosphere.
For those new to Three.js: it's a JavaScript library that abstracts the raw WebGL API and makes 3D scenes, cameras, materials, and animations approachable.
The castle builder: raycasting + grid
Placing buildings uses classic raycasting: a THREE.Raycaster fires a ray from the mouse cursor into the 3D scene, hitting an invisible ground plane. The hit point is snapped to a 2×2-unit grid:
Math.round(x / 2) * 2
That creates the clean tile grid you build on. While a building is selected, a semi-transparent ghost mesh follows the cursor – turning green or red in real time depending on whether the spot is valid (collision with other buildings, insufficient resources, troops in the way).
Walls can be placed in a full row by clicking, holding, and dragging. Weapons like a ballista or cannon can be mounted directly onto existing wall segments – the same raycast checks whether the cursor is over a wall rather than open ground.
Siege waves: state machine + A* pathfinding
Two layers control how an attack plays out:
Layer 1 – state machine: A simple state machine at the top level manages the game flow: build → battle → next round, plus the terminal states won / lost. It decides when building is allowed and when defences fire automatically.
Layer 2 – A* pathfinding: Enemy movement runs an A* algorithm with a custom priority queue (min-heap) over the same grid the builder uses. Enemies find the shortest path to the castle – navigating around walls, but targeting gates specifically because they count as weak points.
When a wall is destroyed or newly built, an internal navigation version counter increments. This invalidates all affected cached routes – enemies recalculate mid-battle rather than blindly following a stale path. Attacks themselves use a simple windup/cooldown timing per unit (a short preparation before the hit, then a cooldown period).
Backend: PHP + MySQL, no framework
On the server side, a deliberately slim PHP backend without a framework (no Laravel, no Symfony) runs against a MySQL/MariaDB database. It handles user accounts, session auth, and the global leaderboard.
For guests and offline scenarios there's a localStorage fallback – you can play without an account at all. Progress syncs when you log in.
This leanness keeps hosting cheap and low-maintenance: a classic shared hosting setup is all it takes.
Tests & deployment
Game logic is covered by Node's built-in test runner; real browser flows (login, auth) are tested with Playwright. Deployment is unglamorous but reliable: a script builds the project and syncs it via rsync/SSH to the webspace – no cloud infrastructure, no Kubernetes, no overhead.
What surprised me: the CSS cascade
Honestly, the toughest opponent wasn't the pathfinding – it was the CSS file. Over many dev sessions, everything got appended to the end of one big file, densely packed, no structure. The result: a new mobile rule was technically correct but got overridden by an earlier, unconditional rule with the same specificity.
In CSS, when specificity is equal, whatever appears lower in the file wins – media query or not.
That sent me down the wrong path more than once, until I wrote a small script to trace the actual cascade order. Valuable lesson learned.
The philosophy behind it
As few dependencies as possible, as much direct control as possible – a stack that does exactly what the game needs, and nothing more. No framework to learn first. No cloud setup eating money before the first player shows up.
The result: a 3D strategy game that runs directly in the browser – on desktop and mobile alike. Try it yourself.
Share
Leave a comment