From 68a378bdd440aff77329504277a07b10a0980b08 Mon Sep 17 00:00:00 2001 From: Fredrik Johansson Date: Tue, 7 Jul 2026 23:05:22 +0200 Subject: [PATCH] =?UTF-8?q?Initial=20commit=20=E2=80=94=20Djupet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Idle cult management game set in Malmö, Sweden. Manage Troende, Andakt, Dread, Hemlighet and Kaos while completing the Grand Ritual before investigators shut you down. Lovecraftian flavour, Henderson- tier chaos events, OSM/Leaflet map with dread spread markers across Rosengård, Värnhemstorget, Turning Torso and Øresundsbron. Vanilla TypeScript + Vite, no framework. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 4 + index.html | 14 + package-lock.json | 1120 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 15 + src/engine.ts | 166 +++++++ src/events.ts | 238 ++++++++++ src/main.ts | 41 ++ src/map.ts | 85 ++++ src/style.css | 276 +++++++++++ src/ui.ts | 214 +++++++++ src/upgrades.ts | 86 ++++ tsconfig.json | 13 + vite.config.ts | 2 + 13 files changed, 2274 insertions(+) create mode 100644 .gitignore create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/engine.ts create mode 100644 src/events.ts create mode 100644 src/main.ts create mode 100644 src/map.ts create mode 100644 src/style.css create mode 100644 src/ui.ts create mode 100644 src/upgrades.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79fef52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.env +src/*.js diff --git a/index.html b/index.html new file mode 100644 index 0000000..4411977 --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + Djupet + + + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c114441 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1120 @@ +{ + "name": "djupet", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "djupet", + "version": "0.1.0", + "devDependencies": { + "typescript": "^5.5.0", + "vite": "^6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4f6e538 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "djupet", + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite --port 5681", + "build": "vite build", + "typecheck": "tsc", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "^5.5.0", + "vite": "^6.0.0" + } +} diff --git a/src/engine.ts b/src/engine.ts new file mode 100644 index 0000000..97f8598 --- /dev/null +++ b/src/engine.ts @@ -0,0 +1,166 @@ +import { STANDARD_EVENTS, KAOS_EVENTS, type GameEvent } from './events.ts'; +import { UPGRADES } from './upgrades.ts'; + +export interface State { + tick: number; + troende: number; + andakt: number; + andaktRate: number; // per second + dread: number; + dreadRate: number; // per second + hemlighet: number; // 0-100, lose at 0 + kaos: number; // 0-100, triggers events at 100 + kaosRate: number; // per second + ritual: number; // 0-100, win at 100 + ritualRate: number; // per second + ritualUnlocked: boolean; + areas: { rosengard: boolean; varnhem: boolean; torso: boolean }; + upgrades: Set; + log: LogEntry[]; + phase: 'playing' | 'won' | 'lost'; + lostReason?: string; + usedEventIds: Set; + nextEventTick: number; + nextKaosTick: number; +} + +export interface LogEntry { + tick: number; + text: string; + kind: 'event' | 'kaos' | 'system' | 'upgrade'; +} + +const EVENT_INTERVAL_TICKS = 180; // ~6s at 30fps +const KAOS_THRESHOLD = 100; +const HEMLIGHET_DRAIN_PER_TROENDE = 0.002; // per second, per troende above 4 + +export function createState(): State { + return { + tick: 0, + troende: 1, + andakt: 10, + andaktRate: 0.3, + dread: 0, + dreadRate: 0, + hemlighet: 100, + kaos: 0, + kaosRate: 0.05, + ritual: 0, + ritualRate: 0, + ritualUnlocked: false, + areas: { rosengard: false, varnhem: false, torso: false }, + upgrades: new Set(), + log: [{ tick: 0, text: 'Mötet började i en fuktig källare i Rosengård. Nio stolar. En av dem är alltid lite för varm.', kind: 'system' }], + phase: 'playing', + usedEventIds: new Set(), + nextEventTick: EVENT_INTERVAL_TICKS, + nextKaosTick: EVENT_INTERVAL_TICKS * 3, + }; +} + +function pickEvent(pool: GameEvent[], state: State): GameEvent | null { + const available = pool.filter(e => { + if (state.usedEventIds.has(e.id)) return false; + const r = e.requires; + if (!r) return true; + if (r.minTroende && state.troende < r.minTroende) return false; + if (r.minDread && state.dread < r.minDread) return false; + if (r.minRitual && state.ritual < r.minRitual) return false; + if (r.unlockedArea && !state.areas[r.unlockedArea]) return false; + return true; + }); + if (!available.length) return null; + const total = available.reduce((s, e) => s + e.weight, 0); + let roll = Math.random() * total; + for (const e of available) { + roll -= e.weight; + if (roll <= 0) return e; + } + return available[available.length - 1]; +} + +function applyEffect(state: State, effect: GameEvent['effect']): void { + if (effect.andakt) state.andakt = Math.max(0, state.andakt + effect.andakt); + if (effect.hemlighet) state.hemlighet = Math.max(0, Math.min(100, state.hemlighet + effect.hemlighet)); + if (effect.kaos) state.kaos = Math.max(0, Math.min(100, state.kaos + effect.kaos)); + if (effect.dread) state.dread = Math.max(0, state.dread + effect.dread); + if (effect.troende) state.troende = Math.max(1, state.troende + effect.troende); + if (effect.ritual && state.ritualUnlocked) state.ritual = Math.min(100, state.ritual + effect.ritual); +} + +const DT = 1 / 30; // 30fps + +export function tick(state: State): State { + if (state.phase !== 'playing') return state; + + const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds) }; + s.tick += 1; + + // Resource accumulation + s.andakt += s.andaktRate * s.troende * DT; + s.dread += s.dreadRate * DT; + if (s.ritualUnlocked) s.ritual = Math.min(100, s.ritual + s.ritualRate * DT); + + // Kaos accumulation — faster with more troende and more dread + const kaosScale = 1 + (s.dread / 200); + s.kaos = Math.min(100, s.kaos + s.kaosRate * kaosScale * DT); + + // Hemlighet drain — more believers = harder to stay hidden + if (s.troende > 4) { + s.hemlighet = Math.max(0, s.hemlighet - HEMLIGHET_DRAIN_PER_TROENDE * (s.troende - 4) * DT); + } + + // Standard event + if (s.tick >= s.nextEventTick) { + const ev = pickEvent(STANDARD_EVENTS, s); + if (ev) { + s.log.unshift({ tick: s.tick, text: ev.text, kind: 'event' }); + applyEffect(s, ev.effect); + s.usedEventIds.add(ev.id); + } + s.nextEventTick = s.tick + EVENT_INTERVAL_TICKS + Math.floor(Math.random() * 60); + if (s.log.length > 40) s.log = s.log.slice(0, 40); + } + + // Kaos event + if (s.kaos >= KAOS_THRESHOLD || s.tick >= s.nextKaosTick) { + const ev = pickEvent(KAOS_EVENTS, s); + if (ev) { + s.log.unshift({ tick: s.tick, text: `⚡ ${ev.text}`, kind: 'kaos' }); + applyEffect(s, ev.effect); + s.usedEventIds.add(ev.id); + } + s.kaos = Math.max(0, s.kaos - 80); + s.nextKaosTick = s.tick + EVENT_INTERVAL_TICKS * 5 + Math.floor(Math.random() * 120); + if (s.log.length > 40) s.log = s.log.slice(0, 40); + } + + // Win / lose checks + if (s.ritual >= 100) { + s.phase = 'won'; + s.log.unshift({ tick: s.tick, text: 'Det är gjort. Turning Torso pekar rakt ned nu. Havet är stilla. Alltför stilla.', kind: 'system' }); + } else if (s.hemlighet <= 0) { + s.phase = 'lost'; + s.lostReason = 'Utredarna slog till vid gryningen. Källaren är tom. Mattan ligger kvar. Under mattan finns fortfarande bordet.'; + s.log.unshift({ tick: s.tick, text: s.lostReason, kind: 'system' }); + } + + return s; +} + +export function buyUpgrade(state: State, id: string): State { + const upgrade = UPGRADES.find(u => u.id === id); + if (!upgrade) return state; + if (!upgrade.available(state)) return state; + if (upgrade.cost.andakt && state.andakt < upgrade.cost.andakt) return state; + if (upgrade.cost.dread && state.dread < upgrade.cost.dread) return state; + + const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds) }; + if (upgrade.cost.andakt) s.andakt -= upgrade.cost.andakt; + if (upgrade.cost.dread) s.dread -= upgrade.cost.dread; + upgrade.effect(s); + if (upgrade.once) s.upgrades.add(id); + s.log.unshift({ tick: s.tick, text: `↳ ${upgrade.name}: ${upgrade.desc}`, kind: 'upgrade' }); + if (s.log.length > 40) s.log = s.log.slice(0, 40); + return s; +} diff --git a/src/events.ts b/src/events.ts new file mode 100644 index 0000000..cde716a --- /dev/null +++ b/src/events.ts @@ -0,0 +1,238 @@ +export type EventEffect = Partial<{ + andakt: number; + hemlighet: number; + kaos: number; + dread: number; + troende: number; + ritual: number; +}>; + +export interface GameEvent { + id: string; + text: string; + effect: EventEffect; + weight: number; + requires?: Partial<{ minTroende: number; minDread: number; minRitual: number; unlockedArea: Area }>; +} + +export type Area = 'rosengard' | 'varnhem' | 'torso'; + +// ── Standard events ────────────────────────────────────────────────────────── + +export const STANDARD_EVENTS: GameEvent[] = [ + // Early / low dread + { + id: 'dreams_lindqvist', + text: 'Broder Lindqvist drömmer om havet varje natt. Han säger att fisken talar till honom. Vi lyssnar.', + effect: { andakt: 5, kaos: 2 }, + weight: 10, + }, + { + id: 'systembolaget', + text: 'En berusad man utanför Systembolaget på Davidshallsgatan erbjöd sig att "hälsa på gubben i havet." Vi är osäkra på om han menar Den Sovande eller sin morfar. Kanske båda.', + effect: { kaos: 8, dread: 3 }, + weight: 8, + }, + { + id: 'skatteverket', + text: 'Skatteverket har skickat ett brev. De undrar om vår religiösa verksamhet är momsbefriad. Syster Hassan svarar dem med ett 47 sidor långt svar. De hör inte av sig igen.', + effect: { hemlighet: -5, andakt: 10 }, + weight: 7, + }, + { + id: 'ducks_torso', + text: 'Det finns en anledning till att alla änder vid Ribersborg vänder sig mot Turning Torso i ett nästan tillbedjande tillstånd. De anar mer än oss.', + effect: { dread: 8, kaos: 3 }, + weight: 9, + }, + { + id: 'ikea_table', + text: 'Ett IKEA-bord av modell LACK har börjat blöda svart vätska. Vi har lagt en matta över det. Det hjälper inte men det ser snyggare ut.', + effect: { dread: 6, kaos: 5 }, + weight: 8, + }, + { + id: 'discord_goran', + text: 'En Malmöbo vid namn Göran har av misstag blivit med i vår Discord-server och tror att vi diskuterar ett brädspel om havet. Hans feedback är konstruktiv.', + effect: { kaos: 12, hemlighet: -8 }, + weight: 6, + }, + { + id: 'neighbor_smell', + text: 'Grannarna har klagat på lukten från källaren. Vi förklarar att det är kimchi. Det är inte kimchi.', + effect: { hemlighet: -6, andakt: 4 }, + weight: 9, + }, + { + id: 'lighthouse_keeper', + text: 'Fyrvaktaren vid Malmöhus har slutat dyka upp. Hans ersättare rapporterar att vattnet sjunger nu på nätterna. Hon har också slutat dyka upp.', + effect: { dread: 10, kaos: 6 }, + weight: 7, + }, + { + id: 'oresund_hum', + text: 'Øresundsbron vibrerar med en frekvens som inte finns i naturen. Danskarna på andra sidan ringer och klagar. Vi svarar inte.', + effect: { dread: 12, hemlighet: -4 }, + weight: 7, + requires: { minDread: 20 }, + }, + { + id: 'turning_torso_spin', + text: 'Turning Torso har börjat rotera igen. Det har inte gjort det på tre år. Arkitektkontoret säger att det är omöjligt. Arkitektkontoret ringer inte mer.', + effect: { dread: 20, kaos: 10 }, + weight: 6, + requires: { minDread: 30 }, + }, + { + id: 'varnhem_pigeons', + text: 'Duvorna på Värnhemstorget har upphört att äta. De sitter bara. De tittar. En av dem har för många ögon.', + effect: { dread: 15, kaos: 8 }, + weight: 7, + requires: { unlockedArea: 'varnhem' }, + }, + { + id: 'varnhem_fountain', + text: 'Fontänen på Värnhemstorget rinner baklänges sedan tisdag. Kommunen har satt upp ett staket. Folk tar selfies.', + effect: { dread: 12, hemlighet: -10, kaos: 5 }, + weight: 6, + requires: { unlockedArea: 'varnhem' }, + }, + { + id: 'bus_driver', + text: 'Bussföraren på linje 5 körde förbi Värnhemstorget tre gånger utan att stanna. Passagerarna märkte det inte. Bussföraren ser nöjd ut.', + effect: { dread: 8, kaos: 6 }, + weight: 8, + requires: { unlockedArea: 'varnhem' }, + }, + { + id: 'investigator_sighting', + text: 'En man i grå kostym har stått utanför lokalen i sex timmar. Han tar anteckningar. Broder Persson hälsade vänligt. Mannen vände sig om och gick. Hans skugga gick inte med.', + effect: { hemlighet: -15, kaos: 10 }, + weight: 5, + requires: { minDread: 40 }, + }, + { + id: 'manuscript_dream', + text: 'Skriften har börjat skriva sig själv på nätterna. De nya raderna är inte på något känt språk. De är inte på något okänt språk heller egentligen.', + effect: { dread: 18, ritual: 3, kaos: 8 }, + weight: 5, + requires: { minRitual: 10 }, + }, + { + id: 'torso_shadow', + text: 'Turning Torsos skugga pekar nu mot havet oavsett var solen står. En student från Malmö Universitet skriver en uppsats om detta. Hon sover inte längre.', + effect: { dread: 22, kaos: 12 }, + weight: 5, + requires: { unlockedArea: 'torso' }, + }, + { + id: 'torso_windows', + text: 'Fönstren i Turning Torso lyser nu i ett mönster. Mönstret är ett namn. Vi kan inte läsa det. Inte än.', + effect: { dread: 30, ritual: 5, kaos: 15 }, + weight: 4, + requires: { unlockedArea: 'torso', minRitual: 30 }, + }, + { + id: 'sea_level', + text: 'Vattennivån i Öresund har sjunkit 4 centimeter sedan måndag. SMHI rapporterar ett "tekniskt fel i mätinstrumenten." Det är inte ett tekniskt fel.', + effect: { dread: 25, kaos: 10 }, + weight: 4, + requires: { minDread: 60 }, + }, + { + id: 'police_curious', + text: 'Polisen i Malmö har börjat fråga om "den religiösa föreningen vid vattnet." Vi skickar dem en broschyr om mindfulness. De tackar och ber om en till.', + effect: { hemlighet: -12, andakt: 8 }, + weight: 5, + requires: { minDread: 50 }, + }, + { + id: 'final_dream', + text: 'I natt drömde alla troende samma sak. Det finns inga ord för vad vi såg. Det finns dock ett ljud. Vi har börjat göra det.', + effect: { dread: 40, ritual: 8, troende: 2, kaos: 20 }, + weight: 3, + requires: { minRitual: 60 }, + }, +]; + +// ── Kaos events (Henderson-tier) ───────────────────────────────────────────── + +export const KAOS_EVENTS: GameEvent[] = [ + { + id: 'k_goran_dog', + text: 'Göran (från Discord-incidenten) har tagit med sig sin hund "Bamse" till ritualen. Bamse verkar inte störd av chantingen. Tvärtom. Bamse leder nu det andra kvartalets böner.', + effect: { kaos: -40, andakt: 20, troende: 1 }, + weight: 10, + }, + { + id: 'k_influencer', + text: 'En influencer har börjat live-streama utanför vår lokal med titeln "mysig sektkänsla i Malmö 🌊✨". Kommentarerna är överväldigande positiva. Dread ökar snabbare nu.', + effect: { kaos: -30, dread: 40, hemlighet: -20 }, + weight: 9, + }, + { + id: 'k_zlatan', + text: 'Zlatan Ibrahimović har postat en bild på Instagram från Stortorget klockan 3 på natten med texten "jag vet." Vi vet inte vad han vet. Han vet.', + effect: { kaos: -50, dread: 35, hemlighet: -5 }, + weight: 8, + }, + { + id: 'k_bygglov', + text: 'Kommunen har beviljat bygglov för "andlig rekreationsanläggning" vid Västra Hamnen. Ceremonin är nu stadsplanegodkänd. Det finns en toalett.', + effect: { kaos: -35, hemlighet: 15, andakt: 25 }, + weight: 8, + }, + { + id: 'k_journalist', + text: 'En journalist från Sydsvenskan skriver ett positivt reportage om "Malmös växande alternativa andlighet." Artikeln vinner pris. Juryn kan inte minnas vad de tyckte om den.', + effect: { kaos: -40, troende: 3, hemlighet: -15 }, + weight: 7, + }, + { + id: 'k_rival_cult', + text: 'En rivaliserande sekt med bas i Hässleholm försöker summera en annan entitet. De summerar fel. De summerar IKEA:s kundtjänst. Det är värre.', + effect: { kaos: -45, dread: 50, hemlighet: 10 }, + weight: 7, + }, + { + id: 'k_duck_army', + text: 'Änderna vid Ribersborg marscherar nu i formation mot Turning Torso. Turisterna filmar det. En orkester spelar av oklar anledning. Det låter rätt.', + effect: { kaos: -60, dread: 60, ritual: 5 }, + weight: 5, + requires: { minDread: 30 }, + }, + { + id: 'k_old_man', + text: 'En pensionerad sjöman med oidentifierbara tatueringar har låst in sig i tornet vid Turning Torso och börjat sjunga. Räddningstjänsten kan inte ta sig in. Sången hjälper faktiskt ritualen.', + effect: { kaos: -50, ritual: 10, hemlighet: -10 }, + weight: 5, + requires: { unlockedArea: 'torso' }, + }, + { + id: 'k_varnhem_kebab', + text: 'Kebabkiosken på Värnhemstorget serverar sedan onsdag ett alternativt menyalternativ som inte finns på tavlan. Kunderna som beställer det återkommer. De ser annorlunda ut.', + effect: { kaos: -40, troende: 4, dread: 30 }, + weight: 6, + requires: { unlockedArea: 'varnhem' }, + }, + { + id: 'k_denmark', + text: 'Danmark har officiellt begärt förklaring om vad som händer i Malmö. Sverige skickar en arbetsgrupp. Arbetsgruppen reser över Øresundsbron och hör inte av sig.', + effect: { kaos: -55, dread: 45, hemlighet: -8 }, + weight: 5, + requires: { minDread: 50 }, + }, + { + id: 'k_goran_promoted', + text: 'Göran har frågat om han kan "ta lite mer ansvar" i föreningen. Vi har utnämnt honom till Väktare av det Tredje Sigillet. Han tolkar det som kassör. Det är ungefär rätt.', + effect: { kaos: -30, andakt: 30, hemlighet: 5 }, + weight: 8, + }, + { + id: 'k_no_survivors', + text: 'Det hände något på Värnhemstorget i natt. Bevakningskamerorna fungerar men visar statiska bilder från 1987. Alla inblandade är okej. Ingen vet vad "okej" betyder längre.', + effect: { kaos: -70, dread: 80, ritual: 8 }, + weight: 3, + requires: { minDread: 60, unlockedArea: 'varnhem' }, + }, +]; diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..defe67b --- /dev/null +++ b/src/main.ts @@ -0,0 +1,41 @@ +import './style.css'; +import { createState, tick, buyUpgrade } from './engine.ts'; +import { initUI, renderState } from './ui.ts'; +import { initMap, updateMap } from './map.ts'; + +let state = createState(); +let lastTime = 0; +const TARGET_FPS = 30; +const FRAME_MS = 1000 / TARGET_FPS; + +function start(): void { + const app = document.getElementById('app')!; + + initUI(app, + (id) => { state = buyUpgrade(state, id); renderState(state); }, + () => { + state = createState(); + document.getElementById('overlay')!.classList.remove('visible'); + renderState(state); + } + ); + + initMap('map'); + renderState(state); + + requestAnimationFrame(loop); +} + +function loop(now: number): void { + if (now - lastTime >= FRAME_MS) { + lastTime = now; + if (state.phase === 'playing') { + state = tick(state); + renderState(state); + updateMap(state.dread, state.areas); + } + } + requestAnimationFrame(loop); +} + +start(); diff --git a/src/map.ts b/src/map.ts new file mode 100644 index 0000000..46da11e --- /dev/null +++ b/src/map.ts @@ -0,0 +1,85 @@ +// Leaflet is loaded via CDN in index.html +// @ts-expect-error - Leaflet global +const L = window.L; + +export interface MapMarker { + id: string; + lat: number; + lng: number; + label: string; + dreadLevel: number; // 0-3 +} + +const LOCATIONS: Omit[] = [ + { id: 'rosengard', lat: 55.5894, lng: 13.0414, label: 'Källaren i Rosengård' }, + { id: 'varnhem', lat: 55.5982, lng: 13.0058, label: 'Värnhemstorget' }, + { id: 'torso', lat: 55.6130, lng: 12.9758, label: 'Turning Torso' }, + { id: 'oresund', lat: 55.5789, lng: 12.9200, label: 'Øresundsbron' }, + { id: 'ribersborg',lat: 55.6063, lng: 12.9652, label: 'Ribersborgsstranden' }, + { id: 'davidshall',lat: 55.5963, lng: 13.0021, label: 'Davidshallsgatan' }, + { id: 'stortorget',lat: 55.6047, lng: 13.0038, label: 'Stortorget' }, +]; + +type LeafletMap = ReturnType; +type LeafletMarker = ReturnType; + +let map: LeafletMap | null = null; +const markers = new Map(); + +function dreadColor(level: number): string { + return ['#1a1a2e', '#4a1a4a', '#8b0a8b', '#cc00cc'][Math.min(level, 3)]; +} + +function dreadRadius(level: number): number { + return [6, 10, 16, 24][Math.min(level, 3)]; +} + +export function initMap(containerId: string): void { + if (!L) return; + map = L.map(containerId, { + center: [55.597, 13.002], + zoom: 12, + zoomControl: false, + attributionControl: false, + }); + + L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { + maxZoom: 18, + }).addTo(map); + + for (const loc of LOCATIONS) { + const m = L.circleMarker([loc.lat, loc.lng], { + radius: 6, + color: '#1a1a2e', + fillColor: '#1a1a2e', + fillOpacity: 0.3, + weight: 1, + }).addTo(map).bindTooltip(loc.label, { className: 'map-tooltip', permanent: false }); + markers.set(loc.id, m); + } +} + +export function updateMap(dread: number, areas: { rosengard: boolean; varnhem: boolean; torso: boolean }): void { + if (!map) return; + + const levels: Record = { + rosengard: areas.rosengard ? Math.min(3, Math.floor(dread / 40) + 1) : Math.min(1, Math.floor(dread / 60)), + varnhem: areas.varnhem ? Math.min(3, Math.floor(dread / 35) + 1) : 0, + torso: areas.torso ? 3 : Math.min(1, Math.floor(dread / 80)), + oresund: Math.min(2, Math.floor(dread / 50)), + ribersborg: Math.min(2, Math.floor(dread / 45)), + davidshall: Math.min(1, Math.floor(dread / 70)), + stortorget: Math.min(2, Math.floor(dread / 55)), + }; + + for (const [id, marker] of markers) { + const level = levels[id] ?? 0; + marker.setStyle({ + radius: dreadRadius(level), + color: dreadColor(level), + fillColor: dreadColor(level), + fillOpacity: 0.15 + level * 0.2, + weight: level > 0 ? 2 : 1, + }); + } +} diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..4abec0e --- /dev/null +++ b/src/style.css @@ -0,0 +1,276 @@ +@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap'); + +:root { + --bg: #06060e; + --panel: #0a0a16; + --border: #1a1a2e; + --text: #c8c8d8; + --dim: #4a4a6a; + --accent: #7a44aa; /* eldritch purple */ + --dread: #cc44cc; /* dread pink */ + --kaos: #ff6600; /* chaos orange */ + --safe: #228844; /* hemlighet green */ + --danger: #cc2222; /* investigator red */ + --ritual: #4488ff; /* ritual blue */ + --win: #00e87a; +} + +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +html { background: var(--bg); } + +body { + background: var(--bg); + color: var(--text); + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 13px; + min-height: 100vh; + overflow-x: hidden; +} + +#app { + display: grid; + grid-template-columns: 280px 1fr 280px; + grid-template-rows: auto 1fr; + min-height: 100vh; + max-width: 1200px; + margin: 0 auto; + gap: 0; +} + +/* ── Header ── */ +#header { + grid-column: 1 / -1; + padding: 16px 20px 12px; + border-bottom: 1px solid var(--border); + display: flex; + align-items: baseline; + gap: 24px; +} + +.brand { + font-size: 20px; + font-weight: 700; + letter-spacing: 3px; + color: var(--dread); + text-shadow: 0 0 20px rgba(204, 68, 204, 0.4); +} + +.brand-sub { + font-size: 10px; + color: var(--dim); + letter-spacing: 0.1em; + text-transform: uppercase; +} + +/* ── Panels ── */ +.panel { + padding: 16px; + border-right: 1px solid var(--border); + overflow-y: auto; +} + +.panel:last-child { border-right: none; border-left: 1px solid var(--border); } + +.panel-title { + font-size: 10px; + color: var(--dim); + text-transform: uppercase; + letter-spacing: 0.1em; + margin-bottom: 12px; + padding-bottom: 6px; + border-bottom: 1px solid var(--border); +} + +/* ── Resources ── */ +.resource { + margin-bottom: 10px; +} + +.resource-label { + font-size: 10px; + color: var(--dim); + text-transform: uppercase; + letter-spacing: 0.08em; + margin-bottom: 3px; +} + +.resource-value { + font-size: 16px; + font-weight: 700; +} + +.resource-bar { + height: 3px; + background: var(--border); + margin-top: 4px; + border-radius: 2px; + overflow: hidden; +} + +.resource-bar-fill { + height: 100%; + border-radius: 2px; + transition: width 0.3s ease; +} + +.bar-hemlighet .resource-bar-fill { background: var(--safe); } +.bar-kaos .resource-bar-fill { background: var(--kaos); } +.bar-ritual .resource-bar-fill { background: var(--ritual); } + +/* ── Map ── */ +#map-container { + background: var(--panel); + position: relative; +} + +#map { + width: 100%; + height: 100%; + min-height: 400px; +} + +/* Leaflet overrides */ +.leaflet-container { background: #06060e !important; } +.map-tooltip { + background: var(--panel) !important; + border: 1px solid var(--border) !important; + color: var(--text) !important; + font-family: inherit !important; + font-size: 11px !important; + padding: 3px 8px !important; + border-radius: 2px !important; + box-shadow: none !important; +} +.map-tooltip::before { display: none !important; } + +/* ── Upgrades ── */ +.upgrade-btn { + display: block; + width: 100%; + background: none; + border: 1px solid var(--border); + color: var(--text); + font-family: inherit; + font-size: 11px; + text-align: left; + padding: 8px 10px; + cursor: pointer; + margin-bottom: 8px; + transition: border-color 0.15s, color 0.15s; + line-height: 1.4; +} + +.upgrade-btn:hover:not(:disabled) { + border-color: var(--accent); + color: var(--dread); +} + +.upgrade-btn:disabled { + opacity: 0.35; + cursor: not-allowed; +} + +.upgrade-btn .up-name { + font-weight: 700; + display: block; + margin-bottom: 2px; + color: var(--accent); +} + +.upgrade-btn .up-desc { + color: var(--dim); + font-size: 10px; + display: block; + margin-bottom: 4px; +} + +.upgrade-btn .up-cost { font-size: 10px; color: var(--dim); } + +/* ── Log ── */ +#log { + padding: 0; + font-size: 11px; + line-height: 1.6; + display: flex; + flex-direction: column; + gap: 0; +} + +.log-entry { + padding: 8px 16px; + border-bottom: 1px solid #0e0e1a; + color: var(--text); +} + +.log-entry.kaos { color: var(--kaos); border-left: 2px solid var(--kaos); } +.log-entry.system { color: var(--dim); font-style: italic; } +.log-entry.upgrade { color: var(--accent); } + +/* ── Overlays ── */ +#overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(6, 6, 14, 0.92); + z-index: 100; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 16px; + text-align: center; +} + +#overlay.visible { display: flex; } + +.overlay-title { + font-size: 28px; + font-weight: 700; + letter-spacing: 4px; + text-transform: uppercase; +} + +.overlay-title.won { color: var(--win); text-shadow: 0 0 30px rgba(0,232,122,0.5); } +.overlay-title.lost { color: var(--danger); } + +.overlay-body { + max-width: 480px; + color: var(--dim); + font-size: 12px; + line-height: 1.8; +} + +.overlay-btn { + background: none; + border: 1px solid var(--dim); + color: var(--text); + font-family: inherit; + font-size: 12px; + letter-spacing: 0.1em; + text-transform: uppercase; + padding: 8px 24px; + cursor: pointer; + margin-top: 8px; + transition: border-color 0.15s, color 0.15s; +} + +.overlay-btn:hover { border-color: var(--accent); color: var(--dread); } + +/* ── Phase label ── */ +.phase-tag { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.1em; + padding: 2px 8px; + border: 1px solid; +} + +.phase-tag.early { color: var(--dim); border-color: var(--dim); } +.phase-tag.mid { color: var(--accent); border-color: var(--accent); } +.phase-tag.late { color: var(--dread); border-color: var(--dread); } + +@media (max-width: 900px) { + #app { grid-template-columns: 1fr; } + .panel { border-right: none; border-bottom: 1px solid var(--border); } + #map { min-height: 250px; } +} diff --git a/src/ui.ts b/src/ui.ts new file mode 100644 index 0000000..e06f3d5 --- /dev/null +++ b/src/ui.ts @@ -0,0 +1,214 @@ +import type { State } from './engine.ts'; +import { UPGRADES } from './upgrades.ts'; + +export function initUI(app: HTMLElement, onUpgrade: (id: string) => void, onRestart: () => void): void { + app.innerHTML = ` + + +
+
Resurser
+ +
+
Troende
+
1
+
+ +
+
Andakt
+
0
+
+ +
+
Dread
+
0
+
+ +
+
Hemlighet
+
100
+
+
+ +
+
Kaos
+
0
+
+
+ + + +
+
Åtgärder
+
+
+
+ +
+
+
+
+ +
+
Aktiva områden
+
+
○ Rosengård
+
○ Värnhemstorget
+
○ Turning Torso
+
+
+
Utredarna
+
Inaktiva
+
+
+ +
+
+
+ +
+ `; + + document.getElementById('overlay-btn')!.addEventListener('click', onRestart); + + // Split the map container: map on top, log below + const mapContainer = document.getElementById('map-container')!; + mapContainer.style.cssText = 'display:flex;flex-direction:column;overflow:hidden;'; + const mapEl = document.getElementById('map')!; + mapEl.style.cssText = 'flex:0 0 45%;min-height:0;'; + const logEl = document.getElementById('log')!; + logEl.style.cssText = 'flex:1;overflow-y:auto;border-top:1px solid var(--border);'; + + // Upgrade click delegation + document.getElementById('upgrades')!.addEventListener('click', e => { + const btn = (e.target as Element).closest('[data-upgrade]') as HTMLElement | null; + if (btn?.dataset['upgrade']) onUpgrade(btn.dataset['upgrade']); + }); +} + +export function renderState(state: State): void { + setText('r-troende', String(state.troende)); + setText('r-andakt', fmt(state.andakt)); + setText('r-andakt-rate', `+${(state.andaktRate * state.troende).toFixed(1)}/s`); + setText('r-dread', fmt(state.dread)); + setText('r-dread-rate', state.dreadRate > 0 ? `+${state.dreadRate.toFixed(1)}/s` : ''); + setText('r-hemlighet', Math.floor(state.hemlighet).toString()); + setText('r-kaos', Math.floor(state.kaos).toString()); + setBar('bar-hemlighet', state.hemlighet); + setBar('bar-kaos', state.kaos); + + const ritualSection = document.getElementById('ritual-section')!; + if (state.ritualUnlocked) { + ritualSection.style.display = ''; + setText('r-ritual', Math.floor(state.ritual).toString()); + setBar('bar-ritual', state.ritual); + } + + // Phase tag + const phaseEl = document.getElementById('phase-tag')!; + if (state.ritual >= 50) { + phaseEl.textContent = 'Sent skede'; phaseEl.className = 'phase-tag late'; + } else if (state.dread >= 30) { + phaseEl.textContent = 'Mitt skede'; phaseEl.className = 'phase-tag mid'; + } else { + phaseEl.textContent = 'Tidigt skede'; phaseEl.className = 'phase-tag early'; + } + + // Areas + setArea('area-rosengard', state.areas.rosengard, 'Rosengård'); + setArea('area-varnhem', state.areas.varnhem, 'Värnhemstorget'); + setArea('area-torso', state.areas.torso, 'Turning Torso'); + + // Investigator status + const inv = document.getElementById('investigator-status')!; + if (state.hemlighet < 20) { + inv.textContent = 'Nära — väldigt nära'; inv.style.color = 'var(--danger)'; + } else if (state.hemlighet < 50) { + inv.textContent = 'Missstänksamma'; inv.style.color = 'var(--kaos)'; + } else { + inv.textContent = 'Inaktiva'; inv.style.color = 'var(--safe)'; + } + + // Upgrades + renderUpgrades(state); + + // Log + renderLog(state); + + // Overlay + if (state.phase !== 'playing') showOverlay(state); +} + +function renderUpgrades(state: State): void { + const container = document.getElementById('upgrades')!; + const available = UPGRADES.filter(u => u.available(state) && (u.once ? !state.upgrades.has(u.id) : true)); + + if (!available.length) { + container.innerHTML = '
Inga tillgängliga åtgärder.
'; + return; + } + + container.innerHTML = available.map(u => { + const canAfford = (!u.cost.andakt || state.andakt >= u.cost.andakt) && (!u.cost.dread || state.dread >= u.cost.dread); + const costs = [ + u.cost.andakt ? `${u.cost.andakt} andakt` : '', + u.cost.dread ? `${u.cost.dread} dread` : '', + ].filter(Boolean).join(', '); + return ``; + }).join(''); +} + +function renderLog(state: State): void { + const log = document.getElementById('log')!; + log.innerHTML = state.log.map(e => + `
${e.text}
` + ).join(''); +} + +function showOverlay(state: State): void { + const ov = document.getElementById('overlay')!; + ov.classList.add('visible'); + const title = document.getElementById('overlay-title')!; + const body = document.getElementById('overlay-body')!; + if (state.phase === 'won') { + title.textContent = 'FULLBORDAT'; + title.className = 'overlay-title won'; + body.textContent = 'Det är gjort. Turning Torso pekar rakt ned. Havet är stilla — alltför stilla. Malmö fortsätter som vanligt. Det är det värsta.'; + } else { + title.textContent = 'AVSLÖJAD'; + title.className = 'overlay-title lost'; + body.textContent = state.lostReason ?? 'Utredarna kom.'; + } +} + +function setText(id: string, val: string): void { + const el = document.getElementById(id); + if (el) el.textContent = val; +} + +function setBar(id: string, pct: number): void { + const el = document.getElementById(id) as HTMLElement | null; + if (el) el.style.width = `${Math.min(100, Math.max(0, pct))}%`; +} + +function setArea(id: string, active: boolean, label: string): void { + const el = document.getElementById(id); + if (el) { + el.textContent = `${active ? '●' : '○'} ${label}`; + el.style.color = active ? 'var(--dread)' : 'var(--dim)'; + } +} + +function fmt(n: number): string { + return Math.floor(n).toLocaleString('sv-SE'); +} diff --git a/src/upgrades.ts b/src/upgrades.ts new file mode 100644 index 0000000..301d966 --- /dev/null +++ b/src/upgrades.ts @@ -0,0 +1,86 @@ +import type { State } from './engine.ts'; + +export interface Upgrade { + id: string; + name: string; + desc: string; + cost: Partial<{ andakt: number; dread: number }>; + effect: (s: State) => void; + available: (s: State) => boolean; + once: boolean; +} + +export const UPGRADES: Upgrade[] = [ + { + id: 'recruit_neighbor', + name: 'Rekrytera en granne', + desc: '+1 Troende. De undrade varför källarlamporna flimrade. Nu vet de.', + cost: { andakt: 20 }, + effect: s => { s.troende += 1; }, + available: s => !s.upgrades.has('first_meeting'), + once: false, + }, + { + id: 'first_meeting', + name: 'Källarmöte i Rosengård', + desc: 'Formalisera mötena. +1 andakt/s. Det luktar lite konstigt men ingen säger något.', + cost: { andakt: 30 }, + effect: s => { s.andaktRate += 1; s.areas.rosengard = true; }, + available: s => s.troende >= 2 && !s.upgrades.has('first_meeting'), + once: true, + }, + { + id: 'the_manuscript', + name: 'Översätt Skriften', + desc: 'Någon hittade ett dokument i en källarlokal vid Davidshallsgatan. Texten översätts bäst som "snälla sluta." Vi fortsätter.', + cost: { andakt: 80 }, + effect: s => { s.dreadRate += 0.5; s.ritualUnlocked = true; }, + available: s => s.troende >= 3 && s.upgrades.has('first_meeting') && !s.upgrades.has('the_manuscript'), + once: true, + }, + { + id: 'idol_under_bridge', + name: 'Idolen under bron', + desc: 'Vi placerade en idol under Øresundsbron vid nattens mörkaste timme. Danskarna la märke till det. De säger ingenting.', + cost: { andakt: 120, dread: 20 }, + effect: s => { s.dreadRate += 1; s.andaktRate += 0.5; }, + available: s => s.upgrades.has('the_manuscript') && !s.upgrades.has('idol_under_bridge'), + once: true, + }, + { + id: 'varnhem_circle', + name: 'Cirkeln på Värnhemstorget', + desc: 'Rita cirkeln. Använd krita. Det är viktigt att det är krita. Kommunen tvättar bort den varje morgon. Den är tillbaka varje kväll.', + cost: { andakt: 200, dread: 40 }, + effect: s => { s.areas.varnhem = true; s.dreadRate += 1.5; s.kaosRate += 0.2; }, + available: s => s.upgrades.has('idol_under_bridge') && s.dread >= 50 && !s.upgrades.has('varnhem_circle'), + once: true, + }, + { + id: 'recruit_2', + name: 'Rekrytera ytterligare troende', + desc: '+2 Troende. Ryktet sprids längs de rätta kanalerna.', + cost: { andakt: 150 }, + effect: s => { s.troende += 2; s.andaktRate += 0.5; }, + available: s => s.upgrades.has('first_meeting') && s.troende < 8, + once: false, + }, + { + id: 'torso_beacon', + name: 'Altaret vid Turning Torso', + desc: 'Byggnaden har alltid lyssnat. Nu ber vi den svara. Den gör det.', + cost: { andakt: 400, dread: 100 }, + effect: s => { s.areas.torso = true; s.dreadRate += 3; s.ritualRate += 0.5; }, + available: s => s.upgrades.has('varnhem_circle') && s.dread >= 100 && !s.upgrades.has('torso_beacon'), + once: true, + }, + { + id: 'grand_ritual_prep', + name: 'Förberedelser för Den Stora Ritualen', + desc: 'Allt har lett hit. Turning Torso pekar. Värnhemstorget väntar. Öresund drar sig tillbaka.', + cost: { andakt: 600, dread: 200 }, + effect: s => { s.ritualRate += 2; s.kaosRate += 0.5; }, + available: s => s.upgrades.has('torso_beacon') && s.ritual >= 50 && !s.upgrades.has('grand_ritual_prep'), + once: true, + }, +]; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1045bf3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..d8e05d8 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,2 @@ +import { defineConfig } from 'vite'; +export default defineConfig({ server: { port: 5681 } });