This is a submission for the June Solstice Game Jam
On the June solstice the sun stays up longer than any other day of the year. Then it sets anyway. SOLSTICE is what happens after that: the longest night, one stone ring, and you holding the last flame.
Play SOLSTICE Β· no install, runs in the browser
What I Built
SOLSTICE is a 3D action survival game where daylight is not background flavor. It is the mechanic.
You are the Sunbearer, keeper of an ancient ring of standing stones. When the solstice sun goes down, shadow creatures crawl out to snuff your flame. You fight back with a glowing blade, dash through dark bolts, and vacuum up the light they drop when they break apart.
The whole run hangs on two meters:
Light is your health, your urgency, and your fuel. It ticks down over time and falls hard when something hits you. Let it hit zero and the night wins.
Dawn is how close you are to sunrise. It climbs as you survive and as you kill. Fill it to 100% and the sky actually changes: the solstice sun crests the stones, the shadows burn off, you win.
Your light is your life, your weapon, and the clock. When it runs out, the night wins.
I built this for the jam theme on purpose. Light, darkness, and time are not three separate ideas in the UI. They are one loop you feel in your hands: slash, dodge, collect, survive.
The arena is Stonehenge inspired. The real monument lines up with the solstice sunrise, so defending that ring felt right. The toughest wave and the Warden of the Long Night boss show up late, right before first light. Darkest before dawn is not just flavor text here.
Controls (laptop friendly, no gamepad needed):
| Input | Action |
|---|---|
W A S D or arrow keys |
Move |
Space or left click |
Light Slash |
Shift |
Dash with brief invulnerability |
E |
Solar Flare, charged AoE ultimate |
Esc |
Pause |
Three difficulties on the start screen: Acolyte (gentler), Sunbearer (default), Eclipse (two bosses, meaner spawns). You can swap difficulty from pause or the end screen if a run feels wrong.
Video Demo
Full run below: sunset, the swarm, the Warden, and the dawn break.
Playable build (single HTML file). You can try the CodePen embed below, but the web link is the best way to play: fullscreen, proper keyboard focus, and fewer iframe limits.
Play here (recommended): https://longphanquangminh.github.io/sunbearer/
Code
Whole game lives in one file. Repo:
SOLSTICE β Hold the Light
A browser-based 3D action-survival game built for the DEV June Solstice Game Jam (2026). You are the Sunbearer, last keeper of an ancient stone ring. As the solstice sun sets, shadow creatures pour from the dark β your Light is your health, your weapon fuel, and the clock. Survive the longest night and reach dawn.
The hook
Your light is your life, your weapon, and the clock. When it runs out, the night wins.
- Light drains over time and drops fast when enemies hit you. Slay shadows to collect light fragments and stay alive.
- Dawn fills as you survive. Hit 100% and the solstice sun crests the horizon β you win.
- The sky, fog, sun, and stars repaint in real time from sunset through midnight to golden dawn.
Controls
| Input | Action |
|---|---|
W A S D / Arrow keys |
Clone and serve locally if you want to poke around (needs HTTP, not file://):
git clone https://github.com/longphanquangminh/sunbearer.git
cd sunbearer
npx serve .
How I Built It
I gave myself a hard constraint: one self contained HTML file, no bundler, no downloaded assets, host it anywhere. Open the page, play. That shaped every decision after it.
Stack in short:
- Three.js r160 over WebGL, loaded with a native
importmapfrom jsDelivr -
UnrealBloomPassfor glow on the blade, orbs, and dawn light - Web Audio only: every sound is synthesized at runtime, zero audio files
- Procedural canvas textures for the ground and particles
- No external models or image assets
Three.js comes in clean with an import map, no build step:
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}
}
</script>
Postprocessing loads dynamically inside try/catch. If bloom fails on a weird GPU, the game still renders instead of dying on a black screen.
The sky is the progress bar
The Dawn meter does more than fill a UI bar. One value, dawn from 0 to 1, drives keyframe blending across sky color, fog, sun angle and intensity, ambient light, hemisphere light, and star opacity. You start at hot sunset, sink into violet dusk and deep midnight, then break into gold when you win. You can read how close sunrise is by looking up, not only by checking HUD numbers.
const SKY = [
// p, sky, fog, sun, sunInt, amb, hemi, stars
[0.00, 0xE8743B, 0x6e3a2e, 0xffb060, 1.5, 0.55, 0.60, 0.0],
[0.50, 0x0c1230, 0x0a1026, 0x6a78c0, 0.35, 0.32, 0.35, 1.0],
[1.00, 0xFFC56B, 0x9a6a3e, 0xffe0a0, 1.7, 0.70, 0.85, 0.0],
];
function updateDay() {
const p = dawn / dawnMax;
const [a, b, t] = segment(SKY, p);
scene.background.lerpColors(a.sky, b.sky, t);
scene.fog.color.lerpColors(a.fog, b.fog, t);
sun.intensity = lerp(a.sunInt, b.sunInt, t);
stars.opacity = lerp(a.stars, b.stars, t);
}
Combat and waves
Melee uses arc hit detection: range plus facing angle, knockback, combo multiplier on chained kills. Enemies are shades, fast wisps, ranged casters that make you move, and the Warden boss on a wave director that ramps up over the night.
Difficulty is a thin multiplier layer at run start: enemy HP, damage, speed, spawn rate, light drain, dawn duration, orb drops, enemy cap. Three distinct feels from one codebase without forking logic everywhere.
Performance on a laptop
Capped device pixel ratio, recycled sprite particle pool, blob shadows on most things with one real shadow casting sun, hard enemy cap. It should stay smooth on a normal laptop without asking for a gaming rig.
The design change that actually mattered
Early builds had separate HP and a survival timer. Too noisy. Merging them into Light made every choice sharper: dash away and let the meter tick, or push in for fragments that refill you. Tuning that drain vs reward loop took the most iteration, which is why I added selectable difficulty instead of pretending one balance fits everyone.
Prize Category
Submitting for Best Google AI Usage.
Google Gemini helped across the whole project, not just this writeup. I used it to generate and iterate on the game code (Three.js setup, combat, wave logic, sky pipeline, Web Audio synthesis), debug issues, and tune balance. I also used it to draft and polish this DEV submission so the theme and technical choices are easy to follow.
My role was directing the design (light as health, solstice theme, single file constraint), reviewing what Gemini produced, playtesting, and cutting or fixing what did not feel right in actual runs. The final game is a back and forth between my intent and Gemini's output, not something I typed out line by line solo.
Disclaimer: This project used Google Gemini for both game development and this blog post. I am noting that openly for transparency and because it fits the jam's Google AI prize category.
Hold the light until dawn. π
If you try it, I would love your high score and honest feedback in the comments. Happy solstice, whichever hemisphere you are in.
Fancy formatted version of this writeup: longphanquangminh.github.io/sunbearer/post
#DEVChallenge #gamedev #javascript #threejs #webgl

Top comments (1)
This one stands out because the 3D survival loop makes the light meter feel active: health, pressure, and fuel all at once. The Dawn meter gives the player a clean reason to keep moving instead of just surviving passively. If you keep iterating, I would add a small controls/camera section near the top, because browser 3D games live or die on whether the player trusts the movement immediately.