Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa9f5eff20 | ||
|
|
18c416d4d2 | ||
|
|
77fd434226 | ||
|
|
122f159b94 | ||
|
|
35553c18dd | ||
|
|
bef71d391a | ||
|
|
b48b3a4f8f |
@@ -1,5 +1,8 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
|
data/
|
||||||
|
backups/
|
||||||
|
game_assets/
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
vite.config.js
|
vite.config.js
|
||||||
vite.config.d.ts
|
vite.config.d.ts
|
||||||
@@ -9,3 +12,4 @@ vite.config.d.ts
|
|||||||
!.env.example
|
!.env.example
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
/public/basis/
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ These requirements apply to every screen, feature, game system, and code change
|
|||||||
|
|
||||||
AYN Thor Android app is the primary product. Browser build is a secondary access path for players without an AYN Thor. Do not weaken the Thor experience to simplify the browser version; provide an adaptive browser fallback instead.
|
AYN Thor Android app is the primary product. Browser build is a secondary access path for players without an AYN Thor. Do not weaken the Thor experience to simplify the browser version; provide an adaptive browser fallback instead.
|
||||||
|
|
||||||
|
## Game asset workflow
|
||||||
|
|
||||||
|
- `game_assets/` is an ignored local source library. Never load or import a runtime asset directly from it.
|
||||||
|
- When adopting a model, animation, texture, audio file, or other game asset from `game_assets/`, copy it into the tracked `src/assets/game/` runtime library first. Preserve its path relative to `game_assets/`.
|
||||||
|
- Use `pnpm assets:import <path-within-game_assets>` for the copy. Use `--replace` only to deliberately update an existing runtime copy. Copy required companion files and license/attribution notices with assets that are not self-contained.
|
||||||
|
- Reference only `src/assets/game/` copies from game code. Keep `game_assets/` as source material for future imports, conversions, and authoring work.
|
||||||
|
|
||||||
## AYN Thor display and layout targets
|
## AYN Thor display and layout targets
|
||||||
|
|
||||||
Design and test against both physical displays in landscape orientation. Physical panel resolution and Android CSS/layout viewport are different concepts; record and validate both.
|
Design and test against both physical displays in landscape orientation. Physical panel resolution and Android CSS/layout viewport are different concepts; record and validate both.
|
||||||
@@ -84,14 +91,29 @@ Stable locked 60 FPS is a release requirement, not a best-effort goal.
|
|||||||
- Favor algorithms and data layouts that reduce CPU and RAM together. Do not trade a large permanent memory increase for a tiny unmeasured CPU gain.
|
- Favor algorithms and data layouts that reduce CPU and RAM together. Do not trade a large permanent memory increase for a tiny unmeasured CPU gain.
|
||||||
- Performance fixes must preserve deterministic rules, input responsiveness, visual clarity, and controller accessibility.
|
- Performance fixes must preserve deterministic rules, input responsiveness, visual clarity, and controller accessibility.
|
||||||
|
|
||||||
|
## Verification scope
|
||||||
|
|
||||||
|
- Verify the changed code and its direct dependencies. Do not run tests, visual QA, or controller checks for unrelated screens or systems by default.
|
||||||
|
- For a screen change, check that screen's applicable display layouts, browser fallback, and controller path only.
|
||||||
|
- For a reusable mechanic or domain change, run its focused tests plus direct consumers affected by the change.
|
||||||
|
- Run viewport, display-layout, and browser-fallback testing only when a change adds or modifies a screen, menu, modal, HUD, overlay, or other UI/layout behavior.
|
||||||
|
- Combat rules, encounter mechanics, world-space telegraphs, animations, VFX, tuning, and other gameplay-only changes do not require viewport testing unless they also change UI or screen layout.
|
||||||
|
- Expand to broader regression or full-suite verification only for shared foundations, cross-cutting changes, risky refactors, release validation, or when explicitly requested.
|
||||||
|
- State what was verified and any deliberately unverified scope in the handoff.
|
||||||
|
|
||||||
## Definition of done
|
## Definition of done
|
||||||
|
|
||||||
A screen or mechanic is not complete until:
|
A screen or UI change is not complete until:
|
||||||
|
|
||||||
1. Its main-display UI is designed and verified at the approximately `960 x 540` Android CSS/layout viewport, with rendering validated against the `1920 x 1080` physical panel.
|
1. Its main-display UI is designed and verified at the approximately `960 x 540` Android CSS/layout viewport, with rendering validated against the `1920 x 1080` physical panel.
|
||||||
2. Its applicable secondary UI is designed and verified at the approximately `620 x 540` Android CSS/layout viewport, with rendering validated against the `1240 x 1080` physical panel.
|
2. Its applicable secondary UI is designed and verified at the approximately `620 x 540` Android CSS/layout viewport, with rendering validated against the `1240 x 1080` physical panel.
|
||||||
3. It has a complete single-display browser fallback.
|
3. It has a complete single-display browser fallback.
|
||||||
4. Every action is reachable and understandable using only a controller, with no click-to-focus step.
|
4. Every action is reachable and understandable using only a controller, with no click-to-focus step.
|
||||||
5. Shared game logic is modular, typed, and testable outside the renderer/UI.
|
|
||||||
6. It introduces no known resource leak, unbounded work, or avoidable hot-path allocation.
|
Every code change is not complete until:
|
||||||
7. Representative Thor hardware sustains the locked 60 FPS target, or the change includes measured evidence and an explicit approved exception.
|
|
||||||
|
1. Shared game logic is modular, typed, and testable outside the renderer/UI.
|
||||||
|
2. It introduces no known resource leak, unbounded work, or avoidable hot-path allocation.
|
||||||
|
3. Representative Thor hardware sustains the locked 60 FPS target, or the change includes measured evidence and an explicit approved exception.
|
||||||
|
|
||||||
|
Gameplay-only mechanic and domain changes do not inherit the screen/UI viewport requirements above.
|
||||||
|
|||||||
@@ -4,6 +4,17 @@ This game uses the same proven local-Gitea pattern as `testgame`: clone from the
|
|||||||
Gitea bare repository on the TrueNAS filesystem, mount that working checkout
|
Gitea bare repository on the TrueNAS filesystem, mount that working checkout
|
||||||
into one Node container, and update it with a local Git pull plus app restart.
|
into one Node container, and update it with a local Git pull plus app restart.
|
||||||
|
|
||||||
|
## What the TrueNAS server does
|
||||||
|
|
||||||
|
The `iwanttoheal-mmo` TrueNAS app is the game's live online server. One Node
|
||||||
|
process serves the browser bundle and authenticated `/api` routes on port `4173`.
|
||||||
|
The reverse proxy exposes both at `https://iwanttoheal.phenomrom.com`.
|
||||||
|
|
||||||
|
SQLite persists accounts, sessions, cloud saves, boss-kill rankings, and
|
||||||
|
roguelike records under `/app/data/game.db`. The separate data mount survives
|
||||||
|
container replacement. TrueNAS Gitea remains the source repository used for
|
||||||
|
deployment; it is not the game database.
|
||||||
|
|
||||||
## Paths
|
## Paths
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -13,6 +24,9 @@ Local Gitea bare repository:
|
|||||||
Runnable working checkout:
|
Runnable working checkout:
|
||||||
/mnt/usbssds/apps/iwanttoheal-mmo/app
|
/mnt/usbssds/apps/iwanttoheal-mmo/app
|
||||||
|
|
||||||
|
Persistent game data:
|
||||||
|
/mnt/usbssds/apps/iwanttoheal-mmo/data
|
||||||
|
|
||||||
Public URL:
|
Public URL:
|
||||||
https://iwanttoheal.phenomrom.com
|
https://iwanttoheal.phenomrom.com
|
||||||
|
|
||||||
@@ -39,7 +53,7 @@ sudo find /mnt -type d -name "i-want-to-heal-mmo.git" -prune -print 2>/dev/null
|
|||||||
Clone entirely through the local filesystem:
|
Clone entirely through the local filesystem:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo mkdir -p /mnt/usbssds/apps/iwanttoheal-mmo
|
sudo mkdir -p /mnt/usbssds/apps/iwanttoheal-mmo/{app,data}
|
||||||
sudo git config --global --add safe.directory \
|
sudo git config --global --add safe.directory \
|
||||||
/mnt/.ix-apps/app_mounts/gitea/data/git/repositories/phenom/i-want-to-heal-mmo.git
|
/mnt/.ix-apps/app_mounts/gitea/data/git/repositories/phenom/i-want-to-heal-mmo.git
|
||||||
sudo git clone \
|
sudo git clone \
|
||||||
@@ -72,8 +86,10 @@ services:
|
|||||||
iwanttoheal:
|
iwanttoheal:
|
||||||
image: node:24-bookworm-slim
|
image: node:24-bookworm-slim
|
||||||
command: >-
|
command: >-
|
||||||
sh -lc "corepack pnpm install --frozen-lockfile && corepack pnpm run build && corepack pnpm start"
|
sh -lc "corepack pnpm install --frozen-lockfile && corepack pnpm run db:init && corepack pnpm run build && corepack pnpm start"
|
||||||
environment:
|
environment:
|
||||||
|
CORS_ORIGINS: "https://iwanttoheal.phenomrom.com,capacitor://localhost,http://localhost"
|
||||||
|
DATA_DIR: /app/data
|
||||||
HOST: 0.0.0.0
|
HOST: 0.0.0.0
|
||||||
PORT: "4173"
|
PORT: "4173"
|
||||||
init: true
|
init: true
|
||||||
@@ -82,11 +98,21 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/usbssds/apps/iwanttoheal-mmo/app:/app
|
- /mnt/usbssds/apps/iwanttoheal-mmo/app:/app
|
||||||
|
- /mnt/usbssds/apps/iwanttoheal-mmo/data:/app/data
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
```
|
```
|
||||||
|
|
||||||
This game has no server database. Do not add `db:init`, `/app/data`, cookie,
|
Do not remove the `/app/data` mount or place the SQLite database inside the source
|
||||||
CORS, or proxy environment settings from the older game.
|
checkout. Back up `/mnt/usbssds/apps/iwanttoheal-mmo/data/game.db` before database
|
||||||
|
migrations or destructive maintenance.
|
||||||
|
|
||||||
|
From the app checkout, create a consistent SQLite backup with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
DATA_DIR=/mnt/usbssds/apps/iwanttoheal-mmo/data \
|
||||||
|
BACKUP_DIR=/mnt/usbssds/apps/iwanttoheal-mmo/backups \
|
||||||
|
corepack pnpm run db:backup
|
||||||
|
```
|
||||||
|
|
||||||
The separate volume protects the old app files and data. The YAML still maps
|
The separate volume protects the old app files and data. The YAML still maps
|
||||||
host port `4173`, so the old and MMO apps cannot run simultaneously while both
|
host port `4173`, so the old and MMO apps cannot run simultaneously while both
|
||||||
|
|||||||
@@ -2,16 +2,23 @@
|
|||||||
|
|
||||||
Playable low-poly third-person combat vertical slice for AYN Thor's dual displays.
|
Playable low-poly third-person combat vertical slice for AYN Thor's dual displays.
|
||||||
|
|
||||||
Offline-first frontend includes three timestamped save slots, optional account sync, local/online overwrite controls, Hunter Profile statistics, boss collection logs, Settings, and PvE/PvP mode entry points.
|
Offline-first frontend includes three timestamped save slots, TrueNAS accounts
|
||||||
|
and cloud saves, Hunter Profile statistics, boss and roguelike leaderboards,
|
||||||
|
boss collection logs, Settings, and PvE/PvP mode entry points. Offline saves
|
||||||
|
remain playable without an account and can be uploaded after sign-in.
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm install
|
pnpm install
|
||||||
|
pnpm dev:api
|
||||||
|
# In a second terminal:
|
||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
The development server listens on `0.0.0.0:4173`.
|
The Vite development server listens on `0.0.0.0:4173` and proxies `/api` to the
|
||||||
|
local production/API server on `127.0.0.1:4174`. Both processes use the same
|
||||||
|
client API contract as TrueNAS.
|
||||||
|
|
||||||
## Android / AYN Thor test APK
|
## Android / AYN Thor test APK
|
||||||
|
|
||||||
@@ -23,6 +30,10 @@ installable debug APK:
|
|||||||
pnpm android:apk
|
pnpm android:apk
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Build explicitly against the TrueNAS API with `pnpm android:apk:truenas`.
|
||||||
|
Native builds also default to `https://iwanttoheal.phenomrom.com` when no API
|
||||||
|
base override is supplied.
|
||||||
|
|
||||||
Output is written under `android/app/build/outputs/apk/debug/`. With Android
|
Output is written under `android/app/build/outputs/apk/debug/`. With Android
|
||||||
platform tools and a connected Thor, build and install it with:
|
platform tools and a connected Thor, build and install it with:
|
||||||
|
|
||||||
@@ -41,12 +52,25 @@ state rather than two independent WebViews.
|
|||||||
Complete first-install, local-Gitea clone, YAML, update, and verification steps:
|
Complete first-install, local-Gitea clone, YAML, update, and verification steps:
|
||||||
[DEPLOYMENT.md](DEPLOYMENT.md).
|
[DEPLOYMENT.md](DEPLOYMENT.md).
|
||||||
|
|
||||||
The production preview server uses the existing deployment address and port:
|
The live TrueNAS web server uses the existing deployment address and port:
|
||||||
|
|
||||||
- Public URL: `https://iwanttoheal.phenomrom.com`
|
- Public URL: `https://iwanttoheal.phenomrom.com`
|
||||||
- Host/container port: `4173`
|
- Host/container port: `4173`
|
||||||
- App directory: `/mnt/usbssds/apps/iwanttoheal-mmo/app`
|
- App directory: `/mnt/usbssds/apps/iwanttoheal-mmo/app`
|
||||||
|
|
||||||
|
### Server architecture
|
||||||
|
|
||||||
|
TrueNAS is the online production game server. Its Node process serves the browser
|
||||||
|
application and authenticated `/api` routes on port `4173`; the reverse proxy
|
||||||
|
exposes both at the public URL above. SQLite data persists at `/app/data/game.db`
|
||||||
|
through the separate TrueNAS data mount.
|
||||||
|
|
||||||
|
The server owns account credentials, 30-day sessions, three cloud-save slots per
|
||||||
|
account, boss-kill rankings, and roguelike highest-round rankings. Passwords use
|
||||||
|
scrypt with per-account salts; clients store only opaque session tokens. Local
|
||||||
|
saves remain available for offline play. Gitea is a separate TrueNAS service used
|
||||||
|
for source hosting and deployment.
|
||||||
|
|
||||||
Clone from the TrueNAS-local Gitea bare repository into the app directory, then
|
Clone from the TrueNAS-local Gitea bare repository into the app directory, then
|
||||||
deploy `compose.yaml`. The expected source path is:
|
deploy `compose.yaml`. The expected source path is:
|
||||||
|
|
||||||
@@ -54,18 +78,16 @@ deploy `compose.yaml`. The expected source path is:
|
|||||||
/mnt/.ix-apps/app_mounts/gitea/data/git/repositories/phenom/i-want-to-heal-mmo.git
|
/mnt/.ix-apps/app_mounts/gitea/data/git/repositories/phenom/i-want-to-heal-mmo.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Compared with the old game configuration:
|
Current configuration:
|
||||||
|
|
||||||
- use `corepack pnpm install --frozen-lockfile`, not `npm ci`;
|
- use `corepack pnpm install --frozen-lockfile`, not `npm ci`;
|
||||||
- remove `npm run db:init` because this game has no server database;
|
- run `pnpm db:init` before starting the production server;
|
||||||
- remove `COOKIE_SECURE`, `CORS_ORIGINS`, and `TRUST_PROXY`; the static Vite
|
- mount `/mnt/usbssds/apps/iwanttoheal-mmo/data` at `/app/data`;
|
||||||
preview server does not consume them;
|
- keep `CORS_ORIGINS` configured for the public site and Capacitor host;
|
||||||
- remove `/app/data`; offline saves live in each player's browser/Android WebView
|
- keep the source checkout and persistent data in separate mounts.
|
||||||
storage, not on TrueNAS.
|
|
||||||
|
|
||||||
The existing reverse proxy can keep forwarding the public hostname to port
|
The existing reverse proxy forwards the public hostname to port `4173`, including
|
||||||
`4173`. Add server data and authentication environment variables only when an
|
all `/api` routes.
|
||||||
actual sync API is introduced.
|
|
||||||
|
|
||||||
## Publish updates to Gitea
|
## Publish updates to Gitea
|
||||||
|
|
||||||
@@ -109,18 +131,18 @@ outside the repository.
|
|||||||
- `WASD` / left stick: move
|
- `WASD` / left stick: move
|
||||||
- `Q` and `E` / D-pad: cycle party target
|
- `Q` and `E` / D-pad: cycle party target
|
||||||
- `1`–`6`: cast Smite, Renew, Shield, Purify, Radiance, Flash Heal
|
- `1`–`6`: cast Smite, Renew, Shield, Purify, Radiance, Flash Heal
|
||||||
- Gamepad: `X`, `Y`, `B`, `A`, `LB`, `RB` map to those abilities
|
- Gamepad: PlayStation `□`, `△`, `○`, `✕`, `L1`, `R1` map to those abilities
|
||||||
- `M`: tactical map
|
- `M`: tactical map
|
||||||
- `I`: inventory and item tooltip
|
- `I`: inventory and item tooltip
|
||||||
- `Enter` / Start: begin or reset encounter
|
- `Enter` / `START`: begin or reset encounter
|
||||||
|
|
||||||
Touch controls on lower display support party targeting, ability casting, map, and inventory.
|
Touch controls on lower display support party targeting, ability casting, map, and inventory.
|
||||||
|
|
||||||
## Prototype scope
|
## Current game scope
|
||||||
|
|
||||||
- Five-member AI party with Disc Priest healer
|
- Five-member AI party with Disc Priest healer
|
||||||
- Animated Druid healer, Knight tank, Ranger, Rogue, and Mage party models
|
- Animated Druid healer, Knight tank, Ranger, Rogue, and Mage party models
|
||||||
- One animated boss: Bulldrome, using the Bull model at 180% of its original prototype scale
|
- Animated bosses using canonical tracked game models
|
||||||
- Telegraph, charge, 0.75-second knockdown, and return-to-tank behavior
|
- Telegraph, charge, 0.75-second knockdown, and return-to-tank behavior
|
||||||
- Three-charge cycle into a five-second stack marker and 300-damage shared pounce
|
- Three-charge cycle into a five-second stack marker and 300-damage shared pounce
|
||||||
- Tank pressure, party-wide Cinder Nova, dispellable Ember Brand
|
- Tank pressure, party-wide Cinder Nova, dispellable Ember Brand
|
||||||
|
|||||||
@@ -102,7 +102,9 @@ public abstract class ControllerBridgeActivity extends BridgeActivity {
|
|||||||
|
|
||||||
float leftStickX = event.getAxisValue(MotionEvent.AXIS_X);
|
float leftStickX = event.getAxisValue(MotionEvent.AXIS_X);
|
||||||
float leftStickY = event.getAxisValue(MotionEvent.AXIS_Y);
|
float leftStickY = event.getAxisValue(MotionEvent.AXIS_Y);
|
||||||
dispatchNativeControllerMotion(leftStickX, leftStickY);
|
float rightStickX = controllerAxisValue(event, MotionEvent.AXIS_Z, MotionEvent.AXIS_RX);
|
||||||
|
float rightStickY = controllerAxisValue(event, MotionEvent.AXIS_RZ, MotionEvent.AXIS_RY);
|
||||||
|
dispatchNativeControllerMotion(leftStickX, leftStickY, rightStickX, rightStickY);
|
||||||
|
|
||||||
Set<String> currentTokens = new HashSet<>();
|
Set<String> currentTokens = new HashSet<>();
|
||||||
addAxisTokens(currentTokens, event.getAxisValue(MotionEvent.AXIS_HAT_X), "Button14", "Button15");
|
addAxisTokens(currentTokens, event.getAxisValue(MotionEvent.AXIS_HAT_X), "Button14", "Button15");
|
||||||
@@ -150,6 +152,14 @@ public abstract class ControllerBridgeActivity extends BridgeActivity {
|
|||||||
if (value >= AXIS_DEAD_ZONE) tokens.add(positive);
|
if (value >= AXIS_DEAD_ZONE) tokens.add(positive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private float controllerAxisValue(MotionEvent event, int primaryAxis, int fallbackAxis) {
|
||||||
|
InputDevice device = event.getDevice();
|
||||||
|
if (device != null && device.getMotionRange(primaryAxis) != null) {
|
||||||
|
return event.getAxisValue(primaryAxis);
|
||||||
|
}
|
||||||
|
return event.getAxisValue(fallbackAxis);
|
||||||
|
}
|
||||||
|
|
||||||
private void dispatchNativeControllerToken(String token, boolean repeat) {
|
private void dispatchNativeControllerToken(String token, boolean repeat) {
|
||||||
if (bridge == null || bridge.getWebView() == null) return;
|
if (bridge == null || bridge.getWebView() == null) return;
|
||||||
String script =
|
String script =
|
||||||
@@ -161,18 +171,19 @@ public abstract class ControllerBridgeActivity extends BridgeActivity {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void dispatchNativeControllerMotion(float x, float y) {
|
private void dispatchNativeControllerMotion(float moveX, float moveY, float lookX, float lookY) {
|
||||||
if (bridge == null || bridge.getWebView() == null) return;
|
if (bridge == null || bridge.getWebView() == null) return;
|
||||||
String script =
|
String script =
|
||||||
"window.dispatchEvent(new CustomEvent('iwt-native-controller-motion',"
|
"window.dispatchEvent(new CustomEvent('iwt-native-controller-motion',"
|
||||||
+ "{detail:{x:" + x + ",y:" + y + "}}));";
|
+ "{detail:{moveX:" + moveX + ",moveY:" + moveY
|
||||||
|
+ ",lookX:" + lookX + ",lookY:" + lookY + "}}));";
|
||||||
bridge.getWebView().post(() -> bridge.getWebView().evaluateJavascript(script, null));
|
bridge.getWebView().post(() -> bridge.getWebView().evaluateJavascript(script, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearHeldControllerState() {
|
private void clearHeldControllerState() {
|
||||||
activeMotionTokens.clear();
|
activeMotionTokens.clear();
|
||||||
lastMotionDispatchAt.clear();
|
lastMotionDispatchAt.clear();
|
||||||
dispatchNativeControllerMotion(0.0f, 0.0f);
|
dispatchNativeControllerMotion(0.0f, 0.0f, 0.0f, 0.0f);
|
||||||
if (bridge == null || bridge.getWebView() == null) return;
|
if (bridge == null || bridge.getWebView() == null) return;
|
||||||
bridge.getWebView().post(() -> bridge.getWebView().evaluateJavascript(
|
bridge.getWebView().post(() -> bridge.getWebView().evaluateJavascript(
|
||||||
"window.dispatchEvent(new Event('iwt-native-controller-reset'));",
|
"window.dispatchEvent(new Event('iwt-native-controller-reset'));",
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ services:
|
|||||||
iwanttoheal:
|
iwanttoheal:
|
||||||
image: node:24-bookworm-slim
|
image: node:24-bookworm-slim
|
||||||
command: >-
|
command: >-
|
||||||
sh -lc "corepack pnpm install --frozen-lockfile && corepack pnpm run build && corepack pnpm start"
|
sh -lc "corepack pnpm install --frozen-lockfile && corepack pnpm run db:init && corepack pnpm run build && corepack pnpm start"
|
||||||
environment:
|
environment:
|
||||||
|
CORS_ORIGINS: "https://iwanttoheal.phenomrom.com,capacitor://localhost,http://localhost"
|
||||||
|
DATA_DIR: /app/data
|
||||||
HOST: 0.0.0.0
|
HOST: 0.0.0.0
|
||||||
PORT: "4173"
|
PORT: "4173"
|
||||||
init: true
|
init: true
|
||||||
@@ -12,4 +14,5 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/usbssds/apps/iwanttoheal-mmo/app:/app
|
- /mnt/usbssds/apps/iwanttoheal-mmo/app:/app
|
||||||
|
- /mnt/usbssds/apps/iwanttoheal-mmo/data:/app/data
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
PRAGMA journal_mode = WAL;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS accounts (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
canonical_username TEXT NOT NULL UNIQUE,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
password_salt TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||||
|
token_hash TEXT NOT NULL UNIQUE,
|
||||||
|
expires_at TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS sessions_token_hash_idx ON sessions(token_hash);
|
||||||
|
CREATE INDEX IF NOT EXISTS sessions_expires_at_idx ON sessions(expires_at);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS hunter_saves (
|
||||||
|
account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||||
|
slot_id INTEGER NOT NULL CHECK (slot_id BETWEEN 1 AND 3),
|
||||||
|
hunter_name TEXT NOT NULL,
|
||||||
|
save_json TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (account_id, slot_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS boss_kill_records (
|
||||||
|
account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||||
|
slot_id INTEGER NOT NULL CHECK (slot_id BETWEEN 1 AND 3),
|
||||||
|
boss_id TEXT NOT NULL,
|
||||||
|
kills INTEGER NOT NULL DEFAULT 0 CHECK (kills >= 0),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (account_id, slot_id, boss_id),
|
||||||
|
FOREIGN KEY (account_id, slot_id) REFERENCES hunter_saves(account_id, slot_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS boss_kill_rank_idx
|
||||||
|
ON boss_kill_records (boss_id, kills DESC, updated_at ASC);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS roguelike_records (
|
||||||
|
account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||||
|
slot_id INTEGER NOT NULL CHECK (slot_id BETWEEN 1 AND 3),
|
||||||
|
highest_round INTEGER NOT NULL DEFAULT 0 CHECK (highest_round >= 0),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (account_id, slot_id),
|
||||||
|
FOREIGN KEY (account_id, slot_id) REFERENCES hunter_saves(account_id, slot_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS roguelike_rank_idx
|
||||||
|
ON roguelike_records (highest_round DESC, updated_at ASC);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS rogue_trials_endless_records (
|
||||||
|
account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||||
|
slot_id INTEGER NOT NULL CHECK (slot_id BETWEEN 1 AND 3),
|
||||||
|
highest_boss_kills INTEGER NOT NULL DEFAULT 0 CHECK (highest_boss_kills >= 0),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (account_id, slot_id),
|
||||||
|
FOREIGN KEY (account_id, slot_id) REFERENCES hunter_saves(account_id, slot_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS rogue_trials_endless_rank_idx
|
||||||
|
ON rogue_trials_endless_records (highest_boss_kills DESC, updated_at ASC);
|
||||||
|
Before Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 436 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 422 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,35 +0,0 @@
|
|||||||
# Blender3D MTL File: Rathalos.blend
|
|
||||||
# Material Count: 3
|
|
||||||
newmtl Rathalos2_em002_BM_02.png
|
|
||||||
Ns 96.078431
|
|
||||||
Ka 0.000000 0.000000 0.000000
|
|
||||||
Kd 0.640000 0.640000 0.640000
|
|
||||||
Ks 0.500000 0.500000 0.500000
|
|
||||||
Ni 1.000000
|
|
||||||
d 1.000000
|
|
||||||
illum 0
|
|
||||||
map_Kd em002_BM_02.png
|
|
||||||
|
|
||||||
|
|
||||||
newmtl Rathalos1_em002_BM_01.png
|
|
||||||
Ns 96.078431
|
|
||||||
Ka 0.000000 0.000000 0.000000
|
|
||||||
Kd 0.640000 0.640000 0.640000
|
|
||||||
Ks 0.500000 0.500000 0.500000
|
|
||||||
Ni 1.000000
|
|
||||||
d 1.000000
|
|
||||||
illum 0
|
|
||||||
map_Kd em002_BM_01.png
|
|
||||||
|
|
||||||
|
|
||||||
newmtl Rathalos3_em002_BM_03.png
|
|
||||||
Ns 96.078431
|
|
||||||
Ka 0.000000 0.000000 0.000000
|
|
||||||
Kd 0.640000 0.640000 0.640000
|
|
||||||
Ks 0.500000 0.500000 0.500000
|
|
||||||
Ni 1.000000
|
|
||||||
d 0.000000
|
|
||||||
illum 0
|
|
||||||
map_Kd em002_BM_03.png
|
|
||||||
|
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 4.8 MiB |
|
Before Width: | Height: | Size: 769 KiB |
|
Before Width: | Height: | Size: 415 KiB |
@@ -1,64 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
KayKit : Character Animations (1.1)
|
|
||||||
|
|
||||||
Created/distributed by Kay Lousberg (www.kaylousberg.com)
|
|
||||||
Creation date: 10/12/2025 09:00
|
|
||||||
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
License: (Creative Commons Zero, CC0)
|
|
||||||
http://creativecommons.org/publicdomain/zero/1.0/
|
|
||||||
|
|
||||||
This content is free to use in personal, educational and commercial projects.
|
|
||||||
You can support me by purchasing an asset pack at the EXTRA or SOURCE tiers, leaving a donation/tip on itch.io, or joining my Patreon.
|
|
||||||
If you wish to credit me you can do so by crediting "Kay Lousberg, www.kaylousberg.com" (this is not mandatory)
|
|
||||||
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
This asset pack is here thanks to all the wonderful people who support KayKit on Patreon and those who buy EXTRA or SOURCE packs on itch.io.
|
|
||||||
|
|
||||||
And a big, special thank you to my Super Supporters on Patreon:
|
|
||||||
|
|
||||||
- Brian McBarron
|
|
||||||
- Silva
|
|
||||||
- Joseph Preston
|
|
||||||
- Leon Burkhardt
|
|
||||||
- NickyBHobbying
|
|
||||||
- Emilia
|
|
||||||
- Darren Laing
|
|
||||||
- Magic Pig Games
|
|
||||||
- Lennart Steinke
|
|
||||||
- OWSC
|
|
||||||
- tenthtonman
|
|
||||||
- Ducain23
|
|
||||||
- flo-bit
|
|
||||||
- Jezzy Sukaemi
|
|
||||||
- Martyn Rushton
|
|
||||||
- Joe
|
|
||||||
- estee
|
|
||||||
- Gustavo
|
|
||||||
- AngyZinn
|
|
||||||
- Alex Cea
|
|
||||||
- Matthew A
|
|
||||||
- Stephen Williams
|
|
||||||
- Wenpu Ng
|
|
||||||
- Kevin
|
|
||||||
- Legionof Light
|
|
||||||
- Dazusu
|
|
||||||
- Arnaud Schlupp
|
|
||||||
- JB
|
|
||||||
- Neil Castle
|
|
||||||
- Cedrico Serbruyns
|
|
||||||
- ovenaut
|
|
||||||
- Pawel Jarosz
|
|
||||||
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
Patreon: http://patreon.com/kaylousberg
|
|
||||||
|
|
||||||
Follow here for updates:
|
|
||||||
http://twitter.com/KayLousberg
|
|
||||||
https://mastodon.gamedev.place/@Kay
|
|
||||||
http://twitter.com/KayLousberg
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
{
|
|
||||||
"asset" : {
|
|
||||||
"generator" : "Khronos glTF Blender I/O v3.4.50",
|
|
||||||
"version" : "2.0"
|
|
||||||
},
|
|
||||||
"scene" : 0,
|
|
||||||
"scenes" : [
|
|
||||||
{
|
|
||||||
"name" : "Scene",
|
|
||||||
"nodes" : [
|
|
||||||
0
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"nodes" : [
|
|
||||||
{
|
|
||||||
"mesh" : 0,
|
|
||||||
"name" : "banner_blue"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"materials" : [
|
|
||||||
{
|
|
||||||
"name" : "texture",
|
|
||||||
"pbrMetallicRoughness" : {
|
|
||||||
"baseColorTexture" : {
|
|
||||||
"index" : 0
|
|
||||||
},
|
|
||||||
"metallicFactor" : 0,
|
|
||||||
"roughnessFactor" : 0.44999998807907104
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"meshes" : [
|
|
||||||
{
|
|
||||||
"name" : "banner_blue",
|
|
||||||
"primitives" : [
|
|
||||||
{
|
|
||||||
"attributes" : {
|
|
||||||
"POSITION" : 0,
|
|
||||||
"TEXCOORD_0" : 1,
|
|
||||||
"NORMAL" : 2
|
|
||||||
},
|
|
||||||
"indices" : 3,
|
|
||||||
"material" : 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"textures" : [
|
|
||||||
{
|
|
||||||
"sampler" : 0,
|
|
||||||
"source" : 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"mimeType" : "image/png",
|
|
||||||
"name" : "dungeon_texture",
|
|
||||||
"uri" : "dungeon_texture.png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"accessors" : [
|
|
||||||
{
|
|
||||||
"bufferView" : 0,
|
|
||||||
"componentType" : 5126,
|
|
||||||
"count" : 187,
|
|
||||||
"max" : [
|
|
||||||
0.7500076293945312,
|
|
||||||
3.7274699211120605,
|
|
||||||
0.6895275712013245
|
|
||||||
],
|
|
||||||
"min" : [
|
|
||||||
-0.749992847442627,
|
|
||||||
0.5313930511474609,
|
|
||||||
0.3776381015777588
|
|
||||||
],
|
|
||||||
"type" : "VEC3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bufferView" : 1,
|
|
||||||
"componentType" : 5126,
|
|
||||||
"count" : 187,
|
|
||||||
"type" : "VEC2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bufferView" : 2,
|
|
||||||
"componentType" : 5126,
|
|
||||||
"count" : 187,
|
|
||||||
"type" : "VEC3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bufferView" : 3,
|
|
||||||
"componentType" : 5123,
|
|
||||||
"count" : 291,
|
|
||||||
"type" : "SCALAR"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"bufferViews" : [
|
|
||||||
{
|
|
||||||
"buffer" : 0,
|
|
||||||
"byteLength" : 2244,
|
|
||||||
"byteOffset" : 0,
|
|
||||||
"target" : 34962
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"buffer" : 0,
|
|
||||||
"byteLength" : 1496,
|
|
||||||
"byteOffset" : 2244,
|
|
||||||
"target" : 34962
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"buffer" : 0,
|
|
||||||
"byteLength" : 2244,
|
|
||||||
"byteOffset" : 3740,
|
|
||||||
"target" : 34962
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"buffer" : 0,
|
|
||||||
"byteLength" : 582,
|
|
||||||
"byteOffset" : 5984,
|
|
||||||
"target" : 34963
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"samplers" : [
|
|
||||||
{
|
|
||||||
"magFilter" : 9729,
|
|
||||||
"minFilter" : 9987
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"buffers" : [
|
|
||||||
{
|
|
||||||
"byteLength" : 6568,
|
|
||||||
"uri" : "banner_blue.bin"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
{
|
|
||||||
"asset" : {
|
|
||||||
"generator" : "Khronos glTF Blender I/O v3.4.50",
|
|
||||||
"version" : "2.0"
|
|
||||||
},
|
|
||||||
"scene" : 0,
|
|
||||||
"scenes" : [
|
|
||||||
{
|
|
||||||
"name" : "Scene",
|
|
||||||
"nodes" : [
|
|
||||||
0
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"nodes" : [
|
|
||||||
{
|
|
||||||
"mesh" : 0,
|
|
||||||
"name" : "banner_brown"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"materials" : [
|
|
||||||
{
|
|
||||||
"name" : "texture",
|
|
||||||
"pbrMetallicRoughness" : {
|
|
||||||
"baseColorTexture" : {
|
|
||||||
"index" : 0
|
|
||||||
},
|
|
||||||
"metallicFactor" : 0,
|
|
||||||
"roughnessFactor" : 0.44999998807907104
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"meshes" : [
|
|
||||||
{
|
|
||||||
"name" : "banner_brown",
|
|
||||||
"primitives" : [
|
|
||||||
{
|
|
||||||
"attributes" : {
|
|
||||||
"POSITION" : 0,
|
|
||||||
"TEXCOORD_0" : 1,
|
|
||||||
"NORMAL" : 2
|
|
||||||
},
|
|
||||||
"indices" : 3,
|
|
||||||
"material" : 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"textures" : [
|
|
||||||
{
|
|
||||||
"sampler" : 0,
|
|
||||||
"source" : 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"mimeType" : "image/png",
|
|
||||||
"name" : "dungeon_texture",
|
|
||||||
"uri" : "dungeon_texture.png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"accessors" : [
|
|
||||||
{
|
|
||||||
"bufferView" : 0,
|
|
||||||
"componentType" : 5126,
|
|
||||||
"count" : 187,
|
|
||||||
"max" : [
|
|
||||||
0.7500076293945312,
|
|
||||||
3.7274699211120605,
|
|
||||||
0.6895275712013245
|
|
||||||
],
|
|
||||||
"min" : [
|
|
||||||
-0.749992847442627,
|
|
||||||
0.5313930511474609,
|
|
||||||
0.3776381015777588
|
|
||||||
],
|
|
||||||
"type" : "VEC3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bufferView" : 1,
|
|
||||||
"componentType" : 5126,
|
|
||||||
"count" : 187,
|
|
||||||
"type" : "VEC2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bufferView" : 2,
|
|
||||||
"componentType" : 5126,
|
|
||||||
"count" : 187,
|
|
||||||
"type" : "VEC3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bufferView" : 3,
|
|
||||||
"componentType" : 5123,
|
|
||||||
"count" : 291,
|
|
||||||
"type" : "SCALAR"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"bufferViews" : [
|
|
||||||
{
|
|
||||||
"buffer" : 0,
|
|
||||||
"byteLength" : 2244,
|
|
||||||
"byteOffset" : 0,
|
|
||||||
"target" : 34962
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"buffer" : 0,
|
|
||||||
"byteLength" : 1496,
|
|
||||||
"byteOffset" : 2244,
|
|
||||||
"target" : 34962
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"buffer" : 0,
|
|
||||||
"byteLength" : 2244,
|
|
||||||
"byteOffset" : 3740,
|
|
||||||
"target" : 34962
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"buffer" : 0,
|
|
||||||
"byteLength" : 582,
|
|
||||||
"byteOffset" : 5984,
|
|
||||||
"target" : 34963
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"samplers" : [
|
|
||||||
{
|
|
||||||
"magFilter" : 9729,
|
|
||||||
"minFilter" : 9987
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"buffers" : [
|
|
||||||
{
|
|
||||||
"byteLength" : 6568,
|
|
||||||
"uri" : "banner_brown.bin"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||