Vad som är nytt att testa, vad som hänt, vad som ligger framför, och
hur spelet faktiskt är byggt — kod, värld, design, ljud. Loggen och
roadmapen läses live ur spelets eget API, så de är aldrig äldre än
senaste publiceringen.
Läser spelets API…
Nytt att testa
Det senaste först. Allt nedan ligger på båda speladresserna.
Senast ut
Valvet — den andra grottan
Ruta 37, 35 · värld 592, 568 · femton rutor norr om lägret, två rutor väster om leden
Två grottor som spelas likadant är en grotta två gånger, så Valvet är
Norrgrottans motsats. Norrgrottan är ett kryp: av hästen
vid tröskeln, ner på magen under taket. Valvet är en valvbåge du går in
genom stående, en hals du kan rida ner, och en sal med
vatten i. Facklan tänds i bågen som i den andra grottan.
Den ligger på vägen till allt annat, till skillnad från Norrgrottan som
ligger trettiofem rutor norrut uppför en avstickare. Det var hela poängen:
en grotta man möter är en grotta som hittas.
Hästen inne i salen. Norrgrottan hade vänt dig i dörren.Precis innanför bågen. Taket håller hela vägen in.
Fyndet ligger i salen: tolv skott, vatten, bär och en
läkedryck. Skotten är svaret på «jag hade önskat man kunde skjuta här» —
du kan skjuta i Allén, i Klyftan och i Valvet, det som saknades
var ammunition. Norrgrottan är fortfarande för trång för vapen.
Sjöstugornafyra stugor kring en gårdsplan vid vattnet
Valvetny grotta · stående · häst tillåten
Timmervägenden långa passagen · fyra händelser längs vägen
Solen är förvaldvandringen börjar i uppehållsväder
Fjällboden med kronorköp, sälj, Ingrid bakom disken
Högt gräs, myrstig, huggorm, fästing424 rutor i låglandet
Klyftan på bergsstigenögonhöjd, häst tillåten
Utzoomat ritar mindre−12 % ombord på tåget, −19 % på perrongen
Världsatlas och studioinspektör115 namngivna saker
Rundan, steg för steg
Rutorna är 16 px. Både ruta och världskoordinat står utskrivna, eftersom
studioinspektören visar båda.
1
Lägret, i sol
Start · ruta 40, 49 · värld 640, 784
Grönt gräs och sol, inte snö. Vädret var aldrig snötungt i sig — men
seeden FJALL-001 råkade dra snö på sina två första dragningar, så varje
ny sida öppnade i yrväder i 150 sekunder. Nu är första perioden alltid
uppehåll; det seedade schemat tar över efter 75 sekunder, med sol 40 %,
regn 30 %, dimma 15 %, snö 10 % och storm 5 %.
Startlägret som det ser ut när du laddar sidan.
2
Fjällboden — köp och sälj
Ruta 32, 47 · värld 520, 752 · åtta rutor väster om lägret
E vid dörren, fram till disken, E igen. Plånboken
börjar på 10 kr, vilket inte räcker till något — det är meningen. Sälj
först: ved 3 kr styck, bär och stenar 1 kr, och Ranger Livs uppdrag
betalar 35 kr dagslön ovanpå fisken. Sedan: fackla 18 kr, vattenflaska
20 kr, läkedryck I 60 kr. Hyllan fylls inte på under en vandring.
Utgråat betyder att 10 kr inte räcker.
3
Valvet
Ruta 37, 35 · värld 592, 568
Skylten står på leden. Två rutor väster om den öppnar sig bågen. Gå in —
eller rid in.
Bågen från spåret in.
4
Timmervägen — den långa vägen
Ruta 60, 30 → 77, 30 · den norra slätten, femton rutor nordost om Valvet
Den långa passagen: sjutton rutor märkt led genom björk och tall,
277 px mot Alléns 215 och Klyftans 122. Bred nog för häst, eftersom en
timmerväg byggdes för en. Fyra saker händer längs vägen — en vedtrave
ingen hämtat, en timmersläde med bruten mede, en märksten vars hugg
pekar mot Valvet, och sista ordet innan grinden. Var och en avfyras en
gång per vandring, och ingen av dem stoppar dig: en
passage är en plats på leden, inte en scen.
Timmervägen, vid slädet. Händelsen säger sitt och du fortsätter gå.
5
Sjöstugorna — stugbyn
Ruta 53–63, rad 68–76 · vid vattnet söder om den norra leden, fyra rutor från tredet
Fyra stugor kring en gårdsplan, en eld, två bänkar, en vedtrave och
någon som bor där. Alla fyra dörrarna öppnar sig. Byggd av det som
redan fanns, utan en enda ny mekanik — och medvetet
allt eller inget: duger inte marken i en annan värld
läggs byn inte alls, för en by som halvt dyker upp är sämre än ingen.
Ingen bastu, med flit: bastun bär en värmemodell, ett eget
verandalager och tester som utgår från att det finns exakt en i
världen. Det blev en fjärde stuga i stället.
Sjöstugorna från söder. Vattnet ligger österut.
De fyra husen är fyra olika rum. Fram tills nu
öppnade varje stuga i världen in i samma rum — samma brits, samma
kamin, samma bord. Nu bär en byggnad ett inredningspaket: Fikstugan
med disk, kaffekanna och Britta bakom den; Samlingsstugan med
biljardbord och soffa mot teven; Förrådet med sina hyllor; och en
vanlig stuga att sova i. En byggnad utan paket får exakt den
stuga spelet alltid haft — lägerstugan är oförändrad.
Fikstugan. Disken är målad, kannan står på den, och Britta står bakom.Samlingsstugan. Tre klot ligger kvar på duken.
Fikstugan har en veranda. Plankdäck, fyra stolpar,
tak och ett räcke, plus en bänk. Taket skymmer utan att blockera:
kollisionen biter bara när en kropps höjd överlappar en låda, och
en vandrare på 17 pixlar når inte upp till 30. Byggd av samma sak
som bastuns veranda redan bevisat.
Verandan på Fikstugan. Dörren är fortfarande nåbar och bänken går att sitta på.
Ett paket kan medvetet inte ändra rummets storlek,
väggar, dörr eller fönster. En andra våning, en balkong och en källare
kräver geometri rummodellen inte har — de är skrivna som en egen
funktion i stället för att fejkas med möbler.
6
Hängbron
Ruta 55–61, rad 108 · över sundet väster om Myrslättens station
Plank över vattnet med räcke på båda sidor, en rutas bredd — tre
vore en vägbank, en bro är en linje. Räcket är kollision: sidan är
en vägg, inte en kant.
Den skulle vara upphöjd. Första utkastet ställde
däcket tio pixlar upp på stolpar, och rundvandringen hittade
vandraren simmande under sitt eget däck: om en kropp är i
vattnet avgörs av rutan den står på, före varje yta ett objekt
erbjuder. Ett upphöjt däck är alltså en ändring i rörelsemodellen,
inte en bro. Världen har dessutom inga raviner — sökningen hittade
tre och alla tre var den gamla grottans egen klyfta.
Hängbron från västra stranden. Stationen ligger sex rutor västerut.
7
Högt gräs
Raderna 56 och söderut, sydväst om lägret · tätast i ruta 34, 57
Långsammare, tyngre och pratigare: mätt ger 900 ms gång 49 px på leden
mot 36 px i gräset. Det prasslar, kostar lite stamina och lämnar en
nedtrampad linje efter dig. Samma värld, samma renderare, samma kamera —
ingen scen, ingen strid.
«The grass is over your knees.»
8
Myrstigen och plankbryggan
Skyltar i ruta 24, 82 och 41, 79 · elva plankor i rutorna 32–37, 79
En skyltad genväg rakt över myren. Den märkta leden mellan samma punkter
går runt — ungefär dubbla vägen — och är torr. Bredvid plankorna blir
kängorna blöta.
Plankorna över myrhålan.
9
Huggorm, fästing och pincetten
Tre huggormar: ruta 28, 82 · 34, 81 · 43, 71
Ormen kan inte bita ur tomma intet: gräset rör sig (0,50 s), den reser sig
och väser (1,31 s), och först då hugger den (2,39 s). Den rör sig i 34 px/s
mot din gång på 53 — att gå därifrån fungerar alltid.
Fästingen är en tröskel, inte ett tärningskast: vid 26 viktade sekunder i
högt gräs fastnar en. Pincetten ligger i startpackningen.
Ryggsäck I → Tweezers → Remove tick.
Två passager med samma mekanik och olika material. I Allén möts löven över
leden; i Klyftan är berget väggen. Kameran glider ner i ögonhöjd, klämmer
lutningen, riktar blicken längs stigen — och lämnar tillbaka exakt samma
kamera vid bortre grinden. Klyftan släpper igenom hästen; Allén ber dig
sitta av, för där går leden mellan trädstammar.
Mitt i skåran. Väggarna reser sig åt båda hållen.Allén. Kronorna sluter sig; skåran har ingen krona alls.
11
Norrgrottan
Skylt i ruta 42, 29 · mynning i ruta 38, 13 · värld 612, 216
Rakt norrut cirka 35 rutor. Ta norra leden, sväng upp på den markerade
stigen vid skylten, följ den norrut och sedan snett uppåt vänster.
Krypgången kräver C. Finns i multiplayer.
Vid mynningen.
12
Tåget
Norrbacka station, ruta 18, 94 · två andra stationer på slingan
Sätt dig på bänken och tryck E. Här var FPS:en sämst i ögonhöjd,
eftersom det inte finns någon siktgräns — från södra slingan ritades nästan
hela kartan. Vagnens skal döljer nu det bakom, och geometrin byggs en gång.
«Norrbacka station · next train in 33 s.»
Årstiden ligger i World studio, inte i Settings
Fyra val: sommar, höst, vinter, vår. Det är inte vädret — vädret är vad himlen
gör den här minuten, årstiden är vad marken gjort i veckor. Den flyttar
snögränsen och inget annat, så den är trygg att byta mitt i en vandring.
Samma läger, samma sekund, vinter.
Loggen
Ur docs/status.md i spelets repo, nyast först.
2026-09-12
Läser loggen…
Hämtas från spelets API vid sidladdning.
Roadmap
En fil per funktion i docs/features/, med vad en spelare gör,
varför den ser ut så, vad som redan fanns, vad den medvetet inte
gör, och hur man vet att den fungerar.
Sju spår som avbröts halvvägs
De ligger som ostädade grenar i repot och finns inte i spelet:
bastun med omklädning, dusch och lounge · rotation när man bygger tält och
stängsel · styrplattans kamerapanorering · häst som vadar i vatten · paketering
till macOS, Steam och mobil · questline och tutorial · utvecklarhandboken och
adminvyn.
Riktningen för tillsammans
docs/together-solo-guild.md går igenom vad WoW-känslan faktiskt
består av — samma värld för alla, att behöva någon till något specifikt,
framsteg som syns utanpå, och ett vi — och vilka delar av den som skulle
förstöra ett vandringsspel. Kort version: inga roller, inga nivåer, inga
raidtider, ingen konkurrens om bär. Ordningen som föreslås är anslagstavlan,
repet över det branta, sparad progression, och sist klubbstugan som gillets
plats med en loggbok som skriver sig själv.
Så är det byggt
Två beroenden i klienten, tre i verktygskedjan, och inget ramverk i
gränssnittet. Allt gränssnitt är vanlig DOM ovanpå en canvas.
Phaser 3.90scen, klocka, input, textur-cache
Firebase 12.3auth, realtime database, firestore
Vite 8 + TypeScript 5.9bygge och typkontroll (JS med JSDoc)
Prettierformatering
Ingen UI-ramHUD, paneler och menyer är DOM
131 källfiler~51 000 rader
109 testfilerNode-tester + Playwright-sviter
Var koden ligger
src/game/spelreglerna · 23 filer, ~16 000 rader. game.js är scenen och ticken; resten är strid, faror, rörelse, NPC:er, butik, tåg, hästar, vertikalitet.
src/perspective/renderaren · 23 filer, ~12 800 rader. Egen mjukvaruprojektion på 2D-canvas: mark, himmel, byggnadsytor, grottskal, vatten, dimma. Inget WebGL.
src/interior/den äldsta grottans egen inomhusscen · 12 filer.
src/audio/ett enda FjallAudio.js, 458 rader, ren Web Audio.
Renderaren, kort
Det finns ingen 3D-motor. src/perspective/view.js projicerar
världens rutnät själv — punkt för punkt, med egen närplansklippning — och
ritar polygoner på en 2D-canvas. Det är därför kameran kan gå från snett
ovanifrån till ögonhöjd utan att något byter scen: det är samma projektion
med andra tal. Kostnaden är att varje ruta är ett fyll- och streckanrop,
vilket är precis vad blocksammanslagningen i docs/render-lod.md
handlar om.
Tester
npm test kör ~75 Node-filer på ren logik utan webbläsare. Utöver
det finns ett fyrtiotal Playwright-sviter som startar riktiga Chrome mot en
Vite-server och spelar: går in i grottan, rider genom bågen, säljer hudar,
drar en vara till actionbaren, mäter FPS. Regeln i repot är att ingenting
publiceras utan att båda är gröna.
Världen och kartan
128 × 128 rutor à 16 px. Allt genereras ur en seed; ingenting är handplacerat
i en editor.
Generatorn lägger terräng, höjd, vatten, skog och hemslingan. Sedan lägger
additiva lager sina egna saker ovanpå, i tur och ordning:
expeditionsleder, bergsvandringen, passagerna, bastuverandan, grottorna,
järnvägen, det höga gräset och butiken. Varje lager är deterministiskt ur
seeden, idempotent, och får aldrig flytta något som redan ligger där. Det är
hela regeln, och den är varför två personer kan bygga i världen utan att
kollidera.
Vad saker heter
npm run atlas kör den riktiga generatorn och skriver
docs/world-atlas.md: varje namngiven sak med id, ruta,
världskoordinat, modulen som lade dit den, och om namnet betyder samma sak i
varje genererad värld. I spelet gör World studio → Inspector
samma sak live: peka på något och få namnet.
Namn, id, ruta, världspixel och källfilen som lade dit saken.
Högfjället — ett annat höjdval
World studio → Terrain height → High fell. Sexton
nivåer à 34 px mot bergens tio à 26: mätt 544 px mot 260, alltså mer
än dubbla klättringen, på samma generator och samma regler. Det är ett
eget val, inte en ändring av den vanliga världen — allt som är
handplacerat, passagerna, grottorna, byn och butiken, är placerat mot
den levererade världens höjder.
Högfjället nära toppen. Samma spel, dubbla höjden.
Design och GUI
Färgerna nedan läses live ur spelets /api/design.json.
En regel styr allt visuellt: det som ligger på skärmen hör
till samma värld som fjället bakom. En panel är en träram med mässingsfas och
hård skugga, ritad på pixelrutnätet, med raka hörn och monospace. Om en kontroll
skulle kunna sitta oförändrad i en vanlig webbapp är den fel här.
Typsnitt
VT323 för allt som är en etikett, en rubrik eller HUD-krom.
Monospace för löptext — det är vad spelet självt använder. Inget proportionellt
typsnitt någonstans: spelet är ritat på ett pixelrutnät och typen hör till det.
Form
Hörnradie 0 överallt. Ram 2 px. Fasen är
inset 2px 2px ljus, inset -2px -2px mörk och skuggan
3px 3px 0 — hård, aldrig mjuk. Rörelse hör till världen, inte till
gränssnittet: paneler dyker upp, de glider inte in.
Menyer och paneler
Allt gränssnitt är DOM ovanpå canvasen, inte ritat i spelet. Det ger riktig
text, riktig markering och riktig tangentnavigering gratis. Menyn är en
<dialog> med fem vyer: Bag, Character, Settings, Friends,
Notes — plus World studio. Ryggsäcken är den enda som får ligga kvar öppen
medan spelet fortsätter.
Samma panelvokabulär i 390 px bredd.
Karaktärer och sprites
All pixelkonst ritas i kod, på canvas, vid uppstart. Det finns
inga PNG-filer med figurer i projektet.
src/assets/ har en fil per familj: npc.js för
människorna, field-art.js för djur och fältsaker,
tools.js för det man håller i handen, cabin-art.js,
sauna-art.js, train-art.js, cave-art.js
för miljöerna. Varje figur är en serie fyllda rektanglar på ett litet rutnät —
en vandrare är 12 × 17 pixlar — i en gemensam palett.
Inzoomat på lägret: vandraren är 12 × 17 pixlar, hästen och elden ritade i samma rutnät och samma palett.
motion.js bygger rörelsen ovanpå: gångcykler, en andningscykel för
stillastående NPC:er (huvudet sjunker en pixelrad ner på axlarna och resten av
silhuetten står still), och riktningsval. directions.js väljer
vilken vy av en figur som ska visas utifrån kamerans vinkel, så samma figur
fungerar både snett ovanifrån och i ögonhöjd. mounted-rider.js
cachar ryttare-och-häst som en delad sammansättning, eftersom den annars skulle
ritas om varje bild.
Hus och rum
Ett hus är ett objekt i världen och ett rum innanför — samma kamera, samma
ljus, samma pixelskala.
src/perspective/building-props.js bygger fasaderna som solida
volymer, och room-faces.js ritar rummet innanför som projicerade
ytor med ögonsidesgallring. Interiören är avsiktligt större än exteriörens
fotavtryck — det klassiska tricket — men förankrad i den: rummets sydvägg
är husets sydvägg, så dörren du gick fram till utifrån är dörren du går
ut genom, och fönstret i norrväggen tittar ut över fjället bakom huset.
Utsidan är solida volymer: väggar med djup, fönster, dörr och skorsten. Innanför står rummet på samma plats.
Därför kunde Fjällboden byggas utan en rad i renderaren: den är ett
cabin-objekt, och enterCabin() bygger vilket som helst.
Samma sak gäller grottorna — Valvet delar geometri, kollision, renderare och
platsbeskrivare med Norrgrottan, och skiljer sig bara i data.
Ljud och effekter
En fil, 458 rader, ren Web Audio. Inga ljudbibliotek.
src/audio/FjallAudio.js bygger en brusbuffert och ett antal
oscillator- och filterkedjor vid start, och spelar sedan korta ljud genom dem:
fotsteg som byter karaktär med underlaget, prasslet i högt gräs, yxan, vattnet,
hästen, elden. Det finns en motif() för små melodislingor — det är
den flöjten vid lägerelden använder. Allt är styrt av en tidsstämpel per ljud så
samma ljud inte kan spelas hundra gånger i sekunden.
Samma läger i regn. Vädret är lager i atmosfären, inte partiklar — marken, himlen och molnskuggorna flyttar sig tillsammans.
Effekterna ligger i renderaren, inte i ett partikelsystem:
väder är lager i atmosfären (atmosphere.js), molnskuggor är en
färgmodulering per ruta, fotavtryck och nedtrampat gräs är märken i
dekalfältet som marken redan ritar, och vatten är en egen yta med
animerad textur. Att gräset kunde läggas till utan ett enda nytt ritanrop beror
just på det fältet.
Multiplayer
Firebase Realtime Database för det som rör sig, Firestore för det som består.
Ett rum har 2–20 platser. Delat i dag: andra vandrare och deras rörelse,
chatt och privata meddelanden, insamling, byggen, dropppade förråd, hästar
och grottornas fynd. Inte delat: djur, faror, inventarie,
plånbok och uppdrag — de är lokala per vandring, och det utges inte för att
vara något annat.
Positioner skickas 8 gånger i sekunden medan man rör sig och tystnar när man
står still. Reglerna i database.rules.json är grinden, och de
testas på riktigt mot Firebase-emulatorn i tests/database-rules.mjs:
en spelare kan inte skriva i någon annans namn, inte teleportera en parkerad häst,
och inte skriva utanför sitt rum.
API och MCP
Två ytor mot samma fakta, båda skrivskyddade och båda genererade ur repot.
/api/index.jsonvad som finns och var
/api/world.jsonde namngivna platserna, med ruta och koordinat
/api/features.jsonplanerade och byggda funktioner
/api/design.jsondesignsystemet, ur spelets egna filer
/api/status.jsonloggen, nyast först
För en assistent finns samma fakta över MCP:
node scripts/mcp-server.mjs i spelets repo, med verktygen
world_find, world_get, feature_list,
feature_read, design_tokens och status.
Handskriven JSON-RPC på hundra rader, utan SDK, och ett test som läser serverns
egen källkod och faller om den någonsin får en skrivande funktion i sig.
Den här sidan använder det själv: loggen, roadmapen, paletten och siffrorna
ovan hämtas live därifrån vid sidladdning.
Färdigheter — vad du kan, och var
Spelet har inga färdighetspoäng och inga nivåer. Det du kan göra är
alltid detsamma; vad platsen tillåter är det som skiftar. Varje
plats — fjället, ett rum, en passage, en grotta, tåget — bär en tabell
över vad som är tillåtet i den, och tabellen nedan är den tabellen.
Gå, springa, huka, krypaWASD · Shift springer · X hukar · C kryper. Att krypa är det enda sättet genom Norrgrottans krypgång, och under ett lågt tak sätts du tillbaka på magen med skälet utskrivet.
HoppaSpace. Avstängt i Norrgrottan och i Grottan — det finns inget utrymme. Till häst, stillastående, stegrar den i stället.
RidaE vid en häst. Tre hästar står i världen. Hästen följer med genom Klyftan, Timmervägen och Valvet, men inte genom Allén eller Norrgrottan — bredden avgör, och den är deklarerad per plats.
Använda det du hållerQ. Yxan hugger, spöet fiskar, kikaren tittar, vihtan piskar, geväret skjuter. Z är kikarsiktet.
SkjutaKräver ammunition, inte ett vapen i sig: skotten finns i förrådslådan i en ledstuga och i fyndet i Valvet. Tillåtet ute, i alla tre passagerna och i Valvet. Inte i rum, inte i Norrgrottan.
Samla och skinnaE på ved, bär, stenar och fällda djur. Skinning kräver kniven. Bara utomhus och i passagerna.
FiskaQ med spöet vid vatten. Bara utomhus — inte i en passage, inte i ett rum.
ByggaB öppnar bygglistan: skydd, eld, staket, tält, hängmatta, campingkök. Bara utomhus, aldrig i en passage eller ett rum.
SimmaV vid vatten. Går inte med full packning — spelet säger varför. Aldrig i en passage, ett rum eller en grotta.
Flyga drönarenG. Bara utomhus. T byter kamera, E plockar upp den där den landar. Bilderna hamnar i ett lokalt galleri — att dela dem är specat, inte byggt.
FacklanTänds automatiskt i en grotta om du har ved. Utan ljus är Norrgrottans sal nästan svart, med flit.
Gräva, fylla flaskan, bryggaE på rätt plats. Flaskan fylls vid vatten eller vid en stugkran; drycker bryggs av bär och vatten.
Äta och drickaR äter. Törst, hunger, illamående, blåsa, tarm och hygien räknas alla redan — att källans kvalitet ska betyda något är specat, inte byggt.
Kartan och anteckningarnaM öppnar kartan, H göm HUD:en, I ryggsäcken, Esc inställningar. Hela listan står under Settings → Controls.
Tillåtet per plats
Genererad ur samma tabeller spelet självt läser
(src/game/places.js). «ja» betyder att handlingen går att
utföra där; ett streck betyder att den är avstängd och att spelet säger
varför när du försöker.
plats rida hoppa drönare bygga krypa simma hugga fiska skjuta gräva
Utomhus ja ja ja ja ja ja ja ja ja ja
Allén — ja — — ja — ja — ja ja
Klyftan ja ja — — ja — ja — ja ja
Timmervägen ja ja — — ja — ja — ja ja
Valvet ja ja — — ja — — — ja —
Norrgrottan — — — — ja — — — — —
Stugan — ja — — ja — — — — —
Bastun — ja — — ja — — — — —
Grottan — — — — ja — — — — —
Det är därför en ny plats är billig att bygga: den ärver hela listan och
deklarerar bara sina undantag. Valvet skiljer sig från Norrgrottan på
exakt tre rader i den här tabellen.
Vidareutveckla — kod att kopiera
Varje exempel nedan är det mönster som faktiskt används i repot, inte
pseudokod. Klistra in, byt siffrorna, kör testerna.
Tre regler som gäller allt som läggs till i världen. De är varför två personer kan bygga samtidigt utan att kollidera.
Deterministiskt. Samma seed ger samma resultat, varje gång. Ingen Math.random() — använd W.hash(seed + "|något").
Idempotent. Att köra lagret två gånger på samma värld ändrar ingenting andra gången.
Flytta aldrig något som redan ligger där. Ett nytt lager får lägga till, och får rensa bort generad växtlighet på sin egen stig. Inget annat.
En ny passage — en plats på leden med egen kamera
Det här är Allén och Klyftan. En post i PASSAGE_DEFS räcker:
grinden, kameraglidningen, lutningsklämmorna och återlämningen vid bortre
grinden kommer gratis.
// src/world/passages.js → PASSAGE_DEFS
Object.freeze({
id: "passage-3",
label: "Bäckfåran",
kind: "cleft", // "canopy" | "cleft" | "burrow"
points: Object.freeze([ // mittlinjen, i rutor
[64, 88],
[69, 86],
[74, 87],
]),
width: 3, // korridorens bredd i rutor
camera: Object.freeze({
eyeLevel: true, // ner i ögonhöjd på vägen in
minPitch: -18,
maxPitch: 26,
yawAlongPath: true, // blicken riktas längs stigen
}),
allow: Object.freeze({ mount: true, jump: true, crawl: true }),
stance: "any", // "any" | "crawl"
canopy: false, // löv över leden?
walls: "rock", // "rock" lägger block längs kanterna
resistance: 0.8, // fartskala i korridoren
})
Verifiera: node tests/passages.mjs och
node tests/browser-passage.cjs.
En ny grotta
Valvet är en post i CAVE_DEFS. gate är det som gör
skillnad mellan ett kryp och en sal — grinden, kollisionen, platsen och
renderaren läser den.
Så här ser varje additivt lager ut — järnvägen, gräset, butiken, grottorna.
Funktionen tar en genererad värld och lämnar tillbaka den.
// src/world/mitt-lager.js
const TILE = 16;
export function addMittLager(world) {
// 1. Idempotent: redan lagt? gå hem.
if (!world?.tiles || !world.objects || world.mittLager) return world;
// 2. Bara på världar som har förutsättningarna.
if (!(world.contentRevision >= 2)) return world;
world.mittLager = [];
// 3. Deterministiskt ur seeden, aldrig Math.random().
const hash = (x, y) => Math.abs(Math.floor(x * 31 + y * 17)) % 100;
for (const [tx, ty] of [[70, 40], [71, 41]]) {
const tile = world.tiles[ty]?.[tx];
if (tile === undefined) continue;
if ([5, 6].includes(tile)) continue; // aldrig i vatten
if (tile === 4) continue; // aldrig på leden
world.objects.push({
id: `mitt-lager-${tx}-${ty}`, // stabilt id, aldrig oN
type: "rock",
solid: true,
x: tx * TILE + TILE / 2,
y: ty * TILE + TILE / 2,
volumes: [[0, 0, 6, 6, 0, 18, false]], // dx, dy, rx, ry, z, höjd
});
world.mittLager.push({ tx, ty });
}
return world;
}
Koppla in det i resetWorld() i src/game/game.js, i
scripts/world-atlas-core.mjsSTAGES, och i
tests/scene-harness.cjs. Kör sedan npm run api så
kommer det med i atlasen och i API:t.
En vara i butiken
// src/game/shop.js → PRICES
rope: Object.freeze({
buy: 26, // vad butiken tar för en sats
lot: 1, // hur många man får
stock: 3, // hur många satser som finns per vandring
sell: 8, // vad butiken betalar per styck
sellLot: 1,
note: "Fyrtio meter. Väger som en dagsranson.",
}),
Varan dyker upp på hyllan först när ITEM_NAMES i
src/game/survival.js känner id:t — annars döljs raden, med flit.
Regeln som testet vaktar: varje buy ska ligga över motsvarande
sell per styck, så ingen vända över disken tjänar pengar.
Pixelkonst — ritad i kod, inga bildfiler
// src/assets/min-konst.js
export function makeMinKonst(scene) {
const texture = (key, w, h, draw) => {
if (scene.textures.exists(key)) return;
const t = scene.textures.createCanvas(key, w, h);
const c = t.context;
const r = (x, y, width, height, col) => {
c.fillStyle = col;
c.fillRect(x, y, width, height);
};
draw(r, c);
t.refresh();
};
// Verktygsikoner är 16×16 som alla andra `tool-`-nycklar, så actionbaren,
// ryggsäcken och hotbaren plockar upp dem utan specialfall.
texture("tool-rope", 16, 16, (r) => {
const hemp = "#b4915a", shade = "#7d6338";
r(3, 3, 10, 2, hemp);
r(3, 7, 10, 2, shade);
r(3, 11, 10, 2, hemp);
r(2, 3, 1, 10, shade);
r(13, 3, 1, 10, shade);
});
}
Platta rektanglar, ingen gradient, ingen kantutjämning, och paletten från
DESIGN.md. En vandrare är 12×17 pixlar.
Ett ljud
// Var som helst i spelet, via scenens ljud:
scene.audio.play("step", terrain, { gain: 0.7, pan: -0.2 });
scene.audio.play("whisk"); // prasslet i högt gräs
scene.audio.play("sizzle"); // elden
scene.audio.horse("canter");
scene.audio.motif([440, 494, 523]); // liten melodislinga, flöjten
// play() släpper igenom samma ljud som oftast var 60:e ms (steg: 120 ms)
// och håller högst 16 källor igång, så en loop kan aldrig dränka spelet.
En funktionsspec
Innan något byggs: en fil i docs/features/. Frontmattern är det
som gör den maskinläsbar — indexet, API:t och MCP-servern läser den.
---
id: min-funktion
title: Vad funktionen heter för en spelare
status: spec # idea | spec | building | built | parked
scope: prototype # prototype | core
owner: daniel
asked: "Daniels egna ord, ordagrant"
touches:
- src/game/…
verify:
- tests/…
---
## Vad en spelare gör
## Varför, och varför så här
## Vad som redan finns
## Avgränsning ← viktigast på ett prototypspår
## Så vet man att den fungerar
## Öppna frågor
Sedan npm run features. Kommandot faller om ett fält saknas eller
om status ligger utanför vokabulären — ett index som accepterar vad som helst
är inget index.
Fråga världen utifrån
# Vad heter saken, och var ligger den?
curl -s https://fjallvandring.netlify.app/api/world.json \
| jq '.entries[] | select(.name | test("Norrbacka"))'
# Vad är planerat, och vad är byggt?
curl -s https://fjallvandring.netlify.app/api/features.json | jq '.features[].title'
# Designsystemet, ur spelets egna filer
curl -s https://fjallvandring.netlify.app/api/design.json | jq '.color.hud'
# För en assistent: samma fakta över MCP
claude mcp add fjallvandring -- node /sökväg/till/fjallvandring/scripts/mcp-server.mjs
Innan något publiceras
npm test # ~75 Node-filer, ren logik, ingen webbläsare
npm run typecheck # tsc över JS med JSDoc
npm run build # Vite
npm run api # atlas → funktionsindex → design → API-filer
# Riktig Chrome mot en Vite-server:
npx vite --host 127.0.0.1 --port 5173 --strictPort &
node tests/browser-passage.cjs
node tests/browser-shop.cjs
node tests/browser-mobile-play.cjs
Regeln i repot: ingenting publiceras utan att både Node-testerna och de
relevanta webbläsarsviterna är gröna, och utan att liveadressen kör exakt
samma kontrollsumma som det testade bygget.
Kända brister
Inget sparas. Plånboken, väskan, fästingen och uppdragen
nollställs med världen. Det är den enskilt största saknade saken, och den
blockerar allt som handlar om att tillhöra något över tid.
Gräset syns svagt vid vanlig gångzoom. Det läser som en mörkare fläck; först när du står i det märks det på farten och ljudet.
Huggormen är svår att få syn på innan den reser sig — realistiskt, men inte nödvändigtvis bra spel.
Djurtillstånd är lokalt. Din huggorm är din.
Maximal utzoomning är fortfarande tyngst av allt, även efter blocksammanslagningen.
Hyllan i butiken fylls inte på och prislistan scrollar på telefon.
Ingen mänsklig speltest av balansen. Allt ovan är mätt, inte spelat igenom.