Add Inflytande resource, expand events, and balance the endgame
All checks were successful
Docker / build-and-push (push) Successful in 42s
All checks were successful
Docker / build-and-push (push) Successful in 42s
Adds an influence currency that scales with follower count and a new "exert influence" action to trade it for secrecy, uncaps recruiting with scaling cost, adds a late-game Andakt sink, and makes hemlighet drain scale with dread/ritual progress so the endgame carries real risk. Includes a Vitest suite that simulates play to check balance, which caught the influence-accrual rate being too slow to be useful early game. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
389
package-lock.json
generated
389
package-lock.json
generated
@@ -9,7 +9,8 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.5.0",
|
"typescript": "^5.5.0",
|
||||||
"vite": "^6.0.0"
|
"vite": "^6.0.0",
|
||||||
|
"vitest": "^4.1.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
@@ -454,6 +455,13 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
|
"version": "1.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.62.2",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
|
||||||
@@ -804,6 +812,31 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/chai": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/deep-eql": "*",
|
||||||
|
"assertion-error": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/deep-eql": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||||
@@ -811,6 +844,153 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@vitest/expect": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.1.0",
|
||||||
|
"@types/chai": "^5.2.2",
|
||||||
|
"@vitest/spy": "4.1.10",
|
||||||
|
"@vitest/utils": "4.1.10",
|
||||||
|
"chai": "^6.2.2",
|
||||||
|
"tinyrainbow": "^3.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/mocker": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/spy": "4.1.10",
|
||||||
|
"estree-walker": "^3.0.3",
|
||||||
|
"magic-string": "^0.30.21"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"msw": "^2.4.9",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"msw": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/pretty-format": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tinyrainbow": "^3.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/runner": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/utils": "4.1.10",
|
||||||
|
"pathe": "^2.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/snapshot": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/pretty-format": "4.1.10",
|
||||||
|
"@vitest/utils": "4.1.10",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
|
"pathe": "^2.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/spy": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/utils": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/pretty-format": "4.1.10",
|
||||||
|
"convert-source-map": "^2.0.0",
|
||||||
|
"tinyrainbow": "^3.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/assertion-error": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chai": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/convert-source-map": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/es-module-lexer": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||||
@@ -853,6 +1033,26 @@
|
|||||||
"@esbuild/win32-x64": "0.25.12"
|
"@esbuild/win32-x64": "0.25.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/estree-walker": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expect-type": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fdir": {
|
"node_modules/fdir": {
|
||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
@@ -886,6 +1086,16 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/magic-string": {
|
||||||
|
"version": "0.30.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
|
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.15",
|
"version": "3.3.15",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
|
||||||
@@ -905,6 +1115,27 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/obug": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/sxzz",
|
||||||
|
"https://opencollective.com/debug"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pathe": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -1000,6 +1231,13 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/siginfo": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -1010,6 +1248,37 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stackback": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/std-env": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tinybench": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tinyexec": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.17",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
@@ -1027,6 +1296,16 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tinyrainbow": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
@@ -1047,6 +1326,7 @@
|
|||||||
"integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
|
"integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
@@ -1115,6 +1395,113 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vitest": {
|
||||||
|
"version": "4.1.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz",
|
||||||
|
"integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/expect": "4.1.10",
|
||||||
|
"@vitest/mocker": "4.1.10",
|
||||||
|
"@vitest/pretty-format": "4.1.10",
|
||||||
|
"@vitest/runner": "4.1.10",
|
||||||
|
"@vitest/snapshot": "4.1.10",
|
||||||
|
"@vitest/spy": "4.1.10",
|
||||||
|
"@vitest/utils": "4.1.10",
|
||||||
|
"es-module-lexer": "^2.0.0",
|
||||||
|
"expect-type": "^1.3.0",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
|
"obug": "^2.1.1",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"picomatch": "^4.0.3",
|
||||||
|
"std-env": "^4.0.0-rc.1",
|
||||||
|
"tinybench": "^2.9.0",
|
||||||
|
"tinyexec": "^1.0.2",
|
||||||
|
"tinyglobby": "^0.2.15",
|
||||||
|
"tinyrainbow": "^3.1.0",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||||
|
"why-is-node-running": "^2.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vitest": "vitest.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@edge-runtime/vm": "*",
|
||||||
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||||
|
"@vitest/browser-playwright": "4.1.10",
|
||||||
|
"@vitest/browser-preview": "4.1.10",
|
||||||
|
"@vitest/browser-webdriverio": "4.1.10",
|
||||||
|
"@vitest/coverage-istanbul": "4.1.10",
|
||||||
|
"@vitest/coverage-v8": "4.1.10",
|
||||||
|
"@vitest/ui": "4.1.10",
|
||||||
|
"happy-dom": "*",
|
||||||
|
"jsdom": "*",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@edge-runtime/vm": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@opentelemetry/api": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-playwright": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-preview": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-webdriverio": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/coverage-istanbul": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/coverage-v8": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/ui": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"happy-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jsdom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/why-is-node-running": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"siginfo": "^2.0.0",
|
||||||
|
"stackback": "0.0.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"why-is-node-running": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,12 @@
|
|||||||
"dev": "vite --port 5681",
|
"dev": "vite --port 5681",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"typecheck": "tsc",
|
"typecheck": "tsc",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.5.0",
|
"typescript": "^5.5.0",
|
||||||
"vite": "^6.0.0"
|
"vite": "^6.0.0",
|
||||||
|
"vitest": "^4.1.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const VERSION = '0.2.0';
|
export const VERSION = '0.3.0';
|
||||||
|
|
||||||
export interface ChangelogEntry {
|
export interface ChangelogEntry {
|
||||||
version: string;
|
version: string;
|
||||||
@@ -7,6 +7,19 @@ export interface ChangelogEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CHANGELOG: ChangelogEntry[] = [
|
export const CHANGELOG: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
version: '0.3.0',
|
||||||
|
date: '2026-07-08',
|
||||||
|
changes: [
|
||||||
|
'Utökad händelsepool: fler platser (Malmö C, Kockumskranen, Folkets Park, Triangeln, Limhamns kalkbrott), fler kaos-tillstånd, händelser för större sällskap (fler än 9 troende är inte alltid en fördel)',
|
||||||
|
'Ny resurs: Inflytande — växer snabbare ju fler troende ni är, ett nätverk av tjänster och tystnad',
|
||||||
|
'Ny åtgärd: Utöva inflytande — köp tillbaka hemlighet med inflytande, på bekostnad av lite kaos',
|
||||||
|
'Rekrytera en troende är inte längre begränsat till 12 — men priset stiger brant för varje ny medlem',
|
||||||
|
'Ny sen uppgradering: Fördjupa ritualen — ett oändligt (men allt dyrare) sätt att spendera andakt när Den Stora Ritualen väl förberetts',
|
||||||
|
'Hemlighet eroderar nu snabbare vid hög dread och långt gången ritual — att komma nära slutet är farligare än förut',
|
||||||
|
'Testsvit tillagd (npm test) för att hålla spelet balanserat',
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
version: '0.2.0',
|
version: '0.2.0',
|
||||||
date: '2026-07-07',
|
date: '2026-07-07',
|
||||||
|
|||||||
199
src/engine.test.ts
Normal file
199
src/engine.test.ts
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import { describe, it, expect, afterEach } from 'vitest';
|
||||||
|
import { createState, tick, buyUpgrade, type State } from './engine.ts';
|
||||||
|
import { UPGRADES } from './upgrades.ts';
|
||||||
|
|
||||||
|
// Deterministic PRNG so simulation tests aren't flaky, but still exercise
|
||||||
|
// the real event-selection and random-jitter code paths.
|
||||||
|
function seededRandom(seed: number): () => number {
|
||||||
|
let s = seed;
|
||||||
|
return () => {
|
||||||
|
s = (s * 1103515245 + 12345) & 0x7fffffff;
|
||||||
|
return s / 0x7fffffff;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let restoreRandom: (() => void) | null = null;
|
||||||
|
function seedRandom(seed: number): void {
|
||||||
|
const rnd = seededRandom(seed);
|
||||||
|
const original = Math.random;
|
||||||
|
Math.random = rnd;
|
||||||
|
restoreRandom = () => { Math.random = original; };
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
restoreRandom?.();
|
||||||
|
restoreRandom = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Buy every upgrade whose id is in `ids`, in order, if affordable and available. */
|
||||||
|
function tryBuy(state: State, ids: string[]): State {
|
||||||
|
let s = state;
|
||||||
|
for (const id of ids) {
|
||||||
|
s = buyUpgrade(s, id);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('resource invariants', () => {
|
||||||
|
it('keeps hemlighet, kaos, and ritual within [0, 100] over a long random run', () => {
|
||||||
|
seedRandom(42);
|
||||||
|
let s = createState();
|
||||||
|
for (let i = 0; i < 30000 && s.phase === 'playing'; i++) {
|
||||||
|
s = tick(s);
|
||||||
|
// occasionally recruit / exert influence to exercise those paths too
|
||||||
|
if (i % 500 === 0) s = tryBuy(s, ['recruit_neighbor', 'exert_influence']);
|
||||||
|
expect(s.hemlighet).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(s.hemlighet).toBeLessThanOrEqual(100);
|
||||||
|
expect(s.kaos).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(s.kaos).toBeLessThanOrEqual(100);
|
||||||
|
expect(s.ritual).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(s.ritual).toBeLessThanOrEqual(100);
|
||||||
|
expect(s.troende).toBeGreaterThanOrEqual(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('never lets a kaos event push kaos negative or leave it stuck at 100', () => {
|
||||||
|
seedRandom(7);
|
||||||
|
let s = createState();
|
||||||
|
s.kaos = 99;
|
||||||
|
for (let i = 0; i < 5000 && s.phase === 'playing'; i++) {
|
||||||
|
s = tick(s);
|
||||||
|
}
|
||||||
|
expect(s.kaos).toBeGreaterThanOrEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('the ritual is winnable', () => {
|
||||||
|
it('reaches phase "won" within a reasonable number of ticks under a sensible strategy', () => {
|
||||||
|
seedRandom(1);
|
||||||
|
let s = createState();
|
||||||
|
|
||||||
|
const storyPath = [
|
||||||
|
'first_meeting',
|
||||||
|
'the_manuscript',
|
||||||
|
'blood_pact',
|
||||||
|
'idol_under_bridge',
|
||||||
|
'varnhem_circle',
|
||||||
|
'torso_beacon',
|
||||||
|
'grand_ritual_prep',
|
||||||
|
];
|
||||||
|
|
||||||
|
const MAX_TICKS = 400_000; // ~3.7h of real playtime at 30fps — generous upper bound
|
||||||
|
for (let i = 0; i < MAX_TICKS && s.phase === 'playing'; i++) {
|
||||||
|
s = tick(s);
|
||||||
|
// Defend secrecy proactively every tick — reactive defense (only once
|
||||||
|
// hemlighet is already low) is too slow against cascading events.
|
||||||
|
if (s.hemlighet < 85) s = tryBuy(s, ['exert_influence']);
|
||||||
|
if (i % 30 === 0) {
|
||||||
|
s = tryBuy(s, storyPath);
|
||||||
|
s = tryBuy(s, ['recruit_neighbor']);
|
||||||
|
if (s.upgrades.has('grand_ritual_prep')) s = tryBuy(s, ['deepen_ritual']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(s.phase).toBe('won');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('neglecting secrecy is punishable', () => {
|
||||||
|
it('can lose the game if hemlighet is never defended while growing the cult', () => {
|
||||||
|
seedRandom(3);
|
||||||
|
let s = createState();
|
||||||
|
|
||||||
|
const MAX_TICKS = 400_000;
|
||||||
|
for (let i = 0; i < MAX_TICKS && s.phase === 'playing'; i++) {
|
||||||
|
s = tick(s);
|
||||||
|
// Grow aggressively but NEVER spend on exert_influence or story upgrades
|
||||||
|
// that unlock ritual — just recruit, to stress secrecy drain.
|
||||||
|
if (i % 30 === 0) s = tryBuy(s, ['recruit_neighbor']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A cult that only ever recruits and never manages its secrecy should
|
||||||
|
// eventually be exposed, not idle forever or win by accident.
|
||||||
|
expect(s.phase).toBe('lost');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('recruit_neighbor cost scaling', () => {
|
||||||
|
it('grows monotonically with troende so andakt stays a meaningful sink', () => {
|
||||||
|
const recruit = UPGRADES.find(u => u.id === 'recruit_neighbor')!;
|
||||||
|
let s = createState();
|
||||||
|
let lastCost = 0;
|
||||||
|
for (let i = 0; i < 15; i++) {
|
||||||
|
const cost = recruit.cost(s).andakt!;
|
||||||
|
expect(cost).toBeGreaterThan(lastCost);
|
||||||
|
lastCost = cost;
|
||||||
|
s = { ...s, troende: s.troende + 1 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deepen_ritual cost scaling', () => {
|
||||||
|
it('grows monotonically with each purchase, giving andakt a late-game sink', () => {
|
||||||
|
let s = createState();
|
||||||
|
s.upgrades.add('grand_ritual_prep');
|
||||||
|
s.ritualRate = 2.5; // baseline after torso_beacon + grand_ritual_prep
|
||||||
|
s.andakt = 1_000_000;
|
||||||
|
s.dread = 1_000_000;
|
||||||
|
|
||||||
|
const deepen = UPGRADES.find(u => u.id === 'deepen_ritual')!;
|
||||||
|
let lastCost = 0;
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const cost = deepen.cost(s).andakt!;
|
||||||
|
expect(cost).toBeGreaterThan(lastCost);
|
||||||
|
lastCost = cost;
|
||||||
|
s = buyUpgrade(s, 'deepen_ritual');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('inflytande (influence) accumulation', () => {
|
||||||
|
it('scales faster than linearly with troende (a network effect)', () => {
|
||||||
|
function inflytandeAfter(troende: number, ticks: number): number {
|
||||||
|
let s = createState();
|
||||||
|
s.troende = troende;
|
||||||
|
s.andaktRate = 0; // isolate influence growth
|
||||||
|
for (let i = 0; i < ticks; i++) s = tick(s);
|
||||||
|
return s.inflytande;
|
||||||
|
}
|
||||||
|
|
||||||
|
const at2 = inflytandeAfter(2, 300);
|
||||||
|
const at4 = inflytandeAfter(4, 300);
|
||||||
|
const at8 = inflytandeAfter(8, 300);
|
||||||
|
|
||||||
|
// Doubling troende should more than double influence gained (superlinear).
|
||||||
|
expect(at4).toBeGreaterThan(at2 * 2);
|
||||||
|
expect(at8).toBeGreaterThan(at4 * 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('exert_influence', () => {
|
||||||
|
it('restores hemlighet at the cost of inflytande, and refuses purchase when inflytande is insufficient', () => {
|
||||||
|
let s = createState();
|
||||||
|
s.troende = 2;
|
||||||
|
s.hemlighet = 50;
|
||||||
|
s.inflytande = 0;
|
||||||
|
|
||||||
|
const blocked = buyUpgrade(s, 'exert_influence');
|
||||||
|
expect(blocked.inflytande).toBe(0);
|
||||||
|
expect(blocked.hemlighet).toBe(50);
|
||||||
|
|
||||||
|
s.inflytande = 100;
|
||||||
|
const after = buyUpgrade(s, 'exert_influence');
|
||||||
|
expect(after.hemlighet).toBeGreaterThan(50);
|
||||||
|
expect(after.inflytande).toBeLessThan(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buyUpgrade immutability', () => {
|
||||||
|
it('does not mutate the input state', () => {
|
||||||
|
const s = createState();
|
||||||
|
s.andakt = 1000;
|
||||||
|
s.troende = 2;
|
||||||
|
const frozenTroende = s.troende;
|
||||||
|
const frozenAndakt = s.andakt;
|
||||||
|
buyUpgrade(s, 'recruit_neighbor');
|
||||||
|
expect(s.troende).toBe(frozenTroende);
|
||||||
|
expect(s.andakt).toBe(frozenAndakt);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,6 +6,8 @@ export interface State {
|
|||||||
troende: number;
|
troende: number;
|
||||||
andakt: number;
|
andakt: number;
|
||||||
andaktRate: number; // per second
|
andaktRate: number; // per second
|
||||||
|
inflytande: number;
|
||||||
|
inflytandeRate: number; // multiplier on the base network-effect curve
|
||||||
dread: number;
|
dread: number;
|
||||||
dreadRate: number; // per second
|
dreadRate: number; // per second
|
||||||
hemlighet: number; // 0-100, lose at 0
|
hemlighet: number; // 0-100, lose at 0
|
||||||
@@ -31,12 +33,15 @@ export interface LogEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bump this when State shape changes to invalidate old saves
|
// Bump this when State shape changes to invalidate old saves
|
||||||
const SAVE_VERSION = '001';
|
const SAVE_VERSION = '002';
|
||||||
const SAVE_KEY = `djupet_v${SAVE_VERSION}`;
|
const SAVE_KEY = `djupet_v${SAVE_VERSION}`;
|
||||||
|
|
||||||
const EVENT_INTERVAL_TICKS = 180; // ~6s at 30fps
|
const EVENT_INTERVAL_TICKS = 180; // ~6s at 30fps
|
||||||
const KAOS_THRESHOLD = 100;
|
const KAOS_THRESHOLD = 100;
|
||||||
const HEMLIGHET_DRAIN_PER_TROENDE = 0.002; // per second, per troende above 4
|
const HEMLIGHET_DRAIN_PER_TROENDE = 0.002; // per second, per troende above 4
|
||||||
|
const DREAD_HEMLIGHET_DRAIN = 0.35; // per second, saturating with dread — the deeper it goes, the more it's noticed
|
||||||
|
const RITUAL_HEMLIGHET_DRAIN = 0.25; // per second at ritual=100 — an active ritual is hard to hide
|
||||||
|
const INFLYTANDE_BASE_RATE = 0.15; // per second, scales with troende^1.2 — a network effect
|
||||||
|
|
||||||
export function createState(): State {
|
export function createState(): State {
|
||||||
return {
|
return {
|
||||||
@@ -44,6 +49,8 @@ export function createState(): State {
|
|||||||
troende: 1,
|
troende: 1,
|
||||||
andakt: 10,
|
andakt: 10,
|
||||||
andaktRate: 0.3,
|
andaktRate: 0.3,
|
||||||
|
inflytande: 0,
|
||||||
|
inflytandeRate: 1,
|
||||||
dread: 0,
|
dread: 0,
|
||||||
dreadRate: 0,
|
dreadRate: 0,
|
||||||
hemlighet: 100,
|
hemlighet: 100,
|
||||||
@@ -102,6 +109,7 @@ export function tick(state: State): State {
|
|||||||
|
|
||||||
// Resource accumulation
|
// Resource accumulation
|
||||||
s.andakt += s.andaktRate * s.troende * DT;
|
s.andakt += s.andaktRate * s.troende * DT;
|
||||||
|
s.inflytande += INFLYTANDE_BASE_RATE * s.inflytandeRate * Math.pow(s.troende, 1.2) * DT;
|
||||||
s.dread += s.dreadRate * DT;
|
s.dread += s.dreadRate * DT;
|
||||||
if (s.ritualUnlocked) s.ritual = Math.min(100, s.ritual + s.ritualRate * DT);
|
if (s.ritualUnlocked) s.ritual = Math.min(100, s.ritual + s.ritualRate * DT);
|
||||||
|
|
||||||
@@ -109,10 +117,11 @@ export function tick(state: State): State {
|
|||||||
const kaosScale = 1 + (s.dread / 200);
|
const kaosScale = 1 + (s.dread / 200);
|
||||||
s.kaos = Math.min(100, s.kaos + s.kaosRate * kaosScale * DT);
|
s.kaos = Math.min(100, s.kaos + s.kaosRate * kaosScale * DT);
|
||||||
|
|
||||||
// Hemlighet drain — more believers = harder to stay hidden
|
// Hemlighet drain — more believers, more dread, and a deepening ritual all draw attention
|
||||||
if (s.troende > 4) {
|
const troendeDrain = s.troende > 4 ? HEMLIGHET_DRAIN_PER_TROENDE * (s.troende - 4) : 0;
|
||||||
s.hemlighet = Math.max(0, s.hemlighet - HEMLIGHET_DRAIN_PER_TROENDE * (s.troende - 4) * DT);
|
const dreadDrain = DREAD_HEMLIGHET_DRAIN * (s.dread / (s.dread + 150));
|
||||||
}
|
const ritualDrain = s.ritualUnlocked ? RITUAL_HEMLIGHET_DRAIN * (s.ritual / 100) : 0;
|
||||||
|
s.hemlighet = Math.max(0, s.hemlighet - (troendeDrain + dreadDrain + ritualDrain) * DT);
|
||||||
|
|
||||||
// Standard event
|
// Standard event
|
||||||
if (s.tick >= s.nextEventTick) {
|
if (s.tick >= s.nextEventTick) {
|
||||||
@@ -266,12 +275,15 @@ export function buyUpgrade(state: State, id: string): State {
|
|||||||
const upgrade = UPGRADES.find(u => u.id === id);
|
const upgrade = UPGRADES.find(u => u.id === id);
|
||||||
if (!upgrade) return state;
|
if (!upgrade) return state;
|
||||||
if (!upgrade.available(state)) return state;
|
if (!upgrade.available(state)) return state;
|
||||||
if (upgrade.cost.andakt && state.andakt < upgrade.cost.andakt) return state;
|
const cost = upgrade.cost(state);
|
||||||
if (upgrade.cost.dread && state.dread < upgrade.cost.dread) return state;
|
if (cost.andakt && state.andakt < cost.andakt) return state;
|
||||||
|
if (cost.dread && state.dread < cost.dread) return state;
|
||||||
|
if (cost.inflytande && state.inflytande < cost.inflytande) return state;
|
||||||
|
|
||||||
const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds) };
|
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 (cost.andakt) s.andakt -= cost.andakt;
|
||||||
if (upgrade.cost.dread) s.dread -= upgrade.cost.dread;
|
if (cost.dread) s.dread -= cost.dread;
|
||||||
|
if (cost.inflytande) s.inflytande -= cost.inflytande;
|
||||||
upgrade.effect(s);
|
upgrade.effect(s);
|
||||||
if (upgrade.once) s.upgrades.add(id);
|
if (upgrade.once) s.upgrades.add(id);
|
||||||
s.log.unshift({ tick: s.tick, text: `↳ ${upgrade.name}: ${upgrade.desc}`, kind: 'upgrade' });
|
s.log.unshift({ tick: s.tick, text: `↳ ${upgrade.name}: ${upgrade.desc}`, kind: 'upgrade' });
|
||||||
|
|||||||
201
src/events.ts
201
src/events.ts
@@ -282,6 +282,166 @@ export const STANDARD_EVENTS: GameEvent[] = [
|
|||||||
weight: 4,
|
weight: 4,
|
||||||
requires: { minRitual: 30, minDread: 50 },
|
requires: { minRitual: 30, minDread: 50 },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── Fler troende, fler problem ──
|
||||||
|
{
|
||||||
|
id: 'troende_schism',
|
||||||
|
text: 'Nio troende är fler än vi någonsin haft. Två av dem har börjat diskutera huruvida Den Sovande ska tilltalas i bestämd eller obestämd form. Diskussionen tar tid från ritualen.',
|
||||||
|
effect: { kaos: 10, andakt: -8 },
|
||||||
|
weight: 7,
|
||||||
|
requires: { minTroende: 9 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'troende_potluck',
|
||||||
|
text: 'Med så många i lokalen har vi infört fikaschema. Broder Lindqvists kanelbullar innehåller något som inte står på innehållsförteckningen. Ingen har klagat. Ingen kommer att klaga.',
|
||||||
|
effect: { andakt: 20, kaos: 6 },
|
||||||
|
weight: 7,
|
||||||
|
requires: { minTroende: 6 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'troende_census',
|
||||||
|
text: 'Skatteverket vill räkna medlemmarna i vår förening. Tolv namn står på listan. Vi är elva. Ingen minns vem den tolfte är. Alla minns att hen alltid suttit på den varma stolen.',
|
||||||
|
effect: { hemlighet: -10, dread: 14 },
|
||||||
|
weight: 6,
|
||||||
|
requires: { minTroende: 11 },
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Nya platser, nya konsekvenser ──
|
||||||
|
{
|
||||||
|
id: 'malmo_c_departures',
|
||||||
|
text: 'Avgångstavlan på Malmö C visar ett tåg mot en station som heter samma sak baklänges. Det avgår om exakt 13 minuter, varje dag, för evigt. Ingen har åkt med det än. Någon kommer att göra det snart.',
|
||||||
|
effect: { dread: 16, kaos: 7 },
|
||||||
|
weight: 7,
|
||||||
|
requires: { minDread: 15 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'kockums_crane',
|
||||||
|
text: 'Kockumskranen, som revs för länge sedan, syns nu i horisonten igen på klara kvällar. Stadsmuseet har inga förklaringar. De har slutat leta efter dem.',
|
||||||
|
effect: { dread: 20, kaos: 9 },
|
||||||
|
weight: 6,
|
||||||
|
requires: { minDread: 25 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'folkets_park_carousel',
|
||||||
|
text: 'Karusellen i Folkets Park har börjat gå av sig själv efter stängning. Vaktmästaren säger att den går fortare för varje varv. Han har slutat räkna varven.',
|
||||||
|
effect: { dread: 13, kaos: 8 },
|
||||||
|
weight: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'moderna_museet',
|
||||||
|
text: 'Moderna Museet Malmö har ställt ut ett verk ingen konstnär signerat. Besöksstatistiken visar att alla som tittar på det stannar exakt 13 minuter. Ingen kommer ihåg vad de såg. Alla ler på väg ut.',
|
||||||
|
effect: { dread: 11, andakt: 6, kaos: 5 },
|
||||||
|
weight: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hyllie_construction',
|
||||||
|
text: 'Bygget vid Hyllie har stoppats efter att grävmaskinisten hittade "något organiskt, men fel format" två meter ner. Kommunen har täckt över hålet. Hålet är fortfarande varmt.',
|
||||||
|
effect: { dread: 24, kaos: 10 },
|
||||||
|
weight: 5,
|
||||||
|
requires: { minDread: 30 },
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Hemlighet-fokuserade händelser ──
|
||||||
|
{
|
||||||
|
id: 'low_secrecy_press',
|
||||||
|
text: 'Ett anonymt tips har nått Sydsvenskan om "aktivitet i en källarlokal." Reportern ringer tre gånger. Vi svarar inte. Hon skriver artikeln ändå, baserad på "källor med insyn."',
|
||||||
|
effect: { hemlighet: -18, kaos: 10 },
|
||||||
|
weight: 6,
|
||||||
|
requires: { minDread: 20 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'secrecy_recovered',
|
||||||
|
text: 'Syster Hassan har övertygat grannskapsföreningen om att våra sammankomster är en läsecirkel för nordisk kriminallitteratur. De ber om att få vara med. Vi säger nej vänligt.',
|
||||||
|
effect: { hemlighet: 12, andakt: 5 },
|
||||||
|
weight: 8,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Ritual-komplikationer ──
|
||||||
|
{
|
||||||
|
id: 'ritual_wrong_verse',
|
||||||
|
text: 'Broder Persson läste rad 217 istället för 218 under gårdagens ceremoni. Skillnaden är en bokstav. Effekten är inte en bokstavs skillnad. Vi har lagt till bokmärken.',
|
||||||
|
effect: { ritual: -3, kaos: 14, dread: 8 },
|
||||||
|
weight: 6,
|
||||||
|
requires: { minRitual: 15 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ritual_translation',
|
||||||
|
text: 'En doktorand i lingvistik vid Lunds universitet har oavsiktligt löst tre rader av Skriften medan hon försökte identifiera "ett ovanligt baltiskt dialektdrag." Hon vet inte vad hon hittade. Vi vet.',
|
||||||
|
effect: { ritual: 6, dread: 15, hemlighet: -6 },
|
||||||
|
weight: 5,
|
||||||
|
requires: { minRitual: 20 },
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Inflytandets pris ──
|
||||||
|
{
|
||||||
|
id: 'influence_favor_owed',
|
||||||
|
text: 'Tjänstemannen vi charmade förra veckan ber om en gentjänst. Han vill bara "titta på lokalen." Vi visar honom källaren, inte källaren under källaren. Han ler för mycket för att ha märkt skillnaden.',
|
||||||
|
effect: { hemlighet: -6, kaos: 8, andakt: 6 },
|
||||||
|
weight: 7,
|
||||||
|
requires: { minDread: 15 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'influence_debt_network',
|
||||||
|
text: 'Broder Perssons kontaktnät sträcker sig längre än vi trodde. En rådman, en fastighetsmäklare, en person på Länsstyrelsen. Ingen av dem minns exakt varför de hjälper oss. De bara gör det.',
|
||||||
|
effect: { hemlighet: 10, andakt: 12 },
|
||||||
|
weight: 7,
|
||||||
|
requires: { minTroende: 4 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'influence_backfires',
|
||||||
|
text: 'Journalisten vi "lugnade ner" förra månaden har börjat drömma om oss istället. Hon ringer mitt i natten och frågar om vi "har hört sången än." Vi har inte sagt något om en sång.',
|
||||||
|
effect: { hemlighet: -12, dread: 16, kaos: 10 },
|
||||||
|
weight: 6,
|
||||||
|
requires: { minDread: 25 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'influence_favor_bank',
|
||||||
|
text: 'En av våra "vänner i systemet" har bett om en tjänst tillbaka: att få komma till nästa sammankomst. Vi säger ja. Han sitter längst bak. Han antecknar ingenting. Han ler hela tiden.',
|
||||||
|
effect: { troende: 1, hemlighet: -8, kaos: 6 },
|
||||||
|
weight: 6,
|
||||||
|
requires: { minTroende: 5 },
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Fler vardagsmalmö-händelser ──
|
||||||
|
{
|
||||||
|
id: 'moving_boxes',
|
||||||
|
text: 'Vi har flyttat verksamheten till en större lokal "för läsecirkeln." Flyttfirman noterade att en av lådorna vägde för lite för sin storlek och en annan för mycket. De sa ingenting. De tog dubbel betalt.',
|
||||||
|
effect: { andakt: 15, kaos: 5 },
|
||||||
|
weight: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ribersborg_kallbadhus',
|
||||||
|
text: 'Bastuvakten vid Ribersborgs kallbadhus har slutat elda bastun. Den är ändå varm. Han säger att den värmer sig själv nu, "underifrån." Han har bytt lås på dörren till det egna huset.',
|
||||||
|
effect: { dread: 14, kaos: 6 },
|
||||||
|
weight: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'augustenborg_greenroof',
|
||||||
|
text: 'Ett av de gröna taken i Augustenborg har börjat växa en växt ingen botaniker känner igen. Kommunens ekolog tog ett prov. Provet är fortfarande i tillväxt i sitt provrör, i ett låst skåp.',
|
||||||
|
effect: { dread: 12, andakt: 5 },
|
||||||
|
weight: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'triangeln_mall',
|
||||||
|
text: 'Rulltrappan på Triangeln köpcentrum stannade i tre minuter med alla passagerare kvar på den, orörliga, leende. Sedan startade den igen. Ingen av dem minns resan. Alla köpte samma sak efteråt.',
|
||||||
|
effect: { dread: 15, kaos: 9 },
|
||||||
|
weight: 7,
|
||||||
|
requires: { minDread: 20 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'davidshall_church',
|
||||||
|
text: 'Klockorna i S:t Johannes kyrka slog tretton slag klockan tolv på natten. Kyrkoherden säger att klockorna är mekaniskt oförmögna till det. Han har inte sovit sedan dess. Han ler ändå på söndagarna.',
|
||||||
|
effect: { dread: 18, hemlighet: -5 },
|
||||||
|
weight: 6,
|
||||||
|
requires: { minDread: 20 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'limhamn_quarry',
|
||||||
|
text: 'Kalkbrottet i Limhamn har fyllts med vatten i hundra år. Dykarklubben rapporterar en ny tunnel på botten som inte fanns förra veckan. De har slutat dyka där. De pratar inte om varför.',
|
||||||
|
effect: { dread: 20, ritual: 2 },
|
||||||
|
weight: 6,
|
||||||
|
requires: { minDread: 30 },
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// ── Kaos events (Henderson-tier) ─────────────────────────────────────────────
|
// ── Kaos events (Henderson-tier) ─────────────────────────────────────────────
|
||||||
@@ -405,4 +565,45 @@ export const KAOS_EVENTS: GameEvent[] = [
|
|||||||
weight: 2,
|
weight: 2,
|
||||||
requires: { minDread: 75, minRitual: 50 },
|
requires: { minDread: 75, minRitual: 50 },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'k_malmofestivalen',
|
||||||
|
text: 'Malmöfestivalen har av misstag bokat oss för en scen vid Gustav Adolfs Torg. Chantingen tolkas som experimentell körmusik. Vi vinner publikens pris. Ingen av juryn minns att de röstade.',
|
||||||
|
effect: { kaos: -35, andakt: 40, troende: 2, hemlighet: -12 },
|
||||||
|
weight: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'k_mmx_conspiracy',
|
||||||
|
text: 'Ett konspirationsforum har utsett oss till "årets mest underskattade hot mot verkligheten." Medlemmarna anländer i busslass för att "kolla läget." Tre av dem stannar för gott.',
|
||||||
|
effect: { kaos: -45, troende: 3, dread: 35, hemlighet: -15 },
|
||||||
|
weight: 6,
|
||||||
|
requires: { minTroende: 6 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'k_folkets_park_ferris',
|
||||||
|
text: 'Pariserhjulet i Folkets Park har fastnat med alla vagnar exakt vända mot Turning Torso. Nöjesfältet stänger "för underhåll." Underhållet kommer aldrig att bli klart.',
|
||||||
|
effect: { kaos: -55, dread: 55, ritual: 6 },
|
||||||
|
weight: 5,
|
||||||
|
requires: { unlockedArea: 'torso', minDread: 40 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'k_researcher_joins',
|
||||||
|
text: 'Doktoranden som råkade översätta tre rader av Skriften har sökt medlemskap. Hon säger att hon "bara vill se hur det slutar." Vi tror henne inte. Vi släpper in henne ändå.',
|
||||||
|
effect: { kaos: -40, troende: 1, ritual: 8, hemlighet: -10 },
|
||||||
|
weight: 6,
|
||||||
|
requires: { minRitual: 20 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'k_favor_called_in',
|
||||||
|
text: 'Alla våra kontakter i systemet ringer samtidigt och ber om samma gentjänst: en plats vid nästa ceremoni. Vi kan inte säga nej till alla. Lokalen är plötsligt fullsatt av folk i kostym som ler för mycket.',
|
||||||
|
effect: { kaos: -50, troende: 3, hemlighet: -20, andakt: 20 },
|
||||||
|
weight: 6,
|
||||||
|
requires: { minTroende: 5 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'k_influence_network_wakes',
|
||||||
|
text: 'Nätverket av "vänner i systemet" visar sig vara äldre och djupare än vi trodde. En av dem säger sig ha "tjänat samma sak sedan 1962, i en annan form." Han menar det inte som en gåta.',
|
||||||
|
effect: { kaos: -60, ritual: 10, hemlighet: -15, dread: 40 },
|
||||||
|
weight: 5,
|
||||||
|
requires: { minDread: 40 },
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
21
src/ui.ts
21
src/ui.ts
@@ -63,6 +63,11 @@ export function initUI(app: HTMLElement, onUpgrade: (id: string) => void, onRest
|
|||||||
<div class="resource-value" id="r-andakt">0</div>
|
<div class="resource-value" id="r-andakt">0</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="resource">
|
||||||
|
<div class="resource-label">Inflytande</div>
|
||||||
|
<div class="resource-value" id="r-inflytande">0</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="resource">
|
<div class="resource">
|
||||||
<div class="resource-label">Dread <span id="r-dread-rate" style="color:var(--dread);font-size:10px;"></span></div>
|
<div class="resource-label">Dread <span id="r-dread-rate" style="color:var(--dread);font-size:10px;"></span></div>
|
||||||
<div class="resource-value" style="color:var(--dread);" id="r-dread">0</div>
|
<div class="resource-value" style="color:var(--dread);" id="r-dread">0</div>
|
||||||
@@ -145,6 +150,7 @@ export function renderState(state: State): void {
|
|||||||
setText('r-troende', String(state.troende));
|
setText('r-troende', String(state.troende));
|
||||||
setText('r-andakt', fmt(state.andakt));
|
setText('r-andakt', fmt(state.andakt));
|
||||||
setText('r-andakt-rate', `+${(state.andaktRate * state.troende).toFixed(1)}/s`);
|
setText('r-andakt-rate', `+${(state.andaktRate * state.troende).toFixed(1)}/s`);
|
||||||
|
setText('r-inflytande', fmt(state.inflytande));
|
||||||
setText('r-dread', fmt(state.dread));
|
setText('r-dread', fmt(state.dread));
|
||||||
setText('r-dread-rate', state.dreadRate > 0 ? `+${state.dreadRate.toFixed(1)}/s` : '');
|
setText('r-dread-rate', state.dreadRate > 0 ? `+${state.dreadRate.toFixed(1)}/s` : '');
|
||||||
setText('r-hemlighet', Math.floor(state.hemlighet).toString());
|
setText('r-hemlighet', Math.floor(state.hemlighet).toString());
|
||||||
@@ -179,7 +185,7 @@ export function renderState(state: State): void {
|
|||||||
if (state.hemlighet < 20) {
|
if (state.hemlighet < 20) {
|
||||||
inv.textContent = 'Nära — väldigt nära'; inv.style.color = 'var(--danger)';
|
inv.textContent = 'Nära — väldigt nära'; inv.style.color = 'var(--danger)';
|
||||||
} else if (state.hemlighet < 50) {
|
} else if (state.hemlighet < 50) {
|
||||||
inv.textContent = 'Missstänksamma'; inv.style.color = 'var(--kaos)';
|
inv.textContent = 'Misstänksamma'; inv.style.color = 'var(--kaos)';
|
||||||
} else {
|
} else {
|
||||||
inv.textContent = 'Inaktiva'; inv.style.color = 'var(--safe)';
|
inv.textContent = 'Inaktiva'; inv.style.color = 'var(--safe)';
|
||||||
}
|
}
|
||||||
@@ -202,8 +208,9 @@ function renderUpgrades(state: State): void {
|
|||||||
|
|
||||||
// Only rebuild DOM when availability or affordability changes
|
// Only rebuild DOM when availability or affordability changes
|
||||||
const key = available.map(u => {
|
const key = available.map(u => {
|
||||||
const canAfford = (!u.cost.andakt || state.andakt >= u.cost.andakt) && (!u.cost.dread || state.dread >= u.cost.dread);
|
const cost = u.cost(state);
|
||||||
return `${u.id}:${canAfford}`;
|
const canAfford = (!cost.andakt || state.andakt >= cost.andakt) && (!cost.dread || state.dread >= cost.dread) && (!cost.inflytande || state.inflytande >= cost.inflytande);
|
||||||
|
return `${u.id}:${canAfford}:${cost.andakt ?? ''}`;
|
||||||
}).join('|');
|
}).join('|');
|
||||||
|
|
||||||
if (key === lastUpgradeKey) return;
|
if (key === lastUpgradeKey) return;
|
||||||
@@ -215,10 +222,12 @@ function renderUpgrades(state: State): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = available.map(u => {
|
container.innerHTML = available.map(u => {
|
||||||
const canAfford = (!u.cost.andakt || state.andakt >= u.cost.andakt) && (!u.cost.dread || state.dread >= u.cost.dread);
|
const cost = u.cost(state);
|
||||||
|
const canAfford = (!cost.andakt || state.andakt >= cost.andakt) && (!cost.dread || state.dread >= cost.dread) && (!cost.inflytande || state.inflytande >= cost.inflytande);
|
||||||
const costs = [
|
const costs = [
|
||||||
u.cost.andakt ? `${u.cost.andakt} andakt` : '',
|
cost.andakt ? `${cost.andakt} andakt` : '',
|
||||||
u.cost.dread ? `${u.cost.dread} dread` : '',
|
cost.dread ? `${cost.dread} dread` : '',
|
||||||
|
cost.inflytande ? `${cost.inflytande} inflytande` : '',
|
||||||
].filter(Boolean).join(', ');
|
].filter(Boolean).join(', ');
|
||||||
return `<button class="upgrade-btn" data-upgrade="${u.id}" ${canAfford ? '' : 'disabled'}>
|
return `<button class="upgrade-btn" data-upgrade="${u.id}" ${canAfford ? '' : 'disabled'}>
|
||||||
<span class="up-name">${u.name}</span>
|
<span class="up-name">${u.name}</span>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export interface Upgrade {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
cost: Partial<{ andakt: number; dread: number }>;
|
cost: (s: State) => Partial<{ andakt: number; dread: number; inflytande: number }>;
|
||||||
effect: (s: State) => void;
|
effect: (s: State) => void;
|
||||||
available: (s: State) => boolean;
|
available: (s: State) => boolean;
|
||||||
once: boolean;
|
once: boolean;
|
||||||
@@ -14,17 +14,26 @@ export const UPGRADES: Upgrade[] = [
|
|||||||
{
|
{
|
||||||
id: 'recruit_neighbor',
|
id: 'recruit_neighbor',
|
||||||
name: 'Rekrytera en troende',
|
name: 'Rekrytera en troende',
|
||||||
desc: '+1 Troende. De undrade varför källarlamporna flimrade. Nu vet de.',
|
desc: '+1 Troende. De undrade varför källarlamporna flimrade. Nu vet de. Fler munnar att mätta — priset stiger för varje ny medlem.',
|
||||||
cost: { andakt: 20 },
|
cost: s => ({ andakt: Math.round(20 * Math.pow(1.35, s.troende)) }),
|
||||||
effect: s => { s.troende += 1; },
|
effect: s => { s.troende += 1; },
|
||||||
available: s => s.troende < 12,
|
available: () => true,
|
||||||
|
once: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'exert_influence',
|
||||||
|
name: 'Utöva inflytande',
|
||||||
|
desc: 'Ring några kontakter. En tjänsteman blir plötsligt mycket samarbetsvillig. En journalist glömmer varför hon ringde. +hemlighet, men ryktet sprids ändå lite. +kaos',
|
||||||
|
cost: () => ({ inflytande: 30 }),
|
||||||
|
effect: s => { s.hemlighet = Math.min(100, s.hemlighet + 8); s.kaos = Math.min(100, s.kaos + 3); },
|
||||||
|
available: s => s.troende >= 2,
|
||||||
once: false,
|
once: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'first_meeting',
|
id: 'first_meeting',
|
||||||
name: 'Källarmöte i Rosengård',
|
name: 'Källarmöte i Rosengård',
|
||||||
desc: 'Broder Lindqvist vet en källarlokal. Den ska egentligen vara ett förråd. Hyresvärden vet inte om den. Det är okänt varför han inte vet om den. +1 andakt/s.',
|
desc: 'Broder Lindqvist vet en källarlokal. Den ska egentligen vara ett förråd. Hyresvärden vet inte om den. Det är okänt varför han inte vet om den. +1 andakt/s.',
|
||||||
cost: { andakt: 30 },
|
cost: () => ({ andakt: 30 }),
|
||||||
effect: s => { s.andaktRate += 1; s.areas.rosengard = true; },
|
effect: s => { s.andaktRate += 1; s.areas.rosengard = true; },
|
||||||
available: s => s.troende >= 2 && !s.upgrades.has('first_meeting'),
|
available: s => s.troende >= 2 && !s.upgrades.has('first_meeting'),
|
||||||
once: true,
|
once: true,
|
||||||
@@ -33,7 +42,7 @@ export const UPGRADES: Upgrade[] = [
|
|||||||
id: 'the_manuscript',
|
id: 'the_manuscript',
|
||||||
name: 'Översätt Skriften',
|
name: 'Översätt Skriften',
|
||||||
desc: 'Dokumentet hittades inmurad i en vägg vid Davidshallsgatan, bakom kakel från 1923. Texten rör sig om man tittar bort. Lingvisten vi anlitade satt tyst i tre timmar och gick sedan hem. Han skickade översättningen nästa morgon utan kommentar. Vi läser den inte högt.',
|
desc: 'Dokumentet hittades inmurad i en vägg vid Davidshallsgatan, bakom kakel från 1923. Texten rör sig om man tittar bort. Lingvisten vi anlitade satt tyst i tre timmar och gick sedan hem. Han skickade översättningen nästa morgon utan kommentar. Vi läser den inte högt.',
|
||||||
cost: { andakt: 80 },
|
cost: () => ({ andakt: 80 }),
|
||||||
effect: s => { s.dreadRate += 0.5; s.ritualUnlocked = true; },
|
effect: s => { s.dreadRate += 0.5; s.ritualUnlocked = true; },
|
||||||
available: s => s.troende >= 3 && s.upgrades.has('first_meeting') && !s.upgrades.has('the_manuscript'),
|
available: s => s.troende >= 3 && s.upgrades.has('first_meeting') && !s.upgrades.has('the_manuscript'),
|
||||||
once: true,
|
once: true,
|
||||||
@@ -42,7 +51,7 @@ export const UPGRADES: Upgrade[] = [
|
|||||||
id: 'blood_pact',
|
id: 'blood_pact',
|
||||||
name: 'Paktens Förnyelse',
|
name: 'Paktens Förnyelse',
|
||||||
desc: 'Det är en gammal pakt. Någon tecknade den för länge sedan. Vi vet inte vem. Vi förnyar den ändå — det verkar artigare. Skriften vet om det innan vi är klara.',
|
desc: 'Det är en gammal pakt. Någon tecknade den för länge sedan. Vi vet inte vem. Vi förnyar den ändå — det verkar artigare. Skriften vet om det innan vi är klara.',
|
||||||
cost: { andakt: 150, dread: 30 },
|
cost: () => ({ andakt: 150, dread: 30 }),
|
||||||
effect: s => { s.andaktRate += 1.5; s.hemlighet = Math.max(0, s.hemlighet - 10); },
|
effect: s => { s.andaktRate += 1.5; s.hemlighet = Math.max(0, s.hemlighet - 10); },
|
||||||
available: s => s.upgrades.has('the_manuscript') && !s.upgrades.has('blood_pact'),
|
available: s => s.upgrades.has('the_manuscript') && !s.upgrades.has('blood_pact'),
|
||||||
once: true,
|
once: true,
|
||||||
@@ -51,7 +60,7 @@ export const UPGRADES: Upgrade[] = [
|
|||||||
id: 'idol_under_bridge',
|
id: 'idol_under_bridge',
|
||||||
name: 'Idolen under bron',
|
name: 'Idolen under bron',
|
||||||
desc: 'Vi fann den redan nergrävd under Øresundsbron. Vi behövde bara gräva ner den djupare. Materialet är okänt. Det är varmt mot handen. Danskarna la märke till ingenting. Det är värre.',
|
desc: 'Vi fann den redan nergrävd under Øresundsbron. Vi behövde bara gräva ner den djupare. Materialet är okänt. Det är varmt mot handen. Danskarna la märke till ingenting. Det är värre.',
|
||||||
cost: { andakt: 120, dread: 20 },
|
cost: () => ({ andakt: 120, dread: 20 }),
|
||||||
effect: s => { s.dreadRate += 1; s.andaktRate += 0.5; },
|
effect: s => { s.dreadRate += 1; s.andaktRate += 0.5; },
|
||||||
available: s => s.upgrades.has('the_manuscript') && !s.upgrades.has('idol_under_bridge'),
|
available: s => s.upgrades.has('the_manuscript') && !s.upgrades.has('idol_under_bridge'),
|
||||||
once: true,
|
once: true,
|
||||||
@@ -60,7 +69,7 @@ export const UPGRADES: Upgrade[] = [
|
|||||||
id: 'varnhem_circle',
|
id: 'varnhem_circle',
|
||||||
name: 'Cirkeln på Värnhemstorget',
|
name: 'Cirkeln på Värnhemstorget',
|
||||||
desc: 'Rita cirkeln. Använd krita — det är viktigt att det är krita, vi vet inte varför men vi känner det. Kommunen tvättar bort den varje morgon. Den är tillbaka varje kväll. Nu är den tillbaka lite större.',
|
desc: 'Rita cirkeln. Använd krita — det är viktigt att det är krita, vi vet inte varför men vi känner det. Kommunen tvättar bort den varje morgon. Den är tillbaka varje kväll. Nu är den tillbaka lite större.',
|
||||||
cost: { andakt: 200, dread: 40 },
|
cost: () => ({ andakt: 200, dread: 40 }),
|
||||||
effect: s => { s.areas.varnhem = true; s.dreadRate += 1.5; s.kaosRate += 0.2; },
|
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'),
|
available: s => s.upgrades.has('idol_under_bridge') && s.dread >= 50 && !s.upgrades.has('varnhem_circle'),
|
||||||
once: true,
|
once: true,
|
||||||
@@ -69,7 +78,7 @@ export const UPGRADES: Upgrade[] = [
|
|||||||
id: 'torso_beacon',
|
id: 'torso_beacon',
|
||||||
name: 'Altaret vid Turning Torso',
|
name: 'Altaret vid Turning Torso',
|
||||||
desc: 'Calatrava-Santiago ritade byggnaden som en vridande ryggrad. Det är korrekt. Det är inte en metafor. Byggnaden har alltid lyssnat. Nu ber vi den svara. Den gör det. Svaret är ett ljud under hörselgränsen som vi känner i tänderna.',
|
desc: 'Calatrava-Santiago ritade byggnaden som en vridande ryggrad. Det är korrekt. Det är inte en metafor. Byggnaden har alltid lyssnat. Nu ber vi den svara. Den gör det. Svaret är ett ljud under hörselgränsen som vi känner i tänderna.',
|
||||||
cost: { andakt: 400, dread: 100 },
|
cost: () => ({ andakt: 400, dread: 100 }),
|
||||||
effect: s => { s.areas.torso = true; s.dreadRate += 3; s.ritualRate += 0.5; },
|
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'),
|
available: s => s.upgrades.has('varnhem_circle') && s.dread >= 100 && !s.upgrades.has('torso_beacon'),
|
||||||
once: true,
|
once: true,
|
||||||
@@ -78,9 +87,22 @@ export const UPGRADES: Upgrade[] = [
|
|||||||
id: 'grand_ritual_prep',
|
id: 'grand_ritual_prep',
|
||||||
name: 'Förberedelser för Den Stora Ritualen',
|
name: 'Förberedelser för Den Stora Ritualen',
|
||||||
desc: 'Allt har lett hit. Turning Torso pekar. Värnhemstorget svarar. Öresund drar sig tillbaka som om det vet vad som kommer. Skriften är nu fullständig. Den sista sidan skrevs av sig själv i natt. Det är ett recept.',
|
desc: 'Allt har lett hit. Turning Torso pekar. Värnhemstorget svarar. Öresund drar sig tillbaka som om det vet vad som kommer. Skriften är nu fullständig. Den sista sidan skrevs av sig själv i natt. Det är ett recept.',
|
||||||
cost: { andakt: 600, dread: 200 },
|
cost: () => ({ andakt: 600, dread: 200 }),
|
||||||
effect: s => { s.ritualRate += 2; s.kaosRate += 0.5; },
|
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'),
|
available: s => s.upgrades.has('torso_beacon') && s.ritual >= 50 && !s.upgrades.has('grand_ritual_prep'),
|
||||||
once: true,
|
once: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'deepen_ritual',
|
||||||
|
name: 'Fördjupa ritualen',
|
||||||
|
desc: 'Ännu ett lager av mening under det förra. Andakt är inget värt om det bara samlas i en burk. Varje fördjupning kostar mer än den förra — Den Sovande blir kräsnare ju närmre ni kommer.',
|
||||||
|
cost: s => ({ andakt: Math.round(300 * Math.pow(1.6, ritualDeepenings(s))), dread: 50 }),
|
||||||
|
effect: s => { s.ritualRate += 0.3; },
|
||||||
|
available: s => s.upgrades.has('grand_ritual_prep'),
|
||||||
|
once: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function ritualDeepenings(s: State): number {
|
||||||
|
return Math.round((s.ritualRate - 2.5) / 0.3);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user