Skyland
π mission codename: skylands survival β funday plugin blueprint for a coding agent
-
π― objective
- deliver a web-first, 1β4 player co-op craft-defense game as a funday plugin
- integration types: iframe-themeable client and dedicated-server (nakama) via agones
- target pilot: 10k dau, 12β20 min sessions, p95 ws rtt β€ 120 ms in eu-central
- monetization: cosmetics + season pass (no pay-to-win)
- art: low-poly stylized; device coverage: desktop first, mobile capable via pwa
-
πΊοΈ scope of work
- build plugin-compliant client with theme messaging, pwa shell, and realtime netcode
- run nakama authoritative servers as agones game servers with autoscaling
- provide data schema, liveops hooks, analytics taxonomy, and infra automation
- ship developer tooling, ci/cd, observability, load & e2e tests, security hardening
-
ποΈ funday plugin architecture alignment
-
dynamic serving via /games/[β¦path]/+server.ts
-
plugin types used
- iframe-themeable for the web client ui and game loop
- dedicated-server for the nakama authoritative match runtime under agones
-
single source of truth: funday-plugin.json manifest (client) and server manifest (server)
-
-
π repo & directory layout (monorepo)
games/ ββ sky-islands-survival/ β ββ funday-plugin.json # client plugin manifest β ββ index.html # iframe entry (bootstraps sveltekit app) β ββ assets/ # images, audio, fonts, hdr, glb/ktx2 β β ββ images/ β β ββ models/ β β ββ textures/ β β ββ audio/ β ββ client/ # sveltekit + threlte β β ββ src/ β β β ββ app.html β β β ββ routes/+layout.svelte β β β ββ lib/game/ β β β β ββ core/ # ecs, scheduler, rng, fixed-step β β β β ββ net/ # nakama client, clock sync, interp β β β β ββ scenes/ # loading, hub, match β β β β ββ systems/ # input, camera, building, vfx β β β β ββ ui/ # hud, chat, shop, settings β β β β ββ content/ # recipes, drops, tech-tree (data) β β β β ββ assets/loader.ts # ktx2/draco pipelines β β ββ static/ # pwa icons, manifest.webmanifest β β ββ vite.config.ts β β ββ package.json β ββ server/ # nakama authoritative server β β ββ funday-server.json # dedicated-server manifest β β ββ modules/ # go modules for match/economy β β β ββ match/ β β β ββ economy/ β β β ββ rpc/ β β ββ sql/ # migrations β β ββ Dockerfile β β ββ Makefile β ββ ops/ β β ββ docker-compose.dev.yml # local: nakama, cockroach, redis, satori β β ββ k8s/ # agones GameServer, fleet, cm, svc, hpa β β ββ terraform/ # cluster, crdb, redis, namespaces β β ββ grafana-dashboards/ β ββ tests/ β β ββ e2e/ # playwright β β ββ load/ # k6 websocket scenarios β β ββ bots/ # headless match bots β ββ scripts/ # ci helpers, asset pipeline β ββ README.md -
π§Ύ funday client manifest (funday-plugin.json)
{ "name": "sky-islands-survival", "version": "1.0.0", "integrationType": "iframe-themeable", "theme": "funday-dark", "metadata": { "title": "Sky Islands Survival", "description": "Co-op craft-defense on floating islands.", "genre": ["Co-op", "Survival", "Crafting"], "maxPlayers": 4, "minPlayers": 1, "thumbnail": "/images/games/sky-islands.svg", "screenshots": ["/images/games/sis-1.png", "/images/games/sis-2.png"] }, "deployment": { "resources": { "cpu": "100m", "memory": "128Mi" } }, "api": { "endpoints": ["/rpc/season/progress", "/rpc/shop/roll", "/rpc/cosmetics/equip"], "webhooks": [] } } -
π§Ύ funday dedicated-server manifest (server/funday-server.json)
{ "name": "sky-islands-survival-server", "version": "1.0.0", "integrationType": "dedicated-server", "image": "registry.example.com/funday/sis-nakama:1.0.0", "agones": { "fleet": { "replicas": 2, "maxReplicas": 20 }, "ports": [{ "name": "nakama", "containerPort": 7350, "protocol": "UDP" }] }, "deployment": { "resources": { "cpu": "500m", "memory": "512Mi" }, "env": [ { "name": "DB_URL", "valueFrom": "secret:crdb_url" }, { "name": "REDIS_ADDR", "valueFrom": "secret:redis_addr" }, { "name": "SATORI_API_KEY", "valueFrom": "secret:satori_key" } ] } } -
π¨ theme integration in iframe
<!doctype html> <html lang="en" data-theme="funday-dark"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <script src="https://cdn.tailwindcss.com"></script> <link href="https://cdn.jsdelivr.net/npm/daisyui@5.1.12/dist/full.css" rel="stylesheet" /> </head> <body class="bg-base-100 text-base-content"> <div id="app"></div> <script type="module" src="/client/src/main.ts"></script> <script> window.addEventListener("message", (event) => { if (event?.data?.type === "funday:theme-inject") { const { theme, colors } = event.data document.documentElement.setAttribute("data-theme", theme) window.dispatchEvent(new CustomEvent("funday-theme", { detail: { theme, colors } })) } }) if (window.parent !== window) { window.parent.postMessage( { type: "funday:game-ready", game: "sky-islands-survival" }, "*", ) } </script> </body> </html> -
π§ gameplay brief
- loop: gather β craft/fortify β night raid defend β extract/checkpoint β repeat
- coop: drop-in/out up to 4; private party or public matchmaking
- persistence: base blueprint, tech tree, cosmetics; seasonal island seeds
-
π realtime netcode
- transport: websocket (json mvp β protobuf later), 20 hz server tick, client fixed-step 60 fps with interpolation
- authority: server authoritative for resources, build placement, enemy ai, damage; client predicts movement and building ghosting
- time sync: rtt/clock skew smoothing, input seq with ack, 120β200 ms buffer on mobile
- interest management: per-client entity culling by island cell and distance
-
π°οΈ nakama server modules (go)
-
match loop responsibilities
- integrate inputs β validate β simulate ai β produce state deltas
- craft/build validators (grid snaps, resource consumption with versioned writes)
- raid director scaler (team power, wave cadence, biome modifiers)
- checkpoint to db each wave end and on extract
-
rpc endpoints
- claim daily, cosmetics equip, shop roll, season progress
-
storage
- users, profiles, inventories, cosmetics, islands, recipes, seasons
-
caching
- redis for presence, cooldowns, ephemeral match snapshots
-
-
π€ websocket opcodes
1 join_match { partyId, loadout } 2 input { seq, axes:{x,y}, actions:{build:bool, attack:int, use:int}, ts } 3 state_delta { authoredFrame, seqAck, ents:[{id,t,p,r,hp,...}], events:[{type,...}] } 4 craft_request { recipeId, inputsVer } 5 build_place { blueprintId, grid:{x,y,z}, rot, ghostId } 6 chat { channel:'team'|'party', text } 7 extract_request {} 8 emote { id } 9 ping { t } 10 pong { t } -
ποΈ data model (sql sketch)
create table users ( id uuid primary key, created_at timestamptz not null default now(), auth_provider text not null, region text ); create table cosmetics ( user_id uuid references users(id), slot text, -- 'head','body','tool' item_id text, equipped boolean default false, primary key (user_id, slot, item_id) ); create table inventories ( user_id uuid references users(id) primary key, version int not null default 1, items jsonb not null default '[]' ); create table islands ( id uuid primary key, owner_user_id uuid references users(id), mode text, -- 'private','shared' seed bigint not null, biome text, tech_level int not null default 0, blueprint jsonb not null default '{}' ); create table seasons ( id text primary key, config jsonb not null, starts_at timestamptz, ends_at timestamptz ); create table leaderboards ( season_id text references seasons(id), category text, user_id uuid references users(id), score int, updated_at timestamptz default now(), primary key (season_id, category, user_id) ); -
π§° client architecture (sveltekit + threlte)
-
core
- fixed-step scheduler (dt buckets), rng seed per match, deterministic input packing
- ecs-lite pattern: components, systems, queries; render separated from sim
-
rendering
- threlte/three scene graph, lod tiers, pooled vfx, post chain minimal
- ktx2 textures, draco gltf; lazy stream content by island cell
-
physics
- rapier-wasm for local feedback only; server collision is primitive validation
-
ui/ux
- daisyui components; hud with health, wave timer, inventory, quick craft bar
- chat with quick pings; settings panel; responsive with safe-area insets
-
storage
- local slots for options; resume checkpoint id
-
pwa
- workbox; offline shell; background asset warm-up; install prompt gating
-
-
π§ͺ testing strategy
-
unit
- go: match rules, economy math
- ts: input packing, clock sync, reconciler
-
integration
- headless nakama with bots; 100 simulated matches in ci nightly
-
e2e
- playwright: join/leave, build, defend, extract on chromium, firefox, webkit
-
load
- k6 websocket: soak 2k concurrent per node; scenarios for spikes, churn
-
desync detection
- snapshot/restore harness; divergence alerts if checksum mismatch
-
-
π§° developer commands
# bootstrap make deps # run local stack docker compose -f ops/docker-compose.dev.yml up --build # run client dev server pnpm --filter @sis/client dev # build client pnpm --filter @sis/client build # run nakama tests make -C games/sky-islands-survival/server test # e2e pnpm --filter @sis/tests e2e # load test k6 run tests/load/ws_soak.js -
π§ͺ sample k6 websocket snippet
import ws from "k6/ws" import { check, sleep } from "k6" export const options = { vus: 500, duration: "5m" } export default function () { const url = "wss://server.example.com/ws" ws.connect(url, {}, (socket) => { socket.on("open", () => { socket.send(JSON.stringify({ op: 1, partyId: null })) }) socket.on("message", (d) => { /* track pings/state */ }) socket.setInterval( () => socket.send(JSON.stringify({ op: 2, seq: Date.now(), axes: { x: 0, y: 1 } })), 50, ) socket.setTimeout(() => socket.close(), 60000) }) } -
π§© nakama go match loop skeleton
func MatchLoop(ctx context.Context, logger runtime.Logger, nk runtime.NakamaModule, initialState *State, tick int64) (*State, []runtime.MatchDataSend, string) { s := *initialState applyInputs(&s, tick) simulateAI(&s, tick) deltas := buildDeltas(&s, tick) msgs := []runtime.MatchDataSend{{ OpCode: 3, // state_delta Data: deltas, Presences: s.Presences, }} if shouldCheckpoint(tick) { checkpoint(ctx, nk, &s) } return &s, msgs, "" } -
π security & privacy
- iframe sandbox: allow-scripts, allow-pointer-lock, disallow top nav; csp strict with asset whitelists
- xss protection: sanitize chat; never inject raw html from network
- storage: no third-party cookies; localStorage limited to client settings; gameplay data via server only
- api hardening: rate limits per ip/user; signed inputs are not requiredβuse server authority instead
- auth: guest β email link/oauth; rotate tokens; logout invalidates refresh
- gdpr: data export and delete rpc; pii minimization
- cheat resistance: server validates damage, resource spends, build placements; replayable inputs; anomaly detection via telemetry
-
π analytics & liveops (satori)
-
event taxonomy
- session_start, session_end, match_join, match_end, craft, build_place, damage_dealt, damage_taken, raid_wave, extract, shop_open, purchase, cosmetic_equip
-
properties
- user_id, party_size, biome, tech_level, wave, build_id, recipe_id, ping_ms, fps, region
-
flags
- drop_rates_vX, raid_scaler_vY, shop_rotation_vZ
-
dashboards
- retention d1/d7; funnel to wave 3; economy sinks/sources; latency heatmaps
-
-
πΈ monetization flows
-
cosmetics store
- soft currency earn; hard currency purchase (platform-compliant)
- preview in scene; equip persists in cosmetics table
-
season pass
- free/premium tracks; xp from raids and dailies; weekly challenges feed
-
compliance
- parental gate for purchases; receipt verify server-side; refunds revoke entitlements
-
-
π― performance budgets
- initial load β€ 3.0 mb gz main + β€ 10 mb assets streamed; first interactive β€ 6 s on mid-tier mobile over 4g
- frame: β€ 8 ms cpu main thread budget at 60 fps on desktop; 30 fps fallback mobile
- draw calls β€ 120; triangles β€ 400k in view; texture atlas usage; gpu timers tracked
- network: β€ 18 kb/s avg per client during combat; delta comp; interest culling
-
π observability
-
metrics
- server: tick duration, backlog, ws rtt p50/p95, match count, errors
- client: fps, cpu time, gc pauses, ws rtt, reconnects
-
logging
- structured json; correlate by match_id, user_id, seq
-
tracing
- opentelemetry spans: join β match loop β checkpoint β extract
-
alerts
- p95 rtt > 180 ms 10 min, match errors > 1%, auth failures surge, memory > 80%
-
-
π ci/cd pipeline (github actions sketch)
name: sis-ci on: [push, pull_request] jobs: client: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v2 - run: pnpm i --frozen-lockfile - run: pnpm -r build - run: pnpm -r test - uses: actions/upload-artifact@v4 with: { name: client-dist, path: games/sky-islands-survival/client/build } server: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: { go-version: "1.22.x" } - run: make -C games/sky-islands-survival/server test - run: docker build -t registry/sis-nakama:${{ github.sha }} games/sky-islands-survival/server - run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login -u token --password-stdin registry - run: docker push registry/sis-nakama:${{ github.sha }} e2e: needs: [client, server] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: pnpm -r e2e -
βοΈ agones/k8s excerpts
apiVersion: "agones.dev/v1" kind: Fleet metadata: { name: sis-nakama } spec: replicas: 2 template: spec: ports: [{ name: nakama, containerPort: 7350 }] template: spec: containers: - name: nakama image: registry.example.com/funday/sis-nakama:1.0.0 env: - name: DB_URL valueFrom: { secretKeyRef: { name: crdb, key: url } } - name: REDIS_ADDR valueFrom: { secretKeyRef: { name: redis, key: addr } } - name: SATORI_API_KEY valueFrom: { secretKeyRef: { name: satori, key: api_key } } -
π§© theme communication examples
// client/src/lib/theme.ts export function bindFundayTheme() { window.addEventListener("funday-theme", (e: any) => { const { theme, colors } = e.detail document.documentElement.setAttribute("data-theme", theme) // propagate to shaders/materials if needed }) } -
πΈοΈ nakama js client bootstrap
import { Client, Session } from "@heroiclabs/nakama-js" const client = new Client("defaultkey", location.hostname, "7350", location.protocol === "https:") let session: Session export async function connect() { session = await client.authenticateDevice(`device-${crypto.randomUUID()}`, true) const socket = client.createSocket(false) await socket.connect(session) return socket } -
π§± build placement validation (go)
func ValidateBuildPlace(s *State, u UserID, bp BuildPlace) error { if !HasResources(s, u, bp.BlueprintId) { return errNoResources } if !GridAligned(bp.Grid) { return errGrid } if OverlapsRestricted(s, bp) { return errOverlap } return nil } -
π§΅ asset pipeline commands
# convert textures to ktx2 npx ktx2 --uastc 4 assets/textures/src/**/*.png -o assets/textures/ktx2/ # draco compress models npx gltf-pipeline -i assets/models/src/ -o assets/models/ -d # generate asset manifest with hashes node scripts/mk-manifest.mjs -
π§° local testing quickstart
# 1) start infra docker compose -f ops/docker-compose.dev.yml up -d # 2) run nakama with modules and db migrations make -C games/sky-islands-survival/server migrate run # 3) run client dev pnpm --filter @sis/client dev --open # 4) open http://localhost:5173/games/sky-islands-survival/ -
π§ͺ platform route smoke test
curl http://localhost:5173/games/sky-islands-survival/index.html | head -
π‘οΈ security checklist
- csp headers set; only self cdn and allowed font domains
- iframe sandbox without top navigation; no storage access to parent
- input sanitation for chat and names; profanity filter
- rate-limit rpc; captchas on abusive create-party bursts
- db roles least privilege; rotate creds; daily backups
-
π§ accessibility & localization
- ui contrast meets wcag aa in dark and light themes
- scalable text, rem-based sizing; focus states; keyboard navigation in menus
- i18n json catalogs; rtl safe layouts; number/date regionalization
-
π§― runbooks
-
incident triage
- elevated ws rtt: check regional saturation, fleet replicas, redis latency
- desync spike: roll back latest match module; inspect checksums
- crash loops: fetch pod logs, inspect ooms, reduce wave size flag
-
rollback
- blue/green modules; toggle feature flags; drain agones fleet
-
-
π§° publishing steps
- validate funday-plugin.json with schema
- verify theme messaging and responsiveness
- optimize assets and regenerate manifest
- run e2e across chromium/firefox/webkit
- tag client and server releases; push images; apply agones fleet update
- register game in platform catalog and provide screenshots/readme
-
β production readiness checklist
- plugin manifests present and valid
- perf budgets met on mid-tier mobile and desktop
- p95 rtt < 120 ms eu; error rate < 1%
- observability dashboards populated; alerts green for 24h burn-in
- load test at 2k ccu per node passes; no memory leaks
- security review complete; backups verified; dr run tested
-
π§© funday modal integration notes
- set integrationType to iframe-themeable in client manifest
- respond to funday:theme-inject events
- post funday:game-ready when boot complete
- emit status events optionally: { type:βfunday:game-statusβ, status:βloading|ready|errorβ }
-
ποΈ example ui snippet (daisyui)
<div class="p-3"> <div class="stats shadow"> <div class="stat"> <div class="stat-title">Wave</div> <div class="stat-value">{wave}</div> </div> <div class="stat"> <div class="stat-title">Resources</div> <div class="stat-value">{wood}</div> </div> </div> <button class="btn btn-primary mt-3" onclick={startMatch}>Start</button> </div> -
π§ coding standards
- typescript strict, eslint + prettier; go vet + staticcheck
- commit style conventional commits; trunk-based development
- zero any types; explicit json schemas; pb later for binary
- deterministic sim inputs; never trust client side collisions
-
π fun day inspirations
- daily island seed remix hour for boosted drops
- photo mode with postcard export
- glider races between waves for mini-events
- cozy campfire lobby with emotes before matchmaking
-
π acceptance criteria
- player can host/join match, build defenses, survive wave 3, extract with rewards
- theme integration responds instantly; pwa installable; reconnect after network blip
- analytics events fire and appear in dashboards
- ci passes unit/integration/e2e; load test within budgets
- publishing checklist fully green
-
π§© appendices
-
client bootstrap main.ts
import App from "./app/App.svelte" import { bindFundayTheme } from "$lib/theme" bindFundayTheme() const app = new App({ target: document.getElementById("app")! }) export default app -
docker-compose.dev.yml excerpt
services: cockroach: image: cockroachdb/cockroach:latest command: start-single-node --insecure ports: ["26257:26257", "8080:8080"] redis: image: redis:7 ports: ["6379:6379"] nakama: image: heroiclabs/nakama:3 depends_on: [cockroach, redis] ports: ["7350:7350", "7351:7351"] volumes: ["../server/modules:/nakama/data/modules", "../server/sql:/nakama/data/sql"] -
service worker register
if ("serviceWorker" in navigator) { window.addEventListener("load", () => navigator.serviceWorker.register("/service-worker.js")) } -
json schema example for input
{ "$id": "sis.input.v1", "type": "object", "properties": { "seq": { "type": "integer", "minimum": 0 }, "axes": { "type": "object", "properties": { "x": { "type": "number" }, "y": { "type": "number" } }, "required": ["x", "y"] }, "actions": { "type": "object" }, "ts": { "type": "integer" } }, "required": ["seq", "axes", "ts"], "additionalProperties": false }
-
-
𧨠go/no-go rule
- only merge to main when production readiness checklist is met and burn-in can start within 24 hours with oncall scheduled
-
π§ end of briefing
- start with local stack, bring up the client, verify theme handshake, connect to nakama, run a 4-player bot match, then proceed to load and e2e gates
- keep the core principles: web-first performance, server authority, small delightful loops, and liveops-friendly content