Utvecklingens fältbok

Fältboken

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.

En häst inne i Valvets sal, med stalagmiter och fackelljus.
Hästen inne i salen. Norrgrottan hade vänt dig i dörren.
Inne i Valvets valvbåge, tänd fackla, stalagmiter i taket och på golvet.
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östugorna fyra stugor kring en gårdsplan vid vattnet
Valvet ny grotta · stående · häst tillåten
Timmervägen den långa passagen · fyra händelser längs vägen
Solen är förvald vandringen börjar i uppehållsväder
Fjällboden med kronor köp, sälj, Ingrid bakom disken
Högt gräs, myrstig, huggorm, fästing 424 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ör 115 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. 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 i sol.
    Startlägret som det ser ut när du laddar sidan.
  2. 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.

    Inne i Fjällboden med hyllan och priserna.
    Utgråat betyder att 10 kr inte räcker.
  3. 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.

    Valvets mynning sedd från leden.
    Bågen från spåret in.
  4. 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: björkar över leden och en timmersläde vid sidan.
    Timmervägen, vid slädet. Händelsen säger sitt och du fortsätter gå.
  5. 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.

    Fyra röda stugor kring en gårdsplan vid vattnet.
    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 inifrån: disk med kaffekanna, två bord och Britta bakom disken.
    Fikstugan. Disken är målad, kannan står på den, och Britta står bakom.
    Samlingsstugan inifrån: biljardbord med klot, soffa och tv.
    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.

    Fikstugans veranda: plankdäck, fyra stolpar, tak, räcke och en bänk.
    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. 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: plankdäck med räcke över sundet.
    Hängbron från västra stranden. Stationen ligger sex rutor västerut.
  7. 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.

    Vandraren i en gräsfläck.
    «The grass is over your knees.»
  8. 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.

    Plankbryggan genom myren.
    Plankorna över myrhålan.
  9. 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.

    Ryggsäcken öppen med pincetten.
    Ryggsäcken ligger kvar öppen medan du spelar.
  10. 10

    Klyftan och Allén

    Klyftan: ruta 105, 66 → 112, 63 · Allén: ruta 81, 70

    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.

    Vandraren i Klyftan.
    Mitt i skåran. Väggarna reser sig åt båda hållen.
    Allén med krontak av löv.
    Allén. Kronorna sluter sig; skåran har ingen krona alls.
  11. 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.

    Norrgrottans mynning.
    Vid mynningen.
  12. 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.
    «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 i vinter, snö ner i dalen.
Samma läger, samma sekund, vinter.

Loggen

Ur docs/status.md i spelets repo, nyast först.

  1. 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.90 scen, klocka, input, textur-cache
Firebase 12.3 auth, realtime database, firestore
Vite 8 + TypeScript 5.9 bygge och typkontroll (JS med JSDoc)
Prettier formatering
Ingen UI-ram HUD, paneler och menyer är DOM
131 källfiler ~51 000 rader
109 testfiler Node-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/ui/ gränssnittet · 41 filer, ~12 900 rader. HUD, ryggsäck, actionbar, chatt, kartan, studion, pekkontroller.
src/world/ världen · 10 filer. Generatorn plus additiva lager: leder, passager, grottor, järnväg, gräs, butik.
src/assets/ all pixelkonst · 16 filer, ritad i kod på canvas. Inga bildfiler.
src/net/ multiplayer · 15 filer. Rum, transport, delad värld, fjärrvandrare.
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.

Studioinspektören visar namn, ruta och källfil.
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.

En mycket hög bergssida med terrasserad grå klippa och vandraren mitt i den.
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.

Fjällboden i telefonformat.
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.

Närbild på vandraren, en häst och elden vid lägerstugan.
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.

En stuga utifrån, med fönster, dörr och skorsten.
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.

