diff --git a/flake.nix b/flake.nix index e153f4d..7f7341d 100644 --- a/flake.nix +++ b/flake.nix @@ -18,6 +18,19 @@ let pkgs = nixpkgs.legacyPackages.${system}; python = pkgs.python3; + + # Build the Svelte frontend + frontend = pkgs.buildNpmPackage { + pname = "pirats-frontend"; + version = "0.1.0"; + src = ./frontend; + npmDepsHash = "sha256-ekYBi0oUjtnsfdig2B0yrmT7KbSc9IB1DVbTCRwYI24="; + installPhase = '' + mkdir -p $out + cp -r dist $out/ + ''; + }; + in { default = python.pkgs.buildPythonApplication { @@ -35,7 +48,6 @@ fastapi sqlmodel uvicorn - jinja2 python-multipart httpx ]; @@ -43,6 +55,11 @@ nativeCheckInputs = with python.pkgs; [ pytestCheckHook ]; + + preBuild = '' + mkdir -p src/pirats/static + cp -r ${frontend}/dist/* src/pirats/static/ + ''; pythonImportsCheck = [ "pirats" ]; }; @@ -59,6 +76,7 @@ inputsFrom = [ self.packages.${system}.default ]; packages = with pkgs; [ pkgs.python3.pkgs.pytest + pkgs.nodejs ]; }; } diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..54a2631 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,43 @@ +# Svelte + Vite + +This template should help get you started developing with Svelte in Vite. + +## Recommended IDE Setup + +[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). + +## Need an official Svelte framework? + +Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. + +## Technical considerations + +**Why use this over SvelteKit?** + +- It brings its own routing solution which might not be preferable for some users. +- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. + +This template contains as little as possible to get started with Vite + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. + +Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. + +**Why include `.vscode/extensions.json`?** + +Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. + +**Why enable `checkJs` in the JS template?** + +It is likely that most cases of changing variable types in runtime are likely to be accidental, rather than deliberate. This provides advanced typechecking out of the box. Should you like to take advantage of the dynamically-typed nature of JavaScript, it is trivial to change the configuration. + +**Why is HMR not preserving my local component state?** + +HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/sveltejs/svelte-hmr/tree/master/packages/svelte-hmr#preservation-of-local-state). + +If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. + +```js +// store.js +// An extremely simple external store +import { writable } from 'svelte/store' +export default writable(0) +``` diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..2b56de4 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend + + +
+ + + diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json new file mode 100644 index 0000000..c7a0b10 --- /dev/null +++ b/frontend/jsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "moduleResolution": "bundler", + "target": "ESNext", + "module": "ESNext", + /** + * svelte-preprocess cannot figure out whether you have + * a value or a type, so tell TypeScript to enforce using + * `import type` instead of `import` for Types. + */ + "verbatimModuleSyntax": true, + "isolatedModules": true, + "resolveJsonModule": true, + /** + * To have warnings / errors of the Svelte compiler at the + * correct position, enable source maps by default. + */ + "sourceMap": true, + "esModuleInterop": true, + "types": ["vite/client"], + "skipLibCheck": true, + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable this if you'd like to use dynamic types. + */ + "checkJs": true + }, + /** + * Use global.d.ts instead of compilerOptions.types + * to avoid limiting type declarations. + */ + "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..de8c438 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1186 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "svelte-spa-router": "^5.1.0" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^7.1.2", + "svelte": "^5.55.5", + "vite": "^8.0.12" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.10.tgz", + "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.1.2.tgz", + "integrity": "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1", + "magic-string": "^0.30.21", + "obug": "^2.1.0", + "vitefu": "^1.1.2" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.46.4", + "vite": "^8.0.0-beta.7 || ^8.0.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "license": "MIT" + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.11.tgz", + "integrity": "sha512-gPdx+I+BjYEinNMQaBXFjbaJVyoPMU4ZODg5mE+M4DqVG9VusAVHHjcBX+zqyITlI0DIARwDMMzZwAWj36dRoQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "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/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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/obug": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", + "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "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.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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/regexparam": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.2.tgz", + "integrity": "sha512-A1PeDEYMrkLrfyOwv2jwihXbo9qxdGD3atBYQA9JJgreAx8/7rC6IUkWOw2NQlOxLp2wL0ifQbh1HuidDfYA6w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "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/svelte": { + "version": "5.56.3", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.3.tgz", + "integrity": "sha512-w7JvrM5IFl5cmfbY0TLik9o7mjRUJmRMhOR51tBPu708Gr/MjbGs7VnJnr/B0CaXeI4vtnOh7RKxDr0cwhMdDA==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.10", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.8.1", + "esm-env": "^1.2.1", + "esrap": "^2.2.11", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-spa-router": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-5.1.0.tgz", + "integrity": "sha512-YGtXOF+advJnQBvS4mvRnHVUF0qpSI2pUrSxidz4s7X9x80IKEgYzupx6j46gZcIcN3BYjmuLUbsgQv1S5bZzQ==", + "license": "MIT", + "dependencies": { + "regexparam": "2.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ItalyPaleAle" + }, + "peerDependencies": { + "svelte": "^5.0.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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..6c4b248 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,19 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^7.1.2", + "svelte": "^5.55.5", + "vite": "^8.0.12" + }, + "dependencies": { + "svelte-spa-router": "^5.1.0" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte new file mode 100644 index 0000000..0f27a6f --- /dev/null +++ b/frontend/src/App.svelte @@ -0,0 +1,18 @@ + + +
+ +
diff --git a/frontend/src/app.css b/frontend/src/app.css new file mode 100644 index 0000000..1e0e604 --- /dev/null +++ b/frontend/src/app.css @@ -0,0 +1,11 @@ +@import './assets/css/style.css'; +@import './assets/css/core.css'; +@import './assets/css/components.css'; +@import './assets/css/welcome.css'; +@import './assets/css/lobby.css'; +@import './assets/css/character.css'; +@import './assets/css/scene-setup.css'; +@import './assets/css/scene-play.css'; +@import './assets/css/card.css'; +@import './assets/css/upkeep.css'; +@import './assets/css/admin.css'; diff --git a/frontend/src/assets/css/admin.css b/frontend/src/assets/css/admin.css new file mode 100644 index 0000000..b5a9568 --- /dev/null +++ b/frontend/src/assets/css/admin.css @@ -0,0 +1,28 @@ +/* --- Admin Panel --- */ +.admin-players-list { + display: flex; + flex-direction: column; + gap: 1rem; + margin-top: 1rem; +} + +.admin-player-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + gap: 1rem; + flex-wrap: wrap; +} + +.player-info-basic { + font-size: 1.05rem; +} + +.link-copy-action { + display: flex; + gap: 0.5rem; + align-items: center; + flex: 1; + max-width: 500px; +} diff --git a/frontend/src/assets/css/card.css b/frontend/src/assets/css/card.css new file mode 100644 index 0000000..4eb3161 --- /dev/null +++ b/frontend/src/assets/css/card.css @@ -0,0 +1,244 @@ +/* Card Widget (Mini) */ +.card-mini { + width: 32px; + height: 48px; + border-radius: 4px; + border: 1px solid var(--text-muted); + background: var(--bg-dark); + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 2px 4px; + font-size: 0.7rem; + font-weight: 900; +} + +.card-mini.base-card { + border-color: var(--gold); + color: var(--gold); + background: rgba(212,175,55,0.1); +} + +.card-mini.rotated { + transform: rotate(90deg); + margin: 0 4px; +} + +.card-mini.success { + border-color: var(--neon-emerald); + color: var(--neon-emerald); +} + +.card-mini.failure { + border-color: var(--red-suit); + color: var(--red-suit); +} + +.card-mini .val { + align-self: flex-start; +} + +.card-mini .suit { + align-self: flex-end; +} + +.card-mini .owner { + font-size: 0.5rem; + text-transform: uppercase; + position: absolute; + bottom: -1px; + left: 1px; + color: var(--text-muted); +} + +/* Card Widget (Medium) */ +.card-medium { + width: 75px; + height: 112px; + border-radius: 6px; + border: 1.5px solid var(--glass-border); + background: linear-gradient(135deg, var(--bg-ocean-light) 0%, var(--bg-dark) 100%); + box-shadow: 0 3px 10px rgba(0,0,0,0.4); + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0.3rem; + position: relative; + font-size: 0.85rem; +} + +.card-medium .card-corner { + display: flex; + flex-direction: column; + align-items: center; + line-height: 1; +} + +.card-medium .card-corner .val { + font-weight: 900; + font-size: 0.9rem; +} + +.card-medium .card-corner .suit { + font-size: 0.75rem; +} + +.card-medium.suit-c, .card-medium.suit-s { + border-color: rgba(226, 232, 240, 0.15); +} + +.card-medium.suit-h, .card-medium.suit-d { + border-color: rgba(255, 42, 95, 0.15); +} + +.card-medium.joker-card { + border-color: var(--gold); + background: linear-gradient(135deg, #1d1b10 0%, var(--bg-dark) 100%); +} + +.card-medium.suit-c .val, .card-medium.suit-c .suit, +.card-medium.suit-s .val, .card-medium.suit-s .suit { + color: var(--black-suit); +} + +.card-medium.suit-h .val, .card-medium.suit-h .suit, +.card-medium.suit-d .val, .card-medium.suit-d .suit { + color: var(--red-suit); +} + +.card-medium .card-center { + font-size: 1.8rem; + text-align: center; + align-self: center; + margin: auto 0; +} + +.card-medium.suit-c .card-center, .card-medium.suit-s .card-center { color: var(--black-suit); } +.card-medium.suit-h .card-center, .card-medium.suit-d .card-center { color: var(--red-suit); } + +/* Card Widget (Large) */ +.card-large { + width: 110px; + height: 165px; + border-radius: 8px; + border: 2px solid var(--glass-border); + background: linear-gradient(135deg, var(--bg-ocean-light) 0%, var(--bg-dark) 100%); + box-shadow: 0 4px 15px rgba(0,0,0,0.5); + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0.5rem; + position: relative; + transition: var(--transition-smooth); +} + +.card-large:hover { + transform: translateY(-8px) scale(1.05); + border-color: var(--gold); + box-shadow: 0 10px 25px rgba(212, 175, 55, 0.2); +} + +.card-large.suit-c, .card-large.suit-s { + border-color: rgba(226, 232, 240, 0.15); +} +.card-large.suit-c:hover, .card-large.suit-s:hover { + border-color: var(--black-suit); + box-shadow: 0 10px 25px var(--black-glow); +} + +.card-large.suit-h, .card-large.suit-d { + border-color: rgba(255, 42, 95, 0.15); +} +.card-large.suit-h:hover, .card-large.suit-d:hover { + border-color: var(--red-suit); + box-shadow: 0 10px 25px var(--red-glow); +} + +.card-large.joker-card { + border-color: var(--gold); + background: linear-gradient(135deg, #1d1b10 0%, var(--bg-dark) 100%); +} + +.card-large .card-corner { + display: flex; + flex-direction: column; + align-items: center; + line-height: 1.1; +} + +.card-large .card-corner .val { + font-size: 1.2rem; + font-weight: 900; +} + +.card-large .card-corner .suit { + font-size: 1rem; +} + +.card-large.suit-c .val, .card-large.suit-c .suit, +.card-large.suit-s .val, .card-large.suit-s .suit { + color: var(--black-suit); +} + +.card-large.suit-h .val, .card-large.suit-h .suit, +.card-large.suit-d .val, .card-large.suit-d .suit { + color: var(--red-suit); +} + +.card-large .card-center { + font-size: 2.8rem; + text-align: center; + align-self: center; + margin: auto 0; +} + +.card-large.suit-c .card-center, .card-large.suit-s .card-center { color: var(--black-suit); } +.card-large.suit-h .card-center, .card-large.suit-d .card-center { color: var(--red-suit); } + +.card-large .card-tech-overlay { + position: absolute; + bottom: 0.5rem; + left: 0.5rem; + right: 0.5rem; + text-align: center; +} + +.tech-tag { + background: rgba(0, 0, 0, 0.7); + border: 1px solid var(--gold); + color: var(--gold); + font-size: 0.65rem; + padding: 0.15rem 0.3rem; + border-radius: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.joker-action { + display: flex; + gap: 2px; +} + +/* Drag and Drop Interface Styles */ +.card-large[draggable="true"] { + cursor: grab; +} + +.card-large[draggable="true"]:active { + cursor: grabbing; +} + +.card-large.dragging { + opacity: 0.4; + border-style: dashed; + transform: scale(0.95); +} + +.obstacle-item.drag-over { + border-color: var(--gold) !important; + background: rgba(212, 175, 55, 0.1) !important; + box-shadow: 0 0 20px rgba(212, 175, 55, 0.2) !important; + transform: translateY(-2px) scale(1.01); + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} diff --git a/frontend/src/assets/css/character.css b/frontend/src/assets/css/character.css new file mode 100644 index 0000000..13ea2f4 --- /dev/null +++ b/frontend/src/assets/css/character.css @@ -0,0 +1,442 @@ +/* --- Character Creation & Sheet Grid --- */ +.creation-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + margin-top: 2rem; +} + +@media (max-width: 900px) { + .creation-grid { + grid-template-columns: 1fr; + } +} + +.section-desc { + font-size: 0.9rem; + color: var(--text-muted); + margin-bottom: 1rem; +} + +.record-line { + background: rgba(0,0,0,0.2); + padding: 0.75rem 1rem; + border-radius: 6px; + margin-bottom: 0.75rem; + border-left: 3px solid var(--gold); +} + +.delegation-box { + margin-bottom: 1.25rem; + padding: 1.25rem; + text-align: left; +} + +.delegation-box h4 { + font-size: 0.95rem; + color: var(--text-primary); + margin-bottom: 0.5rem; +} + +.answer-box { + font-style: italic; + color: var(--neon-cyan); + font-size: 1.05rem; +} + +.author-label { + font-size: 0.8rem; + color: var(--text-muted); + font-style: normal; +} + +.waiting-box { + font-size: 0.9rem; + color: var(--gold); + display: flex; + align-items: center; + gap: 0.5rem; +} + +.inbox-list:has(.inbox-item) .inbox-empty-message { + display: none; +} + +.inbox-item { + padding: 1rem; + margin-bottom: 0.75rem; + border-left: 3px solid var(--neon-cyan); + background: rgba(0, 242, 254, 0.02); +} + +.inbox-item label { + display: block; + margin-bottom: 0.5rem; + font-size: 0.95rem; +} + +.submitted-techniques .tech-chip { + background: rgba(212,175,55,0.05); + border: 1px solid var(--gold); + color: var(--gold); + padding: 0.5rem; + border-radius: 6px; + margin-bottom: 0.5rem; + font-family: var(--font-heading); +} + +.tech-chip { + padding: 0.6rem 1rem; + font-size: 0.95rem; +} + +.tech-assignment-pill { + background: rgba(0, 242, 254, 0.05); + border: 1px solid var(--neon-cyan); + color: var(--text-primary); + padding: 0.6rem 1rem; + border-radius: 6px; + margin-bottom: 0.5rem; + text-align: left; + display: flex; + align-items: center; + gap: 1rem; +} + +.card-rank { + background: var(--neon-cyan); + color: var(--bg-dark); + font-weight: 900; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + font-size: 0.85rem; +} + +/* --- Character Sheet layout --- */ +.character-sheet-details { + text-align: left; +} + +.sheet-group { + background: rgba(0,0,0,0.15); + padding: 1rem; + border-radius: 8px; + border-left: 2px solid var(--gold-dark); +} + +.sheet-group h4 { + color: var(--gold); + font-size: 0.95rem; + margin-bottom: 0.5rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.footnote { + font-size: 0.8rem; + color: var(--text-muted); +} + +.techniques-list-sheet { + list-style: none; +} + +.techniques-list-sheet li { + margin-bottom: 0.25rem; + font-size: 0.95rem; +} + +.footnote-desc { + font-size: 0.8rem; + color: var(--text-muted); + margin-top: 0.15rem; + margin-left: 1.5rem; +} + +.margin-top-small { + margin-top: 0.5rem; +} + +/* --- Visual & Tactile Technique Assignment --- */ +.face-tech-drag-form { + margin-top: 1.5rem; +} + +.available-techniques-container { + background: rgba(255, 255, 255, 0.02); + border: 1px dashed var(--glass-border); + border-radius: 12px; + padding: 1.25rem; + margin-bottom: 2rem; + box-shadow: inset 0 0 15px rgba(0,0,0,0.2); +} + +.available-techniques-container h4 { + font-family: var(--font-heading); + color: var(--gold); + font-size: 1.1rem; + margin-bottom: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.instruction-help { + font-size: 0.8rem; + color: var(--text-muted); + margin-bottom: 1rem; +} + +.techniques-drag-pool { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + justify-content: center; + min-height: 50px; + align-items: center; +} + +.draggable-tech-chip { + background: linear-gradient(135deg, rgba(22, 36, 59, 0.9) 0%, rgba(13, 23, 38, 0.9) 100%); + border: 1px solid var(--gold); + color: var(--text-primary); + padding: 0.6rem 1rem; + border-radius: 8px; + cursor: grab; + user-select: none; + font-family: var(--font-body); + font-size: 0.95rem; + font-weight: 500; + transition: var(--transition-smooth); + box-shadow: 0 4px 10px rgba(0,0,0,0.3), 0 0 5px rgba(212, 175, 55, 0.1); + display: flex; + align-items: center; + gap: 0.5rem; +} + +.draggable-tech-chip:hover { + transform: translateY(-2px); + border-color: var(--neon-cyan); + box-shadow: 0 6px 15px rgba(0, 0, 0, 0.4), 0 0 10px rgba(0, 242, 254, 0.3); +} + +.draggable-tech-chip:active { + cursor: grabbing; +} + +.draggable-tech-chip.dragging { + opacity: 0.4; + border-style: dashed; +} + +.draggable-tech-chip.selected { + border-color: var(--neon-cyan); + box-shadow: 0 0 15px rgba(0, 242, 254, 0.5); + background: rgba(0, 242, 254, 0.1); +} + +.draggable-tech-chip.assigned-hidden { + opacity: 0.2; + pointer-events: none; + cursor: not-allowed; + border-color: var(--text-muted); +} + +.drag-icon { + color: var(--gold); + font-size: 0.8rem; + opacity: 0.7; +} + +/* Slots Grid */ +.face-cards-slots-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.5rem; + margin-bottom: 2rem; +} + +@media (max-width: 768px) { + .face-cards-slots-grid { + grid-template-columns: 1fr; + } +} + +.face-card-slot-wrapper { + display: flex; + justify-content: center; +} + +.face-card-slot { + position: relative; + width: 100%; + max-width: 220px; + aspect-ratio: 2 / 3; + min-height: 280px; + background: linear-gradient(135deg, rgba(13, 23, 38, 0.8) 0%, rgba(7, 11, 18, 0.9) 100%); + border: 2px dashed rgba(212, 175, 55, 0.3); + border-radius: 12px; + padding: 1rem; + display: flex; + flex-direction: column; + justify-content: space-between; + transition: var(--transition-smooth); + overflow: hidden; + cursor: pointer; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4); +} + +.face-card-slot:hover { + border-color: rgba(212, 175, 55, 0.7); + box-shadow: 0 12px 30px rgba(212, 175, 55, 0.15); + transform: translateY(-4px); +} + +.face-card-slot.drag-over { + border-color: var(--neon-cyan); + border-style: solid; + background: rgba(0, 242, 254, 0.05); + box-shadow: 0 0 20px rgba(0, 242, 254, 0.2); + transform: scale(1.02); +} + +.face-card-slot.has-assignment { + border-style: solid; + border-color: var(--gold); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5), 0 0 15px rgba(212, 175, 55, 0.15); +} + +.card-bg-letter { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-family: var(--font-heading); + font-size: 8rem; + font-weight: 900; + color: rgba(255, 255, 255, 0.025); + line-height: 1; + pointer-events: none; + user-select: none; + transition: var(--transition-smooth); +} + +.face-card-slot.has-assignment .card-bg-letter { + color: rgba(212, 175, 55, 0.04); +} + +.card-slot-header, .card-slot-footer { + display: flex; + justify-content: space-between; + align-items: center; + font-family: var(--font-heading); + font-weight: 700; + font-size: 1.1rem; + color: var(--text-muted); + pointer-events: none; + user-select: none; +} + +.face-card-slot.has-assignment .card-slot-header, +.face-card-slot.has-assignment .card-slot-footer { + color: var(--gold); +} + +.card-slot-body { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + z-index: 2; + padding: 0.5rem 0; +} + +.card-title { + font-family: var(--font-heading); + font-size: 1.2rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 0.75rem; + pointer-events: none; +} + +.face-card-slot.has-assignment .card-title { + color: var(--gold); +} + +.drop-zone-placeholder { + font-size: 0.8rem; + color: var(--text-muted); + border: 1px dashed rgba(255, 255, 255, 0.1); + background: rgba(0, 0, 0, 0.2); + border-radius: 6px; + padding: 0.5rem 0.75rem; + transition: var(--transition-smooth); + pointer-events: none; +} + +.face-card-slot:hover .drop-zone-placeholder { + color: var(--text-primary); + border-color: rgba(255, 255, 255, 0.2); +} + +.assigned-tech-content { + width: 100%; + display: none; +} + +.face-card-slot.has-assignment .drop-zone-placeholder { + display: none; +} + +.face-card-slot.has-assignment .assigned-tech-content { + display: block; +} + +.assigned-tech-chip { + background: rgba(212, 175, 55, 0.08); + border: 1px solid var(--gold); + color: var(--text-primary); + padding: 0.5rem; + border-radius: 8px; + font-size: 0.85rem; + position: relative; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); + word-break: break-word; + animation: fadeIn 0.3s ease; +} + +.remove-tech-btn { + position: absolute; + top: -8px; + right: -8px; + width: 18px; + height: 18px; + border-radius: 50%; + background: #c0392b; + border: 1px solid #e74c3c; + color: white; + font-size: 10px; + font-weight: 900; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + box-shadow: 0 2px 5px rgba(0,0,0,0.5); + transition: var(--transition-smooth); +} + +.remove-tech-btn:hover { + background: #e74c3c; + transform: scale(1.1); +} + +@keyframes fadeIn { + from { opacity: 0; transform: scale(0.9); } + to { opacity: 1; transform: scale(1); } +} diff --git a/frontend/src/assets/css/components.css b/frontend/src/assets/css/components.css new file mode 100644 index 0000000..e3abba0 --- /dev/null +++ b/frontend/src/assets/css/components.css @@ -0,0 +1,301 @@ +/* --- UI Panels & Glassmorphism --- */ +.glass-panel { + background: var(--glass-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--glass-border); + border-radius: 12px; + padding: 2rem; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + transition: var(--transition-smooth); +} + +.glass-panel:hover { + border-color: rgba(212, 175, 55, 0.35); + box-shadow: 0 8px 32px rgba(212, 175, 55, 0.05); +} + +.card { + background: var(--bg-ocean-light); + border: 1px solid var(--glass-border); + border-radius: 12px; + padding: 1.5rem; + margin-bottom: 1.5rem; + box-shadow: 0 4px 15px rgba(0,0,0,0.3); + transition: var(--transition-smooth); +} + +.card h3 { + font-family: var(--font-heading); + color: var(--gold); + margin-bottom: 1rem; + border-bottom: 1px solid rgba(212, 175, 55, 0.2); + padding-bottom: 0.5rem; +} + +/* --- Forms & Controls --- */ +.form-group { + margin-bottom: 1.25rem; + text-align: left; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-size: 0.95rem; + color: var(--text-muted); + font-weight: 500; +} + +.input-field { + width: 100%; + padding: 0.75rem 1rem; + background: rgba(7, 11, 18, 0.8); + border: 1px solid var(--glass-border); + border-radius: 8px; + color: var(--text-primary); + font-family: var(--font-body); + font-size: 1rem; + transition: var(--transition-smooth); +} + +.input-field:focus { + outline: none; + border-color: var(--neon-cyan); + box-shadow: 0 0 10px rgba(0, 242, 254, 0.2); +} + +.select-field { + width: 100%; + padding: 0.75rem 1rem; + background: rgba(7, 11, 18, 0.8); + border: 1px solid var(--glass-border); + border-radius: 8px; + color: var(--text-primary); + font-family: var(--font-body); + font-size: 1rem; + transition: var(--transition-smooth); + cursor: pointer; +} + +.select-field:focus { + outline: none; + border-color: var(--neon-cyan); +} + +.input-row { + display: flex; + gap: 0.5rem; +} + +.input-row .input-field { + flex: 1; +} + +/* --- Buttons --- */ +.btn { + display: inline-block; + padding: 0.75rem 1.5rem; + font-family: var(--font-body); + font-weight: 700; + font-size: 1rem; + text-transform: uppercase; + letter-spacing: 1px; + border: none; + border-radius: 8px; + cursor: pointer; + transition: var(--transition-smooth); + text-decoration: none; +} + +.btn-primary { + background: linear-gradient(135deg, var(--gold) 0%, var(--gold-dark) 100%); + color: var(--text-dark); + box-shadow: 0 4px 15px var(--gold-glow); +} + +.btn-primary:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(212, 175, 55, 0.6); +} + +.btn-secondary { + background: rgba(255,255,255,0.08); + border: 1px solid rgba(255,255,255,0.15); + color: var(--text-primary); +} + +.btn-secondary:hover:not(:disabled) { + background: rgba(255,255,255,0.15); + transform: translateY(-2px); +} + +.btn-deep { + background: linear-gradient(135deg, #152238 0%, #0d1726 100%); + border: 1px solid var(--neon-cyan); + color: var(--neon-cyan); + box-shadow: 0 4px 15px rgba(0, 242, 254, 0.1); +} + +.btn-deep:hover:not(:disabled) { + background: var(--neon-cyan); + color: var(--text-dark); + box-shadow: 0 4px 20px rgba(0, 242, 254, 0.4); + transform: translateY(-2px); +} + +.btn-danger { + background: linear-gradient(135deg, #7a1c1c 0%, #c0392b 100%); + color: var(--text-primary); + box-shadow: 0 4px 15px rgba(192, 57, 43, 0.2); +} + +.btn-danger:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(192, 57, 43, 0.5); +} + +.btn-gold { + background: transparent; + border: 2px solid var(--gold); + color: var(--gold); + box-shadow: 0 0 10px rgba(212,175,55,0.1); +} + +.btn-gold:hover:not(:disabled) { + background: var(--gold); + color: var(--bg-dark); + box-shadow: 0 0 20px var(--gold-glow); + transform: translateY(-2px); +} + +.btn-full { + width: 100%; +} + +.btn-small { + padding: 0.4rem 0.8rem; + font-size: 0.8rem; +} + +.btn-large { + padding: 1rem 2rem; + font-size: 1.1rem; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none !important; + box-shadow: none !important; +} + +.glow-effect:hover { + filter: brightness(1.2); +} + +/* --- Event Log --- */ +.event-log-fab { + position: fixed; + bottom: 20px; + right: 20px; + width: 60px; + height: 60px; + border-radius: 50%; + background: var(--glass-bg); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border: 1px solid var(--gold); + color: var(--gold); + font-size: 1.5rem; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5), 0 0 10px rgba(212, 175, 55, 0.2); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 1000; + transition: var(--transition-smooth); +} + +.event-log-fab:hover { + transform: scale(1.1); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.6), 0 0 15px rgba(212, 175, 55, 0.4); +} + +.event-log-panel { + position: fixed; + bottom: 90px; + right: 20px; + width: 350px; + max-width: calc(100vw - 40px); + height: 400px; + max-height: calc(100vh - 120px); + background: var(--glass-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--glass-border); + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); + display: flex; + flex-direction: column; + z-index: 999; + transition: transform 0.3s ease, opacity 0.3s ease; +} + +.event-log-panel.hidden { + transform: translateY(20px) scale(0.95); + opacity: 0; + pointer-events: none; +} + +.event-log-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.event-log-header h3 { + margin: 0; + font-family: var(--font-heading); + font-size: 1.1rem; + color: var(--gold); +} + +.event-log-container { + flex: 1; + overflow-y: auto; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.event-list { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 0.8rem; +} + +.event-item { + font-size: 0.9rem; + background: rgba(0, 0, 0, 0.3); + padding: 0.75rem; + border-radius: 8px; + border-left: 3px solid var(--neon-cyan); +} + +.event-time { + display: block; + font-size: 0.75rem; + color: var(--text-muted); + margin-bottom: 0.25rem; +} + +.event-msg { + color: var(--text-primary); +} diff --git a/frontend/src/assets/css/core.css b/frontend/src/assets/css/core.css new file mode 100644 index 0000000..b5bce89 --- /dev/null +++ b/frontend/src/assets/css/core.css @@ -0,0 +1,237 @@ +/* --- Variables & Tokens --- */ +:root { + --bg-dark: #070b12; + --bg-ocean: #0d1726; + --bg-ocean-light: #16243b; + + --gold: #d4af37; + --gold-glow: rgba(212, 175, 55, 0.4); + --gold-dark: #aa841c; + + --neon-cyan: #00f2fe; + --neon-emerald: #00ff87; + + --red-suit: #ff2a5f; + --red-glow: rgba(255, 42, 95, 0.3); + --black-suit: #e2e8f0; + --black-glow: rgba(226, 232, 240, 0.2); + + --glass-bg: rgba(13, 23, 38, 0.7); + --glass-bg-hover: rgba(22, 36, 59, 0.85); + --glass-border: rgba(212, 175, 55, 0.2); + --glass-border-focus: rgba(0, 242, 254, 0.4); + + --text-primary: #f1f5f9; + --text-muted: #94a3b8; + --text-dark: #0f172a; + + --font-heading: 'Cinzel', serif; + --font-body: 'Outfit', sans-serif; + + --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* --- Base Reset & Setup --- */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background: radial-gradient(circle at 50% 50%, var(--bg-ocean) 0%, var(--bg-dark) 100%); + color: var(--text-primary); + font-family: var(--font-body); + font-size: 16px; + line-height: 1.6; + min-height: 100vh; + display: flex; + flex-direction: column; + overflow-x: hidden; +} + +/* Custom Scrollbar */ +::-webkit-scrollbar { + width: 8px; +} +::-webkit-scrollbar-track { + background: var(--bg-dark); +} +::-webkit-scrollbar-thumb { + background: var(--gold-dark); + border-radius: 4px; +} +::-webkit-scrollbar-thumb:hover { + background: var(--gold); +} + +/* --- Layout Components --- */ +.app-header { + background: rgba(7, 11, 18, 0.95); + border-bottom: 2px solid var(--gold); + box-shadow: 0 4px 20px rgba(0,0,0,0.5); + padding: 1rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; + position: sticky; + top: 0; + z-index: 100; +} + +.logo-container { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.logo-container h1 { + font-family: var(--font-heading); + font-size: 1.8rem; + font-weight: 900; + letter-spacing: 2px; + background: linear-gradient(135deg, var(--gold) 30%, #fff 70%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + text-shadow: 0 0 10px var(--gold-glow); +} + +.math-magic { + font-family: var(--font-heading); + font-size: 2.2rem; + color: var(--neon-cyan); + text-shadow: 0 0 10px var(--neon-cyan), 0 0 20px var(--neon-cyan); + animation: pulse-glow 3s infinite ease-in-out; +} + +.header-status { + display: flex; + align-items: center; + gap: 1rem; +} + +.player-pill { + background: rgba(0, 242, 254, 0.1); + border: 1px solid var(--neon-cyan); + padding: 0.25rem 0.75rem; + border-radius: 20px; + font-size: 0.9rem; + font-weight: 700; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.player-pill .bullet { + width: 8px; + height: 8px; + background-color: var(--neon-emerald); + border-radius: 50%; + box-shadow: 0 0 8px var(--neon-emerald); +} + +.phase-pill { + background: rgba(212, 175, 55, 0.1); + border: 1px solid var(--gold); + color: var(--gold); + padding: 0.25rem 0.75rem; + border-radius: 20px; + font-size: 0.9rem; + font-weight: 700; + letter-spacing: 1px; +} + +.app-main { + flex: 1; + padding: 2rem; + max-width: 1400px; + width: 100%; + margin: 0 auto; +} + +.app-footer { + background: var(--bg-dark); + padding: 1.5rem; + text-align: center; + border-top: 1px solid rgba(255,255,255,0.05); + font-size: 0.85rem; + color: var(--text-muted); +} + +/* --- Loader / Spinner widgets --- */ +.loader-container { + padding: 4rem; +} + +.spinner { + width: 50px; + height: 50px; + border: 3px solid rgba(0, 242, 254, 0.1); + border-radius: 50%; + border-top-color: var(--neon-cyan); + animation: spin 1s ease-in-out infinite; + margin: 0 auto 1.5rem; +} + +.spinner-small { + width: 20px; + height: 20px; + border: 2px solid rgba(255,255,255,0.1); + border-radius: 50%; + border-top-color: var(--gold); + animation: spin 1s ease-in-out infinite; + display: inline-block; +} + +/* --- Alert styles --- */ +.alert { + padding: 0.75rem 1.25rem; + margin-bottom: 1.5rem; + border-radius: 8px; + text-align: center; + font-weight: 500; +} + +.alert-danger { + background: rgba(192, 57, 43, 0.2); + border: 1px solid #c0392b; + color: #e74c3c; +} + +/* --- Animation keyframes --- */ +@keyframes spin { + to { transform: rotate(360deg); } +} + +@keyframes pulse-glow { + 0% { + text-shadow: 0 0 10px var(--neon-cyan), 0 0 20px var(--neon-cyan); + transform: scale(1); + } + 50% { + text-shadow: 0 0 15px var(--neon-cyan), 0 0 30px var(--neon-cyan), 0 0 40px var(--neon-cyan); + transform: scale(1.05); + } + 100% { + text-shadow: 0 0 10px var(--neon-cyan), 0 0 20px var(--neon-cyan); + transform: scale(1); + } +} + +/* --- Utilities --- */ +.text-center { text-align: center; } +.text-left { text-align: left; } +.margin-top { margin-top: 1.5rem; } +.gold-text { color: var(--gold); } +.center-block { margin: 1rem auto; max-width: 500px; } +.inline-form { display: flex; gap: 0.5rem; width: 100%; } +.inline-group { display: flex; gap: 0.5rem; width: 100%; } +.select-small { padding: 0.5rem; font-size: 0.9rem; flex: 1; } +.select-xsmall { padding: 0.4rem; font-size: 0.85rem; } +.btn-small { padding: 0.4rem 0.8rem; font-size: 0.85rem; } + +/* --- Ghost World Styles --- */ +.ghost-world { + filter: grayscale(95%) brightness(0.85) sepia(15%) contrast(1.05); + transition: filter 1.2s ease-in-out; +} diff --git a/frontend/src/assets/css/lobby.css b/frontend/src/assets/css/lobby.css new file mode 100644 index 0000000..9b48fe1 --- /dev/null +++ b/frontend/src/assets/css/lobby.css @@ -0,0 +1,106 @@ +/* --- Lobby / Hold View --- */ +.lobby-view { + max-width: 700px; + margin: 2rem auto; +} + +.lobby-view h2 { + font-family: var(--font-heading); + font-size: 2.2rem; + color: var(--gold); + margin-bottom: 0.5rem; +} + +.lobby-status-box { + margin: 2rem 0; +} + +.player-chips { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + justify-content: center; + margin-top: 1rem; +} + +.player-chip { + background: rgba(255,255,255,0.06); + border: 1px solid rgba(255,255,255,0.1); + padding: 0.5rem 1.25rem; + border-radius: 30px; + display: flex; + align-items: center; + gap: 0.5rem; + transition: var(--transition-smooth); +} + +.player-chip.current-user { + background: rgba(0, 242, 254, 0.15); + border-color: var(--neon-cyan); + box-shadow: 0 0 10px rgba(0,242,254,0.15); +} + +.player-chip.voted-chip { + border-color: var(--neon-emerald); + background-color: rgba(0,255,135,0.05); +} + +.player-chip.not-voted-chip { + border-color: rgba(255,255,255,0.1); +} + +.creator-badge { + background: var(--gold); + color: var(--text-dark); + font-size: 0.7rem; + font-weight: 700; + padding: 0.1rem 0.4rem; + border-radius: 4px; + text-transform: uppercase; +} + +.links-box { + text-align: left; + margin-bottom: 2rem; +} + +.link-item { + margin-bottom: 1.5rem; +} + +.link-item h4 { + color: var(--text-primary); + margin-bottom: 0.5rem; +} + +.copy-container { + display: flex; + gap: 0.5rem; +} + +.copy-input { + flex: 1; + background: #05080e; + border: 1px solid var(--glass-border); + color: var(--text-primary); + padding: 0.5rem 1rem; + border-radius: 8px; + font-family: var(--font-body); + font-size: 0.95rem; +} + +.copy-input.admin-input { + border-color: var(--gold-dark); + color: var(--gold); +} + +.admin-link-item { + border-top: 1px dashed rgba(212, 175, 55, 0.3); + padding-top: 1.5rem; +} + +.info-text { + font-size: 0.9rem; + color: var(--text-muted); + margin-bottom: 0.5rem; +} diff --git a/frontend/src/assets/css/scene-play.css b/frontend/src/assets/css/scene-play.css new file mode 100644 index 0000000..c0a969f --- /dev/null +++ b/frontend/src/assets/css/scene-play.css @@ -0,0 +1,277 @@ +/* --- Scene Play Board Layout --- */ +.scene-view-layout { + display: grid; + grid-template-columns: 1.4fr 1fr; + gap: 2rem; +} + +@media (max-width: 1100px) { + .scene-view-layout { + grid-template-columns: 1fr; + } +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(212, 175, 55, 0.2); + padding-bottom: 0.5rem; + margin-bottom: 1rem; +} + +.card-header h3 { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} + +.deck-counter { + font-size: 0.85rem; + color: var(--gold); + font-weight: 700; +} + +/* --- Active Obstacle Item Display --- */ +.obstacles-container { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.obstacle-item { + border: 1px solid var(--glass-border); + border-radius: 10px; + padding: 1.25rem; + background: rgba(7, 11, 18, 0.4); + display: grid; + grid-template-columns: 0.8fr 2fr 1fr 1fr; + gap: 1rem; + position: relative; + overflow: hidden; +} + +@media (max-width: 768px) { + .obstacle-item { + grid-template-columns: 0.8fr 2fr 1.5fr; + } +} + +@media (max-width: 600px) { + .obstacle-item { + grid-template-columns: 1fr; + } +} + +/* Color theme mappings for suits */ +.suit-c { border-left: 4px solid var(--black-suit); } +.suit-s { border-left: 4px solid var(--black-suit); } +.suit-h { border-left: 4px solid var(--red-suit); } +.suit-d { border-left: 4px solid var(--red-suit); } + +.obstacle-meta { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: rgba(0,0,0,0.3); + border-radius: 6px; + padding: 0.5rem; +} + +.suit-badge { + font-weight: 900; + font-size: 1.1rem; +} + +.suit-c .suit-badge, .suit-s .suit-badge { color: var(--black-suit); } +.suit-h .suit-badge, .suit-d .suit-badge { color: var(--red-suit); } + +.card-code { + font-size: 0.8rem; + color: var(--text-muted); + font-family: var(--font-heading); +} + +.obstacle-details h4 { + font-family: var(--font-heading); + color: var(--text-primary); + margin-bottom: 0.25rem; + font-size: 1.1rem; +} + +.obstacle-details .desc { + font-size: 0.85rem; + color: var(--text-muted); +} + +.obstacle-value-display { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: rgba(212, 175, 55, 0.04); + border: 1px dashed var(--glass-border); + border-radius: 6px; + padding: 0.5rem; +} + +.obstacle-successes-display { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: rgba(0, 255, 135, 0.04); + border: 1px dashed rgba(0, 255, 135, 0.25); + border-radius: 6px; + padding: 0.5rem; +} + +.obstacle-successes-display .val-number { + color: var(--neon-emerald); +} + +.val-label { + font-size: 0.75rem; + color: var(--text-muted); + text-transform: uppercase; +} + +.val-number { + font-size: 1.8rem; + font-family: var(--font-heading); + font-weight: 900; + color: var(--gold); +} + +.played-column { + grid-column: span 4; + border-top: 1px solid rgba(255,255,255,0.05); + padding-top: 0.75rem; + margin-top: 0.5rem; +} + +@media (max-width: 600px) { + .played-column { + grid-column: span 1; + } +} + +.played-column h5 { + font-size: 0.8rem; + color: var(--text-muted); + margin-bottom: 0.5rem; +} + +.column-cards-flex { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.75rem; +} + +/* --- Challenges Section --- */ +.challenges-container { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.challenge-item { + background: rgba(255,255,255,0.02); + border: 1px solid var(--glass-border); + border-radius: 8px; + padding: 1.25rem; + text-align: left; +} + +.challenge-item h4 { + font-family: var(--font-heading); + color: var(--neon-cyan); + font-size: 1.2rem; + margin-bottom: 0.25rem; +} + +.challenge-item .desc { + font-size: 0.9rem; + color: var(--text-muted); + margin-bottom: 1rem; +} + +.applied-obstacles h5 { + font-size: 0.85rem; + color: var(--text-muted); + margin-bottom: 0.5rem; +} + +.applied-obs-row { + padding: 0.75rem 1rem; + margin-bottom: 0.5rem; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 0.75rem; +} + +.obs-info { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.obs-info .symbol { + font-size: 1.2rem; +} + +.play-card-form { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.select-xsmall { + padding: 0.25rem 0.5rem; + font-size: 0.8rem; + width: 140px; + background: var(--bg-dark); +} + +/* --- Deep Control panel --- */ +.deep-text { + color: var(--neon-cyan) !important; +} + +.deep-divider { + border: 0; + height: 1px; + background: linear-gradient(to right, rgba(0, 242, 254, 0), rgba(0, 242, 254, 0.4), rgba(0, 242, 254, 0)); + margin: 2rem 0; +} + +.obstacles-checkboxes { + display: flex; + flex-direction: column; + gap: 0.5rem; + background: rgba(0,0,0,0.2); + padding: 1rem; + border-radius: 8px; + max-height: 150px; + overflow-y: auto; + border: 1px solid var(--glass-border); +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + font-size: 0.95rem; +} + +.hand-flex { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: flex-start; +} diff --git a/frontend/src/assets/css/scene-setup.css b/frontend/src/assets/css/scene-setup.css new file mode 100644 index 0000000..cc8d232 --- /dev/null +++ b/frontend/src/assets/css/scene-setup.css @@ -0,0 +1,83 @@ +/* --- Scene Setup / Role Choose --- */ +.setup-grid { + display: grid; + grid-template-columns: 1.2fr 1fr; + gap: 2rem; + margin: 2rem 0; +} + +@media (max-width: 800px) { + .setup-grid { + grid-template-columns: 1fr; + } +} + +.role-buttons { + display: flex; + flex-direction: column; + gap: 1.5rem; + margin-top: 1.5rem; +} + +.role-btn { + padding: 1.5rem; + font-size: 1.2rem; + background: rgba(255,255,255,0.03); + border: 2px solid rgba(255,255,255,0.1); + color: var(--text-muted); +} + +.role-btn.pirat-btn:hover, .role-btn.pirat-btn.active { + border-color: var(--neon-emerald); + color: var(--text-primary); + background-color: rgba(0,255,135,0.06); + box-shadow: 0 0 15px rgba(0,255,135,0.15); +} + +.role-btn.deep-btn:hover, .role-btn.deep-btn.active { + border-color: var(--neon-cyan); + color: var(--text-primary); + background-color: rgba(0,242,254,0.06); + box-shadow: 0 0 15px rgba(0,242,254,0.15); +} + +.large-badge { + padding: 0.5rem 2rem; + font-size: 1.5rem; + border-radius: 8px; + font-family: var(--font-heading); + margin-top: 0.5rem; +} + +.role-badge.deep { + background: rgba(0, 242, 254, 0.15); + border: 2px solid var(--neon-cyan); + color: var(--neon-cyan); + box-shadow: 0 0 15px rgba(0,242,254,0.1); +} + +.role-badge.pirat { + background: rgba(0, 255, 135, 0.15); + border: 2px solid var(--neon-emerald); + color: var(--neon-emerald); + box-shadow: 0 0 15px rgba(0,255,135,0.1); +} + +.list-chips { + justify-content: flex-start; +} + +.list-chips .player-chip { + padding: 0.4rem 1rem; + font-size: 0.9rem; +} + +.deep-chip { + border-color: var(--neon-cyan); + background: rgba(0, 242, 254, 0.03); +} + +.pirat-chip { + border-color: var(--neon-emerald); + background: rgba(0, 255, 135, 0.03); +} diff --git a/frontend/src/assets/css/style.css b/frontend/src/assets/css/style.css new file mode 100644 index 0000000..2a5d954 --- /dev/null +++ b/frontend/src/assets/css/style.css @@ -0,0 +1,18 @@ +/* ========================================================================== + RATS WITH GATS DESIGN SYSTEM & STYLESHEET + A weathered math-magic pirate aesthetic: deep-ocean dark mode, glassmorphism, + neon math runes, gold accents, and animated card widgets. + + This file imports modular stylesheets divided by page phase and components. + ========================================================================== */ + +@import url("core.css"); +@import url("components.css"); +@import url("card.css"); +@import url("welcome.css"); +@import url("lobby.css"); +@import url("character.css"); +@import url("scene-setup.css"); +@import url("scene-play.css"); +@import url("upkeep.css"); +@import url("admin.css"); diff --git a/frontend/src/assets/css/upkeep.css b/frontend/src/assets/css/upkeep.css new file mode 100644 index 0000000..5a949a8 --- /dev/null +++ b/frontend/src/assets/css/upkeep.css @@ -0,0 +1,100 @@ +/* --- Between Scenes Upkeep --- */ +.between-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + margin: 2rem 0; +} + +@media (max-width: 800px) { + .between-grid { + grid-template-columns: 1fr; + } +} + +.voting-card, .deep-rest-card { + text-align: left; +} + +.voted-confirmation-box { + padding: 1.5rem; + text-align: center; + border: 1px solid var(--neon-emerald); +} + +.success-text { + color: var(--neon-emerald); + font-weight: 700; +} + +.ranks-ledger { + list-style: none; +} + +.ranks-ledger li { + padding: 0.5rem; + border-bottom: 1px solid rgba(255,255,255,0.05); + display: flex; + justify-content: space-between; +} + +.deep-badge { + background: rgba(0, 242, 254, 0.15); + border: 1px solid var(--neon-cyan); + color: var(--neon-cyan); + padding: 0.1rem 0.4rem; + font-size: 0.75rem; + border-radius: 4px; +} + +.pirat-badge { + background: rgba(0, 255, 135, 0.15); + border: 1px solid var(--neon-emerald); + color: var(--neon-emerald); + padding: 0.1rem 0.4rem; + font-size: 0.75rem; + border-radius: 4px; +} + +/* --- Deep Upkeep Hand Refresh Drag & Drop --- */ +.upkeep-drag-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + margin: 2rem 0; +} + +@media (max-width: 768px) { + .upkeep-drag-container { + grid-template-columns: 1fr; + } +} + +.upkeep-box { + min-height: 400px; + display: flex; + flex-direction: column; +} + +.upkeep-box h3 { + margin-bottom: 1rem; + font-family: var(--font-heading); +} + +.upkeep-flex { + flex: 1; + display: flex; + flex-wrap: wrap; + gap: 1rem; + padding: 1.5rem; + background: rgba(0, 0, 0, 0.3); + border: 1px dashed var(--glass-border); + border-radius: 8px; + align-content: flex-start; + min-height: 300px; + transition: var(--transition-smooth); +} + +.upkeep-box[ondragover]:hover .upkeep-flex { + border-color: var(--neon-cyan); +} diff --git a/frontend/src/assets/css/welcome.css b/frontend/src/assets/css/welcome.css new file mode 100644 index 0000000..a06e78e --- /dev/null +++ b/frontend/src/assets/css/welcome.css @@ -0,0 +1,50 @@ +/* --- Welcome / Main Menu Screen --- */ +.welcome-container { + max-width: 900px; + margin: 4rem auto; +} + +.welcome-hero h2 { + font-family: var(--font-heading); + font-size: 2.5rem; + color: var(--text-primary); + margin-bottom: 1rem; +} + +.welcome-hero .subtitle { + font-size: 1.2rem; + color: var(--text-muted); + margin-bottom: 3rem; +} + +.welcome-actions { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; +} + +@media (max-width: 768px) { + .welcome-actions { + grid-template-columns: 1fr; + } +} + +.action-card { + padding: 2.5rem 1.5rem; + display: flex; + flex-direction: column; + justify-content: space-between; + height: 300px; +} + +.action-card h3 { + font-family: var(--font-heading); + color: var(--gold); + font-size: 1.5rem; + margin-bottom: 0.75rem; +} + +.action-card p { + color: var(--text-muted); + margin-bottom: 2rem; +} diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png new file mode 100644 index 0000000..02251f4 Binary files /dev/null and b/frontend/src/assets/hero.png differ diff --git a/frontend/src/assets/svelte.svg b/frontend/src/assets/svelte.svg new file mode 100644 index 0000000..c5e0848 --- /dev/null +++ b/frontend/src/assets/svelte.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend/src/components/CharacterCreationPhase.svelte b/frontend/src/components/CharacterCreationPhase.svelte new file mode 100644 index 0000000..bc34773 --- /dev/null +++ b/frontend/src/components/CharacterCreationPhase.svelte @@ -0,0 +1,527 @@ + + +
+
+

Character Sheet Creation

+

Fill in your Rat Records, delegate questions to your crew, and prepare your Math Magic Techniques.

+
+ + +
+
+ + {#if devMode} +
+ 🛠 Dev Mode Tools + +
+ {/if} + + +
+ + +
+

I. Basic Rat Records {basicComplete ? '✅' : '❌'}

+ + {#if basicComplete} + +
+
Look: {state.player.avatar_look}
+
Smell: {state.player.avatar_smell}
+
First Words: "{state.player.first_words}"
+
Good at Math? {state.player.good_at_math}
+
+ {:else} + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+ +
+ {/if} +
+ + +
+
+

II. Crew Delegations {delegateComplete ? '✅' : '❌'}

+ {#if !delegateComplete} + + {/if} +
+

You must let other players decide what they like and hate about your character.

+ +
+ {#if delegateComplete} +
+

What does another rat LIKE about you?

+
+ {state.player.other_like ? `"${state.player.other_like}"` : '(Waiting for answer...)'} +
+
— {getPlayerName(state.player.other_like_from_player_id)}
+
+
+

What does another rat HATE about you?

+
+ {state.player.other_hate ? `"${state.player.other_hate}"` : '(Waiting for answer...)'} +
+
— {getPlayerName(state.player.other_hate_from_player_id)}
+
+ {:else} +

Randomly assign crewmates to write your relationships by clicking "Auto-Assign Tasks".

+ {/if} +
+
+ + +
+

III. Your Inbox (Task List)

+

Answer these questions for your crewmates. Be creative and have fun with it!

+
+ {#each state.players as p} + {#if p.other_like_from_player_id === state.player.id && !p.other_like} +
+ +
+ + +
+
+ {/if} + {#if p.other_hate_from_player_id === state.player.id && !p.other_hate} +
+ +
+ + +
+
+ {/if} + {/each} + {#if state.players.filter(p => (p.other_like_from_player_id === state.player.id && !p.other_like) || (p.other_hate_from_player_id === state.player.id && !p.other_hate)).length === 0} +

No pending tasks! Wait for other players to assign you tasks.

+ {/if} +
+
+ + +
+

IV. Secret Techniques {techniquesComplete ? '✅' : '❌'}

+ + {#if createdTechniques.length === 0} +

Create 3 techniques that you will swap with other players. Make them cool or ridiculous!

+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+ +
+ {:else if createdTechniques.length === 3 && swappedTechniques.length < 3} + + {:else if !techniquesComplete} + {#if swappedTechniques.length === 3} +
+

Drag and drop your assigned techniques onto your Face Cards:

+ + {#if faceError} +
{faceError}
+ {/if} + +
+

Available Swapped Techniques

+

Drag a technique onto a card slot below to assign it.

+
+ {#each swappedTechniques as tech} +
e.dataTransfer.setData('text/plain', tech)} + class="draggable-tech-chip {tech === jackTech || tech === queenTech || tech === kingTech ? 'assigned-hidden' : ''}" + > + ⋮⋮ {tech} +
+ {/each} +
+
+ + +
+ + +
+
{ + const tech = e.dataTransfer.getData('text/plain'); + if (tech) { + if (queenTech === tech) queenTech = ''; + if (kingTech === tech) kingTech = ''; + jackTech = tech; + } + }}> +
J
+
+ J +
+
+
Jack
+
Drop Technique Here
+
+ {#if jackTech} +
+ {jackTech} + jackTech = ''}>× +
+ {/if} +
+
+ +
+
+ + +
+
{ + const tech = e.dataTransfer.getData('text/plain'); + if (tech) { + if (jackTech === tech) jackTech = ''; + if (kingTech === tech) kingTech = ''; + queenTech = tech; + } + }}> +
Q
+
+ Q +
+
+
Queen
+
Drop Technique Here
+
+ {#if queenTech} +
+ {queenTech} + queenTech = ''}>× +
+ {/if} +
+
+ +
+
+ + +
+
{ + const tech = e.dataTransfer.getData('text/plain'); + if (tech) { + if (jackTech === tech) jackTech = ''; + if (queenTech === tech) queenTech = ''; + kingTech = tech; + } + }}> +
K
+
+ K +
+
+
King
+
Drop Technique Here
+
+ {#if kingTech} +
+ {kingTech} + kingTech = ''}>× +
+ {/if} +
+
+ +
+
+
+ +
+ +
+
+ {:else} +
+ Waiting for all players to submit techniques so they can be shuffled... +
+ {/if} + {:else} +
+
J {state.player.tech_jack}
+
Q {state.player.tech_queen}
+
K {state.player.tech_king}
+

+ + Ready! Waiting for other players to finish J/Q/K assignment... +

+
+ {/if} +
+ + +
+

V. Crew Creation Status

+

See the creation progress of the entire crew in real time.

+
+
    + {#each state.players as p} +
  • + {p.name} + + {#if p.tech_jack} + Ready + {:else if p.created_techniques && JSON.parse(p.created_techniques).length === 3} + Assigning Face Cards + {:else} + Writing Sheet + {/if} + +
  • + {/each} +
+
+
+
+
diff --git a/frontend/src/components/LobbyPhase.svelte b/frontend/src/components/LobbyPhase.svelte new file mode 100644 index 0000000..ed978cc --- /dev/null +++ b/frontend/src/components/LobbyPhase.svelte @@ -0,0 +1,79 @@ + + +
+

{state.game.crew_name || "Ship's Hold"} (Lobby)

+

Wait here as the mathematical mages gather stowaway rats for the spell. Once everyone is in, the captain will initiate the transformation.

+ +
+

Joined Crew members

+
+ {#each state.players as p} +
+ 🐀 + {p.name} + {#if p.is_creator}Creator{/if} +
+ {/each} +
+
+ + + +
+ {#if state.player.is_creator} + {#if state.players.length >= 1} + + {:else} + + {/if} + {:else} +
+
+

Waiting for Captain to cast the spell...

+
+ {/if} +
+
diff --git a/frontend/src/components/ScenePhase.svelte b/frontend/src/components/ScenePhase.svelte new file mode 100644 index 0000000..702f491 --- /dev/null +++ b/frontend/src/components/ScenePhase.svelte @@ -0,0 +1,503 @@ + + +
+ +
+ + +
+
+

🌊 The Obstacle List

+ 🎴 Deck: {state.game.deck_cards ? JSON.parse(state.game.deck_cards).length : 0} cards left +
+ +
+ +
+
+ +
+ {#if isCaptain} + + đŸ´â€â˜ ī¸ Captain: {state.players.find(p => p.id === state.player.id)?.name} (Rank {state.players.find(p => p.id === state.player.id)?.rank}) + + {:else} + + đŸ´â€â˜ ī¸ Captain: {state.players.reduce((max, p) => p.rank > max.rank ? p : max, state.players[0])?.name || 'None'} + + {/if} +
+ + +
+ Active Roster: + {#each state.players as p} + {#if p.role === 'deep'} + + 🌊 {p.name} (Deep) + + {:else if p.role === 'pirat'} + + {#if p.is_ghost}đŸ‘ģ{:else if p.is_dead}💀{:else}🐀{/if} {p.name} (Rank {p.rank}){p.id === state.players.reduce((max, p2) => p2.rank > max.rank ? p2 : max, state.players[0])?.id && !p.is_ghost && !p.is_dead ? ' ⭐' : ''} + + {/if} + {/each} +
+
+
+ + {#if error} +
{error}
+ {/if} + + {#each state.obstacles as obs} + {@const column_cards = JSON.parse(obs.played_cards || '[]')} + {@const active_card_code = column_cards.length > 0 ? column_cards[column_cards.length - 1].card : obs.original_card} + {@const success_count = column_cards.filter(c => c.success).length} + {@const is_completed = success_count >= state.players.filter(p => p.role !== 'deep').length} +
{ if (!is_completed) e.preventDefault(); }} + on:dragenter={(e) => { if (!is_completed) e.currentTarget.classList.add("drag-over"); }} + on:dragleave={(e) => { if (!is_completed) e.currentTarget.classList.remove("drag-over"); }} + on:drop={(e) => { + if (is_completed) return; + e.preventDefault(); + e.currentTarget.classList.remove("drag-over"); + const cardCode = e.dataTransfer.getData('text/plain'); + if (cardCode) { + if (cardCode === 'JOKER') playJoker(obs.id, cardCode); + else playCard(obs.id, cardCode); + } + }}> +
+
+
+ {active_card_code === 'JOKER' ? '🃏' : active_card_code.slice(0, -1)} + {active_card_code === 'JOKER' ? '' : {'C': 'â™Ŗī¸', 'S': 'â™ ī¸', 'H': 'â™Ĩī¸', 'D': 'â™Ļī¸'}[active_card_code.slice(-1)]} +
+ +
+ {#if active_card_code === 'JOKER'} + 🃏 + {:else} + {{'C': 'â™Ŗī¸', 'S': 'â™ ī¸', 'H': 'â™Ĩī¸', 'D': 'â™Ļī¸'}[active_card_code.slice(-1)]} + {/if} +
+ +
+ {active_card_code === 'JOKER' ? '🃏' : active_card_code.slice(0, -1)} + {active_card_code === 'JOKER' ? '' : {'C': 'â™Ŗī¸', 'S': 'â™ ī¸', 'H': 'â™Ĩī¸', 'D': 'â™Ļī¸'}[active_card_code.slice(-1)]} +
+
+
+
+

{obs.title}

+

{obs.description}

+
+ + +
+ Current Difficulty + + {#if ['J', 'Q', 'K'].includes(obs.original_card.slice(0, -1))} + Rank ({state.player.rank}) + {:else} + {obs.current_value} + {/if} + +
+ + +
+ Successes + + {success_count} / {state.players.filter(p => p.role !== 'deep').length} + +
+ + +
+
Card History (Column)
+
+
+ {obs.original_card.slice(0, -1)} + {{'C': 'â™Ŗī¸', 'S': 'â™ ī¸', 'H': 'â™Ĩī¸', 'D': 'â™Ļī¸'}[obs.original_card.slice(-1)]} +
+ {#each column_cards as pc} +
+ {pc.card === 'JOKER' ? '🃏' : pc.card.slice(0, -1)} + {pc.card === 'JOKER' ? '' : {'C': 'â™Ŗī¸', 'S': 'â™ ī¸', 'H': 'â™Ĩī¸', 'D': 'â™Ļī¸'}[pc.card.slice(-1)]} + {pc.player_name.slice(0,4)} +
+ {/each} +
+
+ + {#if is_completed && state.player.role === 'deep'} +
+ +
+ {/if} +
+ {:else} +

No active obstacles. Deep players can end the scene!

+ {/each} + + + +
+
+
+ + +
+ + {#if state.player.role === "deep"} +
+

🌊 Deep Control Panel

+ +
+

Crew Objectives

+
+ + + +
+
+ +
+

Pi-Rat Personal Objectives

+

You control completion. Only mark in sequence (1 -> 2 -> 3).

+ {#each state.players.filter(p => p.role === 'pirat') as p} +
+ {p.name} +
+ + + +
+
+ {/each} +
+ + +
+

Conclude the scene once players have made attempts or the obstacle list is depleted.

+ +
+
+ {/if} + + +
+

🃏 Your Hand

+

Keep your cards secret! {#if state.player.role !== "deep"}Drag a card onto an active obstacle to play it.{/if}

+ +
+ {#each hand as card} +
{ + e.dataTransfer.setData('text/plain', card); + e.currentTarget.classList.add("dragging"); + }} + on:dragend={(e) => { + e.currentTarget.classList.remove("dragging"); + }}> +
+ {card === 'JOKER' ? '🃏' : card.slice(0, -1)} + {card === 'JOKER' ? '' : {'C': 'â™Ŗī¸', 'S': 'â™ ī¸', 'H': 'â™Ĩī¸', 'D': 'â™Ļī¸'}[card.slice(-1)]} +
+ +
+ {#if card === 'JOKER'} + 🃏 + {:else} + {{'C': 'â™Ŗī¸', 'S': 'â™ ī¸', 'H': 'â™Ĩī¸', 'D': 'â™Ļī¸'}[card.slice(-1)]} + {/if} +
+ +
+ {#if card.slice(0, -1) === "J"} +
J: "{state.player.tech_jack}"
+ {:else if card.slice(0, -1) === "Q"} +
Q: "{state.player.tech_queen}"
+ {:else if card.slice(0, -1) === "K"} +
K: "{state.player.tech_king}"
+ {/if} +
+ +
+ {card === 'JOKER' ? '🃏' : card.slice(0, -1)} + {card === 'JOKER' ? '' : {'C': 'â™Ŗī¸', 'S': 'â™ ī¸', 'H': 'â™Ĩī¸', 'D': 'â™Ļī¸'}[card.slice(-1)]} +
+
+ {:else} +

Your hand is empty! (You draw cards when playing cards that match the suit color of the obstacle.)

+ {/each} +
+
+ + + {#if state.player.role !== "deep"} +
+

🐀 {state.player.name}'s Character Sheet

+ +
+ {#if state.player.is_dead} +
+

💀 You have Died / Retired!

+

Your story arc is complete. You will choose to become a Ghost or roll a new character during the upkeep phase between scenes.

+
+ {/if} + + {#if state.player.is_ghost} +
+

đŸ‘ģ Playing as a Ghost

+

You are in the Ghost World (grayscale view). You cannot interact well with the living but you can still help them resolve challenges!

+
+ {/if} + + {#if state.player.needs_name} +
+

đŸ´â€â˜ ī¸ You Earned a Name!

+

You completed your second objective and ranked up! What is your new Pirate Name?

+
+ + +
+
+ {/if} + +
+

Description:

+

Look: {state.player.avatar_look}

+

Smell: {state.player.avatar_smell}

+

First Words: "{state.player.first_words}"

+
+ +
+

Assigned Secret Techniques (J/Q/K):

+
    +
  • Jack (J): "{state.player.tech_jack}"
  • +
  • Queen (Q): "{state.player.tech_queen}"
  • +
  • King (K): "{state.player.tech_king}"
  • +
+
+ +
+

Crew Objectives

+
+ + + +
+
+ +
+

Personal Objectives

+
+ + + +
+
+
+
+ {/if} +
+
+ + +
+ + + {#if showEventLog} +
+ {#each state.events as event} +
+ {event.message} +
+ {:else} +
No events yet.
+ {/each} +
+ {/if} +
+ + diff --git a/frontend/src/components/SceneSetupPhase.svelte b/frontend/src/components/SceneSetupPhase.svelte new file mode 100644 index 0000000..74b1739 --- /dev/null +++ b/frontend/src/components/SceneSetupPhase.svelte @@ -0,0 +1,131 @@ + + +
+

Scene Setup (Scene #{state.game.current_scene_number})

+

Select your role for the upcoming scene. There must be at least one Pi-Rat and one Deep player. If you played the Deep in the last scene, you must play a Pi-Rat this scene!

+ +
+ +
+

Select Your Role

+ +
+ + + +
+ +
+ {#if !allowedRoles.includes('pirat')} + {#if state.game.current_scene_number === 1} +

You are the highest ranked player, so you must play The Deep for the first scene!

+ {:else} +

You are the only eligible player for The Deep this scene!

+ {/if} + {/if} + {#if !allowedRoles.includes('deep')} + {#if state.game.current_scene_number === 1} +

Only the highest ranked player(s) can play The Deep in the first scene!

+ {:else} +

You played The Deep last scene, so you must play a Pi-Rat!

+ {/if} + {/if} +
+
+ + +
+

Live Roster Selection

+
+ {#each state.players as p} +
+ {p.name} (Rank {p.rank}) + {#if p.role === 'pirat'} + Pi-Rat + {:else if p.role === 'deep'} + The Deep + {:else} + Choosing... + {/if} +
+ {/each} +
+
+
+ +
+ {#if error} +
{error}
+ {/if} + + {#if canStart} + + {:else if !allRolesAssigned} +

Waiting for all players to choose a role...

+ {:else if !deepPlayer} +

Someone must play as The Deep!

+ {:else if !piratPlayer} +

Someone must play as a Pi-Rat!

+ {:else} +

Waiting for The Deep or Creator to start the scene...

+ {/if} +
+
diff --git a/frontend/src/components/UpkeepPhase.svelte b/frontend/src/components/UpkeepPhase.svelte new file mode 100644 index 0000000..e41bc80 --- /dev/null +++ b/frontend/src/components/UpkeepPhase.svelte @@ -0,0 +1,437 @@ + + +
+

Between Scenes (Scene #{state.game.current_scene_number} Concluded)

+

Upkeep and tallying. Nominate a crewmate to Rank Up, redraw hand cards for resting Deep players, and ready up for the next scene.

+ + {#if state.player.is_dead && !state.player.is_ghost} + +
+

💀 Your Pi-Rat Has Died!

+

+ Your story arc is complete! You went out in a blaze of glory (or retired honorably). You must now choose how your journey continues: +

+ + {#if !isRolling} +
+ + +
+

+ Ghosts look back into the living world, watch over friends, and help their crew. New recruits start fresh with a randomized starting Rank between 1 and 3. +

+ {:else} +
+

Create Your New Recruit

+ + {#if rollingError} +
{rollingError}
+ {/if} + +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ + +
+
+ {/if} +
+ {:else} +
+ +
+

1. Pirate Ranking (Voting)

+

Nominate one crewmate who best exemplified pirate qualities in the previous scene. Previous Deep players are ineligible to receive votes.

+ + {#if state.game.phase === 'deep_upkeep'} +
+

âœ”ī¸ Voting complete! Results tallied.

+
+ {:else if state.votes.find(v => v.voter_player_id === state.player.id)} +
+

âœ”ī¸ Vote submitted! Waiting for other players to submit their nominations.

+
+ {:else if state.player.role === 'deep'} +
+

The Deep does not vote.

+
+ {:else} +
+
+ + +
+
+ {/if} + + + {#if state.game.phase !== 'deep_upkeep'} +
+

Nomination Progress:

+
+ {#each state.players as p} + {@const voted = state.votes.some(v => v.voter_player_id === p.id) || p.role === 'deep'} +
+ {p.name} + {voted ? 'Done' : 'Voting...'} +
+ {/each} +
+
+ {/if} +
+ + +
+

2. Resting Deep Upkeep

+ + {#if state.player.role === "deep"} +
+

You controlled the Deep in the last scene and your Pi-Rat was resting. You will discard your current hand and redraw back up to your maximum hand size.

+ + {#if state.game.phase === 'between_scenes'} +

Voting is currently in progress. Hand refresh will begin after voting concludes.

+ {:else if state.game.phase === 'deep_upkeep'} +

+ Your Hand Size Limit: {maxHandSize} cards.
+ Drag cards to the "Discard Pile" zone to discard them, or click them to move them. +

+ +
+ +
+

Hand (Keep)

+
+ {#each keepCards as card} +
handleDragStart(e, card)} + on:click={() => moveToDiscard(card)} + style="cursor: pointer;" + title="Click or drag to discard"> +
+ {card === 'JOKER' ? '🃏' : card.slice(0, -1)} + {card === 'JOKER' ? '' : {'C': 'â™Ŗī¸', 'S': 'â™ ī¸', 'H': 'â™Ĩī¸', 'D': 'â™Ļī¸'}[card.slice(-1)]} +
+
+ {#if card === 'JOKER'} + 🃏 + {:else} + {{'C': 'â™Ŗī¸', 'S': 'â™ ī¸', 'H': 'â™Ĩī¸', 'D': 'â™Ļī¸'}[card.slice(-1)]} + {/if} +
+
+ {card === 'JOKER' ? '🃏' : card.slice(0, -1)} + {card === 'JOKER' ? '' : {'C': 'â™Ŗī¸', 'S': 'â™ ī¸', 'H': 'â™Ĩī¸', 'D': 'â™Ļī¸'}[card.slice(-1)]} +
+
+ {:else} +

No cards in hand.

+ {/each} +
+
+ + +
+

Discard Pile

+
+ {#each discardCards as card} +
handleDragStart(e, card)} + on:click={() => moveToKeep(card)} + style="cursor: pointer;" + title="Click or drag to keep"> +
+ {card === 'JOKER' ? '🃏' : card.slice(0, -1)} + {card === 'JOKER' ? '' : {'C': 'â™Ŗī¸', 'S': 'â™ ī¸', 'H': 'â™Ĩī¸', 'D': 'â™Ļī¸'}[card.slice(-1)]} +
+
+ {#if card === 'JOKER'} + 🃏 + {:else} + {{'C': 'â™Ŗī¸', 'S': 'â™ ī¸', 'H': 'â™Ĩī¸', 'D': 'â™Ļī¸'}[card.slice(-1)]} + {/if} +
+
+ {card === 'JOKER' ? '🃏' : card.slice(0, -1)} + {card === 'JOKER' ? '' : {'C': 'â™Ŗī¸', 'S': 'â™ ī¸', 'H': 'â™Ĩī¸', 'D': 'â™Ļī¸'}[card.slice(-1)]} +
+
+ {:else} +

Drag cards here to discard them.

+ {/each} +
+
+
+ + {#if isOverLimit} +
+

âš ī¸ Discard Required: You have selected {keepCards.length} cards to keep, which exceeds your maximum hand limit of {maxHandSize}. Please discard at least {keepCards.length - maxHandSize} card(s).

+
+ {/if} + + + {/if} +
+ {:else} + {#if state.game.phase === 'between_scenes'} +

Your Pi-Rat was active. Your hand cards carry over to the next scene.

+ {:else if state.game.phase === 'deep_upkeep'} +

Waiting for resting Deep player to refresh their hand...

+ {/if} + {/if} + +
+

Crew Rank Ledger:

+
    + {#each state.players as p} +
  • + {p.name}: Rank {p.rank} + {#if p.role === 'deep'}Deep{:else if p.is_ghost}Ghost{:else}Pi-Rat{/if} +
  • + {/each} +
+
+
+
+ + +
+ {#if state.game.phase === 'deep_upkeep'} + {#if state.player.role === 'deep'} +

âš ī¸ Please complete your Hand Refresh above to proceed to the next scene.

+ {:else} +
+

Waiting for resting Deep player to refresh their hand...

+
+
+ {/if} + {:else} + {#if state.player.role === 'pirat' && !state.votes.find(v => v.voter_player_id === state.player.id)} +

âš ī¸ You must nominate a crewmate above before you can ready up.

+ + {:else} + {#if state.player.is_ready} +
+

Ready! Waiting for other players to ready up...

+
+
+ {:else} + + {/if} + {/if} + {/if} +
+ {/if} +
diff --git a/frontend/src/lib/Counter.svelte b/frontend/src/lib/Counter.svelte new file mode 100644 index 0000000..20bf4c9 --- /dev/null +++ b/frontend/src/lib/Counter.svelte @@ -0,0 +1,5 @@ + + + diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js new file mode 100644 index 0000000..8324f6c --- /dev/null +++ b/frontend/src/lib/api.js @@ -0,0 +1,23 @@ +export async function apiRequest(endpoint, method = 'GET', data = null) { + const options = { + method, + headers: {}, + }; + + if (data) { + options.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + options.body = new URLSearchParams(data).toString(); + } + + const res = await fetch(`/api${endpoint}`, options); + if (!res.ok) { + let msg = res.statusText; + try { + const errBody = await res.json(); + if (errBody.error) msg = errBody.error; + else if (errBody.detail) msg = errBody.detail; + } catch(e) {} + throw new Error(msg); + } + return res.json(); +} diff --git a/frontend/src/lib/suggestions.js b/frontend/src/lib/suggestions.js new file mode 100644 index 0000000..5eac957 --- /dev/null +++ b/frontend/src/lib/suggestions.js @@ -0,0 +1,1423 @@ +// Auto-generated math-pirate suggestions pool for character creation +const SUGGESTIONS = { + "look": [ + "A cyber-pirate waistcoat buttoned with old copper coins", + "A mechanical leather vest covered in algebraic graffiti", + "A salty tail glowing with neon math runes", + "A ink-stained paw replaced with a brass protractor-claw", + "A barnacle-covered paw replaced with a brass protractor-claw", + "A cyber-pirate tricorn hat with a built-in compass", + "A salty tricorn hat with a built-in compass", + "A scurvy snout that twitches in rhythm with prime numbers", + "A brass-plated coat patterned like a checkerboard", + "A brass telescope permanently strapped to the left ear", + "A neon-glowing tail glowing with neon math runes", + "A scurvy paw replaced with a brass protractor-claw", + "A ancient whisker arrangement forming a perfect sine wave", + "A brass-plated leather vest covered in algebraic graffiti", + "A brass-plated peg-leg with a hidden slide-rule drawer", + "A polished tail glowing with neon math runes", + "A barnacle-covered snout with geometry scars", + "A ancient bandana soaked in salty seawater and ink", + "A ancient coat patterned like a checkerboard", + "A rusty snout that twitches in rhythm with prime numbers", + "A weather-beaten tail curled into a Fibonacci spiral", + "A gilded paw replaced with a brass protractor-claw", + "A gilded tricorn hat with a built-in compass", + "A brass-plated tail glowing with neon math runes", + "A mechanical satchel full of crumpled grid paper", + "A mechanical tricorn hat with nested abacus beads", + "A one-eyed leather vest covered in algebraic graffiti", + "A ancient tail glowing with neon math runes", + "A rusty leather vest covered in algebraic graffiti", + "A one-eyed paw replaced with a brass protractor-claw", + "A phosphorescent tail curled into a Fibonacci spiral", + "A weather-beaten paw replaced with a brass protractor-claw", + "A brass-plated whisker arrangement forming a perfect sine wave", + "A ancient snout that twitches in rhythm with prime numbers", + "A polished tricorn hat with a built-in compass", + "A scurvy whisker arrangement forming a perfect sine wave", + "A polished paw replaced with a brass protractor-claw", + "A gilded tricorn hat with nested abacus beads", + "A scarf patterned with infinite decimals of Pi", + "A cyber-pirate leather vest covered in algebraic graffiti", + "A barnacle-covered bandana soaked in salty seawater and ink", + "A phosphorescent tricorn hat with a built-in compass", + "A gilded waistcoat buttoned with old copper coins", + "A cyber-pirate satchel full of crumpled grid paper", + "A tricorn hat folded from an old calculus exam", + "A barnacle-covered tail curled into a Fibonacci spiral", + "A barnacle-covered satchel full of crumpled grid paper", + "A neon-glowing eyepatch displaying coordinate maps", + "A weather-beaten bandana soaked in salty seawater and ink", + "A gilded whisker arrangement forming a perfect sine wave", + "A one-eyed coat patterned like a checkerboard", + "A polished eyepatch displaying coordinate maps", + "A salty tricorn hat with nested abacus beads", + "A gilded eyepatch displaying coordinate maps", + "A mechanical waistcoat buttoned with old copper coins", + "A phosphorescent coat patterned like a checkerboard", + "A tiny chalkboard strapped to the forearm for quick calculations", + "A phosphorescent waistcoat buttoned with old copper coins", + "A ancient eyepatch displaying coordinate maps", + "A brass-plated eyepatch displaying coordinate maps", + "Whiskers braided with tiny colorful abacus beads", + "A salty satchel full of crumpled grid paper", + "A barnacle-covered eyepatch displaying coordinate maps", + "A cyber-pirate tail curled into a Fibonacci spiral", + "A cyber-pirate whisker arrangement forming a perfect sine wave", + "A weather-beaten snout with geometry scars", + "A ink-stained waistcoat buttoned with old copper coins", + "A neon-glowing tail curled into a Fibonacci spiral", + "A ink-stained eyepatch displaying coordinate maps", + "A weather-beaten whisker arrangement forming a perfect sine wave", + "A brass-plated tricorn hat with nested abacus beads", + "A polished waistcoat buttoned with old copper coins", + "A salty tail curled into a Fibonacci spiral", + "A scurvy satchel full of crumpled grid paper", + "A gilded tail glowing with neon math runes", + "A scurvy coat patterned like a checkerboard", + "A barnacle-covered tricorn hat with a built-in compass", + "A bandana decorated with coordinate grids", + "A barnacle-covered tricorn hat with nested abacus beads", + "A weather-beaten tail glowing with neon math runes", + "A rusty tail glowing with neon math runes", + "A scurvy waistcoat buttoned with old copper coins", + "A mechanical peg-leg with a hidden slide-rule drawer", + "A silver claw replacement shaped like a protractor", + "A salty paw replaced with a brass protractor-claw", + "A golden tooth engraved with the division sign", + "A ancient tail curled into a Fibonacci spiral", + "A barnacle-covered peg-leg with a hidden slide-rule drawer", + "A barnacle-covered waistcoat buttoned with old copper coins", + "A ink-stained whisker arrangement forming a perfect sine wave", + "A scurvy peg-leg with a hidden slide-rule drawer", + "A ink-stained snout that twitches in rhythm with prime numbers", + "A one-eyed tail glowing with neon math runes", + "A salty peg-leg with a hidden slide-rule drawer", + "A phosphorescent bandana soaked in salty seawater and ink", + "A cyber-pirate snout that twitches in rhythm with prime numbers", + "A ancient tricorn hat with a built-in compass", + "A one-eyed satchel full of crumpled grid paper", + "A rusty paw replaced with a brass protractor-claw", + "A salty waistcoat buttoned with old copper coins", + "A brass-plated tricorn hat with a built-in compass", + "A ancient paw replaced with a brass protractor-claw", + "An eye patch made of a black spade card", + "A ink-stained coat patterned like a checkerboard", + "A barnacle-covered whisker arrangement forming a perfect sine wave", + "A neon-glowing bandana soaked in salty seawater and ink", + "A barnacle-covered tail glowing with neon math runes", + "A one-eyed waistcoat buttoned with old copper coins", + "Fur bleached in the pattern of a Venn diagram", + "A one-eyed snout that twitches in rhythm with prime numbers", + "A one-eyed snout with geometry scars", + "A ink-stained leather vest covered in algebraic graffiti", + "A polished satchel full of crumpled grid paper", + "A brass-plated snout with geometry scars", + "A brass-plated tail curled into a Fibonacci spiral", + "A neon-glowing snout with geometry scars", + "Fur that smells faintly of chalk dust and gunpowder", + "A cyber-pirate paw replaced with a brass protractor-claw", + "A weather-beaten eyepatch displaying coordinate maps", + "A brass-plated satchel full of crumpled grid paper", + "A ancient snout with geometry scars", + "A ancient satchel full of crumpled grid paper", + "A miniature compass hanging from the left nostril ring", + "A polished tricorn hat with nested abacus beads", + "A cyber-pirate tail glowing with neon math runes", + "A one-eyed eyepatch displaying coordinate maps", + "A ink-stained tricorn hat with a built-in compass", + "A cyber-pirate eyepatch displaying coordinate maps", + "A one-eyed peg-leg with a hidden slide-rule drawer", + "A peg leg carved from a heavy wooden slide rule", + "A barnacle-covered coat patterned like a checkerboard", + "A phosphorescent tail glowing with neon math runes", + "A salty snout that twitches in rhythm with prime numbers", + "A cyber-pirate snout with geometry scars", + "A ink-stained snout with geometry scars", + "A glowing neon abacus tattooed on their back", + "A rusty peg-leg with a hidden slide-rule drawer", + "A rusty snout with geometry scars", + "An abacus-wheel peg leg that rolls instead of clicks", + "A neon-glowing waistcoat buttoned with old copper coins", + "A polished coat patterned like a checkerboard", + "A mechanical coat patterned like a checkerboard", + "A phosphorescent tricorn hat with nested abacus beads", + "A brass-plated waistcoat buttoned with old copper coins", + "A barnacle-covered leather vest covered in algebraic graffiti", + "A ancient tricorn hat with nested abacus beads", + "Ear notched in the shape of a perfect right angle", + "A ink-stained tricorn hat with nested abacus beads", + "A mechanical paw replaced with a brass protractor-claw", + "A weather-beaten tricorn hat with a built-in compass", + "A gilded satchel full of crumpled grid paper", + "A ink-stained satchel full of crumpled grid paper", + "A salty coat patterned like a checkerboard", + "A gilded peg-leg with a hidden slide-rule drawer", + "A ink-stained bandana soaked in salty seawater and ink", + "A gilded tail curled into a Fibonacci spiral", + "A mechanical whisker arrangement forming a perfect sine wave", + "A phosphorescent peg-leg with a hidden slide-rule drawer", + "A polished bandana soaked in salty seawater and ink", + "A monocle that displays neon green equations", + "A polished snout that twitches in rhythm with prime numbers", + "A mechanical tail curled into a Fibonacci spiral", + "A phosphorescent leather vest covered in algebraic graffiti", + "A scurvy tail curled into a Fibonacci spiral", + "A scurvy eyepatch displaying coordinate maps", + "A phosphorescent snout that twitches in rhythm with prime numbers", + "A neon-glowing snout that twitches in rhythm with prime numbers", + "A salty whisker arrangement forming a perfect sine wave", + "A phosphorescent paw replaced with a brass protractor-claw", + "A scurvy bandana soaked in salty seawater and ink", + "A neon-glowing whisker arrangement forming a perfect sine wave", + "A scurvy snout with geometry scars", + "A neon-glowing tricorn hat with nested abacus beads", + "A brass-plated bandana soaked in salty seawater and ink", + "A scurvy tricorn hat with nested abacus beads", + "A ancient peg-leg with a hidden slide-rule drawer", + "A mechanical bandana soaked in salty seawater and ink", + "A polished leather vest covered in algebraic graffiti", + "A gilded snout that twitches in rhythm with prime numbers", + "A scurvy tail glowing with neon math runes", + "A neon-glowing leather vest covered in algebraic graffiti", + "A brass-plated snout that twitches in rhythm with prime numbers", + "A rusty coat patterned like a checkerboard", + "A ink-stained tail glowing with neon math runes", + "A tail wrapped in copper wire that sparks when they get excited", + "A gilded leather vest covered in algebraic graffiti", + "A salty eyepatch displaying coordinate maps", + "A weather-beaten waistcoat buttoned with old copper coins", + "A gilded snout with geometry scars", + "A mechanical eyepatch displaying coordinate maps", + "A rusty waistcoat buttoned with old copper coins", + "A cyber-pirate tricorn hat with nested abacus beads", + "A salty bandana soaked in salty seawater and ink", + "A weather-beaten satchel full of crumpled grid paper", + "A rusty whisker arrangement forming a perfect sine wave", + "A salty snout with geometry scars", + "A cyber-pirate bandana soaked in salty seawater and ink", + "A ancient waistcoat buttoned with old copper coins", + "A rusty tricorn hat with nested abacus beads", + "A rusty eyepatch displaying coordinate maps" + ], + "smell": [ + "reminiscent of cheese and damp wood", + "suspiciously smelling of ink and sour lemons", + "suspiciously smelling of ink and spilled ink", + "pleasantly smelling of rum and damp wood", + "faintly of seaweed and damp wood", + "faintly of pine tar and sour lemons", + "faintly of ink and lightning sparks", + "like a mix of pine tar and spilled ink", + "strongly of ozone and sour lemons", + "like a mix of ozone and salty air", + "suspiciously smelling of seaweed and rusty iron", + "suspiciously smelling of seaweed and spilled ink", + "faintly of cheese and sour lemons", + "like a mix of ink and lightning sparks", + "strongly of gunpowder and spilled ink", + "pleasantly smelling of seaweed and peppermint oil", + "suspiciously smelling of rum and lightning sparks", + "suspiciously smelling of ozone and stale grog", + "pleasantly smelling of rum and burnt abacuses", + "strongly of cheese and damp wood", + "Ozone sparks and sulfur", + "reminiscent of gunpowder and wet tail fur", + "strongly of cheese and burnt abacuses", + "like a mix of pine tar and wet tail fur", + "strongly of cheese and spilled ink", + "like a mix of ozone and damp wood", + "pleasantly smelling of ink and sour lemons", + "pleasantly smelling of copper and wet tail fur", + "faintly of rum and burnt abacuses", + "suspiciously smelling of gunpowder and rusty iron", + "like a mix of copper and lightning sparks", + "pleasantly smelling of seaweed and burnt abacuses", + "reminiscent of copper and lightning sparks", + "reminiscent of cheese and burnt abacuses", + "pleasantly smelling of parchment and burnt abacuses", + "like a mix of seaweed and damp wood", + "Gunpowder smoke and melted candle wax", + "like a mix of ozone and burnt abacuses", + "faintly of rum and stale grog", + "like a mix of cheese and spilled ink", + "suspiciously smelling of parchment and sour lemons", + "faintly of copper and wet tail fur", + "faintly of cheese and wet tail fur", + "faintly of cheese and damp wood", + "faintly of pine tar and rusty iron", + "strongly of gunpowder and peppermint oil", + "faintly of pine tar and damp wood", + "strongly of seaweed and damp wood", + "like a mix of copper and wet tail fur", + "suspiciously smelling of cheese and salty air", + "faintly of rum and lightning sparks", + "reminiscent of chalk dust and spilled ink", + "strongly of copper and wet tail fur", + "pleasantly smelling of pine tar and damp wood", + "Damp sea charts and ozone", + "like a mix of cheese and stale grog", + "strongly of ink and rusty iron", + "like a mix of seaweed and salty air", + "pleasantly smelling of chalk dust and spilled ink", + "pleasantly smelling of chalk dust and peppermint oil", + "suspiciously smelling of copper and salty air", + "reminiscent of ozone and wet tail fur", + "faintly of rum and salty air", + "reminiscent of chalk dust and sour lemons", + "suspiciously smelling of pine tar and spilled ink", + "like a mix of cheese and wet tail fur", + "strongly of ink and sour lemons", + "suspiciously smelling of chalk dust and damp wood", + "Burnt tea leaves and old books", + "faintly of parchment and wet tail fur", + "faintly of copper and sour lemons", + "faintly of chalk dust and spilled ink", + "faintly of gunpowder and lightning sparks", + "reminiscent of parchment and wet tail fur", + "pleasantly smelling of parchment and rusty iron", + "reminiscent of cheese and stale grog", + "reminiscent of ink and wet tail fur", + "pleasantly smelling of gunpowder and burnt abacuses", + "reminiscent of parchment and stale grog", + "pleasantly smelling of chalk dust and lightning sparks", + "reminiscent of copper and peppermint oil", + "pleasantly smelling of ink and peppermint oil", + "Ozone and seaweed-wrapped tobacco", + "strongly of cheese and rusty iron", + "faintly of chalk dust and burnt abacuses", + "pleasantly smelling of seaweed and lightning sparks", + "suspiciously smelling of rum and stale grog", + "like a mix of rum and burnt abacuses", + "suspiciously smelling of rum and peppermint oil", + "suspiciously smelling of copper and burnt abacuses", + "reminiscent of pine tar and spilled ink", + "reminiscent of parchment and lightning sparks", + "suspiciously smelling of ink and rusty iron", + "like a mix of pine tar and damp wood", + "pleasantly smelling of chalk dust and salty air", + "faintly of copper and burnt abacuses", + "pleasantly smelling of ozone and stale grog", + "like a mix of cheese and lightning sparks", + "strongly of pine tar and salty air", + "like a mix of seaweed and rusty iron", + "pleasantly smelling of seaweed and sour lemons", + "suspiciously smelling of gunpowder and lightning sparks", + "suspiciously smelling of ink and peppermint oil", + "like a mix of seaweed and wet tail fur", + "reminiscent of gunpowder and burnt abacuses", + "suspiciously smelling of pine tar and sour lemons", + "pleasantly smelling of ink and lightning sparks", + "reminiscent of pine tar and peppermint oil", + "pleasantly smelling of rum and rusty iron", + "like a mix of gunpowder and wet tail fur", + "suspiciously smelling of pine tar and lightning sparks", + "pleasantly smelling of copper and spilled ink", + "strongly of ozone and wet tail fur", + "reminiscent of ink and burnt abacuses", + "like a mix of copper and stale grog", + "faintly of copper and stale grog", + "like a mix of copper and peppermint oil", + "suspiciously smelling of copper and stale grog", + "suspiciously smelling of cheese and burnt abacuses", + "strongly of ozone and spilled ink", + "Burnt cheese and gunpowder", + "pleasantly smelling of seaweed and salty air", + "pleasantly smelling of seaweed and stale grog", + "like a mix of ozone and peppermint oil", + "strongly of pine tar and rusty iron", + "strongly of gunpowder and stale grog", + "reminiscent of parchment and rusty iron", + "faintly of copper and lightning sparks", + "faintly of chalk dust and sour lemons", + "suspiciously smelling of chalk dust and stale grog", + "Freshly sharpened pencils and vinegar", + "pleasantly smelling of parchment and sour lemons", + "pleasantly smelling of ozone and sour lemons", + "faintly of cheese and rusty iron", + "like a mix of seaweed and burnt abacuses", + "reminiscent of gunpowder and damp wood", + "like a mix of cheese and sour lemons", + "reminiscent of seaweed and stale grog", + "like a mix of gunpowder and peppermint oil", + "reminiscent of ozone and stale grog", + "strongly of seaweed and burnt abacuses", + "faintly of ink and rusty iron", + "faintly of ink and spilled ink", + "suspiciously smelling of pine tar and peppermint oil", + "strongly of parchment and lightning sparks", + "reminiscent of chalk dust and damp wood", + "suspiciously smelling of gunpowder and stale grog", + "faintly of ozone and lightning sparks", + "faintly of rum and spilled ink", + "strongly of pine tar and lightning sparks", + "strongly of pine tar and burnt abacuses", + "pleasantly smelling of rum and spilled ink", + "reminiscent of rum and stale grog", + "faintly of copper and rusty iron", + "suspiciously smelling of ozone and wet tail fur", + "strongly of seaweed and spilled ink", + "suspiciously smelling of parchment and salty air", + "strongly of copper and lightning sparks", + "pleasantly smelling of cheese and peppermint oil", + "reminiscent of cheese and peppermint oil", + "pleasantly smelling of seaweed and rusty iron", + "suspiciously smelling of ozone and lightning sparks", + "strongly of rum and peppermint oil", + "faintly of chalk dust and peppermint oil", + "like a mix of rum and wet tail fur", + "like a mix of cheese and damp wood", + "reminiscent of chalk dust and wet tail fur", + "reminiscent of rum and peppermint oil", + "suspiciously smelling of copper and spilled ink", + "reminiscent of cheese and sour lemons", + "pleasantly smelling of parchment and damp wood", + "reminiscent of chalk dust and salty air", + "pleasantly smelling of pine tar and salty air", + "strongly of rum and wet tail fur", + "strongly of ink and damp wood", + "like a mix of pine tar and peppermint oil", + "strongly of gunpowder and sour lemons", + "faintly of ink and burnt abacuses", + "like a mix of rum and sour lemons", + "pleasantly smelling of cheese and sour lemons", + "strongly of cheese and lightning sparks", + "reminiscent of chalk dust and burnt abacuses", + "Salty parchment and copper coins", + "strongly of rum and lightning sparks", + "suspiciously smelling of seaweed and burnt abacuses", + "reminiscent of cheese and rusty iron", + "faintly of cheese and lightning sparks", + "like a mix of cheese and peppermint oil", + "reminiscent of ink and spilled ink", + "faintly of pine tar and peppermint oil", + "faintly of parchment and burnt abacuses", + "reminiscent of parchment and burnt abacuses", + "strongly of pine tar and peppermint oil", + "pleasantly smelling of ozone and peppermint oil", + "like a mix of parchment and rusty iron", + "faintly of ink and stale grog", + "pleasantly smelling of pine tar and burnt abacuses", + "suspiciously smelling of cheese and peppermint oil", + "strongly of gunpowder and rusty iron", + "reminiscent of cheese and spilled ink" + ], + "first_words": [ + "By the deep sea! the probability indicates we are sinking logarithmically!", + "I have transformed... and my hunger is logarithmic!", + "Avast! the geometry of this deck requires the coordinates of the treasure island!", + "Blow me down! the geometry of this deck requires the shortest boarding vector!", + "Blast it! the slope of the sea shows a prime number of hazards!", + "Aha! the abacus predicts infinite cheese!", + "Blow me down! the probability indicates the shortest boarding vector!", + "Arrr, the hypotenuse is magnificent!", + "To the brig! the slope of the sea shows a prime number of hazards!", + "Shiver my timbers! the coordinates point to the gats are ready to fire!", + "Arrr! my tail calculations reveal a division by zero!", + "Avast! the slope of the sea shows the coordinates of the treasure island!", + "Arrr! the tangent of the gat implies a division by zero!", + "Shiver my timbers! the slope of the sea shows an escape velocity of ten knots!", + "Hear me crew! the slope of the sea shows a division by zero!", + "Shiver my timbers! the probability indicates a division by zero!", + "Hear me crew! the captain's slide rule shows infinite cheese!", + "Avast! the geometry of this deck requires the gats are ready to fire!", + "Hear me crew! the tangent of the gat implies a perfect circle of doom!", + "Aha! the formula for cheese says we are sinking logarithmically!", + "Aha! the tangent of the gat implies a jackpot of gold doubloons!", + "Aha! the probability indicates infinite cheese!", + "To the brig! my tail calculations reveal a division by zero!", + "To the brig! the tangent of the gat implies a jackpot of gold doubloons!", + "Look alive! the slope of the sea shows infinite cheese!", + "Look alive! the geometry of this deck requires a perfect circle of doom!", + "Shiver my timbers! the captain's slide rule shows we are sinking logarithmically!", + "Blast it! the tangent of the gat implies the shortest boarding vector!", + "To the horizon, at a 45-degree angle!", + "To the brig! the formula for cheese says the gats are ready to fire!", + "Arrr! the captain's slide rule shows the shortest boarding vector!", + "Blow me down! the coordinates point to an escape velocity of ten knots!", + "Hear me crew! the probability indicates we are sinking logarithmically!", + "Look alive! the matrix of the hold contains a jackpot of gold doubloons!", + "Blow me down! the slope of the sea shows an escape velocity of ten knots!", + "Aha! the slope of the sea shows infinite cheese!", + "Arrr, divide by zero!", + "Arrr! the captain's slide rule shows infinite cheese!", + "Arrr! the coordinates point to the coordinates of the treasure island!", + "Look alive! my tail calculations reveal we are sinking logarithmically!", + "To the brig! the slope of the sea shows an escape velocity of ten knots!", + "Blast it! the coordinates point to an escape velocity of ten knots!", + "Aha! the matrix of the hold contains we are sinking logarithmically!", + "Look alive! the coordinates point to the gats are ready to fire!", + "Arrr! the geometry of this deck requires the coordinates of the treasure island!", + "Blast it! the slope of the sea shows an escape velocity of ten knots!", + "Arrr! the slope of the sea shows a jackpot of gold doubloons!", + "By the deep sea! the probability indicates the shortest boarding vector!", + "Hear me crew! the coordinates point to an escape velocity of ten knots!", + "Look alive! the geometry of this deck requires infinite cheese!", + "Arrr! the abacus predicts infinite cheese!", + "Arrr! the slope of the sea shows infinite cheese!", + "Avast! the probability indicates the shortest boarding vector!", + "Avast! the abacus predicts a prime number of hazards!", + "Blow me down! the slope of the sea shows a perfect circle of doom!", + "Blow me down! the formula for cheese says the coordinates of the treasure island!", + "Hear me crew! the coordinates point to the coordinates of the treasure island!", + "To the brig! the captain's slide rule shows the coordinates of the treasure island!", + "Hear me crew! my tail calculations reveal a prime number of hazards!", + "Aha! the tangent of the gat implies a division by zero!", + "Arrr! the geometry of this deck requires a jackpot of gold doubloons!", + "Blow me down! my tail calculations reveal a jackpot of gold doubloons!", + "To the brig! the coordinates point to we are sinking logarithmically!", + "Blow me down! the geometry of this deck requires we are sinking logarithmically!", + "By the deep sea! the slope of the sea shows the shortest boarding vector!", + "Hear me crew! the formula for cheese says infinite cheese!", + "Shiver my timbers! the coordinates point to a prime number of hazards!", + "Shiver my timbers! the matrix of the hold contains the coordinates of the treasure island!", + "Aha! the formula for cheese says the shortest boarding vector!", + "Avast! the slope of the sea shows we are sinking logarithmically!", + "Aha! my tail calculations reveal the shortest boarding vector!", + "By the deep sea! the matrix of the hold contains the coordinates of the treasure island!", + "Arrr! the matrix of the hold contains a prime number of hazards!", + "Shiver my timbers! my tail calculations reveal a perfect circle of doom!", + "Arrr! the geometry of this deck requires we are sinking logarithmically!", + "Hear me crew! the slope of the sea shows a perfect circle of doom!", + "Aha! the matrix of the hold contains an escape velocity of ten knots!", + "Shiver my timbers! the captain's slide rule shows the gats are ready to fire!", + "Blow me down! the captain's slide rule shows the coordinates of the treasure island!", + "Hear me crew! the coordinates point to the shortest boarding vector!", + "Blast it! the captain's slide rule shows the shortest boarding vector!", + "Arrr! the coordinates point to the shortest boarding vector!", + "Avast! the geometry of this deck requires a perfect circle of doom!", + "Arrr! the coordinates point to infinite cheese!", + "Arrr! the tangent of the gat implies infinite cheese!", + "Look alive! the slope of the sea shows a jackpot of gold doubloons!", + "Avast! the slope of the sea shows a division by zero!", + "Blow me down! the coordinates point to the shortest boarding vector!", + "Shiver my timbers! the geometry of this deck requires a division by zero!", + "Arrr! the formula for cheese says we are sinking logarithmically!", + "Blast it! the geometry of this deck requires the coordinates of the treasure island!", + "Look alive! the matrix of the hold contains a division by zero!", + "To the brig! the captain's slide rule shows the gats are ready to fire!", + "Hear me crew! the probability indicates an escape velocity of ten knots!", + "Avast! the tangent of the gat implies a perfect circle of doom!", + "Shiver my timbers! the captain's slide rule shows infinite cheese!", + "Shiver my decimals!", + "Blast it! the probability indicates an escape velocity of ten knots!", + "Arrr! the probability indicates the gats are ready to fire!", + "Arrr! the captain's slide rule shows the coordinates of the treasure island!", + "Blow me down! the tangent of the gat implies the shortest boarding vector!", + "Shiver my timbers! the matrix of the hold contains we are sinking logarithmically!", + "By the deep sea! the abacus predicts we are sinking logarithmically!", + "Avast! the slope of the sea shows an escape velocity of ten knots!", + "Aha! the tangent of the gat implies an escape velocity of ten knots!", + "Blow me down! the tangent of the gat implies an escape velocity of ten knots!", + "Avast! the probability indicates we are sinking logarithmically!", + "Aha! the slope of the sea shows a perfect circle of doom!", + "Look alive! my tail calculations reveal the coordinates of the treasure island!", + "Shiver my timbers! the formula for cheese says infinite cheese!", + "Aha! the matrix of the hold contains the shortest boarding vector!", + "Aha! the geometry of this deck requires the shortest boarding vector!", + "Blast it! the probability indicates a division by zero!", + "To the brig! the formula for cheese says a division by zero!", + "Shiver my timbers! the slope of the sea shows a jackpot of gold doubloons!", + "By the deep sea! the tangent of the gat implies a perfect circle of doom!", + "By the deep sea! the formula for cheese says we are sinking logarithmically!", + "Blow me down! the matrix of the hold contains a division by zero!", + "Blow me down! the captain's slide rule shows an escape velocity of ten knots!", + "Arrr! the geometry of this deck requires a division by zero!", + "Blow me down! the abacus predicts a perfect circle of doom!", + "Avast! the matrix of the hold contains a jackpot of gold doubloons!", + "To the brig! my tail calculations reveal a prime number of hazards!", + "Aha! the geometry of this deck requires we are sinking logarithmically!", + "Hear me crew! the tangent of the gat implies a division by zero!", + "Hear me crew! the abacus predicts a jackpot of gold doubloons!", + "Arrr! my tail calculations reveal a prime number of hazards!", + "By the deep sea! the probability indicates a jackpot of gold doubloons!", + "By the deep sea! the formula for cheese says the shortest boarding vector!", + "By the deep sea! the geometry of this deck requires the gats are ready to fire!", + "Hear me crew! the abacus predicts a division by zero!", + "By the deep sea! the tangent of the gat implies an escape velocity of ten knots!", + "Blast it! the probability indicates we are sinking logarithmically!", + "Hear me crew! the geometry of this deck requires infinite cheese!", + "Avast! the captain's slide rule shows the shortest boarding vector!", + "To the brig! the slope of the sea shows a division by zero!", + "Hear me crew! the captain's slide rule shows the gats are ready to fire!", + "Avast! the tangent of the gat implies a jackpot of gold doubloons!", + "Hear me crew! my tail calculations reveal the shortest boarding vector!", + "Shiver my timbers! the formula for cheese says the coordinates of the treasure island!", + "Hear me crew! the matrix of the hold contains a perfect circle of doom!", + "By the deep sea! the geometry of this deck requires the coordinates of the treasure island!", + "Look alive! the formula for cheese says the coordinates of the treasure island!", + "Gats, cheese, and geometry\u2014that's all a rat needs!", + "Arrr! the probability indicates a jackpot of gold doubloons!", + "Hear me crew! the probability indicates a prime number of hazards!", + "Blow me down! the geometry of this deck requires an escape velocity of ten knots!", + "To the brig! the matrix of the hold contains a division by zero!", + "Blast it! my tail calculations reveal infinite cheese!", + "Shiver my timbers! my tail calculations reveal a jackpot of gold doubloons!", + "Shiver my timbers! the matrix of the hold contains the gats are ready to fire!", + "Hear me crew! the abacus predicts the gats are ready to fire!", + "Avast! the geometry of this deck requires we are sinking logarithmically!", + "Shiver my timbers! the tangent of the gat implies we are sinking logarithmically!", + "Avast! the tangent of the gat implies a prime number of hazards!", + "Avast! my tail calculations reveal infinite cheese!", + "Arrr! the matrix of the hold contains the shortest boarding vector!", + "Blow me down! the slope of the sea shows a prime number of hazards!", + "Aha! the coordinates point to a division by zero!", + "To the brig! the matrix of the hold contains we are sinking logarithmically!", + "Look alive! the formula for cheese says we are sinking logarithmically!", + "Look alive! the abacus predicts a division by zero!", + "By the deep sea! the coordinates point to an escape velocity of ten knots!", + "Blow me down! the coordinates point to we are sinking logarithmically!", + "By the deep sea! the probability indicates the coordinates of the treasure island!", + "To the brig! the formula for cheese says a prime number of hazards!", + "To the brig! the probability indicates infinite cheese!", + "Look alive! the tangent of the gat implies an escape velocity of ten knots!", + "Shiver my timbers! the probability indicates we are sinking logarithmically!", + "By the deep sea! the abacus predicts infinite cheese!", + "Blow me down! the matrix of the hold contains the coordinates of the treasure island!", + "Arrr! my tail calculations reveal a perfect circle of doom!", + "Avast! the captain's slide rule shows an escape velocity of ten knots!", + "Arrr! the captain's slide rule shows an escape velocity of ten knots!", + "Blast it! the abacus predicts a prime number of hazards!", + "Blast it! my tail calculations reveal a division by zero!", + "Blow me down! the probability indicates a jackpot of gold doubloons!", + "By the deep sea! the coordinates point to a division by zero!", + "By the deep sea! the abacus predicts a jackpot of gold doubloons!", + "Blast it! the captain's slide rule shows a prime number of hazards!", + "Aha! the slope of the sea shows the coordinates of the treasure island!", + "To the brig! the matrix of the hold contains a jackpot of gold doubloons!", + "Look alive! the tangent of the gat implies the gats are ready to fire!", + "Blast it! the matrix of the hold contains a prime number of hazards!", + "Arrr! the tangent of the gat implies a perfect circle of doom!", + "Avast! the slope of the sea shows a jackpot of gold doubloons!", + "Shiver my timbers! the formula for cheese says a prime number of hazards!", + "By the deep sea! my tail calculations reveal we are sinking logarithmically!", + "Hear me crew! the slope of the sea shows a jackpot of gold doubloons!", + "Aha! the geometry of this deck requires infinite cheese!", + "To the brig! the geometry of this deck requires the coordinates of the treasure island!", + "Look alive! the coordinates point to the coordinates of the treasure island!", + "Look alive! the formula for cheese says the shortest boarding vector!", + "By the deep sea! the geometry of this deck requires a prime number of hazards!", + "To the brig! the tangent of the gat implies we are sinking logarithmically!", + "Hear me crew! the coordinates point to a perfect circle of doom!", + "Aha! the formula for cheese says infinite cheese!", + "By the deep sea! the formula for cheese says a prime number of hazards!", + "Aha! the matrix of the hold contains the gats are ready to fire!", + "Arrr! my tail calculations reveal the gats are ready to fire!" + ], + "good_at_math": [ + "Thinks they are, they uses slide rules to navigate stormy seas but gets confused by negative numbers.", + "Thinks they are, they remembers the first 100 digits of Pi and has the abacus tattoos to prove it.", + "Yes, they understands the probability of drawing a Joker but gets confused by negative numbers.", + "According to the abacus, they can balance the captain's ledgers in their head but gets confused by negative numbers.", + "No, they can solve equations under threat of cannon fire but gets distracted by shiny brass coins.", + "No, they knows how to divide cheese by zero and has the abacus tattoos to prove it.", + "In theory, they can solve equations under threat of cannon fire but gets distracted by shiny brass coins.", + "On paper, they can solve equations under threat of cannon fire but only when drunk on grog.", + "Absolutely, they remembers the first 100 digits of Pi and has a certificate from the Stowaway Academy.", + "No, they understands the geometry of boarding leaps and has the abacus tattoos to prove it.", + "Thinks they are, they can solve equations under threat of cannon fire but refuses to use prime numbers.", + "Barely, they can calculate the trajectory of a gat bullet but refuses to use prime numbers.", + "Only, they understands the geometry of boarding leaps but only when drunk on grog.", + "Absolutely, they can solve equations under threat of cannon fire but gets confused by negative numbers.", + "Thinks they are, they understands the geometry of boarding leaps but only when cheese is involved.", + "Thinks they are, they knows how to divide cheese by zero but only when cheese is involved.", + "On paper, they remembers the first 100 digits of Pi but only in base-3.", + "On paper, they knows how to divide cheese by zero and has a certificate from the Stowaway Academy.", + "According to the abacus, they uses slide rules to navigate stormy seas and has the abacus tattoos to prove it.", + "Yes, they can count abacus beads in pitch black hold but only when cheese is involved.", + "No, they can calculate the trajectory of a gat bullet but gets confused by negative numbers.", + "No, they interprets sea maps as coordinate grids but thinks fractions are bad luck.", + "Secretly, they can solve equations under threat of cannon fire but thinks fractions are bad luck.", + "On paper, they uses slide rules to navigate stormy seas and has a certificate from the Stowaway Academy.", + "According to the abacus, they interprets sea maps as coordinate grids but only in base-3.", + "In theory, they interprets sea maps as coordinate grids but gets distracted by shiny brass coins.", + "Thinks they are, they knows how to divide cheese by zero but thinks fractions are bad luck.", + "Secretly, they can balance the captain's ledgers in their head but only when cheese is involved.", + "Only, they interprets sea maps as coordinate grids and has the abacus tattoos to prove it.", + "According to the abacus, they can balance the captain's ledgers in their head but gets distracted by shiny brass coins.", + "Only, they uses slide rules to navigate stormy seas but refuses to use prime numbers.", + "No, they remembers the first 100 digits of Pi but only when drunk on grog.", + "On paper, they can calculate the trajectory of a gat bullet but gets confused by negative numbers.", + "In theory, they uses slide rules to navigate stormy seas but thinks fractions are bad luck.", + "Thinks they are, they understands the geometry of boarding leaps but refuses to use prime numbers.", + "Absolutely, they can count abacus beads in pitch black hold but gets confused by negative numbers.", + "Yes, they can solve equations under threat of cannon fire but only when drunk on grog.", + "Only, they understands the geometry of boarding leaps and has the abacus tattoos to prove it.", + "Thinks they are, they can balance the captain's ledgers in their head but refuses to use prime numbers.", + "Secretly, they can solve equations under threat of cannon fire but gets confused by negative numbers.", + "No, they can calculate the trajectory of a gat bullet and has the abacus tattoos to prove it.", + "Yes, specializes in escape vector geometry.", + "Secretly, they knows how to divide cheese by zero but only when drunk on grog.", + "No, they understands the geometry of boarding leaps and has a certificate from the Stowaway Academy.", + "Absolutely, they understands the probability of drawing a Joker and has the abacus tattoos to prove it.", + "Absolutely, they interprets sea maps as coordinate grids but only in base-3.", + "Yes, they can calculate the trajectory of a gat bullet but only when cheese is involved.", + "In theory, they can balance the captain's ledgers in their head but gets confused by negative numbers.", + "No, they uses slide rules to navigate stormy seas but refuses to use prime numbers.", + "According to the abacus, they can solve equations under threat of cannon fire but gets distracted by shiny brass coins.", + "Absolutely, they remembers the first 100 digits of Pi but gets confused by negative numbers.", + "Thinks they are, they uses slide rules to navigate stormy seas but only when drunk on grog.", + "Yes, they can count abacus beads in pitch black hold but only in base-3.", + "Secretly, they can calculate the trajectory of a gat bullet but only when drunk on grog.", + "On paper, they can calculate the trajectory of a gat bullet but only when drunk on grog.", + "Only, they can solve equations under threat of cannon fire but gets confused by negative numbers.", + "Yes, they can count abacus beads in pitch black hold and has the abacus tattoos to prove it.", + "In theory, they can balance the captain's ledgers in their head but only when cheese is involved.", + "Barely, they interprets sea maps as coordinate grids but thinks fractions are bad luck.", + "According to the abacus, they interprets sea maps as coordinate grids and claims numbers speak to them in dreams.", + "Thinks they are, they can balance the captain's ledgers in their head but thinks fractions are bad luck.", + "Absolutely, they knows how to divide cheese by zero but refuses to use prime numbers.", + "Secretly, they understands the geometry of boarding leaps but only when drunk on grog.", + "In theory, they can calculate the trajectory of a gat bullet but only in base-3.", + "Barely, they knows how to divide cheese by zero and has a certificate from the Stowaway Academy.", + "Absolutely, they remembers the first 100 digits of Pi but gets distracted by shiny brass coins.", + "Thinks they are, they understands the geometry of boarding leaps but thinks fractions are bad luck.", + "On paper, they remembers the first 100 digits of Pi but gets confused by negative numbers.", + "On paper, they understands the probability of drawing a Joker but gets distracted by shiny brass coins.", + "Secretly, they can solve equations under threat of cannon fire but only when drunk on grog.", + "In theory, they interprets sea maps as coordinate grids and has the abacus tattoos to prove it.", + "In theory, they knows how to divide cheese by zero and has the abacus tattoos to prove it.", + "Absolutely, they knows how to divide cheese by zero but only in base-3.", + "Only, they remembers the first 100 digits of Pi but only when cheese is involved.", + "Only, they understands the probability of drawing a Joker but only when cheese is involved.", + "Barely, they can balance the captain's ledgers in their head but gets distracted by shiny brass coins.", + "No, they understands the probability of drawing a Joker and has a certificate from the Stowaway Academy.", + "In theory, they remembers the first 100 digits of Pi but only when cheese is involved.", + "Yes, they can count abacus beads in pitch black hold but only when drunk on grog.", + "In theory, they can calculate the trajectory of a gat bullet and claims numbers speak to them in dreams.", + "Only, they understands the probability of drawing a Joker and has a certificate from the Stowaway Academy.", + "Secretly, they understands the geometry of boarding leaps and has a certificate from the Stowaway Academy.", + "Secretly, they remembers the first 100 digits of Pi but only when cheese is involved.", + "Absolutely, they understands the geometry of boarding leaps and has a certificate from the Stowaway Academy.", + "Thinks they are, they understands the probability of drawing a Joker and claims numbers speak to them in dreams.", + "On paper, they understands the probability of drawing a Joker but only when cheese is involved.", + "Only, they understands the geometry of boarding leaps and has a certificate from the Stowaway Academy.", + "Thinks they are, they remembers the first 100 digits of Pi but thinks fractions are bad luck.", + "No, they understands the probability of drawing a Joker but thinks fractions are bad luck.", + "On paper, they can calculate the trajectory of a gat bullet but thinks fractions are bad luck.", + "In theory, they can calculate the trajectory of a gat bullet but only when drunk on grog.", + "Yes, but only in base-8 because they lost two claws to a cat.", + "No, they uses slide rules to navigate stormy seas but gets distracted by shiny brass coins.", + "Only, they uses slide rules to navigate stormy seas and has a certificate from the Stowaway Academy.", + "Thinks they are, they can count abacus beads in pitch black hold but gets confused by negative numbers.", + "Absolutely, they understands the probability of drawing a Joker but gets confused by negative numbers.", + "Secretly, they can calculate the trajectory of a gat bullet but only in base-3.", + "In theory, they interprets sea maps as coordinate grids but only in base-3.", + "Absolutely, they understands the probability of drawing a Joker but refuses to use prime numbers.", + "Secretly, they remembers the first 100 digits of Pi and claims numbers speak to them in dreams.", + "Thinks they are, they can count abacus beads in pitch black hold and has a certificate from the Stowaway Academy.", + "Secretly, they uses slide rules to navigate stormy seas and has the abacus tattoos to prove it.", + "Absolutely, they interprets sea maps as coordinate grids and has the abacus tattoos to prove it.", + "Only, they understands the geometry of boarding leaps but gets distracted by shiny brass coins.", + "Secretly, they uses slide rules to navigate stormy seas but only in base-3.", + "According to the abacus, they knows how to divide cheese by zero but gets confused by negative numbers.", + "According to the abacus, they interprets sea maps as coordinate grids and has the abacus tattoos to prove it.", + "No, they understands the probability of drawing a Joker and claims numbers speak to them in dreams.", + "No, they interprets sea maps as coordinate grids and has the abacus tattoos to prove it.", + "According to the abacus, they understands the probability of drawing a Joker but only in base-3.", + "Thinks they are, they can calculate the trajectory of a gat bullet but gets confused by negative numbers.", + "Absolutely, they remembers the first 100 digits of Pi and claims numbers speak to them in dreams.", + "Secretly, they interprets sea maps as coordinate grids and has a certificate from the Stowaway Academy.", + "Absolutely, they can solve equations under threat of cannon fire and has a certificate from the Stowaway Academy.", + "Yes, they understands the probability of drawing a Joker but thinks fractions are bad luck.", + "Absolutely, they can count abacus beads in pitch black hold and has the abacus tattoos to prove it.", + "Absolutely, they uses slide rules to navigate stormy seas and has the abacus tattoos to prove it.", + "According to the abacus, they understands the geometry of boarding leaps and claims numbers speak to them in dreams.", + "Only, they can solve equations under threat of cannon fire and has a certificate from the Stowaway Academy.", + "Absolutely, they understands the geometry of boarding leaps but gets confused by negative numbers.", + "Absolutely, they understands the geometry of boarding leaps but gets distracted by shiny brass coins.", + "Absolutely, they interprets sea maps as coordinate grids but only when cheese is involved.", + "No, they can balance the captain's ledgers in their head but gets confused by negative numbers.", + "Absolutely, they can solve equations under threat of cannon fire but refuses to use prime numbers.", + "No, they can solve equations under threat of cannon fire but thinks fractions are bad luck.", + "Barely, they uses slide rules to navigate stormy seas but only in base-3.", + "In theory, they knows how to divide cheese by zero but only in base-3.", + "Thinks they are, they can solve equations under threat of cannon fire but only when drunk on grog.", + "Absolutely, they uses slide rules to navigate stormy seas but only when drunk on grog.", + "Only, they understands the probability of drawing a Joker but only when drunk on grog.", + "They can compute a Fibonacci sequence in under three seconds.", + "Secretly, they knows how to divide cheese by zero and claims numbers speak to them in dreams.", + "Thinks they are, they can count abacus beads in pitch black hold but gets distracted by shiny brass coins.", + "Secretly, they interprets sea maps as coordinate grids and claims numbers speak to them in dreams.", + "Absolutely, they can solve equations under threat of cannon fire but only when drunk on grog.", + "Yes, they can balance the captain's ledgers in their head but gets distracted by shiny brass coins.", + "Only, they knows how to divide cheese by zero and has the abacus tattoos to prove it.", + "Thinks they are, they can calculate the trajectory of a gat bullet but only when cheese is involved.", + "According to the abacus, they can calculate the trajectory of a gat bullet and has the abacus tattoos to prove it.", + "In theory, they remembers the first 100 digits of Pi but gets distracted by shiny brass coins.", + "According to the abacus, they can balance the captain's ledgers in their head but thinks fractions are bad luck.", + "No, they understands the probability of drawing a Joker but only in base-3.", + "In theory, they uses slide rules to navigate stormy seas but gets confused by negative numbers.", + "Thinks they are, they uses slide rules to navigate stormy seas but refuses to use prime numbers.", + "Secretly, they interprets sea maps as coordinate grids but only when drunk on grog.", + "Yes, they interprets sea maps as coordinate grids but gets confused by negative numbers.", + "No, they understands the geometry of boarding leaps but only in base-3.", + "Secretly, they remembers the first 100 digits of Pi but thinks fractions are bad luck.", + "Absolutely, they can count abacus beads in pitch black hold but refuses to use prime numbers.", + "In theory, they uses slide rules to navigate stormy seas and has the abacus tattoos to prove it.", + "Yes, they can count abacus beads in pitch black hold but gets confused by negative numbers.", + "Yes, they can solve equations under threat of cannon fire but only in base-3.", + "Absolutely, they can balance the captain's ledgers in their head and has a certificate from the Stowaway Academy.", + "Yes, they can balance the captain's ledgers in their head and claims numbers speak to them in dreams.", + "Absolutely, they can calculate the trajectory of a gat bullet and claims numbers speak to them in dreams.", + "On paper, they knows how to divide cheese by zero but thinks fractions are bad luck.", + "Thinks they are, they understands the probability of drawing a Joker but only when cheese is involved.", + "Thinks they are, they uses slide rules to navigate stormy seas but only in base-3.", + "Barely, they understands the probability of drawing a Joker but gets distracted by shiny brass coins.", + "Only, they understands the geometry of boarding leaps but gets confused by negative numbers.", + "On paper, they knows how to divide cheese by zero but only when drunk on grog.", + "On paper, they can count abacus beads in pitch black hold but gets confused by negative numbers.", + "No, they understands the probability of drawing a Joker but only when cheese is involved.", + "Barely, they knows how to divide cheese by zero but refuses to use prime numbers.", + "On paper, they understands the geometry of boarding leaps but thinks fractions are bad luck.", + "In theory, they can balance the captain's ledgers in their head but gets distracted by shiny brass coins.", + "Yes, they remembers the first 100 digits of Pi but only when cheese is involved.", + "Only, they knows how to divide cheese by zero but only when cheese is involved.", + "Only, they can balance the captain's ledgers in their head but only when cheese is involved.", + "Absolutely, they can count abacus beads in pitch black hold and has a certificate from the Stowaway Academy.", + "Barely, they can count abacus beads in pitch black hold but gets confused by negative numbers.", + "On paper, they can count abacus beads in pitch black hold and claims numbers speak to them in dreams.", + "Absolutely, they understands the geometry of boarding leaps and claims numbers speak to them in dreams.", + "Barely, they knows how to divide cheese by zero but only when cheese is involved.", + "Thinks they are, they can solve equations under threat of cannon fire but thinks fractions are bad luck.", + "In theory, they interprets sea maps as coordinate grids but refuses to use prime numbers.", + "Yes, they understands the probability of drawing a Joker but gets distracted by shiny brass coins.", + "In theory, they knows how to divide cheese by zero and claims numbers speak to them in dreams.", + "Yes, they interprets sea maps as coordinate grids and claims numbers speak to them in dreams.", + "Only, they remembers the first 100 digits of Pi and claims numbers speak to them in dreams.", + "Only, they uses slide rules to navigate stormy seas and has the abacus tattoos to prove it.", + "In theory, they can balance the captain's ledgers in their head but only in base-3.", + "On paper, they remembers the first 100 digits of Pi and has a certificate from the Stowaway Academy.", + "According to the abacus, they understands the geometry of boarding leaps but gets distracted by shiny brass coins.", + "On paper, they can solve equations under threat of cannon fire and has a certificate from the Stowaway Academy.", + "In theory, they understands the probability of drawing a Joker but refuses to use prime numbers.", + "Absolutely, they can balance the captain's ledgers in their head and has the abacus tattoos to prove it.", + "On paper, they can balance the captain's ledgers in their head but only when drunk on grog.", + "No, they can count abacus beads in pitch black hold but only when drunk on grog.", + "Barely, they uses slide rules to navigate stormy seas but only when drunk on grog.", + "No, they can balance the captain's ledgers in their head but thinks fractions are bad luck.", + "According to the abacus, they remembers the first 100 digits of Pi but refuses to use prime numbers.", + "Barely, they understands the probability of drawing a Joker but only when drunk on grog.", + "Yes, they can balance the captain's ledgers in their head but thinks fractions are bad luck.", + "Only, they can solve equations under threat of cannon fire but only when cheese is involved.", + "Absolutely, they interprets sea maps as coordinate grids and claims numbers speak to them in dreams.", + "On paper, they can calculate the trajectory of a gat bullet but gets distracted by shiny brass coins.", + "Barely, they can count abacus beads in pitch black hold and has the abacus tattoos to prove it.", + "Thinks they are, they interprets sea maps as coordinate grids but only when cheese is involved.", + "According to the abacus, they remembers the first 100 digits of Pi and claims numbers speak to them in dreams." + ], + "like": [ + "They secretly saves the gunpowder barrels during storm navigation.", + "They always shares the captain's stash with mathematical precision.", + "They faithfully guards the escape vectors when the seagulls attack.", + "They always shares the backup abacus even under heavy fire.", + "They cheerfully carries the coordinate charts when the seagulls attack.", + "They always shares the compass dial because they are a true friend.", + "They never steals the backup abacus to keep the crew happy.", + "They cleanly polishes the captain's stash because they are a true friend.", + "They secretly saves the gunpowder barrels just to be nice.", + "They cleanly polishes the gunpowder barrels during storm navigation.", + "They faithfully guards the compass dial during storm navigation.", + "They keep the gats polished and oiled.", + "They cleanly polishes the gunpowder barrels with mathematical precision.", + "They secretly saves the spare gats without asking for a share.", + "They secretly saves the gunpowder barrels to keep the crew happy.", + "They expertly calculates the captain's stash to keep the crew happy.", + "They secretly saves the escape vectors even under heavy fire.", + "They secretly saves the golden abacus beads to ensure a perfect escape.", + "They cheerfully carries the cheese wedges without asking for a share.", + "They never steals the cheese wedges with mathematical precision.", + "They quietly hums shanties for the captain's stash to ensure a perfect escape.", + "They quietly hums shanties for the captain's stash without asking for a share.", + "They always shares the compass dial to ensure a perfect escape.", + "They always shares the coordinate charts without asking for a share.", + "They never steals the captain's stash in base-10 code.", + "They expertly calculates the golden abacus beads without asking for a share.", + "They quietly hums shanties for the coordinate charts just to be nice.", + "They expertly calculates the cheese wedges when the seagulls attack.", + "They cheerfully carries the wet hold biscuits even under heavy fire.", + "They quietly hums shanties for the coordinate charts without asking for a share.", + "They boldly defends the gunpowder barrels without asking for a share.", + "They faithfully guards the coordinate charts in base-10 code.", + "They always shares the cheese wedges to keep the crew happy.", + "They always shares the cheese wedges with mathematical precision.", + "They never steals the gunpowder barrels without asking for a share.", + "They never steals the wet hold biscuits without asking for a share.", + "They expertly calculates the backup abacus in base-10 code.", + "They brilliantly solves the coordinate charts to ensure a perfect escape.", + "They expertly calculates the wet hold biscuits to ensure a perfect escape.", + "They cleanly polishes the escape vectors in base-10 code.", + "They secretly saves the gunpowder barrels to ensure a perfect escape.", + "They brilliantly solves the escape vectors even under heavy fire.", + "They cheerfully carries the gunpowder barrels because they are a true friend.", + "They boldly defends the gunpowder barrels because they are a true friend.", + "They boldly defends the compass dial in base-10 code.", + "They secretly saves the cheese wedges when the seagulls attack.", + "They quietly hums shanties for the compass dial with mathematical precision.", + "They cheerfully carries the gunpowder barrels without asking for a share.", + "They faithfully guards the wet hold biscuits with mathematical precision.", + "They quietly hums shanties for the compass dial when the seagulls attack.", + "They expertly calculates the coordinate charts when the seagulls attack.", + "They cleanly polishes the wet hold biscuits when the seagulls attack.", + "They boldly defends the compass dial to keep the crew happy.", + "They quietly hums shanties for the spare gats just to be nice.", + "They faithfully guards the cheese wedges even under heavy fire.", + "They cheerfully carries the spare gats because they are a true friend.", + "They faithfully guards the coordinate charts even under heavy fire.", + "They faithfully guards the compass dial with mathematical precision.", + "They cleanly polishes the wet hold biscuits with mathematical precision.", + "They quietly hums shanties for the captain's stash in base-10 code.", + "They never steals the coordinate charts with mathematical precision.", + "They cleanly polishes the gunpowder barrels even under heavy fire.", + "They cleanly polishes the spare gats even under heavy fire.", + "They boldly defends the escape vectors with mathematical precision.", + "They secretly saves the backup abacus when the seagulls attack.", + "They faithfully guards the coordinate charts when the seagulls attack.", + "They always shares the gunpowder barrels in base-10 code.", + "They never steals the captain's stash even under heavy fire.", + "They brilliantly solves the wet hold biscuits to ensure a perfect escape.", + "They faithfully guards the backup abacus because they are a true friend.", + "They expertly calculates the compass dial in base-10 code.", + "They secretly saves the backup abacus to ensure a perfect escape.", + "They expertly calculates the compass dial when the seagulls attack.", + "They brilliantly solves the backup abacus in base-10 code.", + "They expertly calculates the coordinate charts because they are a true friend.", + "They cleanly polishes the gunpowder barrels without asking for a share.", + "They expertly calculates the coordinate charts in base-10 code.", + "They expertly calculates the cheese wedges even under heavy fire.", + "They boldly defends the compass dial when the seagulls attack.", + "They cheerfully carries the wet hold biscuits during storm navigation.", + "They always shares the escape vectors because they are a true friend.", + "They cheerfully carries the escape vectors even under heavy fire.", + "They faithfully guards the captain's stash in base-10 code.", + "They boldly defends the compass dial because they are a true friend.", + "They faithfully guards the golden abacus beads even under heavy fire.", + "They cleanly polishes the golden abacus beads because they are a true friend.", + "They secretly saves the cheese wedges with mathematical precision.", + "They always shares the wet hold biscuits to keep the crew happy.", + "They quietly hums shanties for the spare gats without asking for a share.", + "They never steals the captain's stash to ensure a perfect escape.", + "They faithfully guards the gunpowder barrels because they are a true friend.", + "They quietly hums shanties for the wet hold biscuits with mathematical precision.", + "They never steals the cheese wedges when the seagulls attack.", + "They faithfully guards the compass dial to ensure a perfect escape.", + "They quietly hums shanties for the spare gats in base-10 code.", + "They never steals the compass dial even under heavy fire.", + "They faithfully guards the cheese wedges just to be nice.", + "They cleanly polishes the captain's stash when the seagulls attack.", + "They cleanly polishes the golden abacus beads just to be nice.", + "They expertly calculates the escape vectors even under heavy fire.", + "They never steals the compass dial when the seagulls attack.", + "They quietly hums shanties for the golden abacus beads when the seagulls attack.", + "They cheerfully carries the coordinate charts in base-10 code.", + "They faithfully guards the cheese wedges when the seagulls attack.", + "They secretly saves the wet hold biscuits without asking for a share.", + "They faithfully guards the compass dial because they are a true friend.", + "They always shares the backup abacus because they are a true friend.", + "They cleanly polishes the wet hold biscuits in base-10 code.", + "They boldly defends the backup abacus to ensure a perfect escape.", + "They cheerfully carries the spare gats even under heavy fire.", + "They brilliantly solves the coordinate charts with mathematical precision.", + "They secretly saves the compass dial because they are a true friend.", + "They faithfully guards the coordinate charts because they are a true friend.", + "They expertly calculates the backup abacus without asking for a share.", + "They never steals the coordinate charts during storm navigation.", + "They secretly saves the captain's stash when the seagulls attack.", + "They brilliantly solves the backup abacus just to be nice.", + "They boldly defends the gunpowder barrels to keep the crew happy.", + "They cleanly polishes the compass dial to ensure a perfect escape.", + "They expertly calculates the golden abacus beads with mathematical precision.", + "They secretly saves the coordinate charts because they are a true friend.", + "They never steals the gunpowder barrels because they are a true friend.", + "They secretly saves the gunpowder barrels when the seagulls attack.", + "They always shares the golden abacus beads in base-10 code.", + "They secretly saves the gunpowder barrels because they are a true friend.", + "They faithfully guards the coordinate charts with mathematical precision.", + "They expertly calculates the captain's stash just to be nice.", + "They cleanly polishes the gunpowder barrels because they are a true friend.", + "They quietly hums shanties for the compass dial even under heavy fire.", + "They secretly saves the captain's stash in base-10 code.", + "They brilliantly solves the wet hold biscuits just to be nice.", + "They cleanly polishes the wet hold biscuits because they are a true friend.", + "They brilliantly solves the spare gats when the seagulls attack.", + "They always shares the escape vectors with mathematical precision.", + "They never steals the backup abacus in base-10 code.", + "They cheerfully carries the escape vectors because they are a true friend.", + "They cheerfully carries the gunpowder barrels in base-10 code.", + "They always shares the escape vectors without asking for a share.", + "They cleanly polishes the golden abacus beads without asking for a share.", + "They expertly calculates the coordinate charts without asking for a share.", + "They faithfully guards the golden abacus beads to ensure a perfect escape.", + "They brilliantly solves the golden abacus beads with mathematical precision.", + "They brilliantly solves the backup abacus without asking for a share.", + "They cheerfully carries the captain's stash when the seagulls attack.", + "They expertly calculates the escape vectors during storm navigation.", + "They never steals the gunpowder barrels when the seagulls attack.", + "They expertly calculates the captain's stash even under heavy fire.", + "They brilliantly solves the golden abacus beads without asking for a share.", + "They boldly defends the compass dial even under heavy fire.", + "They tell the best stories about the Great Cheese Reef.", + "They never steals the escape vectors just to be nice.", + "They faithfully guards the spare gats because they are a true friend.", + "They quietly hums shanties for the cheese wedges when the seagulls attack.", + "They expertly calculates the escape vectors because they are a true friend.", + "They always shares the backup abacus when the seagulls attack.", + "They quietly hums shanties for the cheese wedges to keep the crew happy.", + "They brilliantly solves the cheese wedges to ensure a perfect escape.", + "They never steals the wet hold biscuits when the seagulls attack.", + "They brilliantly solves the golden abacus beads even under heavy fire.", + "They quietly hums shanties for the captain's stash to keep the crew happy.", + "They quietly hums shanties for the gunpowder barrels to ensure a perfect escape.", + "They boldly defends the escape vectors to keep the crew happy.", + "They always shares the spare gats to keep the crew happy.", + "They quietly hums shanties for the gunpowder barrels even under heavy fire.", + "They brilliantly solves the compass dial even under heavy fire.", + "They cleanly polishes the cheese wedges in base-10 code.", + "They brilliantly solves the captain's stash just to be nice.", + "They quietly hums shanties for the cheese wedges because they are a true friend.", + "They brilliantly solves the compass dial just to be nice.", + "They cleanly polishes the backup abacus to keep the crew happy.", + "They never steals the cheese wedges to keep the crew happy.", + "They secretly saves the cheese wedges in base-10 code.", + "They boldly defends the wet hold biscuits because they are a true friend.", + "They always shares the escape vectors during storm navigation.", + "They quietly hums shanties for the golden abacus beads to keep the crew happy.", + "They brilliantly solves the wet hold biscuits in base-10 code.", + "They cleanly polishes the spare gats to keep the crew happy.", + "They cheerfully carries the captain's stash with mathematical precision.", + "They boldly defends the backup abacus just to be nice.", + "They expertly calculates the coordinate charts even under heavy fire.", + "They quietly hums shanties for the backup abacus in base-10 code.", + "They faithfully guards the gunpowder barrels during storm navigation.", + "They always shares the backup abacus to ensure a perfect escape.", + "They faithfully guards the spare gats just to be nice.", + "They cleanly polishes the cheese wedges because they are a true friend.", + "They always shares the gunpowder barrels to keep the crew happy.", + "They always shares the backup abacus just to be nice.", + "They brilliantly solves the backup abacus when the seagulls attack.", + "They expertly calculates the wet hold biscuits to keep the crew happy.", + "They faithfully guards the escape vectors to keep the crew happy.", + "They expertly calculates the coordinate charts during storm navigation.", + "They secretly saves the coordinate charts in base-10 code.", + "They cheerfully carries the cheese wedges just to be nice.", + "They expertly calculates the escape vectors to keep the crew happy.", + "They always shares the golden abacus beads during storm navigation.", + "They faithfully guards the captain's stash without asking for a share.", + "They cheerfully carries the backup abacus in base-10 code.", + "They cheerfully carries the compass dial during storm navigation.", + "They expertly calculates the wet hold biscuits just to be nice.", + "They secretly saves the compass dial when the seagulls attack." + ], + "hate": [ + "They annoyingly chews the coordinate maps but only in base-3.", + "They secretly hoards the gat barrel oil but gets confused by negative numbers.", + "They loudly whistles the abacus beads and has a certificate from the Stowaway Academy.", + "They clumsily breaks the geometry homework and has the abacus tattoos to prove it.", + "They frequently drops the bilge keys but only when drunk on grog.", + "They always claims the navigator's compass but only when drunk on grog.", + "They carelessly stains the abacus beads but only when drunk on grog.", + "They carelessly stains the hammock ropes and has the abacus tattoos to prove it.", + "They constantly complains about the navigator's compass but refuses to use prime numbers.", + "They stubbornly argues about the slide rule but only in base-3.", + "They annoyingly chews the abacus beads but only in base-3.", + "They secretly hoards the dry gunpowder bags and has the abacus tattoos to prove it.", + "They always claims the geometry homework but only when drunk on grog.", + "They accidently misplaces the navigator's compass but thinks fractions are bad luck.", + "They carelessly stains the gat barrel oil but gets distracted by shiny brass coins.", + "They annoyingly chews the coordinate maps but gets confused by negative numbers.", + "They carelessly stains the slide rule but refuses to use prime numbers.", + "They stubbornly argues about the coordinate maps and claims numbers speak to them in dreams.", + "They stubbornly argues about the dry gunpowder bags but gets distracted by shiny brass coins.", + "They loudly whistles the gat barrel oil but only when cheese is involved.", + "They accidently misplaces the navigator's compass and has the abacus tattoos to prove it.", + "They frequently drops the geometry homework and claims numbers speak to them in dreams.", + "They carelessly stains the coordinate maps but only in base-3.", + "They accidently misplaces the hammock ropes and has a certificate from the Stowaway Academy.", + "They accidently misplaces the gat barrel oil and claims numbers speak to them in dreams.", + "They stubbornly argues about the dry gunpowder bags and has a certificate from the Stowaway Academy.", + "They clumsily breaks the slide rule and has a certificate from the Stowaway Academy.", + "They chew their tail when nervous.", + "They annoyingly chews the hammock ropes but refuses to use prime numbers.", + "They annoyingly chews the hammock ropes but thinks fractions are bad luck.", + "They loudly whistles the gat barrel oil and has the abacus tattoos to prove it.", + "They frequently drops the hammock ropes but gets distracted by shiny brass coins.", + "They annoyingly chews the hammock ropes but gets confused by negative numbers.", + "They accidently misplaces the cheese wedges but gets distracted by shiny brass coins.", + "They always claims the hammock ropes but gets confused by negative numbers.", + "They secretly hoards the coordinate maps and claims numbers speak to them in dreams.", + "They clumsily breaks the slide rule and claims numbers speak to them in dreams.", + "They annoyingly chews the gat barrel oil but only in base-3.", + "They always claims the slide rule but only in base-3.", + "They stubbornly argues about the bilge keys but only when drunk on grog.", + "They loudly whistles the cheese wedges but refuses to use prime numbers.", + "They accidently misplaces the slide rule but thinks fractions are bad luck.", + "They accidently misplaces the abacus beads and claims numbers speak to them in dreams.", + "They loudly whistles the hammock ropes but only in base-3.", + "They secretly hoards the navigator's compass but only in base-3.", + "They accidently misplaces the navigator's compass but gets distracted by shiny brass coins.", + "They clumsily breaks the hammock ropes but gets distracted by shiny brass coins.", + "They always claims the gat barrel oil and has the abacus tattoos to prove it.", + "They always claims the navigator's compass and claims numbers speak to them in dreams.", + "They always claims the hammock ropes and claims numbers speak to them in dreams.", + "They clumsily breaks the navigator's compass but only when drunk on grog.", + "They loudly whistles the navigator's compass and has the abacus tattoos to prove it.", + "They stubbornly argues about the navigator's compass but refuses to use prime numbers.", + "They always claims the cheese wedges but thinks fractions are bad luck.", + "They frequently drops the bilge keys but gets confused by negative numbers.", + "They talk about imaginary numbers in their sleep.", + "They secretly hoards the dry gunpowder bags but only when cheese is involved.", + "They stubbornly argues about the coordinate maps but gets confused by negative numbers.", + "They secretly hoards the navigator's compass but only when drunk on grog.", + "They accidently misplaces the coordinate maps and has a certificate from the Stowaway Academy.", + "They clumsily breaks the abacus beads but thinks fractions are bad luck.", + "They accidently misplaces the coordinate maps but only in base-3.", + "They annoyingly chews the coordinate maps but only when drunk on grog.", + "They accidently misplaces the coordinate maps but thinks fractions are bad luck.", + "They clumsily breaks the coordinate maps but gets confused by negative numbers.", + "They clumsily breaks the geometry homework but gets distracted by shiny brass coins.", + "They frequently drops the abacus beads but gets distracted by shiny brass coins.", + "They secretly hoards the gat barrel oil but only when cheese is involved.", + "They stubbornly argues about the bilge keys and claims numbers speak to them in dreams.", + "They clumsily breaks the geometry homework and claims numbers speak to them in dreams.", + "They carelessly stains the hammock ropes but gets confused by negative numbers.", + "They frequently drops the dry gunpowder bags but thinks fractions are bad luck.", + "They always claims the geometry homework but refuses to use prime numbers.", + "They always claims the cheese wedges but only in base-3.", + "They always claims the geometry homework and has a certificate from the Stowaway Academy.", + "They annoyingly chews the dry gunpowder bags but refuses to use prime numbers.", + "They loudly whistles the cheese wedges and has a certificate from the Stowaway Academy.", + "They clumsily breaks the dry gunpowder bags but only when cheese is involved.", + "They stubbornly argues about the gat barrel oil but only when drunk on grog.", + "They annoyingly chews the navigator's compass but only in base-3.", + "They annoyingly chews the hammock ropes but gets distracted by shiny brass coins.", + "They carelessly stains the gat barrel oil and claims numbers speak to them in dreams.", + "They secretly hoards the geometry homework and has a certificate from the Stowaway Academy.", + "They secretly hoards the hammock ropes and has the abacus tattoos to prove it.", + "They frequently drops the gat barrel oil and claims numbers speak to them in dreams.", + "They accidently misplaces the hammock ropes and claims numbers speak to them in dreams.", + "They frequently drops the coordinate maps but gets distracted by shiny brass coins.", + "They carelessly stains the gat barrel oil but only in base-3.", + "They always claims the cheese wedges but only when cheese is involved.", + "They loudly whistles the gat barrel oil but thinks fractions are bad luck.", + "They frequently drops the dry gunpowder bags but gets confused by negative numbers.", + "They clumsily breaks the dry gunpowder bags but gets distracted by shiny brass coins.", + "They always claims the bilge keys and has the abacus tattoos to prove it.", + "They constantly complains about the geometry homework but only when drunk on grog.", + "They carelessly stains the slide rule and has the abacus tattoos to prove it.", + "They constantly complains about the slide rule but refuses to use prime numbers.", + "They chew on the gunpowder bags.", + "They constantly complains about the navigator's compass but only in base-3.", + "They annoyingly chews the geometry homework but only when drunk on grog.", + "They always claims the bilge keys but refuses to use prime numbers.", + "They stubbornly argues about the slide rule but gets distracted by shiny brass coins.", + "They carelessly stains the hammock ropes but only when drunk on grog.", + "They accidently misplaces the navigator's compass but only when drunk on grog.", + "They accidently misplaces the coordinate maps but refuses to use prime numbers.", + "They stubbornly argues about the gat barrel oil but thinks fractions are bad luck.", + "They carelessly stains the cheese wedges but thinks fractions are bad luck.", + "They loudly whistles the navigator's compass but gets distracted by shiny brass coins.", + "They constantly complains about the dry gunpowder bags but refuses to use prime numbers.", + "They annoyingly chews the hammock ropes but only when drunk on grog.", + "They constantly complains about the geometry homework but refuses to use prime numbers.", + "They stubbornly argues about the navigator's compass but gets distracted by shiny brass coins.", + "They stubbornly argues about the gat barrel oil but refuses to use prime numbers.", + "They frequently drops the slide rule and has the abacus tattoos to prove it.", + "They stubbornly argues about the navigator's compass but only when cheese is involved.", + "They annoyingly chews the cheese wedges but only when cheese is involved.", + "They stubbornly argues about the geometry homework but refuses to use prime numbers.", + "They stubbornly argues about the dry gunpowder bags but gets confused by negative numbers.", + "They constantly complains about the hammock ropes and claims numbers speak to them in dreams.", + "They clumsily breaks the dry gunpowder bags but gets confused by negative numbers.", + "They frequently drops the geometry homework but only when cheese is involved.", + "They accidently misplaces the slide rule but only when drunk on grog.", + "They clumsily breaks the bilge keys but only when drunk on grog.", + "They annoyingly chews the cheese wedges and has the abacus tattoos to prove it.", + "They accidently misplaces the bilge keys but only when cheese is involved.", + "They loudly whistles the dry gunpowder bags and has the abacus tattoos to prove it.", + "They loudly whistles the slide rule but refuses to use prime numbers.", + "They frequently drops the navigator's compass but gets distracted by shiny brass coins.", + "They secretly hoards the geometry homework but gets confused by negative numbers.", + "They carelessly stains the abacus beads but thinks fractions are bad luck.", + "They stubbornly argues about the hammock ropes but only when cheese is involved.", + "They accidently misplaces the abacus beads but gets distracted by shiny brass coins.", + "They always claims the bilge keys but thinks fractions are bad luck.", + "They always claims the gat barrel oil but only when drunk on grog.", + "They loudly whistles the slide rule but only when cheese is involved.", + "They annoyingly chews the slide rule and has a certificate from the Stowaway Academy.", + "They carelessly stains the slide rule but only when drunk on grog.", + "They constantly complains about the cheese wedges but only when drunk on grog.", + "They clumsily breaks the gat barrel oil but gets distracted by shiny brass coins.", + "They annoyingly chews the navigator's compass and claims numbers speak to them in dreams.", + "They stubbornly argues about the geometry homework but only when cheese is involved.", + "They clumsily breaks the dry gunpowder bags and has the abacus tattoos to prove it.", + "They carelessly stains the coordinate maps but thinks fractions are bad luck.", + "They annoyingly chews the slide rule and has the abacus tattoos to prove it.", + "They stubbornly argues about the slide rule and claims numbers speak to them in dreams.", + "They frequently drops the navigator's compass and has the abacus tattoos to prove it.", + "They clumsily breaks the slide rule but gets distracted by shiny brass coins.", + "They clumsily breaks the cheese wedges and has a certificate from the Stowaway Academy.", + "They carelessly stains the bilge keys but thinks fractions are bad luck.", + "They stubbornly argues about the cheese wedges but refuses to use prime numbers.", + "They annoyingly chews the slide rule but refuses to use prime numbers.", + "They stubbornly argues about the coordinate maps and has a certificate from the Stowaway Academy.", + "They constantly complains about the hammock ropes but refuses to use prime numbers.", + "They accidently misplaces the hammock ropes but only in base-3.", + "They stubbornly argues about the abacus beads but only when cheese is involved.", + "They always claims the bilge keys but gets distracted by shiny brass coins.", + "They constantly complains about the abacus beads but gets distracted by shiny brass coins.", + "They accidently misplaces the navigator's compass but only when cheese is involved.", + "They always claims the coordinate maps and has the abacus tattoos to prove it.", + "They annoyingly chews the navigator's compass but only when drunk on grog.", + "They annoyingly chews the bilge keys but gets distracted by shiny brass coins.", + "They stubbornly argues about the navigator's compass but gets confused by negative numbers.", + "They annoyingly chews the gat barrel oil but refuses to use prime numbers.", + "They stubbornly argues about the hammock ropes but gets distracted by shiny brass coins.", + "They secretly hoards the abacus beads but gets confused by negative numbers.", + "They carelessly stains the bilge keys but only when drunk on grog.", + "They always claims the dry gunpowder bags but refuses to use prime numbers.", + "They always claims the slide rule but refuses to use prime numbers.", + "They carelessly stains the dry gunpowder bags but gets distracted by shiny brass coins.", + "They carelessly stains the navigator's compass but refuses to use prime numbers.", + "They always claims the geometry homework but gets confused by negative numbers.", + "They frequently drops the cheese wedges but only when cheese is involved.", + "They always claims the abacus beads but gets distracted by shiny brass coins.", + "They stubbornly argues about the dry gunpowder bags but only when drunk on grog.", + "They loudly whistles the slide rule but gets confused by negative numbers.", + "They carelessly stains the bilge keys and has the abacus tattoos to prove it.", + "They stubbornly argues about the cheese wedges and has a certificate from the Stowaway Academy.", + "They loudly whistles the gat barrel oil but gets distracted by shiny brass coins.", + "They constantly complains about the hammock ropes but gets distracted by shiny brass coins.", + "They accidently misplaces the cheese wedges but only when cheese is involved.", + "They frequently drops the bilge keys but gets distracted by shiny brass coins.", + "They stubbornly argues about the abacus beads and has a certificate from the Stowaway Academy.", + "They secretly hoards the dry gunpowder bags but gets distracted by shiny brass coins.", + "They clumsily breaks the hammock ropes but gets confused by negative numbers.", + "They always claims the navigator's compass and has the abacus tattoos to prove it.", + "They loudly whistles the dry gunpowder bags but refuses to use prime numbers.", + "They constantly complains about the hammock ropes but only in base-3.", + "They accidently misplaces the geometry homework but only in base-3.", + "They clumsily breaks the bilge keys but gets distracted by shiny brass coins.", + "They always claims the abacus beads but only in base-3.", + "They stubbornly argues about the navigator's compass and has a certificate from the Stowaway Academy.", + "They frequently drops the bilge keys and has the abacus tattoos to prove it.", + "They annoyingly chews the cheese wedges but only in base-3.", + "They carelessly stains the navigator's compass but only in base-3.", + "They stubbornly argues about the gat barrel oil and claims numbers speak to them in dreams.", + "They secretly hoards the bilge keys but only in base-3.", + "They loudly whistles the coordinate maps but gets distracted by shiny brass coins.", + "They frequently drops the gat barrel oil and has a certificate from the Stowaway Academy.", + "They stubbornly argues about the slide rule and has the abacus tattoos to prove it.", + "They accidently misplaces the dry gunpowder bags and has a certificate from the Stowaway Academy.", + "They frequently drops the slide rule but gets confused by negative numbers." + ], + "techniques": [ + "Geometric Drift of the Cheese Reef", + "Parabolic boarding leap", + "Fibonacci Drift of the Stormy Sea", + "Geometric Leap of Euler", + "Trigonometric Flurry of the Abacus", + "Prime Number Slash of the Gat", + "Logarithmic Dodge of Carlos", + "Parabolic Flurry of the Gat", + "Logarithmic Slash of Euler", + "Logarithmic Drift of the Gat", + "Hypotenuse Shield of the Coordinate Plane", + "Algebraic Parry of the Stormy Sea", + "Hypotenuse Parry of the Deep", + "Hypotenuse Drift of Shrapnel", + "Binary Vortex of Gauss", + "Geometric Leap of the Deep", + "Trigonometric Drift of Shrapnel", + "Algebraic Slam of the Abacus", + "Hypotenuse Slash of Euler", + "Geometric Vortex of Euler", + "Hypotenuse Leap of the Gat", + "Trigonometric Slam of the Deep", + "Parabolic Shield of the Deep", + "Geometric Slash of the Gat", + "Geometric Slash of Gauss", + "Binary Barrage of the Gat", + "Fibonacci Vortex of Carlos", + "Geometric Slam of the Coordinate Plane", + "Prime Number Slash of the Deep", + "Prime Number Flurry of the Abacus", + "Logarithmic Flurry of Shrapnel", + "Vector Leap of Euler", + "Binary Drift of the Cheese Reef", + "Algebraic Leap of Shrapnel", + "Fibonacci Dodge of the Abacus", + "Vector Leap of the Coordinate Plane", + "Algebraic Leap of Gauss", + "Prime Number Leap of Euler", + "Trigonometric Dodge of the Cheese Reef", + "Fibonacci Barrage of Carlos", + "Geometric Parry of the Gat", + "Binary Shield of the Gat", + "Fibonacci Slash of Euler", + "Trigonometric Flurry of the Coordinate Plane", + "Cheese smell smoke screen", + "Parabolic Barrage of Shrapnel", + "Geometric Dodge of Gauss", + "Trigonometric Drift of the Deep", + "Algebraic Parry of Shrapnel", + "Vector Dodge of the Abacus", + "Algebraic Vortex of Carlos", + "Binary Flurry of the Abacus", + "Algebraic Slam of the Coordinate Plane", + "Algebraic Slam of the Deep", + "Logarithmic Barrage of the Deep", + "Binary Drift of Carlos", + "Logarithmic Flurry of the Abacus", + "Logarithmic Slam of Carlos", + "Parabolic Slash of the Stormy Sea", + "Parabolic Barrage of Carlos", + "Logarithmic Dodge of the Abacus", + "Fibonacci Slam of Gauss", + "Geometric Shield of the Coordinate Plane", + "Trigonometric Barrage of the Abacus", + "Algebraic Flurry of the Coordinate Plane", + "Hypotenuse Barrage of the Gat", + "Vector Vortex of the Gat", + "Geometric Parry of the Cheese Reef", + "Vector Drift of Euler", + "Vector Slash of the Stormy Sea", + "Binary Shield of Euler", + "Hypotenuse Parry of the Abacus", + "Trigonometric Drift of Euler", + "Fibonacci Parry of the Gat", + "Vector Parry of Gauss", + "Hypotenuse Shield of the Deep", + "Hypotenuse Slam of the Gat", + "Trigonometric Dodge of the Gat", + "Fibonacci Barrage of the Deep", + "Vector Dodge of Carlos", + "Algebraic Slam of the Stormy Sea", + "Prime Number Dodge of Shrapnel", + "Vector Slash of the Gat", + "Hypotenuse Flurry of the Gat", + "Fibonacci Flurry of the Gat", + "Logarithmic Leap of the Abacus", + "Fibonacci Slash of Gauss", + "Hypotenuse Parry of Shrapnel", + "Trigonometric Barrage of Carlos", + "Hypotenuse Parry of the Coordinate Plane", + "Trigonometric Shield of the Gat", + "Prime Number Parry of Euler", + "Vector Barrage of Shrapnel", + "Vector Slam of the Gat", + "Vector Flurry of the Abacus", + "Binary Drift of Gauss", + "Prime Number Slam of Euler", + "Prime Number Leap of Carlos", + "Algebraic Barrage of the Stormy Sea", + "Geometric Barrage of the Gat", + "Logarithmic Leap of the Deep", + "Logarithmic Barrage of the Gat", + "Angle of attack swipe", + "Trigonometric Parry of the Cheese Reef", + "Fibonacci Slash of the Coordinate Plane", + "Hypotenuse Shield of the Gat", + "Vector Shield of Gauss", + "Trigonometric Shield of Gauss", + "Vector Slash of the Abacus", + "Binary Shield of the Cheese Reef", + "Parabolic Parry of the Deep", + "Geometric Leap of Gauss", + "Logarithmic Shield of the Deep", + "Logarithmic Drift of Shrapnel", + "Trigonometric Flurry of Carlos", + "Logarithmic Slash of the Cheese Reef", + "Fibonacci Slam of the Abacus", + "Vector Parry of the Gat", + "Geometric Shield of the Deep", + "Logarithmic Flurry of Gauss", + "Hypotenuse Barrage of Euler", + "Hypotenuse Vortex of the Cheese Reef", + "Hypotenuse Slash of Gauss", + "Vector Parry of the Stormy Sea", + "Logarithmic Slam of the Stormy Sea", + "Hypotenuse Dodge of the Abacus", + "Geometric Leap of the Stormy Sea", + "Prime Number Shield of the Coordinate Plane", + "Binary Barrage of Shrapnel", + "Hypotenuse Shield of Euler", + "Geometric Drift of the Abacus", + "Prime Number Drift of Shrapnel", + "Logarithmic Vortex of the Abacus", + "Vector Slam of the Abacus", + "Hypotenuse Flurry of the Abacus", + "Trigonometric Slam of Euler", + "Parabolic Slam of Euler", + "Vector Shield of Carlos", + "Trigonometric Dodge of the Deep", + "Fibonacci Vortex of the Abacus", + "Geometric Shield of the Cheese Reef", + "Algebraic Drift of Gauss", + "Trigonometric Slash of the Cheese Reef", + "Parabolic Shield of Shrapnel", + "Fibonacci Shield of Shrapnel", + "Hypotenuse Dodge of Shrapnel", + "Vector Slash of the Coordinate Plane", + "Binary Vortex of the Cheese Reef", + "Geometric Slash of Shrapnel", + "Fibonacci Slash of the Cheese Reef", + "Logarithmic Vortex of the Gat", + "Algebraic Flurry of Euler", + "Logarithmic Barrage of the Abacus", + "Logarithmic Slash of the Stormy Sea", + "Vector Drift of the Gat", + "Trigonometric Shield of Euler", + "Trigonometric Shield of the Deep", + "Trigonometric Leap of Gauss", + "Prime Number Shield of the Deep", + "Algebraic Barrage of the Coordinate Plane", + "Vector Flurry of the Gat", + "Hypotenuse Slam of the Coordinate Plane", + "Geometric Vortex of the Gat", + "Binary Slam of the Gat", + "Binary Slam of the Cheese Reef", + "Parabolic Drift of Shrapnel", + "Prime Number Drift of the Stormy Sea", + "Logarithmic Parry of the Cheese Reef", + "Parabolic Leap of the Abacus", + "Binary Dodge of Shrapnel", + "Parabolic Barrage of the Abacus", + "Parabolic Vortex of the Coordinate Plane", + "Prime Number Dodge of Carlos", + "Fibonacci Barrage of the Abacus", + "Vector Barrage of the Coordinate Plane", + "Prime Number Drift of the Abacus", + "Algebraic Slash of the Gat", + "Binary Drift of the Coordinate Plane", + "Hypotenuse Flurry of the Stormy Sea", + "Trigonometric Slam of the Stormy Sea", + "Hypotenuse Vortex of Euler", + "Fibonacci Leap of the Cheese Reef", + "Fibonacci Parry of the Abacus", + "Trigonometric Slash of the Coordinate Plane", + "Parabolic Shield of Carlos", + "Trigonometric Leap of the Coordinate Plane", + "Trigonometric Leap of Carlos", + "Vector Parry of Euler", + "Hypotenuse Barrage of the Coordinate Plane", + "Binary Parry of Euler", + "Geometric Flurry of Euler", + "Parabolic Dodge of the Cheese Reef", + "Geometric Leap of the Abacus", + "Binary Leap of the Gat", + "Binary Barrage of the Deep", + "Fibonacci Shield of the Coordinate Plane", + "Fibonacci Drift of the Deep", + "Trigonometric Shield of Shrapnel", + "Parabolic Slash of Euler", + "Hypotenuse Parry of the Gat" + ] +}; + +export function getSuggestion(type) { + const arr = SUGGESTIONS[type]; + if (!arr) return ""; + return arr[Math.floor(Math.random() * arr.length)]; +} diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..458c7a8 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,9 @@ +import { mount } from 'svelte' +import './app.css' +import App from './App.svelte' + +const app = mount(App, { + target: document.getElementById('app'), +}) + +export default app diff --git a/frontend/src/pages/Admin.svelte b/frontend/src/pages/Admin.svelte new file mode 100644 index 0000000..804d650 --- /dev/null +++ b/frontend/src/pages/Admin.svelte @@ -0,0 +1,89 @@ + + +
+

Admin Panel

+ + {#if !data} +
+

Enter Admin Key

+
+ + +
+ {#if error} +
{error}
+ {/if} +
+ {:else} +
+

Game: {data.game.crew_name}

+

Phase: {data.game.phase}

+

Admin Key: {data.game.admin_key}

+ +

Players

+ + + + + + + + + + + {#each data.players as p} + + + + + + + {/each} + +
NameRoleStatusRank
{p.name} + {#if p.role === 'deep'} 🌊 Deep + {:else if p.role === 'pirat'} 🐀 Pi-Rat + {:else} None + {/if} + + {#if p.is_ghost} đŸ‘ģ Ghost + {:else if p.is_dead} 💀 Dead + {:else} Alive + {/if} + {p.rank}
+ + +
+ {/if} +
diff --git a/frontend/src/pages/Dashboard.svelte b/frontend/src/pages/Dashboard.svelte new file mode 100644 index 0000000..775a1f4 --- /dev/null +++ b/frontend/src/pages/Dashboard.svelte @@ -0,0 +1,77 @@ + + +{#if error} +
+ Error connecting to game: {error} +
+{/if} + +{#if state} +
+ {#if state.game.phase === 'lobby'} + + {:else if state.game.phase === 'character_creation' || state.game.phase === 'swap_techniques'} + + {:else if state.game.phase === 'scene_setup'} + + {:else if state.game.phase === 'scene'} + + {:else if state.game.phase === 'between_scenes' || state.game.phase === 'deep_upkeep'} + + {:else} +
Unknown phase: {state.game.phase}
+ {/if} +
+{:else if !error} +
+

Loading game state...

+
+{/if} + + diff --git a/frontend/src/pages/Home.svelte b/frontend/src/pages/Home.svelte new file mode 100644 index 0000000..a261024 --- /dev/null +++ b/frontend/src/pages/Home.svelte @@ -0,0 +1,61 @@ + + +
+
+

RATS with GATS

+

A Remote Play Companion App

+
+ +
+

Start a New Game

+
+
+ + +
+ + {#if error} +
{error}
+ {/if} + + +
+
+ + +
diff --git a/frontend/src/pages/Join.svelte b/frontend/src/pages/Join.svelte new file mode 100644 index 0000000..0e5b6e1 --- /dev/null +++ b/frontend/src/pages/Join.svelte @@ -0,0 +1,58 @@ + + +
+
+

Join Crew

+
+ +
+

Enter your Pi-Rat name

+
+
+ + +
+ + {#if error} +
{error}
+ {/if} + + +
+
+
diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js new file mode 100644 index 0000000..0cf7db3 --- /dev/null +++ b/frontend/svelte.config.js @@ -0,0 +1,2 @@ +/** @type {import("@sveltejs/vite-plugin-svelte").SvelteConfig} */ +export default {} diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..e677226 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [svelte()], + server: { + proxy: { + '/api': { + target: 'http://127.0.0.1:8000', + changeOrigin: true, + } + } + } +}) diff --git a/pyproject.toml b/pyproject.toml index 7427aa3..611fc71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,6 @@ dependencies = [ "fastapi", "sqlmodel", "uvicorn", - "jinja2", "python-multipart", ] @@ -28,7 +27,7 @@ pirats = "pirats.main:main" where = ["src"] [tool.setuptools.package-data] -pirats = ["static/**/*", "templates/**/*.html", "templates/*.html"] +pirats = ["static/**/*"] [tool.pytest.ini_options] pythonpath = ["src"] diff --git a/src/pirats/crud_character.py b/src/pirats/crud_character.py index f52c168..6217c9b 100644 --- a/src/pirats/crud_character.py +++ b/src/pirats/crud_character.py @@ -63,11 +63,27 @@ def trigger_technique_swap(db: Session, game: Game): # Build a pool of all techniques with their creators pool = [] + seen_texts = set() + has_duplicates = False + for p in players: techs = json.loads(p.created_techniques) for t in techs: + t_clean = t.strip().lower() + if t_clean in seen_texts: + has_duplicates = True + seen_texts.add(t_clean) pool.append({"text": t, "creator_id": p.id}) + if has_duplicates: + # Force players to resubmit + for p in players: + p.created_techniques = "[]" + db.add(p) + db.commit() + add_game_event(db, game.id, "Duplicate techniques submitted! The pool has been cleared. All players must try again with unique technique names.") + return + # Swap algorithm with simple retry backtracking # We want to assign 3 techniques from the pool to each player, ensuring creator_id != player.id. # For a small number of players, random shuffling with retries is highly efficient. @@ -174,3 +190,88 @@ def assign_face_card_techniques(db: Session, player_id: str, jack: str, queen: s db.add(game) db.commit() add_game_event(db, game.id, "Phase changed: Scene Setup") + +def roll_new_character( + db: Session, + game_id: str, + player_id: str, + avatar_look: str, + avatar_smell: str, + first_words: str, + good_at_math: str +): + player = get_player(db, player_id) + game = get_game(db, game_id) + if not player or not game: + return + + # 1. Return current hand cards to the deck + from .crud_base import get_player_hand, set_player_hand, get_game_deck, set_game_deck, draw_cards_for_player, calculate_max_hand_size + hand = get_player_hand(player) + deck = get_game_deck(game) + deck.extend(hand) + random.shuffle(deck) + set_game_deck(game, deck) + set_player_hand(player, []) + + # 2. Reset properties & randomize starting rank between 1 and 3 + player.rank = random.randint(1, 3) + player.is_ready = False + player.completed_personal_1 = False + player.completed_personal_2 = False + player.completed_personal_3 = False + player.needs_name = False + player.needs_rank_3_bonus = False + player.is_dead = False + player.is_ghost = False + + # Save player-provided details + player.avatar_look = avatar_look.strip() + player.avatar_smell = avatar_smell.strip() + player.first_words = first_words.strip() + player.good_at_math = good_at_math.strip() + + # Scent/Name is "Recruit [Smell]" + clean_smell = avatar_smell.strip() + if clean_smell.lower().startswith("like "): + clean_smell = clean_smell[5:] + player.name = f"Recruit {clean_smell}" + + # 3. Randomly assign 3 new secret techniques from cards.TECHNIQUE_SUGGESTIONS + from .cards import TECHNIQUE_SUGGESTIONS + chosen_techs = random.sample(TECHNIQUE_SUGGESTIONS, 3) + player.created_techniques = json.dumps(chosen_techs) + player.swapped_techniques = json.dumps(chosen_techs) + player.tech_jack = chosen_techs[0] + player.tech_queen = chosen_techs[1] + player.tech_king = chosen_techs[2] + + # 4. Auto-delegate Like/Hate questions to other players + other_players = [p for p in game.players if p.id != player.id] + if other_players: + like_target = random.choice(other_players) + player.other_like_from_player_id = like_target.id + player.other_like = "" + + if len(other_players) >= 2: + remaining = [op for op in other_players if op.id != like_target.id] + hate_target = random.choice(remaining) + else: + hate_target = random.choice(other_players) + player.other_hate_from_player_id = hate_target.id + player.other_hate = "" + else: + player.other_like_from_player_id = None + player.other_like = "" + player.other_hate_from_player_id = None + player.other_hate = "" + + db.add(player) + db.add(game) + db.commit() + + # 5. Draw cards up to new hand size based on new Rank + max_size = calculate_max_hand_size(player, game.players) + draw_cards_for_player(db, game, player, max_size) + + add_game_event(db, game.id, f"{player.name} has joined the crew as a new Rank {player.rank} recruit!") diff --git a/src/pirats/crud_scene.py b/src/pirats/crud_scene.py index bb7d8e1..30fb814 100644 --- a/src/pirats/crud_scene.py +++ b/src/pirats/crud_scene.py @@ -39,6 +39,19 @@ def confirm_scene_setup(db: Session, game_id: str) -> Tuple[bool, str]: p.role = "pirat" db.add(p) db.commit() + + if game.current_scene_number == 1: + max_rank = max((p.rank for p in players), default=1) + highest_ranked_players = [p for p in players if p.rank == max_rank] + + for p in players: + if p.role == "deep" and p not in highest_ranked_players: + return False, f"Only the highest ranked player(s) (Rank {max_rank}) can play The Deep in the first scene." + + if len(highest_ranked_players) == 1: + must_be_deep = highest_ranked_players[0] + if must_be_deep.role != "deep": + return False, f"{must_be_deep.name} is the highest ranked player and must play The Deep in the first scene." # 1. Check previous Deep players # "Players who were Deep Players in the previous Scene must play their Pi-Rats in the following Scene." @@ -150,6 +163,39 @@ def confirm_scene_setup(db: Session, game_id: str) -> Tuple[bool, str]: return True, "Scene started successfully!" +def get_allowed_roles(db: Session, game_id: str, player_id: str) -> list[str]: + game = get_game(db, game_id) + player = get_player(db, player_id) + if not game or not player: + return [] + + players = game.players + allowed = ["pirat", "deep"] + + if game.current_scene_number == 1: + max_rank = max((p.rank for p in players), default=1) + highest_ranked_players = [p for p in players if p.rank == max_rank] + + if player not in highest_ranked_players: + if "deep" in allowed: + allowed.remove("deep") + elif len(highest_ranked_players) == 1: + if "pirat" in allowed: + allowed.remove("pirat") + elif game.current_scene_number > 1: + # Rule 1: Previous Deep must play Pi-Rat (if >= 3 players) + if len(players) >= 3 and player.previous_role == "deep": + if "deep" in allowed: + allowed.remove("deep") + + # Rule 2: If only 1 eligible Deep, they MUST play Deep + eligible_deeps = [p for p in players if p.previous_role != "deep"] + if len(eligible_deeps) == 1 and eligible_deeps[0].id == player_id: + if "pirat" in allowed: + allowed.remove("pirat") + + return allowed + # --- Scene Gameplay Operations --- def play_card_on_obstacle( @@ -182,6 +228,14 @@ def play_card_on_obstacle( played_parsed = cards.parse_card(card_code) orig_obs_parsed = cards.parse_card(obstacle.original_card) + # Check if already completed + played_list = json.loads(obstacle.played_cards) + current_successes = sum(1 for c in played_list if c.get("success") is True) + required_successes = sum(1 for p in game.players if p.role != "deep") + if current_successes >= required_successes: + return False, "This obstacle is already completed.", {} + + # 1. Determine Success/Failure success = False details = "" @@ -416,3 +470,10 @@ def rollback_card_play(db: Session, obstacle_id: str) -> Tuple[bool, str]: db.commit() return True, "Rolled back the last played card." + +def clear_completed_obstacle(db: Session, game_id: str, obstacle_id: str): + obstacle = db.get(Obstacle, obstacle_id) + if obstacle: + add_game_event(db, game_id, f"The Deep cleared the completed obstacle '{obstacle.title}'.") + db.delete(obstacle) + db.commit() diff --git a/src/pirats/main.py b/src/pirats/main.py index 118a71d..e8bcf6a 100644 --- a/src/pirats/main.py +++ b/src/pirats/main.py @@ -1,49 +1,37 @@ from typing import Optional -from fastapi import FastAPI, Request, Form, Depends, HTTPException, status -from fastapi.responses import RedirectResponse, HTMLResponse +from fastapi import FastAPI, Request, Form, Depends, HTTPException, status, APIRouter +from fastapi.responses import HTMLResponse, JSONResponse from fastapi.staticfiles import StaticFiles from sqlmodel import Session, select import uvicorn +import os from pathlib import Path from . import crud from .database import create_db_and_tables, get_session -from .templates import templates, BASE_DIR from .models import GameEvent -app = FastAPI(title="Rats with Gats Remote Play") +BASE_DIR = Path(__file__).parent -# Mount static files -app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static") +app = FastAPI(title="Rats with Gats Remote Play API") +api = APIRouter() # Register startup event to create tables @app.on_event("startup") def on_startup(): create_db_and_tables() -# --- HTTP Core Route Handlers --- +# --- API Core Route Handlers --- -@app.get("/", response_class=HTMLResponse) -def home(request: Request): - return templates.TemplateResponse("index.html", {"request": request}) - -@app.post("/game/create") +@api.post("/game") def create_game_route( crew_name: str = Form(...), db: Session = Depends(get_session) ): game = crud.create_game(db, crew_name=crew_name.strip()) - # Redirect creator to the join page with creator flag - return RedirectResponse(url=f"/game/{game.id}/join?creator=true", status_code=status.HTTP_303_SEE_OTHER) + return {"id": game.id} -@app.get("/game/{game_id}/join", response_class=HTMLResponse) -def join_game_form(request: Request, game_id: str, creator: bool = False, db: Session = Depends(get_session)): - game = crud.get_game(db, game_id) - if not game: - raise HTTPException(status_code=404, detail="Game not found") - return templates.TemplateResponse("join_form.html", {"request": request, "game": game, "creator": creator}) - -@app.post("/game/{game_id}/join") +@api.post("/game/{game_id}/join") def join_game_submit( game_id: str, name: str = Form(...), @@ -56,80 +44,46 @@ def join_game_submit( # First player is automatically the creator is_creator = len(game.players) == 0 player = crud.add_player(db, game_id, name.strip(), is_creator=is_creator) - - return RedirectResponse(url=f"/game/{game_id}/player/{player.id}", status_code=status.HTTP_303_SEE_OTHER) + return {"id": player.id} -@app.get("/game/{game_id}/player/{player_id}", response_class=HTMLResponse) -def player_dashboard(request: Request, game_id: str, player_id: str, db: Session = Depends(get_session)): +@api.get("/game/{game_id}/player/{player_id}/state") +def get_game_state(game_id: str, player_id: str, db: Session = Depends(get_session)): game = crud.get_game(db, game_id) player = crud.get_player(db, player_id) if not game or not player: raise HTTPException(status_code=404, detail="Game or Player not found") - return templates.TemplateResponse("dashboard.html", { - "request": request, - "game": game, - "player": player - }) - -# --- HTMX Core Polling Route --- - -@app.get("/game/{game_id}/player/{player_id}/phase-check") -def phase_check_route(game_id: str, player_id: str, current: str, rank: Optional[int] = None, needs_name: Optional[bool] = None, hand_len: Optional[int] = None, db: Session = Depends(get_session)): - game = crud.get_game(db, game_id) - if not game: - raise HTTPException(status_code=404, detail="Game not found") - - player = crud.get_player(db, player_id) - if not player: - raise HTTPException(status_code=404, detail="Player not found") - - state_changed = False - if game.phase != current: - state_changed = True - elif rank is not None and player.rank != rank: - state_changed = True - elif needs_name is not None and player.needs_name != needs_name: - state_changed = True - elif hand_len is not None: - import json - current_hand = json.loads(player.hand_cards) - if len(current_hand) != hand_len: - state_changed = True - - if state_changed: - # Phase or state changed! Trigger page reload on client via HTMX header - return HTMLResponse(status_code=200, headers={"HX-Trigger": "phase-changed"}) - return HTMLResponse(status_code=204) # No content, no reload - -@app.get("/game/{game_id}/player/{player_id}/events", response_class=HTMLResponse) -def get_events_snippet(request: Request, game_id: str, player_id: str, db: Session = Depends(get_session)): - game = crud.get_game(db, game_id) - if not game: - raise HTTPException(status_code=404, detail="Game not found") - events = db.exec(select(GameEvent).where(GameEvent.game_id == game_id).order_by(GameEvent.timestamp.asc())).all() - return templates.TemplateResponse("event_log_snippet.html", { - "request": request, - "events": events - }) + return { + "game": game.model_dump(), + "player": player.model_dump(), + "players": [p.model_dump() for p in game.players], + "obstacles": [o.model_dump() for o in game.obstacles], + "votes": [v.model_dump() for v in game.votes], + "events": [e.model_dump() for e in events], + } # --- Register Modular Phase Routers --- - -from .phase_view import router as phase_view_router from .routes_lobby import router as lobby_router from .routes_character import router as character_router from .routes_scene import router as scene_router from .routes_upkeep import router as upkeep_router from .routes_admin import router as admin_router -app.include_router(phase_view_router) -app.include_router(lobby_router) -app.include_router(character_router) -app.include_router(scene_router) -app.include_router(upkeep_router) -app.include_router(admin_router) +api.include_router(lobby_router) +api.include_router(character_router) +api.include_router(scene_router) +api.include_router(upkeep_router) +api.include_router(admin_router) + +# Mount API at /api +app.include_router(api, prefix="/api") + +# Mount SPA +static_dir = BASE_DIR / "static" +if os.path.exists(static_dir): + app.mount("/", StaticFiles(directory=static_dir, html=True), name="static") def main(): import argparse diff --git a/src/pirats/models.py b/src/pirats/models.py index 0f282cf..a641416 100644 --- a/src/pirats/models.py +++ b/src/pirats/models.py @@ -7,7 +7,7 @@ class Game(SQLModel, table=True): id: str = Field(default_factory=lambda: str(uuid.uuid4()), primary_key=True) admin_key: str = Field(default_factory=lambda: str(uuid.uuid4())) crew_name: Optional[str] = Field(default=None) - phase: str = Field(default="lobby") # "lobby", "character_creation", "swap_techniques", "scene_setup", "scene", "between_scenes" + phase: str = Field(default="lobby") # "lobby", "character_creation", "swap_techniques", "scene_setup", "scene", "between_scenes", "deep_upkeep" current_scene_number: int = Field(default=1) extra_obstacles: int = Field(default=0) # Extra obstacles for next scene (from Jokers) deck_cards: str = Field(default="[]") # JSON string of list of card codes, e.g. ["AS", "10D", "KH", ...] diff --git a/src/pirats/phase_view.py b/src/pirats/phase_view.py deleted file mode 100644 index af474d2..0000000 --- a/src/pirats/phase_view.py +++ /dev/null @@ -1,95 +0,0 @@ -import json -from typing import Optional -from fastapi import Request, Depends, APIRouter -from fastapi.responses import HTMLResponse -from sqlmodel import Session - -from .database import get_session -from .models import Game, Player, Obstacle, Vote -from . import crud -from . import cards -from .templates import templates - -router = APIRouter() - -@router.get("/game/{game_id}/player/{player_id}/view", response_class=HTMLResponse) -def get_game_phase_view( - request: Request, - game_id: str, - player_id: str, - edit: bool = False, - db: Session = Depends(get_session) -): - game = crud.get_game(db, game_id) - player = crud.get_player(db, player_id) - if not game or not player: - return HTMLResponse("

Session disconnected or expired.

") - - base_url = str(request.base_url).rstrip("/") - - # Context helper functions to run inside Jinja templates - def get_player_name_by_id(*args) -> str: - pid = args[1] if len(args) == 2 else (args[0] if len(args) == 1 else None) - if not pid: - return "None" - p = db.get(Player, pid) - return p.name if p else "Unknown" - - def get_obstacle_by_id(obs_id: str) -> Optional[Obstacle]: - return db.get(Obstacle, obs_id) - - # Core variables - hand = crud.get_player_hand(player) - max_hand_size = crud.calculate_max_hand_size(player, game.players) - - # Compute inbox tasks for current player - inbox_tasks = [] - for p in game.players: - if p.id != player.id: - if p.other_like_from_player_id == player.id and not p.other_like: - inbox_tasks.append({"player": p, "type": "like"}) - if p.other_hate_from_player_id == player.id and not p.other_hate: - inbox_tasks.append({"player": p, "type": "hate"}) - - # Freshly fetch votes to avoid cache stale - from sqlmodel import select - votes = db.exec(select(Vote).where(Vote.game_id == game_id)).all() - - # Calculate eligible deep players - eligible_deeps = [p for p in game.players if p.previous_role != "deep"] - - context = { - "request": request, - "game": game, - "player": player, - "hand": hand, - "max_hand_size": max_hand_size, - "base_url": base_url, - "db": db, - "get_player_name": get_player_name_by_id, - "get_obstacle_by_id": get_obstacle_by_id, - "parse_card": cards.parse_card, - "is_captain": crud.is_player_captain, - "json_loads": json.loads, - "deck_count": len(crud.get_game_deck(game)), - "inbox_tasks": inbox_tasks, - "votes": votes, - "edit_mode": edit, - "eligible_deeps": eligible_deeps - } - - # Select template based on current game phase - if game.phase == "lobby": - return templates.TemplateResponse("lobby_partial.html", context) - elif game.phase in ["character_creation", "swap_techniques"]: - return templates.TemplateResponse("character_creation_partial.html", context) - elif game.phase == "scene_setup": - return templates.TemplateResponse("scene_setup_partial.html", context) - elif game.phase == "scene": - return templates.TemplateResponse("scene_partial.html", context) - elif game.phase == "between_scenes": - return templates.TemplateResponse("between_scenes_partial.html", context) - elif game.phase == "deep_upkeep": - return templates.TemplateResponse("deep_upkeep_partial.html", context) - - return HTMLResponse("

Unknown game state.

") diff --git a/src/pirats/routes_admin.py b/src/pirats/routes_admin.py index 154b65a..4a96c8d 100644 --- a/src/pirats/routes_admin.py +++ b/src/pirats/routes_admin.py @@ -1,14 +1,13 @@ from fastapi import APIRouter, Request, Depends, HTTPException -from fastapi.responses import HTMLResponse +from fastapi.responses import JSONResponse from sqlmodel import Session from .database import get_session from . import crud -from .templates import templates router = APIRouter() # Creator Admin Panel -@router.get("/game/{game_id}/admin", response_class=HTMLResponse) +@router.get("/game/{game_id}/admin") def creator_admin_panel(request: Request, game_id: str, key: str, db: Session = Depends(get_session)): game = crud.get_game(db, game_id) if not game: @@ -17,8 +16,26 @@ def creator_admin_panel(request: Request, game_id: str, key: str, db: Session = raise HTTPException(status_code=403, detail="Forbidden: Invalid admin key") base_url = str(request.base_url).rstrip("/") - return templates.TemplateResponse("admin.html", { - "request": request, - "game": game, + + players_data = [] + for p in game.players: + players_data.append({ + "id": p.id, + "name": p.name, + "rank": p.rank, + "is_dead": p.is_dead, + "is_ghost": p.is_ghost, + "role": p.role, + }) + + return { + "status": "ok", + "game": { + "id": game.id, + "crew_name": game.crew_name, + "phase": game.phase, + "admin_key": game.admin_key + }, + "players": players_data, "base_url": base_url - }) + } diff --git a/src/pirats/routes_character.py b/src/pirats/routes_character.py index 82f120d..5dd56dc 100644 --- a/src/pirats/routes_character.py +++ b/src/pirats/routes_character.py @@ -1,12 +1,9 @@ -import json from typing import Optional -from fastapi import APIRouter, Request, Form, Depends, HTTPException, status -from fastapi.responses import RedirectResponse, HTMLResponse +from fastapi import APIRouter, Form, Depends, HTTPException, status +from fastapi.responses import JSONResponse from sqlmodel import Session from .database import get_session from . import crud -from .models import Player -from .templates import templates router = APIRouter() @@ -32,17 +29,7 @@ def player_save_basic_details( db.add(player) db.commit() - return RedirectResponse(url=f"/game/{game_id}/player/{player_id}/view", status_code=status.HTTP_303_SEE_OTHER) - -# Edit basic character details (reset values to trigger form view) -@router.post("/game/{game_id}/player/{player_id}/edit-basic") -def player_edit_basic_details( - game_id: str, - player_id: str, - db: Session = Depends(get_session) -): - # Trigger HTMX reload with edit mode active - return RedirectResponse(url=f"/game/{game_id}/player/{player_id}/view?edit=true", status_code=status.HTTP_303_SEE_OTHER) + return {"status": "ok"} # Auto-assign unassigned delegated tasks (Like and Hate) randomly to crewmates @router.post("/game/{game_id}/player/{player_id}/delegate/auto") @@ -79,39 +66,11 @@ def player_auto_delegate( db.add(player) db.commit() - return RedirectResponse(url=f"/game/{game_id}/player/{player_id}/view", status_code=status.HTTP_303_SEE_OTHER) - -# Get Crew Creation Status (polling endpoint) -@router.get("/game/{game_id}/player/{player_id}/crew-status", response_class=HTMLResponse) -def player_crew_status( - request: Request, - game_id: str, - player_id: str, - db: Session = Depends(get_session) -): - game = crud.get_game(db, game_id) - player = crud.get_player(db, player_id) - if not game or not player: - raise HTTPException(status_code=404, detail="Not found") - - def get_player_name_by_id(pid: Optional[str]) -> str: - if not pid: - return "None" - p = db.get(Player, pid) - return p.name if p else "Unknown" - - return templates.TemplateResponse("crew_status_snippet.html", { - "request": request, - "game": game, - "player": player, - "json_loads": json.loads, - "get_player_name": get_player_name_by_id - }) + return {"status": "ok"} # Delegate question 5/6 to another player -@router.post("/game/{game_id}/player/{player_id}/delegate/{question_type}", response_class=HTMLResponse) +@router.post("/game/{game_id}/player/{player_id}/delegate/{question_type}") def player_delegate_question( - request: Request, game_id: str, player_id: str, question_type: str, # "like" or "hate" @@ -119,29 +78,11 @@ def player_delegate_question( db: Session = Depends(get_session) ): crud.delegate_question(db, player_id, question_type, target_player_id) - game = crud.get_game(db, game_id) - player = crud.get_player(db, player_id) - if not game or not player: - raise HTTPException(status_code=404, detail="Not found") - - def get_player_name_by_id(pid: Optional[str]) -> str: - if not pid: - return "None" - p = db.get(Player, pid) - return p.name if p else "Unknown" - - return templates.TemplateResponse("delegation_snippet.html", { - "request": request, - "game": game, - "player": player, - "question_type": question_type, - "get_player_name": get_player_name_by_id - }) + return {"status": "ok"} # Revoke a delegated question task -@router.post("/game/{game_id}/player/{player_id}/revoke-delegate/{question_type}", response_class=HTMLResponse) +@router.post("/game/{game_id}/player/{player_id}/revoke-delegate/{question_type}") def player_revoke_delegation( - request: Request, game_id: str, player_id: str, question_type: str, # "like" or "hate" @@ -159,51 +100,7 @@ def player_revoke_delegation( db.add(player) db.commit() - game = crud.get_game(db, game_id) - if not game: - raise HTTPException(status_code=404, detail="Game not found") - - def get_player_name_by_id(pid: Optional[str]) -> str: - if not pid: - return "None" - p = db.get(Player, pid) - return p.name if p else "Unknown" - - return templates.TemplateResponse("delegation_snippet.html", { - "request": request, - "game": game, - "player": player, - "question_type": question_type, - "get_player_name": get_player_name_by_id - }) - -# Fetch the delegation card status (for background polling when waiting) -@router.get("/game/{game_id}/player/{player_id}/delegation/{question_type}", response_class=HTMLResponse) -def player_get_delegation_status( - request: Request, - game_id: str, - player_id: str, - question_type: str, # "like" or "hate" - db: Session = Depends(get_session) -): - game = crud.get_game(db, game_id) - player = crud.get_player(db, player_id) - if not game or not player: - raise HTTPException(status_code=404, detail="Not found") - - def get_player_name_by_id(pid: Optional[str]) -> str: - if not pid: - return "None" - p = db.get(Player, pid) - return p.name if p else "Unknown" - - return templates.TemplateResponse("delegation_snippet.html", { - "request": request, - "game": game, - "player": player, - "question_type": question_type, - "get_player_name": get_player_name_by_id - }) + return {"status": "ok"} # Answer a delegated question for another player @router.post("/game/{game_id}/player/{player_id}/submit-delegate/{target_player_id}/{question_type}") @@ -216,12 +113,11 @@ def player_submit_delegated_answer( db: Session = Depends(get_session) ): crud.submit_delegated_answer(db, target_player_id, question_type, answer.strip(), player_id) - return HTMLResponse("") + return {"status": "ok"} # Submit 3 Secret Pirate Techniques @router.post("/game/{game_id}/player/{player_id}/submit-techniques") def player_submit_techniques( - request: Request, game_id: str, player_id: str, tech1: str = Form(...), @@ -230,23 +126,11 @@ def player_submit_techniques( db: Session = Depends(get_session) ): crud.submit_techniques(db, player_id, tech1.strip(), tech2.strip(), tech3.strip()) - - game = crud.get_game(db, game_id) - player = crud.get_player(db, player_id) - if not game or not player: - raise HTTPException(status_code=404, detail="Not found") - - return templates.TemplateResponse("techniques_snippet.html", { - "request": request, - "game": game, - "player": player, - "json_loads": json.loads - }) + return {"status": "ok"} # Assign Swapped Techniques to Face Cards (Jack, Queen, King) @router.post("/game/{game_id}/player/{player_id}/assign-face-techniques") def player_assign_face_techniques( - request: Request, game_id: str, player_id: str, jack: str = Form(...), @@ -255,19 +139,29 @@ def player_assign_face_techniques( db: Session = Depends(get_session) ): if len({jack, queen, king}) < 3: - # Prevent assigning the same technique to multiple face cards - return HTMLResponse("

Error: Each card must be assigned a unique technique.

", status_code=400) + return JSONResponse({"error": "Each card must be assigned a unique technique."}, status_code=400) crud.assign_face_card_techniques(db, player_id, jack, queen, king) - - game = crud.get_game(db, game_id) - player = crud.get_player(db, player_id) - if not game or not player: - raise HTTPException(status_code=404, detail="Not found") - - return templates.TemplateResponse("techniques_snippet.html", { - "request": request, - "game": game, - "player": player, - "json_loads": json.loads - }) + return {"status": "ok"} + +# Roll a new character after death +@router.post("/game/{game_id}/player/{player_id}/roll-new-character") +def roll_new_character_route( + game_id: str, + player_id: str, + avatar_look: str = Form(...), + avatar_smell: str = Form(...), + first_words: str = Form(...), + good_at_math: str = Form(...), + db: Session = Depends(get_session) +): + crud.roll_new_character( + db, + game_id, + player_id, + avatar_look=avatar_look, + avatar_smell=avatar_smell, + first_words=first_words, + good_at_math=good_at_math + ) + return {"status": "ok"} diff --git a/src/pirats/routes_lobby.py b/src/pirats/routes_lobby.py index c13f39c..ca9b878 100644 --- a/src/pirats/routes_lobby.py +++ b/src/pirats/routes_lobby.py @@ -1,10 +1,8 @@ from typing import Optional -from fastapi import APIRouter, Request, Depends, HTTPException -from fastapi.responses import HTMLResponse +from fastapi import APIRouter, Depends, HTTPException from sqlmodel import Session from .database import get_session from . import crud -from .templates import templates router = APIRouter() @@ -17,15 +15,5 @@ def lobby_start_character_creation(game_id: str, db: Session = Depends(get_sessi db.add(game) db.commit() crud.add_game_event(db, game.id, "Phase changed: Character Creation") - return HTMLResponse(status_code=204) # Return No Content, polling redirects + return {"status": "ok"} -@router.get("/game/{game_id}/lobby/players", response_class=HTMLResponse) -def lobby_players_snippet(request: Request, game_id: str, player_id: Optional[str] = None, db: Session = Depends(get_session)): - game = crud.get_game(db, game_id) - if not game: - return HTMLResponse("") - return templates.TemplateResponse("lobby_players_snippet.html", { - "request": request, - "game": game, - "player_id": player_id - }) diff --git a/src/pirats/routes_scene.py b/src/pirats/routes_scene.py index f0ea135..56a20b3 100644 --- a/src/pirats/routes_scene.py +++ b/src/pirats/routes_scene.py @@ -1,65 +1,39 @@ -import json from typing import Optional -from fastapi import APIRouter, Request, Form, Depends, HTTPException -from fastapi.responses import HTMLResponse +from fastapi import APIRouter, Form, Depends, HTTPException +from fastapi.responses import JSONResponse from sqlmodel import Session from .database import get_session from . import crud -from . import cards -from .templates import templates -from .phase_view import get_game_phase_view router = APIRouter() # Set Role during Scene Setup -@router.post("/game/{game_id}/player/{player_id}/set-role", response_class=HTMLResponse) +@router.post("/game/{game_id}/player/{player_id}/set-role") def player_set_role( - request: Request, game_id: str, player_id: str, - role: str, + role: str = Form(...), db: Session = Depends(get_session) ): crud.update_scene_role(db, player_id, role) - return get_game_phase_view(request, game_id, player_id, db=db) + return {"status": "ok"} + +@router.get("/game/{game_id}/player/{player_id}/allowed-roles") +def allowed_roles_route( + game_id: str, + player_id: str, + db: Session = Depends(get_session) +): + allowed = crud.get_allowed_roles(db, game_id, player_id) + return {"allowed_roles": allowed} # Start Scene (Verify Setup & Shuffle & Draw Obstacles) @router.post("/game/{game_id}/scene/start") -def start_scene_route(request: Request, game_id: str, db: Session = Depends(get_session)): +def start_scene_route(game_id: str, db: Session = Depends(get_session)): success, msg = crud.confirm_scene_setup(db, game_id) if not success: - return HTMLResponse(f"
{msg}
", status_code=400) - return HTMLResponse(status_code=204) - -# Get Scene Roster Snippet -@router.get("/game/{game_id}/scene/roster", response_class=HTMLResponse) -def scene_roster_snippet(request: Request, game_id: str, db: Session = Depends(get_session)): - game = crud.get_game(db, game_id) - if not game: - return HTMLResponse("") - return templates.TemplateResponse("scene_roster_snippet.html", { - "request": request, - "game": game, - "is_captain": crud.is_player_captain - }) - -# Get Scene Obstacles Snippet -@router.get("/game/{game_id}/scene/obstacles", response_class=HTMLResponse) -def scene_obstacles_snippet(request: Request, game_id: str, player_id: str, db: Session = Depends(get_session)): - game = crud.get_game(db, game_id) - player = crud.get_player(db, player_id) - if not game or not player: - return HTMLResponse("") - hand = crud.get_player_hand(player) - return templates.TemplateResponse("scene_obstacles_snippet.html", { - "request": request, - "game": game, - "player": player, - "hand": hand, - "parse_card": cards.parse_card, - "json_loads": json.loads, - "is_captain": crud.is_player_captain - }) + return JSONResponse({"error": msg}, status_code=400) + return {"status": "ok"} # Pi-Rat plays card against an obstacle @router.post("/game/{game_id}/player/{player_id}/play-card") @@ -72,18 +46,13 @@ def play_card_route( ): ok, msg, res = crud.play_card_on_obstacle(db, player_id, obstacle_id, card_code) if not ok: - return HTMLResponse(f"

{msg}

", status_code=400) - - color_prefix = "🟩" if res["success"] else "đŸŸĨ" - drew_text = f" (Drew card: {res['drew_card']})" if res["drew_card"] else "" - drew_attr = "true" if res["drew_card"] else "false" - html = ( - f"
" - f"

{color_prefix} {res['details']}{drew_text}

" - f"Click to dismiss" - f"
" - ) - return HTMLResponse(html, headers={"HX-Trigger": "hand-changed"}) + return JSONResponse({"error": msg}, status_code=400) + return { + "status": "ok", + "details": res["details"], + "drew_card": res["drew_card"], + "success": res["success"] + } # Pi-Rat plays a Joker to replace an obstacle @router.post("/game/{game_id}/player/{player_id}/play-joker") @@ -96,22 +65,15 @@ def play_joker_route( ): ok, msg, res = crud.play_card_on_obstacle(db, player_id, obstacle_id, card_code) if not ok: - return HTMLResponse(f"

{msg}

", status_code=400) - - html = ( - f"
" - f"

🃏 Play Joker: Discarded obstacle and drew a new replacement!

" - f"Click to dismiss" - f"
" - ) - return HTMLResponse(html, headers={"HX-Trigger": "hand-changed"}) + return JSONResponse({"error": msg}, status_code=400) + return {"status": "ok", "message": "Played Joker: Discarded obstacle and drew a new replacement!"} # Toggle Objective Completion Checklist @router.post("/game/{game_id}/player/{player_id}/objective/toggle") def toggle_objective_route( game_id: str, player_id: str, - type: str, # "personal_1", "personal_2", "personal_3", or "crew_1", etc. + type: str = Form(...), # "personal_1", "personal_2", "personal_3", or "crew_1", etc. db: Session = Depends(get_session) ): if type.startswith("crew_"): @@ -131,7 +93,7 @@ def toggle_objective_route( elif type == "personal_3": current_status = player.completed_personal_3 crud.toggle_objective(db, game_id, player_id, type, not current_status) - return HTMLResponse(status_code=204) + return {"status": "ok"} # Deep rollbacks a card play @router.post("/game/{game_id}/scene/rollback") @@ -142,8 +104,8 @@ def rollback_card_play_route( ): ok, msg = crud.rollback_card_play(db, obstacle_id) if not ok: - return HTMLResponse(f"

{msg}

", status_code=400) - return HTMLResponse(status_code=204, headers={"HX-Trigger": "hand-changed"}) + return JSONResponse({"error": msg}, status_code=400) + return {"status": "ok"} # Pi-Rat sets a new name @router.post("/game/{game_id}/player/{player_id}/set-name") @@ -159,7 +121,7 @@ def set_name_route( player.needs_name = False db.add(player) db.commit() - return HTMLResponse(status_code=204) + return {"status": "ok"} # Pi-Rat bonus rank up for another player @router.post("/game/{game_id}/player/{player_id}/bonus-rank-up") @@ -178,7 +140,7 @@ def bonus_rank_up_route( db.add(player) db.commit() crud.evaluate_hand_sizes(db, game_id) - return HTMLResponse(status_code=204) + return {"status": "ok"} # Pi-Rat becomes a ghost @router.post("/game/{game_id}/player/{player_id}/become-ghost") @@ -190,12 +152,23 @@ def become_ghost_route( player = crud.get_player(db, player_id) if player and player.is_dead: player.is_ghost = True + player.is_dead = False db.add(player) db.commit() - return HTMLResponse(status_code=204) + return {"status": "ok"} # Deep ends the scene @router.post("/game/{game_id}/scene/end") def end_scene_route(game_id: str, db: Session = Depends(get_session)): crud.end_scene_and_transition(db, game_id) - return HTMLResponse(status_code=204) + return {"status": "ok"} + +# Deep clears a completed obstacle +@router.post("/game/{game_id}/scene/obstacle/{obstacle_id}/clear") +def clear_obstacle_route( + game_id: str, + obstacle_id: str, + db: Session = Depends(get_session) +): + crud.clear_completed_obstacle(db, game_id, obstacle_id) + return {"status": "ok"} diff --git a/src/pirats/routes_upkeep.py b/src/pirats/routes_upkeep.py index ba62d64..537d65e 100644 --- a/src/pirats/routes_upkeep.py +++ b/src/pirats/routes_upkeep.py @@ -1,24 +1,12 @@ import json -from fastapi import APIRouter, Request, Form, Depends, HTTPException, status -from fastapi.responses import RedirectResponse, HTMLResponse +from fastapi import APIRouter, Form, Depends, HTTPException, status +from fastapi.responses import JSONResponse from sqlmodel import Session from .database import get_session from . import crud -from .templates import templates router = APIRouter() -# Get Between Scenes Voting Roster Snippet -@router.get("/game/{game_id}/between/roster", response_class=HTMLResponse) -def between_scenes_roster_snippet(request: Request, game_id: str, db: Session = Depends(get_session)): - game = crud.get_game(db, game_id) - if not game: - return HTMLResponse("") - return templates.TemplateResponse("voting_roster_snippet.html", { - "request": request, - "game": game - }) - # Cast vote to Rank Up a player @router.post("/game/{game_id}/player/{player_id}/submit-vote") def submit_vote_route( @@ -28,7 +16,7 @@ def submit_vote_route( db: Session = Depends(get_session) ): crud.submit_rank_vote(db, game_id, player_id, nominated_id) - return RedirectResponse(url=f"/game/{game_id}/player/{player_id}/view", status_code=status.HTTP_303_SEE_OTHER) + return {"status": "ok"} # Player Ready for Next Scene @router.post("/game/{game_id}/player/{player_id}/ready-next") @@ -46,7 +34,7 @@ def ready_next_route(game_id: str, player_id: str, db: Session = Depends(get_ses if game and all(p.is_ready for p in game.players): crud.transition_to_deep_upkeep(db, game_id) - return RedirectResponse(url=f"/game/{game_id}/player/{player_id}/view", status_code=status.HTTP_303_SEE_OTHER) + return {"status": "ok"} # Confirm Hand Refresh for Deep player @router.post("/game/{game_id}/player/{player_id}/confirm-refresh") @@ -61,4 +49,4 @@ def confirm_deep_refresh_route( except Exception: discards = [] crud.confirm_deep_refresh(db, player_id, discards) - return RedirectResponse(url=f"/game/{game_id}/player/{player_id}/view", status_code=status.HTTP_303_SEE_OTHER) + return {"status": "ok"} diff --git a/src/pirats/templates.py b/src/pirats/templates.py deleted file mode 100644 index bc3ea6a..0000000 --- a/src/pirats/templates.py +++ /dev/null @@ -1,8 +0,0 @@ -from pathlib import Path -from fastapi.templating import Jinja2Templates - -# Get the directory of this module -BASE_DIR = Path(__file__).resolve().parent - -# Templates setup -templates = Jinja2Templates(directory=BASE_DIR / "templates") diff --git a/src/pirats/templates/admin.html b/src/pirats/templates/admin.html deleted file mode 100644 index 3d4454e..0000000 --- a/src/pirats/templates/admin.html +++ /dev/null @@ -1,59 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Creator Admin Panel - {% if game.crew_name %}{{ game.crew_name }}{% else %}Game #{{ game.id[:8] }}{% endif %}{% endblock %} - -{% block content %} -
-
-

Game Creator Admin Panel

-

Use this dashboard to copy magic sheet links for players who misplace theirs.

-
- -
-
-

Player Magic Links

-
- {% for p in game.players %} -
-
- {{ p.name }} - {% if p.is_creator %}Creator{% endif %} - (Rank {{ p.rank }}, Role: {{ p.role or 'None' }}) -
- -
- {% else %} -

No players have joined yet.

- {% endfor %} -
-
- -
-

Lobby Details

-

Game Phase: {{ game.phase.replace('_', ' ').title() }}

-

Scene Number: {{ game.current_scene_number }}

-

Join URL (Share this with players):

-
- - -
- - -
-
-
-{% endblock %} diff --git a/src/pirats/templates/base.html b/src/pirats/templates/base.html deleted file mode 100644 index 52da35b..0000000 --- a/src/pirats/templates/base.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - {% block title %}Rats with Gats{% endblock %} - - - - - - - - - - - - - - - - -
-
- ΀ -

Rats with Gats

-
- {% block header_extra %}{% endblock %} -
- -
- {% block content %}{% endblock %} -
- - - - diff --git a/src/pirats/templates/between_scenes_partial.html b/src/pirats/templates/between_scenes_partial.html deleted file mode 100644 index ed0a577..0000000 --- a/src/pirats/templates/between_scenes_partial.html +++ /dev/null @@ -1,118 +0,0 @@ -
-

Between Scenes (Scene #{{ game.current_scene_number }} Concluded)

-

Upkeep and tallying. Nominate a crewmate to Rank Up, redraw hand cards for resting Deep players, and ready up for the next scene.

- -
- -
-

1. Pirate Ranking (Voting)

-

Nominate one crewmate who best exemplified pirate qualities in the previous scene. Previous Deep players are ineligible to receive votes.

- - {% set ns_vote = namespace(has_voted=False) %} - {% for v in votes %} - {% if v.voter_player_id == player.id %} - {% set ns_vote.has_voted = True %} - {% endif %} - {% endfor %} - - {% if ns_vote.has_voted %} -
-

âœ”ī¸ Vote submitted! Waiting for other players to submit their nominations.

-
- {% else %} -
-
- - -
-
- {% endif %} - - -
-

Nomination Progress:

-
- {% for p in game.players %} - {% set voted = False %} - {% for v in votes if v.voter_player_id == p.id %} - {% set voted = True %} - {% endfor %} -
- {{ p.name }} - {% if voted %}Voted{% else %}Voting...{% endif %} -
- {% endfor %} -
-
-
- - -
-

2. Resting Deep Upkeep

- - {% if player.previous_role == "deep" %} -
-

You controlled the Deep in the last scene and your Pi-Rat was resting. You will discard your current hand and redraw back up to your maximum hand size (Rank {{ player.rank }} maximum: {{ max_hand_size }} cards).

-

This is handled automatically when transitioning, but you must click 'Ready' below to proceed.

-
- {% else %} -

Your Pi-Rat was active. Your hand cards carry over to the next scene.

- {% endif %} - -
-

Crew Rank Ledger:

-
    - {% for p in game.players %} -
  • - {{ p.name }}: Rank {{ p.rank }} - {% if p.previous_role == 'deep' %}Deep{% else %}Pi-Rat{% endif %} -
  • - {% endfor %} -
-
-
-
- - -
- {% set eligible_count = namespace(value=0) %} - {% for other in game.players if other.id != player.id and other.previous_role != 'deep' %} - {% set eligible_count.value = eligible_count.value + 1 %} - {% endfor %} - - {% if eligible_count.value > 0 and not ns_vote.has_voted %} -

âš ī¸ You must nominate a crewmate above before you can ready up.

- - {% else %} - {% if player.is_ready %} -
-

Ready! Waiting for other players to ready up...

-
-
- {% else %} - - {% endif %} - {% endif %} -
-
- - -
- -{% include "header_status_snippet.html" %} diff --git a/src/pirats/templates/character_creation_partial.html b/src/pirats/templates/character_creation_partial.html deleted file mode 100644 index 643128a..0000000 --- a/src/pirats/templates/character_creation_partial.html +++ /dev/null @@ -1,122 +0,0 @@ -
-
-

Character Sheet Creation

-

Fill in your Rat Records, delegate questions to your crew, and prepare your Math Magic Techniques.

- -
- - -
- - -
-

I. Basic Rat Records

- - {% if player.avatar_look and player.avatar_smell and player.first_words and not edit_mode %} - -
-
Look: {{ player.avatar_look }}
-
Smell: {{ player.avatar_smell }}
-
First Words: "{{ player.first_words }}"
-
Good at Math? {{ player.good_at_math }}
- -
- {% else %} - -
-
- -
- - -
-
-
- -
- - -
-
-
- -
- - -
-
-
- -
- - -
-
- -
- {% endif %} -
- - -
-
-

II. Crew Delegations

- {% if not player.other_like_from_player_id or not player.other_hate_from_player_id %} - - {% endif %} -
-

You must let other players decide what they like and hate about your character.

- -
- - {% set question_type = 'like' %} - {% include 'delegation_snippet.html' %} - - - {% set question_type = 'hate' %} - {% include 'delegation_snippet.html' %} -
-
- - - {% include 'inbox_snippet.html' %} - - - {% include 'techniques_snippet.html' %} - - -
-

V. Crew Creation Status

-

See the creation progress of the entire crew in real time.

-
- {% include 'crew_status_snippet.html' %} -
-
-
-
- - -
- -{% include "header_status_snippet.html" %} diff --git a/src/pirats/templates/crew_status_snippet.html b/src/pirats/templates/crew_status_snippet.html deleted file mode 100644 index 0f68b3d..0000000 --- a/src/pirats/templates/crew_status_snippet.html +++ /dev/null @@ -1,76 +0,0 @@ -
- {% for p in game.players %} - {% set created_techs = json_loads(p.created_techniques) %} - {% set has_submitted_techs = created_techs|length == 3 and created_techs[0] %} - -
-
- - 🐀 {{ p.name }} {% if p.id == player.id %}(You){% endif %} - - - Rank {{ p.rank }} - -
- -
- -
- Basic Records: - {% if p.avatar_look and p.avatar_smell and p.first_words %} - ✅ Complete - {% else %} - âœī¸ Writing... - {% endif %} -
- - -
- Delegated Tasks: -
-
- â€ĸ Like: - {% if p.other_like %} - Done ({{ get_player_name(p.other_like_from_player_id) }}) - {% elif p.other_like_from_player_id %} - Waiting on {{ get_player_name(p.other_like_from_player_id) }} - {% else %} - Not delegated - {% endif %} -
-
- â€ĸ Hate: - {% if p.other_hate %} - Done ({{ get_player_name(p.other_hate_from_player_id) }}) - {% elif p.other_hate_from_player_id %} - Waiting on {{ get_player_name(p.other_hate_from_player_id) }} - {% else %} - Not delegated - {% endif %} -
-
-
- - -
- Secret Techniques: - {% if game.phase == "character_creation" %} - {% if has_submitted_techs %} - ✅ Submitted - {% else %} - âœī¸ Writing... - {% endif %} - {% else %} - - {% if p.is_ready %} - ✅ J/Q/K Assigned - {% else %} - âœī¸ Assigning... - {% endif %} - {% endif %} -
-
-
- {% endfor %} -
diff --git a/src/pirats/templates/dashboard.html b/src/pirats/templates/dashboard.html deleted file mode 100644 index 2292e77..0000000 --- a/src/pirats/templates/dashboard.html +++ /dev/null @@ -1,77 +0,0 @@ -{% extends "base.html" %} - -{% block title %}{% if game.crew_name %}{{ game.crew_name }}{% else %}Game #{{ game.id[:8] }}{% endif %} - {% if player %}{{ player.name }}{% else %}Lobby{% endif %}{% endblock %} - -{% block header_extra %} -
- {% if player %} - {{ player.name }} (Rank {{ player.rank }}) - {% endif %} - {{ game.phase.replace('_', ' ').title() }} -
-{% endblock %} - -{% block content %} - -
-
-
-

Contacting the math-magic spirits...

-
-
- - - - - - - -{% endblock %} diff --git a/src/pirats/templates/deep_upkeep_partial.html b/src/pirats/templates/deep_upkeep_partial.html deleted file mode 100644 index 8919a27..0000000 --- a/src/pirats/templates/deep_upkeep_partial.html +++ /dev/null @@ -1,121 +0,0 @@ -{% if player.previous_role == "deep" %} -
-

Resting Deep Upkeep

-

Select which cards you want to keep and which you want to discard. Drag cards between the boxes, then click "Confirm Hand Refresh". You will draw back up to your maximum hand size of {{ player.rank + 1 }} cards.

- -
- -
-

Keep (Current Hand)

-
- {% for card in hand %} - {% set parsed = parse_card(card) %} -
-
- {{ parsed.value }} - {{ parsed.symbol }} -
-
- {% if parsed.is_joker %} - 🃏 - {% else %} - {{ parsed.symbol }} - {% endif %} -
-
- {{ parsed.value }} - {{ parsed.symbol }} -
-
- {% endfor %} -
-
- - -
-

Discard

-
- -
-
-
- -
- {% if player.is_ready %} -
-

Upkeep confirmed! Waiting for other resting Deep players...

-
-
- {% else %} -
- - - -
- {% endif %} -
-
- - -{% else %} -
-

🌊 Resting Deep Upkeep

-

The players who controlled the Deep in the previous scene are currently refreshing their hands.

-
-

Waiting for resting Deep players to confirm their hand refreshes...

-
-
-
-{% endif %} - - -
- -{% include "header_status_snippet.html" %} diff --git a/src/pirats/templates/delegation_snippet.html b/src/pirats/templates/delegation_snippet.html deleted file mode 100644 index e4a742b..0000000 --- a/src/pirats/templates/delegation_snippet.html +++ /dev/null @@ -1,50 +0,0 @@ -{% if question_type == 'like' %} - {% set ans = player.other_like %} - {% set delegate_id = player.other_like_from_player_id %} -{% else %} - {% set ans = player.other_hate %} - {% set delegate_id = player.other_hate_from_player_id %} -{% endif %} - -
- -

What other pirates {{ question_type.upper() }} about you:

- - {% if ans %} -
"{{ ans }}" — answered by {{ get_player_name(delegate_id) }}
- - - {% elif delegate_id %} -
-
- - Delegated to {{ get_player_name(delegate_id) }}. Waiting... -
- -
- {% else %} -
- - -
- {% endif %} -
diff --git a/src/pirats/templates/event_log_snippet.html b/src/pirats/templates/event_log_snippet.html deleted file mode 100644 index afdd7f5..0000000 --- a/src/pirats/templates/event_log_snippet.html +++ /dev/null @@ -1,21 +0,0 @@ -{% if events %} - - -{% else %} -
No events yet.
-{% endif %} diff --git a/src/pirats/templates/header_status_snippet.html b/src/pirats/templates/header_status_snippet.html deleted file mode 100644 index a221a26..0000000 --- a/src/pirats/templates/header_status_snippet.html +++ /dev/null @@ -1,6 +0,0 @@ -
- {% if player %} - {{ player.name }} (Rank {{ player.rank }}) - {% endif %} - {{ game.phase.replace('_', ' ').title() }} -
diff --git a/src/pirats/templates/inbox_snippet.html b/src/pirats/templates/inbox_snippet.html deleted file mode 100644 index 51545fc..0000000 --- a/src/pirats/templates/inbox_snippet.html +++ /dev/null @@ -1,40 +0,0 @@ -
-

III. Your Tasks for Others

-

Answer questions that your crewmates have delegated to you.

- -
-

No tasks in your inbox. Check back when other players delegate to you!

- {% if inbox_tasks %} - {% for task in inbox_tasks %} - {% set p = task.player %} - {% if task.type == 'like' %} -
-
- -
- - - -
-
-
- {% else %} -
-
- -
- - - -
-
-
- {% endif %} - {% endfor %} - {% endif %} -
-
diff --git a/src/pirats/templates/index.html b/src/pirats/templates/index.html deleted file mode 100644 index 94ed4f4..0000000 --- a/src/pirats/templates/index.html +++ /dev/null @@ -1,36 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Welcome - Rats with Gats{% endblock %} - -{% block content %} -
-
-

Steal Ships. Commit Piracy. Run on Math Magic.

-

A collaborative storytelling web-app to facilitate remote play of the whimsical roleplaying game.

-
- -
-
-

Start a New Adventure

-

Generate a fresh game deck and name your crew of mathematical pirates.

-
-
- -
- -
-
- -
-

Join an Existing Crew

-

Enter a Game ID or Join Link provided by your Captain.

-
-
- -
- -
-
-
-
-{% endblock %} diff --git a/src/pirats/templates/join_form.html b/src/pirats/templates/join_form.html deleted file mode 100644 index 925da29..0000000 --- a/src/pirats/templates/join_form.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Join Crew - Rats with Gats{% endblock %} - -{% block content %} -
-

Join the {% if game.crew_name %}{{ game.crew_name }}{% else %}Pi-Rat{% endif %} Crew!

-

The mathematical transformation spell is about to cast. Enter your name below to stow away in the cargo hold.

- -
-
- - -
- -
- - -
-{% endblock %} diff --git a/src/pirats/templates/lobby_partial.html b/src/pirats/templates/lobby_partial.html deleted file mode 100644 index 4015f49..0000000 --- a/src/pirats/templates/lobby_partial.html +++ /dev/null @@ -1,68 +0,0 @@ -
-

{% if game.crew_name %}{{ game.crew_name }}{% else %}Ship's Hold{% endif %} (Lobby)

-

Wait here as the mathematical mages gather stowaway rats for the spell. Once everyone is in, the captain will initiate the transformation.

- -
-

Joined Crew members

-
- {% for p in game.players %} -
- 🐀 - {{ p.name }} - {% if p.is_creator %}Creator{% endif %} -
- {% endfor %} -
-
- - - -
- {% if player.is_creator %} - {% if game.players|length >= 1 %} - - {% else %} - - {% endif %} - {% else %} -
-
-

Waiting for Captain to cast the spell...

-
- {% endif %} -
-
- - -
- -{% include "header_status_snippet.html" %} diff --git a/src/pirats/templates/lobby_players_snippet.html b/src/pirats/templates/lobby_players_snippet.html deleted file mode 100644 index 97045b3..0000000 --- a/src/pirats/templates/lobby_players_snippet.html +++ /dev/null @@ -1,7 +0,0 @@ -{% for p in game.players %} -
- 🐀 - {{ p.name }} - {% if p.is_creator %}Creator{% endif %} -
-{% endfor %} diff --git a/src/pirats/templates/scene_obstacles_snippet.html b/src/pirats/templates/scene_obstacles_snippet.html deleted file mode 100644 index cfc56e6..0000000 --- a/src/pirats/templates/scene_obstacles_snippet.html +++ /dev/null @@ -1,138 +0,0 @@ - -
-
- - {% set ns = namespace(captain=None) %} - {% for p in game.players %} - {% if is_captain(p, game.players) %} - {% set ns.captain = p %} - {% endif %} - {% endfor %} -
- {% if ns.captain %} - - đŸ´â€â˜ ī¸ Captain: {{ ns.captain.name }} (Rank {{ ns.captain.rank }}) - - {% else %} - - đŸ´â€â˜ ī¸ Captain: None (No Pi-Rats) - - {% endif %} -
- - -
- Active Roster: - {% for p in game.players %} - {% if p.role == 'deep' %} - - 🌊 {{ p.name }} (Deep) - - {% elif p.role == 'pirat' %} - - 🐀 {{ p.name }} (Rank {{ p.rank }}){% if is_captain(p, game.players) %} ⭐{% endif %} - - {% endif %} - {% endfor %} -
-
-
- -{% for obs in game.obstacles %} - {% set column_cards = json_loads(obs.played_cards) %} - {% set active_card_code = column_cards[-1]["card"] if column_cards else obs.original_card %} - {% set active_parsed = parse_card(active_card_code) %} -
-
-
-
- {{ active_parsed.value }} - {{ active_parsed.symbol }} -
- -
- {% if active_parsed.is_joker %} - 🃏 - {% else %} - {{ active_parsed.symbol }} - {% endif %} -
- -
- {{ active_parsed.value }} - {{ active_parsed.symbol }} -
-
-
-
-

{{ obs.title }}

-

{{ obs.description }}

-
- - -
- Current Difficulty - - {% if parse_card(obs.original_card).value in ["J", "Q", "K"] %} - Rank ({{ player.rank }}) - {% else %} - {{ obs.current_value }} - {% endif %} - -
- - - {% set ns_non_deep = namespace(count=0) %} - {% for p in game.players %} - {% if p.role != "deep" %} - {% set ns_non_deep.count = ns_non_deep.count + 1 %} - {% endif %} - {% endfor %} -
- Successes - - {{ obs.success_count }} / {{ ns_non_deep.count }} - -
- - -
-
Card History (Column)
- {% set column_cards = json_loads(obs.played_cards) %} -
-
- {{ obs.original_card[:-1] }} - {{ obs.symbol }} -
- {% for pc in column_cards %} -
-
- {{ pc.card[:-1] if not pc.card.startswith('Joker') else '🃏' }} - {{ parse_card(pc.card).symbol }} - {{ pc.player_name[:4] }} -
- {% if loop.last and player.role == 'deep' %} - - {% endif %} -
- {% endfor %} -
-
-
-{% else %} -

No active obstacles. Deep players can end the scene!

-{% endfor %} diff --git a/src/pirats/templates/scene_partial.html b/src/pirats/templates/scene_partial.html deleted file mode 100644 index fd5c902..0000000 --- a/src/pirats/templates/scene_partial.html +++ /dev/null @@ -1,421 +0,0 @@ -
- -
- - -
-
-

🌊 The Obstacle List

- 🎴 Deck: {{ deck_count }} cards left -
- -
- -
-
- - {% set ns = namespace(captain=None) %} - {% for p in game.players %} - {% if is_captain(p, game.players) %} - {% set ns.captain = p %} - {% endif %} - {% endfor %} -
- {% if ns.captain %} - - đŸ´â€â˜ ī¸ Captain: {{ ns.captain.name }} (Rank {{ ns.captain.rank }}) - - {% else %} - - đŸ´â€â˜ ī¸ Captain: None (No Pi-Rats) - - {% endif %} -
- - -
- Active Roster: - {% for p in game.players %} - {% if p.role == 'deep' %} - - 🌊 {{ p.name }} (Deep) - - {% elif p.role == 'pirat' %} - - 🐀 {{ p.name }} (Rank {{ p.rank }}){% if is_captain(p, game.players) %} ⭐{% endif %} - - {% endif %} - {% endfor %} -
-
-
- - {% for obs in game.obstacles %} - {% set column_cards = json_loads(obs.played_cards) %} - {% set active_card_code = column_cards[-1]["card"] if column_cards else obs.original_card %} - {% set active_parsed = parse_card(active_card_code) %} -
-
-
-
- {{ active_parsed.value }} - {{ active_parsed.symbol }} -
- -
- {% if active_parsed.is_joker %} - 🃏 - {% else %} - {{ active_parsed.symbol }} - {% endif %} -
- -
- {{ active_parsed.value }} - {{ active_parsed.symbol }} -
-
-
-
-

{{ obs.title }}

-

{{ obs.description }}

-
- - -
- Current Difficulty - - {% if parse_card(obs.original_card).value in ["J", "Q", "K"] %} - Rank ({{ player.rank }}) - {% else %} - {{ obs.current_value }} - {% endif %} - -
- - - {% set ns_non_deep = namespace(count=0) %} - {% for p in game.players %} - {% if p.role != "deep" %} - {% set ns_non_deep.count = ns_non_deep.count + 1 %} - {% endif %} - {% endfor %} -
- Successes - - {{ obs.success_count }} / {{ ns_non_deep.count }} - -
- - -
-
Card History (Column)
- {% set column_cards = json_loads(obs.played_cards) %} -
-
- {{ obs.original_card[:-1] }} - {{ obs.symbol }} -
- {% for pc in column_cards %} -
- {{ pc.card[:-1] if not pc.card.startswith('Joker') else '🃏' }} - {{ parse_card(pc.card).symbol }} - {{ pc.player_name[:4] }} -
- {% endfor %} -
-
-
- {% else %} -

No active obstacles. Deep players can end the scene!

- {% endfor %} -
-
-
- - -
- - {% if player.role == "deep" %} -
-

🌊 Deep Control Panel

- -
-

Crew Objectives

-
- - - -
-
- -
-

Pi-Rat Personal Objectives

-

You control completion. Only mark in sequence (1 -> 2 -> 3).

- {% for p in game.players if p.role == 'pirat' %} -
- {{ p.name }} -
- - - -
-
- {% endfor %} -
- - -
-

Conclude the scene once players have made attempts or the obstacle list is depleted.

- -
-
- {% endif %} - - -
-

🃏 Your Hand

-

Keep your cards secret! {% if player.role != "deep" %}Drag a card onto an active obstacle to play it.{% else %}Max hand size: {{ max_hand_size }} cards.{% endif %}

- -
- {% for card in hand %} - {% set parsed = parse_card(card) %} -
-
- {{ parsed.value }} - {{ parsed.symbol }} -
- -
- {% if parsed.is_joker %} - 🃏 - {% else %} - {{ parsed.symbol }} - {% endif %} -
- - -
- {% if parsed.value == "J" %} -
J: "{{ player.tech_jack }}"
- {% elif parsed.value == "Q" %} -
Q: "{{ player.tech_queen }}"
- {% elif parsed.value == "K" %} -
K: "{{ player.tech_king }}"
- {% endif %} -
- -
- {{ parsed.value }} - {{ parsed.symbol }} -
-
- {% else %} -

Your hand is empty! (You draw cards when playing cards that match the suit color of the obstacle.)

- {% endfor %} -
-
- - - {% if player.role != "deep" %} -
-

🐀 Your Character sheet

- -
-
-

Description:

-

Look: {{ player.avatar_look }}

-

Smell: {{ player.avatar_smell }}

-

First Words: "{{ player.first_words }}"

-

Likes: "{{ player.other_like }}" (by {{ get_player_name(player.other_like_from_player_id) }})

-

Hates: "{{ player.other_hate }}" (by {{ get_player_name(player.other_hate_from_player_id) }})

-
- -
-

Assigned Secret Techniques (J/Q/K):

-
    -
  • Jack (J): "{{ player.tech_jack }}"
  • -
  • Queen (Q): "{{ player.tech_queen }}"
  • -
  • King (K): "{{ player.tech_king }}"
  • -
-
- -
-

Objectives Tracker:

-

Deep players control these objectives. They unlock privileges when checked.

-
-
- {% if player.completed_personal_1 %}✅{% else %}âŦœ{% endif %} đŸ”Ģ Personal 1: Get a Gat -
Privilege: +1 to Black cards. Tax: Can ask a Gatted player to do challenges.
-
- -
- {% if player.completed_personal_2 %}✅{% else %}âŦœ{% endif %} 👑 Personal 2: Earn a Name -
Privilege: +1 to Red cards. Tax: Can ask a Named player to do challenges.
- {% if player.needs_name %} -
- - -
- {% endif %} -
- -
- {% if player.completed_personal_3 %}✅{% else %}âŦœ{% endif %} â˜ ī¸ Personal 3: Die Like a Pirate -
Bonus: Rank up another player.
- - {% if player.needs_rank_3_bonus %} -
- - - -
- {% endif %} - - {% if player.is_dead and not player.is_ghost %} -
-

You Died!

-

Your Pi-Rat's story has concluded. You may continue to watch over your crew as a ghost!

- -
- {% endif %} - {% if player.is_ghost %} -
- đŸ‘ģ Ghost Mode Active -

You can still use your hand to help the living, but between scenes you may roll a new recruit.

-
- {% endif %} -
-
-
-
-
- {% endif %} - -
-
- - -
- -{% include "header_status_snippet.html" %} - - diff --git a/src/pirats/templates/scene_roster_snippet.html b/src/pirats/templates/scene_roster_snippet.html deleted file mode 100644 index bde716c..0000000 --- a/src/pirats/templates/scene_roster_snippet.html +++ /dev/null @@ -1,41 +0,0 @@ -
-

🌊 The Deep (NPCs & Hazards)

-
- {% for p in game.players if p.role == 'deep' %} -
- {{ p.name }} - Rank {{ p.rank }} -
- {% else %} -

None selected

- {% endfor %} -
-
- -
-

🐀 Pi-Rats (Crew Players)

-
- {% for p in game.players if p.role == 'pirat' %} -
- {{ p.name }} - Rank {{ p.rank }}{% if is_captain(p, game.players) %} (Captain) {% endif %} -
- {% else %} -

None selected

- {% endfor %} -
-
- -
-

âŗ Yet to Choose

-
- {% for p in game.players if not p.role %} -
- {{ p.name }} - Rank {{ p.rank }} -
- {% else %} -

Everyone has chosen!

- {% endfor %} -
-
diff --git a/src/pirats/templates/scene_setup_partial.html b/src/pirats/templates/scene_setup_partial.html deleted file mode 100644 index 20bb6e6..0000000 --- a/src/pirats/templates/scene_setup_partial.html +++ /dev/null @@ -1,99 +0,0 @@ -
-

Scene Setup (Scene #{{ game.current_scene_number }})

-

Select your role for the upcoming scene. There must be at least one Pi-Rat and one Deep player. If you played the Deep in the last scene, you must play a Pi-Rat this scene!

- -
- -
-

Select Your Role

- - {% set force_pirat = False %} - {% set locked_role = None %} - - - {% if game.current_scene_number == 1 %} - {% if player.rank == 3 %} - {% set locked_role = "deep" %} - {% elif player.rank == 1 %} - {% set locked_role = "pirat" %} - {% endif %} - - {% else %} - {% if player.previous_role == "deep" %} - {% set force_pirat = True %} - {% set locked_role = "pirat" %} - {% elif eligible_deeps|length == 1 %} - {% set locked_role = "deep" %} - {% endif %} - {% endif %} - - {% if locked_role %} -
-

- {% if game.current_scene_number == 1 %} - Based on starting Ranks, you are locked into: - {% elif player.previous_role == "deep" %} - You were Deep in the previous scene! You are locked into: - {% else %} - You are the only player eligible to play the Deep! You are locked into: - {% endif %} -

-
- {{ locked_role.upper() }} -
- -
-
- {% else %} -
- - - -
- {% endif %} -
- - -
-

Live Roster Selection

-
- {% include "scene_roster_snippet.html" %} -
-
-
- -
- {% if error_msg %} -
{{ error_msg }}
- {% endif %} - - - -
-
- - -
- -{% include "header_status_snippet.html" %} diff --git a/src/pirats/templates/techniques_snippet.html b/src/pirats/templates/techniques_snippet.html deleted file mode 100644 index 4a41a18..0000000 --- a/src/pirats/templates/techniques_snippet.html +++ /dev/null @@ -1,337 +0,0 @@ -
- {% if game.phase == "character_creation" %} -

IV. Create 3 Secret Techniques

-

Create 3 techniques that you will swap with other players. Make them cool or ridiculous!

- - {% set created_techs = json_loads(player.created_techniques) %} - {% if created_techs|length == 3 and created_techs[0] %} -
-
#1: {{ created_techs[0] }}
-
#2: {{ created_techs[1] }}
-
#3: {{ created_techs[2] }}
-

- - Techniques submitted! Waiting for other players to submit so we can shuffle and swap them. -

-
- {% else %} - -
-
- -
- - -
-
-
- -
- - -
-
-
- -
- - -
-
- -
- {% endif %} - - {% elif game.phase == "swap_techniques" %} -

IV. Assign Swapped Secret Techniques

-

You received these techniques from your crew. Assign one to Jack, Queen, and King cards.

- - {% set swapped_techs = json_loads(player.swapped_techniques) %} - - {% if player.is_ready %} -
-
J {{ player.tech_jack }}
-
Q {{ player.tech_queen }}
-
K {{ player.tech_king }}
-

- - Ready! Waiting for other players to finish J/Q/K assignment... -

-
- {% else %} -
- - - - - - - -
-

Available Swapped Techniques

-

Drag a technique onto a card slot below, or click/tap a technique and then click/tap a slot to assign it.

-
- {% for t in swapped_techs %} -
- ⋮⋮ {{ t }} -
- {% endfor %} -
-
- - -
- - -
-
-
J
-
- J - ♠ -
-
-
Jack
-
Drag or Click to Assign
-
-
- -
-
- - -
-
-
Q
-
- Q - â™Ĩ -
-
-
Queen
-
Drag or Click to Assign
-
-
- -
-
- - -
-
-
K
-
- K - â™Ļ -
-
-
King
-
Drag or Click to Assign
-
-
- -
-
- -
- - -
- - - {% endif %} - {% endif %} -
diff --git a/src/pirats/templates/voting_roster_snippet.html b/src/pirats/templates/voting_roster_snippet.html deleted file mode 100644 index 52d3621..0000000 --- a/src/pirats/templates/voting_roster_snippet.html +++ /dev/null @@ -1,10 +0,0 @@ -{% for p in game.players %} - {% set voted = False %} - {% for v in votes if v.voter_player_id == p.id %} - {% set voted = True %} - {% endfor %} -
- {{ p.name }} - {% if voted %}Voted{% else %}Voting...{% endif %} -
-{% endfor %} diff --git a/tests/test_game.py b/tests/test_game.py index b7bc434..8463775 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -293,26 +293,14 @@ def test_set_role_endpoint(): def get_session_override(): yield session - # Dynamically find the get_session function object used by the route - get_session_func = None - for route in app.routes: - if route.path == "/game/{game_id}/player/{player_id}/set-role": - for dep in route.dependant.dependencies: - if dep.name == "db": - get_session_func = dep.call - break - if get_session_func: - break - - assert get_session_func is not None, "Could not find get_session dependency in route" - app.dependency_overrides[get_session_func] = get_session_override + from pirats.database import get_session + app.dependency_overrides[get_session] = get_session_override client = TestClient(app) try: - response = client.post(f"/game/{game.id}/player/{player.id}/set-role?role=pirat") + response = client.post(f"/api/game/{game.id}/player/{player.id}/set-role", data={"role": "pirat"}) assert response.status_code == 200 - # The response should contain the rendered scene setup template - assert "Play Pi-Rat" in response.text + assert response.json()["status"] == "ok" # The database should be updated session.refresh(player) assert player.role == "pirat" @@ -344,24 +332,13 @@ def test_create_game_endpoint(): def get_session_override(): yield session - # Dynamically find the get_session function object used by the route - get_session_func = None - for route in app.routes: - if route.path == "/game/create": - for dep in route.dependant.dependencies: - if dep.name == "db": - get_session_func = dep.call - break - if get_session_func: - break - - assert get_session_func is not None, "Could not find get_session dependency in route" - app.dependency_overrides[get_session_func] = get_session_override + from pirats.database import get_session + app.dependency_overrides[get_session] = get_session_override client = TestClient(app, base_url="http://testserver") try: - response = client.post("/game/create", data={"crew_name": "The Math Mutineers"}) - assert response.status_code in [200, 303] # starlette redirects can be 303 + response = client.post("/api/game", data={"crew_name": "The Math Mutineers"}) + assert response.status_code == 200 # Verify the database has the game with the crew name from pirats.models import Game @@ -463,10 +440,9 @@ def test_submit_vote_endpoint(): client = TestClient(app) try: - response = client.post(f"/game/{game.id}/player/{p1.id}/submit-vote", data={"nominated_id": p2.id}) - # TestClient follows redirects by default + response = client.post(f"/api/game/{game.id}/player/{p1.id}/submit-vote", data={"nominated_id": p2.id}) assert response.status_code == 200 - assert "Vote submitted" in response.text + assert response.json()["status"] == "ok" finally: app.dependency_overrides.clear() @@ -505,3 +481,111 @@ def test_single_eligible_deep_player(session): success, msg = crud.confirm_scene_setup(session, game.id) assert success +def test_player_death_toggle(session): + game = crud.create_game(session) + player = crud.add_player(session, game.id, "DeadRat") + assert not player.is_dead + assert not player.completed_personal_3 + + # Toggle objective 3 + crud.toggle_objective(session, game.id, player.id, "personal_3", True) + session.refresh(player) + assert player.is_dead + assert player.completed_personal_3 + + # Toggle off + crud.toggle_objective(session, game.id, player.id, "personal_3", False) + session.refresh(player) + assert not player.is_dead + assert not player.completed_personal_3 + +def test_become_ghost_flow(session): + game = crud.create_game(session) + player = crud.add_player(session, game.id, "DeadRat") + player.is_dead = True + session.add(player) + session.commit() + + from fastapi.testclient import TestClient + from pirats.main import app + from pirats.database import get_session + + def get_session_override(): + yield session + + app.dependency_overrides[get_session] = get_session_override + client = TestClient(app) + + try: + response = client.post(f"/api/game/{game.id}/player/{player.id}/become-ghost") + assert response.status_code == 200 + session.refresh(player) + assert player.is_ghost + assert not player.is_dead + finally: + app.dependency_overrides.clear() + +def test_roll_new_character(session): + game = crud.create_game(session) + p1 = crud.add_player(session, game.id, "P1") + p2 = crud.add_player(session, game.id, "P2") # helper player for delegation target + + p1.is_dead = True + p1.rank = 3 + p1.hand_cards = json.dumps(["2C", "3D", "4H"]) + p1.completed_personal_1 = True + p1.completed_personal_2 = True + p1.completed_personal_3 = True + + session.add_all([p1, p2]) + session.commit() + + crud.roll_new_character( + session, + game.id, + p1.id, + avatar_look="Shiny eyes", + avatar_smell="Spiced Rum", + first_words="No fear!", + good_at_math="Yes" + ) + + session.refresh(p1) + session.refresh(game) + + # Verify properties + assert not p1.is_dead + assert not p1.is_ghost + assert not p1.completed_personal_1 + assert not p1.completed_personal_2 + assert not p1.completed_personal_3 + assert p1.avatar_look == "Shiny eyes" + assert p1.avatar_smell == "Spiced Rum" + assert p1.first_words == "No fear!" + assert p1.good_at_math == "Yes" + + # Verify rank randomized between 1-3 + assert p1.rank in [1, 2, 3] + + # Verify name is "Recruit Spiced Rum" + assert p1.name == "Recruit Spiced Rum" + + # Verify techniques auto-assigned + assert p1.tech_jack != "" + assert p1.tech_queen != "" + assert p1.tech_king != "" + + # Verify delegated targets exist (p2 is the only other player) + assert p1.other_like_from_player_id == p2.id + assert p1.other_hate_from_player_id == p2.id + + # Verify hand was refreshed up to new max hand size + hand = json.loads(p1.hand_cards) + assert len(hand) > 0 + # Old cards returned to deck + deck = json.loads(game.deck_cards) + assert "2C" in deck or "2C" in hand + assert "3D" in deck or "3D" in hand + assert "4H" in deck or "4H" in hand + +