├── src ├── lib │ ├── components │ │ ├── AboutPanel.svelte │ │ ├── SavesPanel.svelte │ │ ├── AchievementsPanel.svelte │ │ ├── ChapterContent.svelte │ │ ├── ChoiceButton.svelte │ │ ├── Header.svelte │ │ ├── SettingsPanel.svelte │ │ ├── ChoiceList.svelte │ │ ├── StatsPanel.svelte │ │ └── Menu.svelte │ ├── stores │ │ ├── state.ts │ │ ├── displaysettings.ts │ │ ├── stats.ts │ │ └── passagestore.ts │ └── wasm.ts ├── pkg │ ├── wasm_module_bg.wasm │ ├── package.json │ ├── wasm_module_bg.wasm.d.ts │ ├── wasm_module.d.ts │ └── wasm_module.js ├── routes │ ├── +layout.ts │ ├── +layout.svelte │ ├── play │ │ └── +page.svelte │ └── +page.svelte ├── app.html └── app.css ├── wasm_module ├── .gitignore ├── src │ ├── wasmtable.rs │ ├── utils.rs │ └── lib.rs ├── Cargo.toml └── Cargo.lock ├── src-tauri ├── build.rs ├── icons │ ├── 32x32.png │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── StoreLogo.png │ ├── Square30x30Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ └── Square310x310Logo.png ├── .gitignore ├── src │ ├── main.rs │ └── lib.rs ├── capabilities │ └── default.json ├── tauri.conf.json └── Cargo.toml ├── static ├── favicon.png ├── image.png ├── logo-red.png └── magium.story ├── .gitignore ├── svelte.config.js ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ └── deploy.yml └── pull_request_template.md ├── tsconfig.json ├── tailwind.config.ts ├── vite.config.js ├── package.json ├── CONTRIBUTING.md ├── README.md ├── CODE_OF_CONDUCT.md └── LICENSE /src/lib/components/AboutPanel.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/components/SavesPanel.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wasm_module/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /src/lib/components/AchievementsPanel.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/static/favicon.png -------------------------------------------------------------------------------- /static/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/static/image.png -------------------------------------------------------------------------------- /wasm_module/src/wasmtable.rs: -------------------------------------------------------------------------------- 1 | pub fn run_guard(fid: u32, gb: &Vec) -> bool{ 2 | true 3 | } 4 | -------------------------------------------------------------------------------- /static/logo-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/static/logo-red.png -------------------------------------------------------------------------------- /static/magium.story: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/static/magium.story -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src/pkg/wasm_module_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/src/pkg/wasm_module_bg.wasm -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Br3nnabee/magium-recrystallized/HEAD/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | magium_recrystallized_lib::run() 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/components/ChapterContent.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the main window", 5 | "windows": ["main"], 6 | "permissions": [ 7 | "core:default", 8 | "opener:default" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | // Tauri doesn't have a Node.js server to do proper SSR 2 | // so we will use adapter-static to prerender the app (SSG) 3 | // See: https://v2.tauri.app/start/frontend/sveltekit/ for more info 4 | export const prerender = true; 5 | export const ssr = false; 6 | 7 | import "../app.css"; 8 | -------------------------------------------------------------------------------- /src/pkg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm_module", 3 | "type": "module", 4 | "version": "0.1.0", 5 | "files": [ 6 | "wasm_module_bg.wasm", 7 | "wasm_module.js", 8 | "wasm_module.d.ts" 9 | ], 10 | "main": "wasm_module.js", 11 | "types": "wasm_module.d.ts", 12 | "sideEffects": [ 13 | "./snippets/*" 14 | ] 15 | } -------------------------------------------------------------------------------- /src-tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ 2 | #[cfg_attr(mobile, tauri::mobile_entry_point)] 3 | pub fn run() { 4 | tauri::Builder::default() 5 | .plugin(tauri_plugin_opener::init()) 6 | .run(tauri::generate_context!()) 7 | .expect("error while running tauri application"); 8 | } 9 | 10 | -------------------------------------------------------------------------------- /src/lib/components/ChoiceButton.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 13 | -------------------------------------------------------------------------------- /wasm_module/src/utils.rs: -------------------------------------------------------------------------------- 1 | pub fn set_panic_hook() { 2 | // When the `console_error_panic_hook` feature is enabled, we can call the 3 | // `set_panic_hook` function at least once during initialization, and then 4 | // we will get better error messages if our code ever panics. 5 | // 6 | // For more details see 7 | // https://github.com/rustwasm/console_error_panic_hook#readme 8 | #[cfg(feature = "console_error_panic_hook")] 9 | console_error_panic_hook::set_once(); 10 | } 11 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-static'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | const dev = process.env.NODE_ENV === 'development'; 5 | const isTauri = process.env.VITE_TAURI_BUILD === 'true'; 6 | const repo = 'magium-recrystallized'; 7 | 8 | /** @type {import('@sveltejs/kit').Config} */ 9 | const config = { 10 | preprocess: vitePreprocess(), 11 | kit: { 12 | adapter: adapter({ 13 | pages: 'build', 14 | assets: 'build', 15 | fallback: 'index.html' 16 | }), 17 | paths: { 18 | 19 | base: dev || isTauri ? '' : `/${repo}` 20 | } 21 | } 22 | }; 23 | 24 | export default config; 25 | 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Setup Node 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: 20 23 | 24 | - run: cargo install wasm-pack 25 | - run: npm ci 26 | - run: npm run build 27 | 28 | - name: Deploy 29 | uses: peaceiris/actions-gh-pages@v4 30 | with: 31 | github_token: ${{ secrets.GITHUB_TOKEN }} 32 | publish_dir: ./build 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const toVar = (shade: string) => `oklch(var(--color-gray-${shade}))` 4 | 5 | const config: Config = { 6 | content: [ 7 | './src/**/*.{html,js,ts,svelte,vue}', 8 | './index.html', 9 | ], 10 | theme: { 11 | extend: { 12 | colors: { 13 | gray: { 14 | 50: toVar('50'), 15 | 100: toVar('100'), 16 | 200: toVar('200'), 17 | 300: toVar('300'), 18 | 400: toVar('400'), 19 | 500: toVar('500'), 20 | 600: toVar('600'), 21 | 700: toVar('700'), 22 | 800: toVar('800'), 23 | 900: toVar('900'), 24 | 950: toVar('950'), 25 | }, 26 | }, 27 | }, 28 | }, 29 | } 30 | 31 | export default config 32 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 | {#if $ready} 27 | 28 | {:else} 29 |
30 | {/if} 31 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "magium-recrystallized", 4 | "version": "0.1.0", 5 | "identifier": "com.magium-recrystallized.app", 6 | "build": { 7 | "beforeDevCommand": "npm run dev", 8 | "devUrl": "http://localhost:1420", 9 | "beforeBuildCommand": "npm run build:tauri", 10 | "frontendDist": "../build" 11 | }, 12 | "app": { 13 | "windows": [ 14 | { 15 | "title": "magium-recrystallized", 16 | "width": 800, 17 | "height": 600 18 | } 19 | ], 20 | "security": { 21 | "csp": null 22 | } 23 | }, 24 | "bundle": { 25 | "active": true, 26 | "targets": "all", 27 | "icon": [ 28 | "icons/32x32.png", 29 | "icons/128x128.png", 30 | "icons/128x128@2x.png", 31 | "icons/icon.icns", 32 | "icons/icon.ico" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 17 | 18 | 19 | Magium: Recrystallized 20 | %sveltekit.head% 21 | 22 | 23 | 24 |
%sveltekit.body%
25 | 26 | 27 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### 📌 Summary 2 | 3 | Briefly describe the changes made in this pull request. 4 | 5 | ### 🧾 Related Issues 6 | 7 | Closes #[issue_number] or Related to #[issue_number] 8 | 9 | ### 🧪 Type of Change 10 | 11 | Please delete options that are not relevant. 12 | 13 | - Bug fix 14 | - New feature 15 | - Breaking change 16 | - Refactor 17 | - Documentation update 18 | - Other (please describe): 19 | 20 | ### 📸 Screenshots / Demo 21 | 22 | If your changes affect the UI or behavior, add screenshots or a short video/gif. 23 | 24 | ### 🔍 Checklist 25 | 26 | - I have tested my changes locally 27 | - I have updated documentation where relevant 28 | - My changes follow the project's code style 29 | - I have linked relevant issues or discussions 30 | - I have not introduced any security or performance regressions 31 | 32 | ### 📚 Additional Notes 33 | 34 | Anything else reviewers should know? 35 | 36 | --- 37 | 38 | Thank you for your contribution to _Magium Recrystallized_! ✨ 39 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { sveltekit } from "@sveltejs/kit/vite"; 3 | import tailwindcss from '@tailwindcss/vite'; 4 | 5 | // @ts-expect-error process is a nodejs global 6 | const host = process.env.TAURI_DEV_HOST; 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig(async () => ({ 10 | plugins: [ 11 | tailwindcss(), 12 | sveltekit(), 13 | ], 14 | 15 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 16 | // 17 | // 1. prevent vite from obscuring rust errors 18 | clearScreen: false, 19 | // 2. tauri expects a fixed port, fail if that port is not available 20 | server: { 21 | port: 1420, 22 | strictPort: true, 23 | host: host || false, 24 | hmr: host 25 | ? { 26 | protocol: "ws", 27 | host, 28 | port: 1421, 29 | } 30 | : undefined, 31 | watch: { 32 | // 3. tell vite to ignore watching `src-tauri` 33 | ignored: ["**/src-tauri/**"], 34 | }, 35 | }, 36 | })); 37 | -------------------------------------------------------------------------------- /src/lib/components/Header.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
13 | 19 | 20 |
21 |

22 | BOOK {book} 23 |

24 | 25 |

26 | CHAPTER {chapter} 27 |

28 |
29 | 30 | 36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "magium-recrystallized" 3 | version = "0.1.0" 4 | description = "A Tauri App" 5 | authors = ["you"] 6 | edition = "2021" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [profile.release] 11 | opt-level = "s" # Optimize for size 12 | lto = true # Enable Link Time Optimization 13 | codegen-units = 1 # Reduce codegen units for better optimization 14 | panic = "abort" # Simplify panic handling 15 | strip = true # Remove debug symbols - breaks appimage bundling on arch 16 | 17 | [lib] 18 | # The `_lib` suffix may seem redundant but it is necessary 19 | # to make the lib name unique and wouldn't conflict with the bin name. 20 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 21 | name = "magium_recrystallized_lib" 22 | crate-type = ["staticlib", "cdylib", "rlib"] 23 | 24 | [build-dependencies] 25 | tauri-build = { version = "2", features = [] } 26 | 27 | [dependencies] 28 | tauri = { version = "2", features = [] } 29 | tauri-plugin-opener = "2" 30 | serde = { version = "1", features = ["derive"] } 31 | serde_json = "1" 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magium-recrystallized", 3 | "version": "0.1.0", 4 | "description": "", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "npm run wasm && vite dev", 8 | "build": "npm run wasm && vite build", 9 | "build:tauri": "VITE_TAURI_BUILD=true vite build", 10 | "preview": "vite preview", 11 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 12 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 13 | "tauri": "tauri", 14 | "wasm": "wasm-pack build ./wasm_module --target web" 15 | }, 16 | "license": "AGPL-3.0-only", 17 | "dependencies": { 18 | "@humanspeak/svelte-markdown": "^0.8.3", 19 | "@tauri-apps/api": "^2", 20 | "@tauri-apps/plugin-opener": "^2" 21 | }, 22 | "devDependencies": { 23 | "@sveltejs/adapter-static": "^3.0.6", 24 | "@sveltejs/kit": "^2.9.0", 25 | "@sveltejs/vite-plugin-svelte": "^5.0.0", 26 | "@tailwindcss/vite": "^4.1.4", 27 | "@tauri-apps/cli": "^2", 28 | "svelte": "^5.0.0", 29 | "svelte-check": "^4.0.0", 30 | "tailwindcss": "^4.1.4", 31 | "typescript": "~5.6.2", 32 | "vite": "^6.0.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Mobile (please complete the following information):** 27 | - Device: [e.g. iPhone6] 28 | - OS: [e.g. iOS8.1] 29 | - Version [e.g. 22] 30 | 31 | **Web (please complete the following information):** 32 | - Hardware/Device: [e.g. iPhone6] 33 | - Browser: [e.g. Chrome, Firefox] 34 | - OS: [e.g. Windows] 35 | - Version [e.g. 22] 36 | 37 | **Desktop (please complete the following information):** 38 | - Hardware/Device: [e.g. Intel i9-13900k, Nvidia RTX 3090 ] 39 | - OS: [e.g. Windows] 40 | - Version [e.g. 22] 41 | 42 | **Additional context** 43 | Add any other context about the problem here. 44 | -------------------------------------------------------------------------------- /src/routes/play/+page.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 |
26 |
27 |
28 | 29 |
30 | 31 |
32 |
37 | 38 |
39 | 40 |
41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /wasm_module/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm_module" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "rlib"] 8 | 9 | [dependencies] 10 | cfg-if = "1.0.0" 11 | byteorder = "1.4" 12 | zstd = { version = "0.11", features = ["legacy"] } 13 | wasm-bindgen = "0.2" 14 | wasm-bindgen-futures = "0.4" 15 | js-sys = "0.3" 16 | web-sys = { version = "0.3", features = [ 17 | "Window", 18 | "Request", 19 | "RequestInit", 20 | "RequestMode", 21 | "Response", 22 | "Headers", 23 | "console", 24 | ] } 25 | zstd-safe = "7.2" 26 | serde = { version = "1.0.219" } 27 | serde-wasm-bindgen = "*" 28 | futures = "*" 29 | 30 | # The `console_error_panic_hook` crate provides better debugging of panics by 31 | # logging them with `console.error`. This is great for development, but requires 32 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for 33 | # code size when deploying. 34 | console_error_panic_hook = { version = "0.1.7", optional = true } 35 | 36 | # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size 37 | # compared to the default allocator's ~10K. It is slower than the default 38 | # allocator, however. 39 | # 40 | # Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. 41 | wee_alloc = { version = "0.4.5", optional = true } 42 | 43 | [dev-dependencies] 44 | wasm-bindgen-test = "0.3.50" 45 | 46 | [profile.release] 47 | # Tell `rustc` to optimize for small code size. 48 | opt-level = "s" 49 | 50 | [features] 51 | # default = ["console_error_panic_hook" ,"wee_alloc"] 52 | default = ["console_error_panic_hook"] 53 | -------------------------------------------------------------------------------- /src/pkg/wasm_module_bg.wasm.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export const memory: WebAssembly.Memory; 4 | export const __wbg_cyoagame_free: (a: number, b: number) => void; 5 | export const cyoagame_new: (a: number, b: number) => any; 6 | export const cyoagame_chunk_ids: (a: number) => any; 7 | export const cyoagame_load_node_full: (a: number, b: number) => any; 8 | export const cyoagame_load_root_node_full: (a: number) => any; 9 | export const __wasm_start: () => void; 10 | export const rust_zstd_wasm_shim_qsort: (a: number, b: number, c: number, d: number) => void; 11 | export const rust_zstd_wasm_shim_malloc: (a: number) => number; 12 | export const rust_zstd_wasm_shim_memcmp: (a: number, b: number, c: number) => number; 13 | export const rust_zstd_wasm_shim_calloc: (a: number, b: number) => number; 14 | export const rust_zstd_wasm_shim_free: (a: number) => void; 15 | export const rust_zstd_wasm_shim_memcpy: (a: number, b: number, c: number) => number; 16 | export const rust_zstd_wasm_shim_memmove: (a: number, b: number, c: number) => number; 17 | export const rust_zstd_wasm_shim_memset: (a: number, b: number, c: number) => number; 18 | export const __wbindgen_exn_store: (a: number) => void; 19 | export const __externref_table_alloc: () => number; 20 | export const __wbindgen_export_2: WebAssembly.Table; 21 | export const __wbindgen_free: (a: number, b: number, c: number) => void; 22 | export const __wbindgen_malloc: (a: number, b: number) => number; 23 | export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; 24 | export const __wbindgen_export_6: WebAssembly.Table; 25 | export const closure60_externref_shim: (a: number, b: number, c: any) => void; 26 | export const closure73_externref_shim: (a: number, b: number, c: any, d: any) => void; 27 | export const __wbindgen_start: () => void; 28 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @custom-variant dark (&:where(.dark, .dark *)); 3 | 4 | /* This simply exists to set custom themes */ 5 | 6 | :root { 7 | /* Default Tailwind gray (cool/blueish) */ 8 | --color-gray-50: oklch(0.985 0.002 247.839); 9 | --color-gray-100: oklch(0.967 0.003 264.542); 10 | --color-gray-200: oklch(0.928 0.006 264.531); 11 | --color-gray-300: oklch(0.872 0.01 258.338); 12 | --color-gray-400: oklch(0.707 0.022 261.325); 13 | --color-gray-500: oklch(0.551 0.027 264.364); 14 | --color-gray-600: oklch(0.446 0.03 256.802); 15 | --color-gray-700: oklch(0.373 0.034 259.733); 16 | --color-gray-800: oklch(0.278 0.033 256.848); 17 | --color-gray-900: oklch(0.21 0.034 264.665); 18 | --color-gray-950: oklch(0.13 0.028 261.692); 19 | } 20 | 21 | [data-theme="warm"] { 22 | /* Warmer colors */ 23 | --color-gray-50: oklch(0.985 0.0014 20); 24 | --color-gray-100: oklch(0.967 0.0022 20); 25 | --color-gray-200: oklch(0.928 0.0043 20); 26 | --color-gray-300: oklch(0.872 0.0072 20); 27 | --color-gray-400: oklch(0.707 0.0158 20); 28 | --color-gray-500: oklch(0.551 0.0194 20); 29 | --color-gray-600: oklch(0.446 0.0216 20); 30 | --color-gray-700: oklch(0.373 0.0245 20); 31 | --color-gray-800: oklch(0.278 0.0238 20); 32 | --color-gray-900: oklch(0.21 0.0245 20); 33 | --color-gray-950: oklch(0.13 0.0202 20); 34 | } 35 | 36 | [data-theme="neutral"] { 37 | /* Fully neutral (no chroma or hue) */ 38 | --color-gray-50: oklch(0.985 0 0); 39 | --color-gray-100: oklch(0.97 0 0); 40 | --color-gray-200: oklch(0.922 0 0); 41 | --color-gray-300: oklch(0.87 0 0); 42 | --color-gray-400: oklch(0.708 0 0); 43 | --color-gray-500: oklch(0.556 0 0); 44 | --color-gray-600: oklch(0.439 0 0); 45 | --color-gray-700: oklch(0.371 0 0); 46 | --color-gray-800: oklch(0.269 0 0); 47 | --color-gray-900: oklch(0.205 0 0); 48 | --color-gray-950: oklch(0.145 0 0); 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/stores/state.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Manages the UI state of the app using Svelte stores. 3 | * Provides primary modes (Game, Menu, Stats) and optional menu sub-modes. 4 | */ 5 | 6 | import { writable } from 'svelte/store'; 7 | 8 | /** Main app screens */ 9 | export enum PrimaryState { 10 | Game = 'Game', 11 | Menu = 'Menu', 12 | Stats = 'Stats', 13 | } 14 | 15 | /** Specific menu tabs when in Menu mode */ 16 | export enum MenuSubstate { 17 | Achievements = 'Achievements', 18 | Saves = 'Saves', 19 | Settings = 'Settings', 20 | About = 'About', 21 | } 22 | 23 | /** 24 | * Full UI state shape: 25 | * - Always has a primary mode 26 | * - When primary is Menu, may specify a substate 27 | */ 28 | export type UIState = 29 | | { primary: PrimaryState.Game } 30 | | { primary: PrimaryState.Stats } 31 | | { primary: PrimaryState.Menu; substate?: MenuSubstate }; 32 | 33 | /** Current UI state store (default: Game) */ 34 | export const uiState = writable({ primary: PrimaryState.Game }); 35 | 36 | /** Switch to Game screen */ 37 | export function openGame() { 38 | uiState.set({ primary: PrimaryState.Game }); 39 | } 40 | 41 | /** Switch to Stats screen */ 42 | export function openStats() { 43 | uiState.set({ primary: PrimaryState.Stats }); 44 | } 45 | 46 | /** 47 | * Open Menu screen. 48 | * @param substate Optional specific menu tab to show 49 | */ 50 | export function openMenu(substate?: MenuSubstate) { 51 | uiState.set({ primary: PrimaryState.Menu, substate }); 52 | } 53 | 54 | /** Close Menu and return to Game screen */ 55 | export function closeMenu() { 56 | uiState.set({ primary: PrimaryState.Game }); 57 | } 58 | 59 | /** 60 | * Toggle between Menu and Game screens. 61 | * When opening Menu, no substate is set by default. 62 | */ 63 | export function toggleMenu() { 64 | uiState.update((s) => 65 | s.primary === PrimaryState.Menu 66 | ? { primary: PrimaryState.Game } 67 | : { primary: PrimaryState.Menu } 68 | ); 69 | } 70 | 71 | -------------------------------------------------------------------------------- /wasm_module/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # CYOA WASM Game Loader 2 | //! 3 | //! This crate provides the WebAssembly entry point and high-level 4 | //! initialization for the CYOA game loader module. It wires up panic hooks, 5 | //! an optional `wee_alloc` global allocator, and exposes the core `decoder` 6 | //! functionality for use in JavaScript via `wasm_bindgen`. 7 | //! 8 | //! ## Features 9 | //! 10 | //! - **Optional Global Allocator**: Enable the `wee_alloc` feature for a 11 | //! minimal allocator suitable for size-constrained WASM builds. 12 | //! - **Panic Hook**: Installs a panic hook that sends Rust panic messages 13 | //! to the browser console via `console.error`, simplifying debugging. 14 | //! - **Auto-Initialization**: Uses `#[wasm_bindgen(start)]` to automatically 15 | //! initialize the module when loaded in JavaScript. 16 | 17 | extern crate cfg_if; 18 | extern crate wasm_bindgen; 19 | 20 | /// Core decoding logic for the CYOA format. 21 | /// 22 | /// The `decoder` module implements the `CyoaGame` struct and its associated 23 | /// methods, including HTTP range probing, TLV parsing, zstd decompression, 24 | /// and the public WASM-bindgen interface. 25 | mod decoder; 26 | 27 | mod wasmtable; 28 | 29 | /// Utility helpers and browser integration code. 30 | /// 31 | /// The `utils` module provides support routines such as setting 32 | /// up a panic hook for better error reporting in the browser. 33 | mod utils; 34 | 35 | use cfg_if::cfg_if; 36 | use wasm_bindgen::prelude::*; 37 | use utils::set_panic_hook; 38 | 39 | cfg_if! { 40 | if #[cfg(feature = "wee_alloc")] { 41 | extern crate wee_alloc; 42 | /// Use `wee_alloc` as the global allocator to minimize WASM binary size. 43 | #[global_allocator] 44 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 45 | } 46 | } 47 | 48 | /// Entry point invoked by `wasm_bindgen` when the module is instantiated. 49 | /// 50 | /// Installs the panic hook so that any Rust panics are forwarded to the 51 | /// browser console as `console.error` messages, improving runtime 52 | /// diagnostics when using the module from JavaScript. 53 | #[wasm_bindgen(start)] 54 | pub fn __wasm_start() { 55 | set_panic_hook(); 56 | } 57 | 58 | -------------------------------------------------------------------------------- /src/lib/stores/displaysettings.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Manages user interface preferences: text width, color theme, and responsive max-width flag. 3 | * Uses Svelte stores backed by localStorage and window events. 4 | */ 5 | 6 | import { readable, writable } from 'svelte/store'; 7 | 8 | /** Available text width options */ 9 | export enum TextWidth { 10 | Full = "full", 11 | Medium = "medium", 12 | Low = "low", 13 | } 14 | 15 | // Attempt to read saved text width from localStorage 16 | const storedWidth = localStorage.getItem("textWidth") as TextWidth | null; 17 | // Validate saved value or default to Full 18 | const initialWidth: TextWidth = 19 | storedWidth && Object.values(TextWidth).includes(storedWidth) 20 | ? storedWidth 21 | : TextWidth.Full; 22 | 23 | /** Writable store for text width preference */ 24 | export const textWidthStore = writable(initialWidth); 25 | 26 | // Persist text width changes to localStorage 27 | textWidthStore.subscribe((width) => { 28 | localStorage.setItem("textWidth", width); 29 | }); 30 | 31 | /** Available color theme options */ 32 | export enum ColorTheme { 33 | Neutral = "neutral", 34 | Cool = "cool", 35 | Warm = "warm", 36 | } 37 | 38 | /** 39 | * Apply a color theme to the document root and save it. 40 | * @param theme - Selected ColorTheme value 41 | */ 42 | function applyColorTheme(theme: ColorTheme) { 43 | document.documentElement.dataset.theme = theme; 44 | localStorage.setItem("colorTheme", theme); 45 | } 46 | 47 | // Initialize theme from localStorage or default to Neutral 48 | const storedColor = localStorage.getItem("colorTheme") as ColorTheme; 49 | export const colorThemeStore = writable( 50 | storedColor && Object.values(ColorTheme).includes(storedColor) 51 | ? storedColor 52 | : ColorTheme.Neutral 53 | ); 54 | 55 | // Apply theme on changes 56 | colorThemeStore.subscribe(applyColorTheme); 57 | 58 | /** 59 | * Pixel threshold above which the UI should use its max-width layout. 60 | * Computed as 28rem (assuming 16px base) * 2.2 scaling factor. 61 | */ 62 | const THRESHOLD = 28 * 16 * 2.2; 63 | 64 | /** 65 | * Readable store that tracks if window width exceeds THRESHOLD. 66 | * Useful for responsive layout decisions. 67 | */ 68 | export const useMaxWidth = readable(false, (set) => { 69 | function update() { 70 | set(window.innerWidth > THRESHOLD); 71 | } 72 | 73 | if (typeof window !== 'undefined') { 74 | update(); 75 | window.addEventListener('resize', update, { passive: true }); 76 | } 77 | 78 | // Cleanup listener on unsubscribe 79 | return () => { 80 | window.removeEventListener('resize', update); 81 | }; 82 | }); 83 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Magium Recrystallized 2 | 3 | Thank you for considering contributing to *Magium Recrystallized*! We welcome all forms of contributions - code, design, writing, bug reports, feature suggestions, and more. 4 | 5 | ## 📋 Code of Conduct 6 | By participating in this project, you agree to abide by our [Code of Conduct](CODE_OF_CONDUCT.md). Be respectful and considerate in your interactions. 7 | 8 | ## 🛠️ How to Contribute 9 | ### 1. Set Up the Project 10 | Make sure you can run the game locally. Instructions can be found in the [README](https://github.com/Br3nnabee/magium-recrystallized/blob/main/README.md) 11 | 12 | ### 2. Pick an Issue or Suggest One 13 | Check the [Issues](https://github.com/br3nnabee/magium-recrystallized/issues) tab. Look for labels such as `good first issue` or `help wanted`. You may also open a new issue to propose improvements or report bugs. 14 | 15 | ### 3. Branch and Code 16 | Create a descriptive branch name: 17 | ```bash 18 | git checkout -b feature/AmazingFeature 19 | ``` 20 | Follow our conventions and ensure your code is clean and well-documented. 21 | 22 | ### 4. Test Your Changes 23 | Please test your changes thoroughly. Make sure you haven’t broken existing features or introduced bugs. Although we don't require integration or unit tests, you may use them on your end and leave them in during your commit. 24 | 25 | ### 5. Commit and Push 26 | Use meaningful commit messages: 27 | ```bash 28 | git commit -m "feat: add new amazing feature using this and that" 29 | git push origin feature/AmazingFeature 30 | ``` 31 | 32 | ### 6. Open a Pull Request 33 | Submit a pull request from your branch to the `dev` branch. Fill out the PR template to help us understand your changes. 34 | 35 | ## 🧪 Coding Standards 36 | - Prefer descriptive variable and function names. 37 | - Keep logic modular and components isolated. 38 | - Follow formatting rules enforced by the linter. 39 | 40 | ## 📄 Project Structure Overview 41 | ``` 42 | ┌── src-tauri/ # Code for the tauri builds. Largely untouched. 43 | ├── src/ # Code for the web app. 44 | │ ├── lib/ # Utility functions etc. 45 | | | ├── components/ # Components that are loaded with svelte. 46 | | | ├── stores/ # Typescript functions defining things stored. 47 | | └── routes/ # The different pages. Largely untouched. 48 | ├── static/ # All static content (images etc.). 49 | └── wasm_module/ # Code for heavier logic such as decoding. 50 | ``` 51 | 52 | ## ❤️ A Note of Thanks 53 | This project exists thanks to contributors like you. Whether you’re here to fix a typo or design a whole new module, your input is valuable. 54 | 55 | Thank you for helping bring *Magium Recrystallized* to life. 56 | 57 | — Br3nnabee and the Magium Community 58 | -------------------------------------------------------------------------------- /src/lib/stores/stats.ts: -------------------------------------------------------------------------------- 1 | import { writable, type Writable } from 'svelte/store'; 2 | 3 | /** Base stat keys */ 4 | export const baseKeys = [ 5 | 'Strength', 6 | 'Speed', 7 | 'Toughness', 8 | 'Reflexes', 9 | 'Hearing', 10 | 'Observation', 11 | 'Ancient Languages', 12 | 'Combat Technique', 13 | 'Premonition', 14 | 'Bluff', 15 | 'Magical sense', 16 | 'Aura hardening' 17 | ] as const; 18 | export type StatKey = typeof baseKeys[number]; 19 | 20 | /** Extra top-level metrics */ 21 | export type ExtraKey = 'magicalPower' | 'magicalKnowledge' | 'availablePoints'; 22 | 23 | /** Union of all metric keys */ 24 | export type AllKeys = StatKey | ExtraKey; 25 | 26 | /** Central stats shape */ 27 | export type Stats = Record; 28 | 29 | export interface StatDelta { 30 | key: AllKeys; 31 | oldValue: number; 32 | newValue: number; 33 | timestamp: Date; 34 | } 35 | 36 | export interface DeltasStore { 37 | subscribe: Writable['subscribe']; 38 | clear: () => void; 39 | } 40 | 41 | export interface StatsStore extends Writable { 42 | deltas: DeltasStore; 43 | addStat: (key: AllKeys, amount: number) => void; 44 | resetDeltas: () => void; 45 | } 46 | 47 | function createStatsStore(): StatsStore { 48 | // Initialize all stats + extras 49 | const initial: Stats = { 50 | // Base stats 51 | Strength: 0, 52 | Speed: 0, 53 | Toughness: 0, 54 | Reflexes: 0, 55 | Hearing: 0, 56 | Observation: 0, 57 | 'Ancient Languages': 0, 58 | 'Combat Technique': 0, 59 | Premonition: 0, 60 | Bluff: 0, 61 | 'Magical sense': 0, 62 | 'Aura hardening': 0, 63 | 64 | // Special metrics 65 | magicalPower: 1, 66 | magicalKnowledge: 1, 67 | 68 | // Pool of points 69 | availablePoints: 4, 70 | }; 71 | 72 | const stats = writable(initial); 73 | const deltasInternal = writable([]); 74 | 75 | const deltas: DeltasStore = { 76 | subscribe: deltasInternal.subscribe, 77 | clear: () => deltasInternal.set([]), 78 | }; 79 | 80 | function addStat(key: AllKeys, amount: number) { 81 | stats.update(curr => { 82 | let delta = amount; 83 | const oldValue = curr[key]; 84 | 85 | // If updating a base stat, enforce max 4 and availablePoints 86 | if ((baseKeys as readonly string[]).includes(key)) { 87 | const maxIncrease = Math.min( 88 | delta, 89 | curr.availablePoints, 90 | 4 - oldValue 91 | ); 92 | if (maxIncrease <= 0) { 93 | console.warn(`Cannot increase ${key} beyond limits`); 94 | return curr; 95 | } 96 | delta = maxIncrease; 97 | } 98 | 99 | const newValue = oldValue + delta; 100 | 101 | // Log the change 102 | deltasInternal.update(log => [ 103 | ...log, 104 | { key, oldValue, newValue, timestamp: new Date() } 105 | ]); 106 | 107 | // Build next state 108 | const next: Stats = { 109 | ...curr, 110 | [key]: newValue, 111 | // Deduct pool if base stat 112 | ...((baseKeys as readonly string[]).includes(key) && { 113 | availablePoints: curr.availablePoints - delta 114 | }) 115 | }; 116 | 117 | return next; 118 | }); 119 | } 120 | 121 | function resetDeltas() { 122 | deltas.clear(); 123 | } 124 | 125 | return { 126 | subscribe: stats.subscribe, 127 | set: stats.set, 128 | update: stats.update, 129 | deltas, 130 | addStat, 131 | resetDeltas 132 | }; 133 | } 134 | 135 | export const statsStore = createStatsStore(); 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Magium Recrystallized 2 | 3 | A community recreation & continuation of Magium, a popular choose-your-own-adventure (CYOA) game by author Cristian Mihailescu. 4 | 5 | ## Table of Contents 6 | 7 | - [Introduction](#introduction) 8 | - [Features](#features) 9 | - [Installation](#installation) 10 | - [Usage](#usage) 11 | - [Contributing](#contributing) 12 | - [Documentation](#documentation) 13 | - [License](#license) 14 | - [Acknowledgments](#acknowledgments) 15 | 16 | ## Introduction 17 | 18 | Magium Recrystallized revives the highly loved narrative-driven experience, allowing players to create and explore meaningful decisions in a fantasy world. Driven by its passionate community, this version not only aims to faithfully recreate the original experience, but modernize its platform and continue the story past the 3 books originally written. 19 | 20 | ## Features 21 | 22 | - **Engaging Narrative:** Experience deep, immersive storytelling with branching plotlines. 23 | - **Community Created Content:** Regular updates made by the writing team continuing the story of Magium. 24 | - **Cross-Platform:** Packaged via Tauri, supporting most desktop and mobile platforms with fast performance and low footprint. 25 | - **Modern Stack:** Built using Svelte, Typescript, and WebAssembly for a responsive and lightweight experience. 26 | 27 | ## Installation 28 | 29 | ### Prerequisites 30 | 31 | - Node.js v18+ 32 | - Rust 2024 (with `wasm32-unknown-unknown target`) 33 | - Tauri CLI (`cargo install tauri-cli`) 34 | - Wasm Pack (`cargo install wasm-pack`) 35 | - Git 36 | 37 | ### Steps 38 | 39 | ```bash 40 | # Clone the repository 41 | git clone https://github.com/Br3nnabee/magium-recrystallized.git 42 | 43 | # Navigate to project directory 44 | cd magium-recrystallized 45 | 46 | # Install Node dependencies 47 | npm install 48 | 49 | # Build the Rust/WASM backend 50 | npm run tauri build 51 | ``` 52 | 53 | ## Usage 54 | 55 | To launch the development version of _Magium Recrystallized_: 56 | 57 | ```bash 58 | # Start development server with Tauri 59 | npm run tauri dev 60 | ``` 61 | 62 | To build a release version: 63 | 64 | ```bash 65 | # Build production release 66 | npm run tauri build 67 | ``` 68 | 69 | ## Contributing 70 | 71 | We welcome contributions! Here's how you can help: 72 | 73 | 1. Fork the repository. 74 | 2. Create your Feature Branch from Dev (`git checkout -b feature/AmazingFeature`) 75 | 3. Commit your changes (`git commit -m 'Add AmazingFeature'`) 76 | 4. Push to the Branch (`git push origin feature/Amazingfeature`) 77 | 5. Open a Pull Request. 78 | 79 | Please follow our coding guidelines and check for open issues before starting major changes. 80 | See the [CONTRIBUTING.md](CONTRIBUTING.md) file for detailed instructions and standards. 81 | 82 | ## Documentation 83 | 84 | Unfortunately, due to the complexity associated with this project and the currently sole developer focusing on simply outputting a finished project ASAP, integrated documentation is not available. That said, comments are included as much as possible, and for the wasm module decent documentation can be easily viewed via `cargo doc --document-private-items --open`. Once more people begin contributing, there will be a concerted effort made to improve the documentation to the point that it is highly browsable, but for the time being this will have to suffice. 85 | 86 | ## License 87 | 88 | Distributed under the AGPL 3.0 License. See [LICENSE](LICENSE) for more information. 89 | 90 | ## Acknowledgments 91 | 92 | - Cristian Mihailescu – original creator of Magium 93 | - Magium Recrystallized contributors – supporting and improving the app 94 | - Writer Team - continuing the story of Magium 95 | - [Magium-SDL](https://github.com/Colaboi2009/Magium-SDL) and [magium-dev](https://github.com/thuiop/magium-dev) - inspiration and reference for this implementation 96 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Magium Recrystallized Code of Conduct 2 | 3 | Like the technical community as a whole, the Magium Recrystallized team and community is made up of a mixture of professionals and volunteers from all over the world, working on every aspect of the mission - including mentorship, teaching, and connecting people. 4 | 5 | Diversity is one of our huge strengths, but it can also lead to communication issues and unhappiness. To that end, we have a few ground rules that we ask people to adhere to. This code applies equally to founders, mentors and those seeking help and guidance. 6 | 7 | This isn’t an exhaustive list of things that you can’t do. Rather, take it in the spirit in which it’s intended - a guide to make it easier to enrich all of us and the technical communities in which we participate. 8 | 9 | This code of conduct applies to all spaces managed by the Magium Recrystallized project. This includes the issue tracker, the discussions panel,, and any other forums created by the project team which the community uses for communication. In addition, violations of this code outside these spaces may affect a person's ability to participate within them. 10 | 11 | If you believe someone is violating the code of conduct, we ask that you report it by sending a message to br3nnabee on discord. 12 | 13 | - **Be friendly and patient.** 14 | - **Be welcoming.** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. 15 | - **Be considerate.** Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language. 16 | - **Be respectful.** Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the Magium Recrystallized community should be respectful when dealing with other members as well as with people outside the Magium Recrystallized community. 17 | - **Be careful in the words that you choose.** We are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to: 18 | - Violent threats or language directed against another person. 19 | - Discriminatory jokes and language. 20 | - Posting sexually explicit or violent material. 21 | - Posting (or threatening to post) other people's personally identifying information ("doxing"). 22 | - Personal insults, especially those using racist or sexist terms. 23 | - Unwelcome sexual attention. 24 | - Advocating for, or encouraging, any of the above behavior. 25 | - Repeated harassment of others. In general, if someone asks you to stop, then stop. 26 | - **When we disagree, try to understand why.** Disagreements, both social and technical, happen all the time and Magium Recrystallized is no exception. It is important that we resolve disagreements and differing views constructively. Remember that we’re different. The strength of Magium Recrystallized comes from its varied community, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes. 27 | 28 | Original text courtesy of the [Speak Up! project](http://web.archive.org/web/20141109123859/http://speakup.io/coc.html). 29 | 30 | ## Questions? 31 | 32 | If you have questions, please see the [Q&A](https://github.com/Br3nnabee/magium-recrystallized/discussions/categories/q-a). If that doesn't answer your questions, feel free to submit a question or DM br3nnabee on discord. 33 | -------------------------------------------------------------------------------- /src/lib/stores/passagestore.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This module provides a Svelte store interface for interacting with the 3 | * webassembly module. It initializes the WASM runtime, fetches nodes from 4 | * the story file, caches them, and offers reactive stores for the current 5 | * story node and navigation. 6 | */ 7 | 8 | import { ready, fetchRootNodeFull, fetchNodeFull } from '$lib/wasm'; 9 | import { writable, derived, get } from 'svelte/store'; 10 | 11 | /** 12 | * Describes a single choice edge from one story node to another. 13 | */ 14 | export interface EdgeInfo { 15 | /** 16 | * Label text displayed for this choice. 17 | */ 18 | label: string; 19 | /** 20 | * Numeric index of the destination node when this choice is selected. 21 | */ 22 | dest: number; 23 | } 24 | 25 | /** 26 | * Represents a story node (scene) in the CYOA engine. 27 | */ 28 | export interface StoryNode { 29 | /** 30 | * The narrative content or description for this node. 31 | */ 32 | content: string; 33 | /** 34 | * Array of outgoing edges (choices) from this node. 35 | */ 36 | edges: EdgeInfo[]; 37 | } 38 | 39 | /** 40 | * In-memory cache mapping node indices to their loaded StoryNode data. 41 | * Uses a Svelte writable store to trigger reactive updates on cache changes. 42 | */ 43 | export const nodeCache = writable>(new Map()); 44 | 45 | /** 46 | * Svelte store holding the index of the currently active node in the story. 47 | */ 48 | export const currentIndex = writable(0); 49 | 50 | /** 51 | * Derived store that returns the StoryNode corresponding to currentIndex. 52 | * Falls back to a placeholder node if the requested index is not yet cached. 53 | */ 54 | export const currentNode = derived( 55 | [nodeCache, currentIndex], 56 | ([$cache, $idx]): StoryNode => 57 | $cache.get($idx) ?? { content: 'Loading…', edges: [] } 58 | ); 59 | 60 | /** 61 | * Initialize the WASM runtime and load the root node (index 0) into cache. 62 | * 63 | * Steps: 64 | * 1. Await the WASM module initialization (ready promise). 65 | * 2. Fetch the root node data via the Rust-generated API. 66 | * 3. Store the root node under index 0 in the cache. 67 | * 4. Set currentIndex to 0, triggering subscribers to display the root. 68 | * 69 | * @returns A Promise that resolves once initialization and caching are complete. 70 | */ 71 | export async function initialize(): Promise { 72 | // Ensure the WASM runtime is loaded 73 | await ready; 74 | 75 | // Retrieve root node from the WASM engine 76 | const root = await fetchRootNodeFull(); 77 | 78 | // Cache the root node and navigate to it 79 | nodeCache.update((m) => m.set(0, root)); 80 | currentIndex.set(0); 81 | } 82 | 83 | /** 84 | * Load a specific story node by its index, cache it, and attempt to prefetch its children. 85 | * 86 | * - If the node is already cached, this is a no-op. 87 | * - Otherwise, it fetches the node, updates the cache, and 88 | * kicks off background fetches for all child nodes (edges.dest). 89 | * - Errors during fetch are caught and logged; the cache is updated with an error node. 90 | * 91 | * @param idx - Numeric index of the node to load. 92 | * @returns A Promise that resolves once the node fetch (and prefetch dispatch) is initiated. 93 | */ 94 | export async function loadNode(idx: number): Promise { 95 | const cache = get(nodeCache); 96 | // Skip loading if already present 97 | if (cache.has(idx)) return; 98 | 99 | try { 100 | // Fetch the node data from WASM 101 | const node = await fetchNodeFull(idx); 102 | nodeCache.update(m => m.set(idx, node)); 103 | 104 | // Prefetch each child node in parallel, logging any failures 105 | const childPromises = node.edges.map(({ dest }) => 106 | fetchNodeFull(dest) 107 | .then(child => nodeCache.update(m => m.set(dest, child))) 108 | .catch(err => { 109 | console.error(`prefetch failed for node #${dest}:`, err); 110 | }) 111 | ); 112 | } catch (e) { 113 | // On error, log and insert an error placeholder in the cache 114 | console.error(`Error loading node #${idx}:`, e); 115 | nodeCache.update(m => 116 | m.set(idx, { content: `Error: ${e}`, edges: [] }) 117 | ); 118 | } 119 | } 120 | 121 | /** 122 | * Navigate to a different story node by updating currentIndex and triggering a load. 123 | * 124 | * @param idx - Numeric index of the target node. 125 | */ 126 | export function goTo(idx: number): void { 127 | // Update active node index 128 | currentIndex.set(idx); 129 | // Begin loading (and caching) the new node asynchronously 130 | loadNode(idx); 131 | } 132 | -------------------------------------------------------------------------------- /src/lib/components/SettingsPanel.svelte: -------------------------------------------------------------------------------- 1 | 80 | 81 |
82 | 160 |
161 | -------------------------------------------------------------------------------- /src/lib/wasm.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This module initializes and interacts with the webassembly module, providing functions to load and 3 | * retrieve nodes (scenes) from a story file. 4 | * 5 | * It ensures a singleton client per story path and offers convenient methods to fetch the root node 6 | * or any node by index, returning structured data suitable for a JavaScript/TypeScript client application. 7 | */ 8 | 9 | import init, { CyoaGame } from '../pkg/wasm_module.js'; 10 | import type { CyoaGame as ClientType } from '../pkg/wasm_module.js'; 11 | import { base } from '$app/paths'; 12 | 13 | /** 14 | * Relative URL to the compiled WebAssembly binary for the CYOA engine. 15 | * Constructed at runtime based on the current module location. 16 | */ 17 | const wasmUrl = new URL('../pkg/wasm_module_bg.wasm', import.meta.url); 18 | 19 | /** 20 | * Absolute path (URL) to the story file to load into the WASM engine. 21 | * Combines SvelteKit's the `base` path alias with the story file name. 22 | */ 23 | export const STORY_PATH = `${base}/magium.story`; 24 | 25 | /** 26 | * Promise that resolves when the WASM module has been initialized. 27 | * Callers should await this before creating or using the CyoaGame client. 28 | */ 29 | export const ready = init({ url: wasmUrl.href }); 30 | 31 | /** 32 | * Low-level representation of a single game edge (choice) returned from WASM. 33 | * @internal 34 | */ 35 | type EdgeRaw = { 36 | /** Text label for the choice presented to the player */ 37 | label: string; 38 | /** Zero-based index of the destination node */ 39 | dest_idx: number; 40 | }; 41 | 42 | /** 43 | * Low-level representation of a game node (scene) returned from WASM. 44 | * @internal 45 | */ 46 | type NodeRaw = { 47 | /** Scene text or content to display */ 48 | content: string; 49 | /** Array of outgoing edges (choices) from this node */ 50 | edges: EdgeRaw[]; 51 | }; 52 | 53 | /** 54 | * Map from story file path to its singleton CyoaGame client instance. 55 | * Ensures we only ever instantiate one WASM client per story. 56 | */ 57 | type ClientMap = Record; 58 | const clients: ClientMap = {}; 59 | 60 | /** 61 | * Get (or create) the singleton WASM client for the configured story. 62 | * Ensures the WASM module is fully initialized before instantiation. 63 | * 64 | * @returns Promise resolving to the CyoaGame client instance. 65 | */ 66 | async function getClient(): Promise { 67 | // Wait until the WASM module is ready 68 | await ready; 69 | 70 | // Lazy-instantiation: create the client if it doesn't exist 71 | if (!(STORY_PATH in clients)) { 72 | clients[STORY_PATH] = new CyoaGame(STORY_PATH); 73 | } 74 | 75 | return clients[STORY_PATH]; 76 | } 77 | 78 | /** 79 | * Public-facing format for edges returned to the application. 80 | */ 81 | export type Edge = { 82 | /** Text label for the choice */ 83 | label: string; 84 | /** Destination node index for this choice */ 85 | dest: number; 86 | }; 87 | 88 | /** 89 | * Fetch the "root" node (index 0) from the story in a single call. 90 | * 91 | * Internally calls the Rust->WASM helper `load_root_node_full`, 92 | * then transforms the raw data into a more ergonomic shape. 93 | * 94 | * @example 95 | * ```ts 96 | * const root = await fetchRootNodeFull(); 97 | * console.log(root.content); 98 | * root.edges.forEach(edge => console.log(edge.label, edge.dest)); 99 | * ``` 100 | * 101 | * @returns Promise resolving to an object with: 102 | * - `content`: the root scene text 103 | * - `edges`: array of choices with labels and destination indices 104 | * 105 | * @throws if the WASM module is not ready or fails to load the node. 106 | */ 107 | export async function fetchRootNodeFull(): Promise<{ 108 | content: string; 109 | edges: Edge[]; 110 | }> { 111 | const client = await getClient(); 112 | // Load the root node (index 0) via the WASM API 113 | const jsNode = (await client.load_root_node_full()) as NodeRaw; 114 | 115 | // Transform raw edges into public-facing Edge objects 116 | const edges: Edge[] = jsNode.edges.map(({ label, dest_idx }) => ({ 117 | label, 118 | dest: dest_idx, 119 | })); 120 | 121 | return { 122 | content: jsNode.content, 123 | edges, 124 | }; 125 | } 126 | 127 | /** 128 | * Fetch any node by its zero-based index in the story. 129 | * 130 | * Internally calls the Rust->WASM helper `load_node_full`, 131 | * then transforms the raw data into a more ergonomic shape. 132 | * 133 | * @param nodeIdx - Zero-based index of the node to load 134 | * 135 | * @example 136 | * ```ts 137 | * const scene = await fetchNodeFull(5); 138 | * console.log(scene.content); 139 | * ``` 140 | * 141 | * @returns Promise resolving to an object with: 142 | * - `content`: the scene text 143 | * - `edges`: array of edges (choices) 144 | * 145 | * @throws if `nodeIdx` is out of range or the WASM call fails. 146 | */ 147 | export async function fetchNodeFull( 148 | nodeIdx: number 149 | ): Promise<{ 150 | content: string; 151 | edges: Edge[]; 152 | }> { 153 | const client = await getClient(); 154 | const jsNode = (await client.load_node_full(nodeIdx)) as NodeRaw; 155 | 156 | // Map fields from WASM format to our public API 157 | const edges: Edge[] = jsNode.edges.map(({ label, dest_idx }) => ({ 158 | label, 159 | dest: dest_idx, 160 | })); 161 | 162 | return { 163 | content: jsNode.content, 164 | edges, 165 | }; 166 | } 167 | -------------------------------------------------------------------------------- /src/lib/components/ChoiceList.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | 20 | 146 | 147 | 148 |
152 | {#each choices as c (c.label)} 153 | 154 | {/each} 155 |
156 | 157 | 158 |
163 | {#each choices as c (c.label)} 164 | 165 | {/each} 166 |
167 | -------------------------------------------------------------------------------- /src/pkg/wasm_module.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * Entry point invoked by `wasm_bindgen` when the module is instantiated. 5 | * 6 | * Installs the panic hook so that any Rust panics are forwarded to the 7 | * browser console as `console.error` messages, improving runtime 8 | * diagnostics when using the module from JavaScript. 9 | */ 10 | export function __wasm_start(): void; 11 | /** 12 | * The main game loader exposed to JavaScript via wasm_bindgen. 13 | * Handles probing, range-requests, parsing TLV, zstd decompression, 14 | * and exposes `load_root_node_full` / `load_node_full` APIs. 15 | */ 16 | export class CyoaGame { 17 | free(): void; 18 | /** 19 | * Constructs a new `CyoaGame` instance by probing the remote file 20 | * at `path` for its total size and HTTP Range support, then fetching 21 | * and parsing the on‐disk index. 22 | * 23 | * # Parameters 24 | * 25 | * - `path`: URL or filesystem path (relative to the site root) of 26 | * the `.cyoa` binary file. 27 | * 28 | * # Returns 29 | * 30 | * - `Ok(CyoaGame)`: if the file was probed successfully and its index 31 | * parsed without error. 32 | * - `Err(JsValue)`: if there was any HTTP error, missing range support, 33 | * invalid magic, out‐of‐range index pointer, or parse failure. 34 | * 35 | * # Examples 36 | * 37 | * ```ignore 38 | * // In JavaScript: 39 | * const game = await new CyoaGame("/games/mystory.cy"); 40 | * ``` 41 | */ 42 | constructor(path: string); 43 | /** 44 | * Returns a JavaScript `Array` of all chunk IDs present in the file’s 45 | * parsed index, formatted as uppercase hex strings. 46 | * 47 | * Each entry is the 3‐byte chunk identifier, e.g. `"000102"`. 48 | * 49 | * # Examples 50 | * 51 | * ```ignore 52 | * let ids = game.chunk_ids(); // ["000001", "000002", …] 53 | * console.log(ids[0]); // "000001" 54 | * ``` 55 | */ 56 | chunk_ids(): Array; 57 | /** 58 | * Loads the node at the given index (into the parsed index vector), 59 | * fully fetching its content text and all outgoing edges—with labels 60 | * and destination indices—all in one batched request (wherever possible). 61 | * 62 | * # Parameters 63 | * 64 | * - `idx`: Zero‐based index into the game’s index entries. Must point 65 | * at a `ChunkType::Node` entry. 66 | * 67 | * # Returns 68 | * 69 | * - `Ok(JsValue)`: A JS object with shape `{ content: string, edges: Array< { label: string, dest_idx: number } > }`. 70 | * - `Err(JsValue)`: If `idx` is out of range, not a node chunk, or any 71 | * network/parse error occurs. 72 | * 73 | * # Errors 74 | * 75 | * - `GameError::Parse("not a node chunk")` if the indexed entry isn’t a node. 76 | * - `GameError::Http` if any range‐request fails. 77 | * - `GameError::Parse(...)` for TLV or decompression failures. 78 | * 79 | * # Examples 80 | * 81 | * ```ignore 82 | * let node = await game.load_node_full(3); 83 | * console.log(node.content); // "You stand at a crossroads..." 84 | * console.log(node.edges.length); // e.g. 2 85 | * ``` 86 | */ 87 | load_node_full(idx: number): Promise; 88 | /** 89 | * Loads the “root” node as specified by the metadata chunk 90 | * `ID_ROOT_POINTER`. This is equivalent to finding the metadata 91 | * entry whose ID is `[0,0,1]`, reading its value as a node‐chunk 92 | * ID, and then calling `load_node_full` on that node’s index. 93 | * 94 | * # Returns 95 | * 96 | * - `Ok(JsValue)`: The same structured object as `load_node_full`. 97 | * - `Err(JsValue)`: If the metadata chunk is missing, invalid, or any 98 | * subsequent fetch/parse fails. 99 | * 100 | * # Errors 101 | * 102 | * - `GameError::MissingRoot` if no metadata chunk with ID `[0,0,1]` is found. 103 | * - All other errors are forwarded from `load_node_full`. 104 | * 105 | * # Examples 106 | * 107 | * ```ignore 108 | * let root = await game.load_root_node_full(); 109 | * console.log(root.content); // The starting passage text 110 | * ``` 111 | */ 112 | load_root_node_full(): Promise; 113 | } 114 | 115 | export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; 116 | 117 | export interface InitOutput { 118 | readonly memory: WebAssembly.Memory; 119 | readonly __wbg_cyoagame_free: (a: number, b: number) => void; 120 | readonly cyoagame_new: (a: number, b: number) => any; 121 | readonly cyoagame_chunk_ids: (a: number) => any; 122 | readonly cyoagame_load_node_full: (a: number, b: number) => any; 123 | readonly cyoagame_load_root_node_full: (a: number) => any; 124 | readonly __wasm_start: () => void; 125 | readonly rust_zstd_wasm_shim_qsort: (a: number, b: number, c: number, d: number) => void; 126 | readonly rust_zstd_wasm_shim_malloc: (a: number) => number; 127 | readonly rust_zstd_wasm_shim_memcmp: (a: number, b: number, c: number) => number; 128 | readonly rust_zstd_wasm_shim_calloc: (a: number, b: number) => number; 129 | readonly rust_zstd_wasm_shim_free: (a: number) => void; 130 | readonly rust_zstd_wasm_shim_memcpy: (a: number, b: number, c: number) => number; 131 | readonly rust_zstd_wasm_shim_memmove: (a: number, b: number, c: number) => number; 132 | readonly rust_zstd_wasm_shim_memset: (a: number, b: number, c: number) => number; 133 | readonly __wbindgen_exn_store: (a: number) => void; 134 | readonly __externref_table_alloc: () => number; 135 | readonly __wbindgen_export_2: WebAssembly.Table; 136 | readonly __wbindgen_free: (a: number, b: number, c: number) => void; 137 | readonly __wbindgen_malloc: (a: number, b: number) => number; 138 | readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; 139 | readonly __wbindgen_export_6: WebAssembly.Table; 140 | readonly closure60_externref_shim: (a: number, b: number, c: any) => void; 141 | readonly closure73_externref_shim: (a: number, b: number, c: any, d: any) => void; 142 | readonly __wbindgen_start: () => void; 143 | } 144 | 145 | export type SyncInitInput = BufferSource | WebAssembly.Module; 146 | /** 147 | * Instantiates the given `module`, which can either be bytes or 148 | * a precompiled `WebAssembly.Module`. 149 | * 150 | * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. 151 | * 152 | * @returns {InitOutput} 153 | */ 154 | export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput; 155 | 156 | /** 157 | * If `module_or_path` is {RequestInfo} or {URL}, makes a request and 158 | * for everything else, calls `WebAssembly.instantiate` directly. 159 | * 160 | * @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. 161 | * 162 | * @returns {Promise} 163 | */ 164 | export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise; 165 | -------------------------------------------------------------------------------- /src/lib/components/StatsPanel.svelte: -------------------------------------------------------------------------------- 1 | 97 | 98 | {#if $uiState.primary === PrimaryState.Stats} 99 | 100 |