Lägret i regn: mörkare mark, moln och regnslöjor.
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.json vad som finns och var
/api/world.json de namngivna platserna, med ruta och koordinat
/api/features.json planerade och byggda funktioner
/api/design.json designsystemet, ur spelets egna filer
/api/status.json loggen, 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, krypa WASD · 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.
Hoppa Space. Avstängt i Norrgrottan och i Grottan — det finns inget utrymme. Till häst, stillastående, stegrar den i stället.
Rida E 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åller Q. Yxan hugger, spöet fiskar, kikaren tittar, vihtan piskar, geväret skjuter. Z är kikarsiktet.
Skjuta Krä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 skinna E på ved, bär, stenar och fällda djur. Skinning kräver kniven. Bara utomhus och i passagerna.
Fiska Q med spöet vid vatten. Bara utomhus — inte i en passage, inte i ett rum.
Bygga B öppnar bygglistan: skydd, eld, staket, tält, hängmatta, campingkök. Bara utomhus, aldrig i en passage eller ett rum.
Simma V vid vatten. Går inte med full packning — spelet säger varför. Aldrig i en passage, ett rum eller en grotta.
Flyga drönaren G. 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.
Facklan Tänds automatiskt i en grotta om du har ved. Utan ljus är Norrgrottans sal nästan svart, med flit.
Gräva, fylla flaskan, brygga E på rätt plats. Flaskan fylls vid vatten eller vid en stugkran; drycker bryggs av bär och vatten.
Äta och dricka R ä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 anteckningarna M ö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.

  1. Deterministiskt. Samma seed ger samma resultat, varje gång. Ingen Math.random() — använd W.hash(seed + "|något").
  2. Idempotent. Att köra lagret två gånger på samma värld ändrar ingenting andra gången.
  3. 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.

// src/world/cave-entrances.js → CAVE_DEFS
export const MY_CAVE = Object.freeze({
  id: "brunnen",
  label: "Brunnen",
  mouthTile: Object.freeze([52, 71]),   // tröskeln = rutans västkant
  spur: Object.freeze([                 // kort avstickare från leden
    Object.freeze([54, 71]),
    Object.freeze([52, 71]),
  ]),
  signs: Object.freeze([
    Object.freeze({
      id: "brunnen-sign",
      tile: Object.freeze([54, 70]),
      text: "BRUNNEN · …",
    }),
  ]),
  // Sektionerna, i världspixlar relativt tröskeln. Grottan går västerut.
  mouth: Object.freeze({ from: -20, to: 0, half: 15, ceiling: 40 }),
  crawl: Object.freeze({ from: -70, to: -20, half: 12, ceiling: 36 }),
  chamberCeiling: 50,
  chamber: Object.freeze([[-70, -12], [-96, -40], [-140, -30],
                          [-150, 20], [-110, 50], [-70, 20]]
    .map((p) => Object.freeze(p))),
  stalagmites: Object.freeze([{ x: -120, y: -20, r: 5, h: 20 }].map(Object.freeze)),
  stalactites: Object.freeze([{ x: -110, y: 10, r: 4, length: 14 }].map(Object.freeze)),
  pool: Object.freeze({ x: -130, y: 15, rx: 18, ry: 11 }),
  bedroll: Object.freeze({ x: -95, y: -25, half: 7, deep: 3 }),
  fireRing: Object.freeze({ x: -105, y: 0, r: 5 }),
  find: Object.freeze({ x: -120, y: 25, contents: Object.freeze({ ammo: 8 }) }),
  // Det här avgör allt: kryp eller sal.
  gate: Object.freeze({
    kind: "cleft",                       // "burrow" för ett kryp
    stance: "any",                       // "crawl" för ett kryp
    allow: Object.freeze({ mount: true, jump: true, crawl: true }),
  }),
});

export const CAVE_DEFS = Object.freeze([NORTH_CAVE, VAULT_CAVE, MY_CAVE]);

Verifiera: node tests/north-cave.mjs && node tests/valvet.mjs.

Ett nytt lager i världen

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.mjs STAGES, 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.