Compare commits

...
24 Commits
Author SHA1 Message Date
Warren H 0e36ca1a41 Release v0.1.21 2026-07-19 2026-07-19 18:48:28 -04:00
Warren H 018e060cdd Release v0.1.20 2026-07-19 2026-07-19 16:37:37 -04:00
Warren H 437e70fc58 Release v0.1.19 2026-07-19 2026-07-19 14:15:30 -04:00
Warren H 7b522e3bc8 Release v0.1.18 2026-07-19 2026-07-19 13:58:47 -04:00
Warren H 016a012c78 Release v0.1.17 2026-07-19 2026-07-19 13:02:46 -04:00
Warren H 5045f17b94 Release v0.1.16 2026-07-18 2026-07-18 20:50:51 -04:00
Warren H ea5d4550a5 Release v0.1.15 2026-07-18 2026-07-18 20:37:45 -04:00
Warren H 40aa23be46 new help section explaining classes and their mechanics 2026-07-18 12:05:34 -04:00
Warren H 638aef22b2 updated some performance stuff 2026-07-17 15:39:31 -04:00
Warren H 10ff124466 new rpg mode 2026-07-17 12:47:42 -04:00
Warren H de7b59ce77 Release v0.1.14 2026-07-13 2026-07-13 23:19:40 -04:00
Warren H 4b31480bec Release v0.1.13 2026-07-13 2026-07-13 22:50:51 -04:00
Warren H ed34c2503c Release v0.1.12 2026-07-13 2026-07-13 22:41:35 -04:00
Warren H 8e48bb4fb6 Release v0.1.11 2026-07-13 2026-07-13 21:31:36 -04:00
Warren H fb41147197 Release v0.1.10 2026-07-13 2026-07-13 20:47:56 -04:00
Warren H aa9f5eff20 Release v0.1.9 2026-07-13 2026-07-13 19:37:30 -04:00
Warren H 18c416d4d2 Release v0.1.8 2026-07-13 2026-07-13 17:21:10 -04:00
Warren H 77fd434226 Release v0.1.7 2026-07-12 2026-07-12 23:39:57 -04:00
Warren H 122f159b94 Release v0.1.6 2026-07-12 2026-07-12 23:20:15 -04:00
Warren H 35553c18dd Release v0.1.5 2026-07-12 2026-07-12 22:10:12 -04:00
Warren H bef71d391a Release v0.1.4 2026-07-12 2026-07-12 13:49:46 -04:00
Warren H b48b3a4f8f Release v0.1.3 2026-07-11 2026-07-11 23:23:02 -04:00
Warren H 076f6cf97c Release v0.1.2 2026-07-11 2026-07-11 00:12:57 -04:00
Warren H 6726c600e4 Release v0.1.1 2026-07-10 2026-07-10 23:47:37 -04:00
935 changed files with 46855 additions and 358658 deletions
+5
View File
@@ -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
@@ -7,5 +10,7 @@ vite.config.d.ts
.env .env
.env.* .env.*
!.env.example !.env.example
/git-token
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
/public/basis/
+26 -4
View File
@@ -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.
+147
View File
@@ -0,0 +1,147 @@
# TrueNAS deployment
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
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
```text
Local Gitea bare repository:
/mnt/.ix-apps/app_mounts/gitea/data/git/repositories/phenom/i-want-to-heal-mmo.git
Runnable working checkout:
/mnt/usbssds/apps/iwanttoheal-mmo/app
Persistent game data:
/mnt/usbssds/apps/iwanttoheal-mmo/data
Public URL:
https://iwanttoheal.phenomrom.com
Container/host port:
4173
```
The bare Gitea repository is not a runnable application directory. Use a local
`git clone`; do not copy the bare repository with `cp`. Local clone/pull keeps
the source transfer on TrueNAS and creates the working tree the container needs.
## First installation
This configuration uses a separate `iwanttoheal-mmo` working directory, so it
does not modify the old app checkout or its data.
Confirm the new repository path. If the first command fails, use the search:
```sh
sudo test -d /mnt/.ix-apps/app_mounts/gitea/data/git/repositories/phenom/i-want-to-heal-mmo.git
sudo find /mnt -type d -name "i-want-to-heal-mmo.git" -prune -print 2>/dev/null
```
Clone entirely through the local filesystem:
```sh
sudo mkdir -p /mnt/usbssds/apps/iwanttoheal-mmo/{app,data}
sudo git config --global --add safe.directory \
/mnt/.ix-apps/app_mounts/gitea/data/git/repositories/phenom/i-want-to-heal-mmo.git
sudo git clone \
/mnt/.ix-apps/app_mounts/gitea/data/git/repositories/phenom/i-want-to-heal-mmo.git \
/mnt/usbssds/apps/iwanttoheal-mmo/app
sudo chown -R truenas_admin:truenas_admin /mnt/usbssds/apps/iwanttoheal-mmo
```
Verify the checkout:
```sh
git -C /mnt/usbssds/apps/iwanttoheal-mmo/app remote -v
git -C /mnt/usbssds/apps/iwanttoheal-mmo/app branch --show-current
ls /mnt/usbssds/apps/iwanttoheal-mmo/app/package.json
```
Expected branch: `main`. Expected origin: local Gitea path above.
## Install as a TrueNAS app
1. Open **Apps**.
2. Open **Discover**.
3. Open the three-dot menu.
4. Select **Install via YAML**.
5. Name the app `iwanttoheal-mmo`.
6. Paste:
```yaml
services:
iwanttoheal:
image: node:24-bookworm-slim
command: >-
sh -lc "corepack pnpm install --frozen-lockfile && corepack pnpm run db:init && corepack pnpm run build && corepack pnpm start"
environment:
CORS_ORIGINS: "https://iwanttoheal.phenomrom.com,capacitor://localhost,http://localhost"
DATA_DIR: /app/data
HOST: 0.0.0.0
PORT: "4173"
init: true
ports:
- "4173:4173"
restart: unless-stopped
volumes:
- /mnt/usbssds/apps/iwanttoheal-mmo/app:/app
- /mnt/usbssds/apps/iwanttoheal-mmo/data:/app/data
working_dir: /app
```
Do not remove the `/app/data` mount or place the SQLite database inside the source
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
host port `4173`, so the old and MMO apps cannot run simultaneously while both
use that host port. Stop the old app, or assign this app an unused host port and
separate reverse-proxy hostname.
After deployment, test from the TrueNAS shell or another LAN machine:
```sh
curl -I http://TRUENAS-IP:4173
```
Expected result: HTTP `200`. Keep the existing HTTPS reverse proxy pointed at
`TRUENAS-IP:4173` for `iwanttoheal.phenomrom.com`.
## Update workflow
Push `main` from the development Mac. Then run on TrueNAS:
```sh
git -C /mnt/usbssds/apps/iwanttoheal-mmo/app pull --ff-only \
/mnt/.ix-apps/app_mounts/gitea/data/git/repositories/phenom/i-want-to-heal-mmo.git \
main
```
This transfers Git objects locally from Gitea storage; it does not download the
game through HTTPS. Restart `iwanttoheal-mmo` in the TrueNAS Apps UI afterward. The
container startup command installs locked dependencies, rebuilds the browser
bundle, and starts port `4173`.
For simultaneous operation, keep the new app name/directory and also assign an
unused host port plus a separate reverse-proxy hostname.
+107 -37
View File
@@ -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:
@@ -30,33 +41,58 @@ platform tools and a connected Thor, build and install it with:
pnpm android:install pnpm android:install
``` ```
The first Android milestone uses the complete single-display fallback. Press The Android host routes the main and tactical surfaces to separate physical
Select (or Tab with a keyboard) to switch between the main game surface and the Thor displays while preserving one authoritative game state. If only one
620 × 540 tactical surface. Native routing to both physical Thor displays is the display is available, Select (or Tab with a keyboard) opens the tactical surface
next milestone; it needs two Android display contexts backed by one shared game over the main game view.
state rather than two independent WebViews.
PC and handheld browsers use the Thor top screen as a responsive, full-viewport
game surface. The compact ability strip keeps combat controls visible; Select
or Tab opens party, map, inventory, and other tactical detail. Use
`?layout=thor-preview` to restore the stacked dual-screen hardware mockup for
browser QA.
## TrueNAS deployment ## TrueNAS deployment
The production preview server uses the existing deployment address and port: Complete first-install, local-Gitea clone, YAML, update, and verification steps:
[DEPLOYMENT.md](DEPLOYMENT.md).
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/app` - App directory: `/mnt/usbssds/apps/iwanttoheal-mmo/app`
Clone the repository into the app directory, then deploy `compose.yaml`. Compared ### Server architecture
with the old game configuration:
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
deploy `compose.yaml`. The expected source path is:
```text
/mnt/.ix-apps/app_mounts/gitea/data/git/repositories/phenom/i-want-to-heal-mmo.git
```
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
@@ -66,11 +102,18 @@ The repository target is:
https://git.whoagland.com/phenom/i-want-to-heal-mmo.git https://git.whoagland.com/phenom/i-want-to-heal-mmo.git
``` ```
Create a Gitea access token with repository write permission, expose it only for Create `git-token` in the repository root. Paste only the Gitea token into it:
the publishing process, then publish from `main`:
```text
gitea_token_value_goes_here
```
Do not add quotes or a `GITEA_TOKEN=` prefix. The exact `/git-token` path is
Git-ignored. Restrict local file access with `chmod 600 git-token`. The publisher
reads it automatically. `GITEA_TOKEN` remains available as an optional override.
Publish from `main` normally:
```bash ```bash
export GITEA_TOKEN="..."
pnpm publish:gitea -- --message "Describe the update" pnpm publish:gitea -- --message "Describe the update"
``` ```
@@ -80,11 +123,11 @@ version tag and Gitea prerelease, and uploads the APK plus its SHA-256 file. It
uses `package.json` version unless that version is already tagged, then advances uses `package.json` version unless that version is already tagged, then advances
the patch number. Pass `--version 0.2.0` to choose an explicit version. the patch number. Pass `--version 0.2.0` to choose an explicit version.
If the TrueNAS app directory is mounted locally, publishing also performs a If the publisher runs where TrueNAS paths are visible, it prefers a
fast-forward-only pull. Otherwise it prints the clone/pull command to run on fast-forward-only pull directly from TrueNAS-local Gitea storage. Otherwise it
TrueNAS before restarting the app. The pull refuses to run when the TrueNAS prints the exact local clone/pull commands to run in the TrueNAS shell. The pull
checkout still points at the old game's repository; move or archive that checkout refuses to run when the checkout still points at the old game's repository;
and clone the new repository first. archive that checkout and clone the new repository first.
Preview checks without changing Git or TrueNAS: Preview checks without changing Git or TrueNAS:
@@ -92,27 +135,54 @@ Preview checks without changing Git or TrueNAS:
pnpm publish:gitea -- --dry-run pnpm publish:gitea -- --dry-run
``` ```
Git push credentials still come from Git's credential manager. Gitea release API Git push credentials still come from Git's credential manager. The configured
credentials come from `GITEA_TOKEN`. Never store access tokens or Android signing Gitea token handles release creation and APK uploads. Android signing keys remain
keys in this repository. outside the repository.
## Controls ## Controls
- `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
- `Select` / `Tab`: open or close the tactical interface on one-screen devices
- `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 ## Character model rollout and rollback
Healer characters use the modular `Rig_Medium` renderer by default. Version 1 composes
head, upper body, lower body, headwear, back item, main hand, and offhand slots while
continuing to use Aelia's shared animation set.
Load a hunter, then open **Appearance Lab** from the main menu. The upper display shows
the real in-game healer renderer with idle, walk, and cast previews. The lower display
selects healer class and cycles every available part. **Save look** persists the current
class, **Reset** restores its authored default, and **Cancel** discards drafts. **Compare
legacy** shows the previous whole-character model without deleting the modular selection.
Some source assets currently fuse related pieces, so version 1 exposes honest combined
slots such as face + hair, shirt + arms, and pants + shoes. These can split into finer
customization slots when compatible rigged assets are added.
The previous whole-GLB renderer remains intact during rollout. Use either rollback:
```text
?characterModels=legacy
VITE_CHARACTER_MODEL_MODE=legacy
```
The query changes one browser/app launch. The environment variable produces a legacy
build. Remove the switch to return to modular rendering.
## 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
@@ -121,6 +191,6 @@ Touch controls on lower display support party targeting, ability casting, map, a
- Android layout targets: approximately 960×540 CSS pixels top and 620×540 CSS pixels bottom - Android layout targets: approximately 960×540 CSS pixels top and 620×540 CSS pixels bottom
- Lower-screen typography scales against its own container, never the main page viewport - Lower-screen typography scales against its own container, never the main page viewport
Current Android build is an installable single-display test host. Shipping to both The Android build uses distinct display contexts that project one authoritative
physical Thor displays still needs distinct Android display contexts that project game state across both physical Thor displays. PC and Steam Deck use the same top
one authoritative game state. surface with an adaptive tactical overlay.
@@ -0,0 +1,214 @@
package com.phenomrom.iwanttoheal;
import android.os.Bundle;
import android.os.SystemClock;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import androidx.activity.OnBackPressedCallback;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import com.getcapacitor.BridgeActivity;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/** Routes every Thor controller event into one JavaScript input service without WebView focus. */
public abstract class ControllerBridgeActivity extends BridgeActivity {
private static final float AXIS_DEAD_ZONE = 0.45f;
private static final long REPEAT_THROTTLE_MS = 55L;
private final long[] lastDpadDispatchAt = new long[16];
private final Set<String> activeMotionTokens = new HashSet<>();
private final Map<String, Long> lastMotionDispatchAt = new HashMap<>();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
dispatchNativeControllerToken("Button1", false);
}
});
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
WindowManager.LayoutParams attributes = getWindow().getAttributes();
attributes.preferredRefreshRate = 60.0f;
getWindow().setAttributes(attributes);
if (bridge != null && bridge.getWebView() != null) {
bridge.getWebView().setOverScrollMode(View.OVER_SCROLL_NEVER);
}
enterImmersiveMode();
}
@Override
public void onPause() {
clearHeldControllerState();
super.onPause();
}
@Override
public void onResume() {
super.onResume();
enterImmersiveMode();
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
String token = controllerToken(event.getKeyCode());
if (token == null || bridge == null) return super.dispatchKeyEvent(event);
if (event.getAction() == KeyEvent.ACTION_DOWN) {
boolean repeat = event.getRepeatCount() > 0;
if (isDpadToken(token) && shouldThrottleDpad(token)) return true;
dispatchNativeControllerToken(token, repeat);
}
return true;
}
@Override
public boolean dispatchGenericMotionEvent(MotionEvent event) {
if (
bridge == null
|| event.getActionMasked() != MotionEvent.ACTION_MOVE
|| !isControllerMotionEvent(event)
) {
return super.dispatchGenericMotionEvent(event);
}
float leftStickX = event.getAxisValue(MotionEvent.AXIS_X);
float leftStickY = event.getAxisValue(MotionEvent.AXIS_Y);
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<>();
addAxisTokens(currentTokens, event.getAxisValue(MotionEvent.AXIS_HAT_X), "Button14", "Button15");
addAxisTokens(currentTokens, event.getAxisValue(MotionEvent.AXIS_HAT_Y), "Button12", "Button13");
addAxisTokens(currentTokens, leftStickX, "Axis0-", "Axis0+");
addAxisTokens(currentTokens, leftStickY, "Axis1-", "Axis1+");
long now = SystemClock.uptimeMillis();
for (String token : currentTokens) {
boolean repeat = activeMotionTokens.contains(token);
long lastDispatch = lastMotionDispatchAt.containsKey(token)
? lastMotionDispatchAt.get(token)
: 0L;
if (!repeat || now - lastDispatch >= REPEAT_THROTTLE_MS) {
dispatchNativeControllerToken(token, repeat);
lastMotionDispatchAt.put(token, now);
}
}
activeMotionTokens.clear();
activeMotionTokens.addAll(currentTokens);
return true;
}
private void enterImmersiveMode() {
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
WindowInsetsControllerCompat controller = WindowCompat.getInsetsController(
getWindow(),
getWindow().getDecorView()
);
controller.hide(WindowInsetsCompat.Type.systemBars());
controller.setSystemBarsBehavior(
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
);
}
private boolean isControllerMotionEvent(MotionEvent event) {
int source = event.getSource();
return (source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
|| (source & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD
|| (source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD;
}
private void addAxisTokens(Set<String> tokens, float value, String negative, String positive) {
if (value <= -AXIS_DEAD_ZONE) tokens.add(negative);
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) {
if (bridge == null || bridge.getWebView() == null) return;
String script =
"window.dispatchEvent(new CustomEvent('iwt-native-controller',"
+ "{detail:{token:'" + token + "',repeat:" + repeat + "}}));";
bridge.getWebView().post(() -> bridge.getWebView().evaluateJavascript(script, null));
}
private void dispatchNativeControllerMotion(float moveX, float moveY, float lookX, float lookY) {
if (bridge == null || bridge.getWebView() == null) return;
String script =
"window.dispatchEvent(new CustomEvent('iwt-native-controller-motion',"
+ "{detail:{moveX:" + moveX + ",moveY:" + moveY
+ ",lookX:" + lookX + ",lookY:" + lookY + "}}));";
bridge.getWebView().post(() -> bridge.getWebView().evaluateJavascript(script, null));
}
private void clearHeldControllerState() {
activeMotionTokens.clear();
lastMotionDispatchAt.clear();
dispatchNativeControllerMotion(0.0f, 0.0f, 0.0f, 0.0f);
if (bridge == null || bridge.getWebView() == null) return;
bridge.getWebView().post(() -> bridge.getWebView().evaluateJavascript(
"window.dispatchEvent(new Event('iwt-native-controller-reset'));",
null
));
}
private boolean shouldThrottleDpad(String token) {
int index = Integer.parseInt(token.substring("Button".length()));
long now = SystemClock.uptimeMillis();
if (now - lastDpadDispatchAt[index] < REPEAT_THROTTLE_MS) return true;
lastDpadDispatchAt[index] = now;
return false;
}
private boolean isDpadToken(String token) {
return token.equals("Button12")
|| token.equals("Button13")
|| token.equals("Button14")
|| token.equals("Button15");
}
private String controllerToken(int keyCode) {
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_A:
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
case KeyEvent.KEYCODE_NUMPAD_ENTER:
return "Button0";
case KeyEvent.KEYCODE_BUTTON_B:
case KeyEvent.KEYCODE_ESCAPE:
return "Button1";
case KeyEvent.KEYCODE_BUTTON_X: return "Button2";
case KeyEvent.KEYCODE_BUTTON_Y: return "Button3";
case KeyEvent.KEYCODE_BUTTON_L1: return "Button4";
case KeyEvent.KEYCODE_BUTTON_R1: return "Button5";
case KeyEvent.KEYCODE_BUTTON_L2: return "Button6";
case KeyEvent.KEYCODE_BUTTON_R2: return "Button7";
case KeyEvent.KEYCODE_BUTTON_SELECT: return "Button8";
case KeyEvent.KEYCODE_BUTTON_START: return "Button9";
case KeyEvent.KEYCODE_BUTTON_THUMBL: return "Button10";
case KeyEvent.KEYCODE_BUTTON_THUMBR: return "Button11";
case KeyEvent.KEYCODE_DPAD_UP: return "Button12";
case KeyEvent.KEYCODE_DPAD_DOWN: return "Button13";
case KeyEvent.KEYCODE_DPAD_LEFT: return "Button14";
case KeyEvent.KEYCODE_DPAD_RIGHT: return "Button15";
default: return null;
}
}
}
@@ -1,44 +1,11 @@
package com.phenomrom.iwanttoheal; package com.phenomrom.iwanttoheal;
import android.os.Bundle; import android.os.Bundle;
import android.view.View;
import android.view.WindowManager;
import androidx.core.view.WindowCompat; public class MainActivity extends ControllerBridgeActivity {
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
registerPlugin(ThorDualScreenPlugin.class);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
WindowManager.LayoutParams attributes = getWindow().getAttributes();
attributes.preferredRefreshRate = 60.0f;
getWindow().setAttributes(attributes);
getBridge().getWebView().setOverScrollMode(View.OVER_SCROLL_NEVER);
getBridge().getWebView().setFocusableInTouchMode(true);
getBridge().getWebView().requestFocus();
enterImmersiveMode();
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) enterImmersiveMode();
}
private void enterImmersiveMode() {
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
WindowInsetsControllerCompat controller = WindowCompat.getInsetsController(
getWindow(),
getWindow().getDecorView()
);
controller.hide(WindowInsetsCompat.Type.systemBars());
controller.setSystemBarsBehavior(
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
);
} }
} }
@@ -0,0 +1,307 @@
package com.phenomrom.iwanttoheal;
import android.app.Presentation;
import android.content.Context;
import android.graphics.Color;
import android.hardware.display.DisplayManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.Display;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import com.getcapacitor.JSArray;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
/** Keeps a dedicated WebView visible on each AYN Thor panel for the activity lifetime. */
@CapacitorPlugin(name = "ThorDualScreen")
public class ThorDualScreenPlugin extends Plugin implements DisplayManager.DisplayListener {
private final Handler handler = new Handler(Looper.getMainLooper());
private DisplayManager displayManager;
private ThorPresentation presentation;
private boolean forceEnabled;
@Override
public void load() {
displayManager = (DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE);
displayManager.registerDisplayListener(this, handler);
}
@PluginMethod
public void getDisplays(PluginCall call) {
Display current = currentDisplay();
JSArray displays = new JSArray();
for (Display display : displayManager.getDisplays()) displays.put(describeDisplay(display, current));
JSObject result = new JSObject();
result.put("currentDisplayId", current == null ? -1 : current.getDisplayId());
result.put("displays", displays);
call.resolve(result);
}
@PluginMethod
public void forceBothDisplays(PluginCall call) {
forceEnabled = true;
getActivity().runOnUiThread(() -> {
try {
DisplayPlan plan = createPlan();
if (plan.target == null) {
call.reject("No active secondary Android display is available.");
return;
}
boolean alreadyOpen = presentation != null
&& presentation.isShowing()
&& presentation.getDisplay().getDisplayId() == plan.target.getDisplayId();
if (!alreadyOpen) openPlan(plan);
JSObject result = describeDisplay(plan.target, currentDisplay());
result.put("opened", true);
result.put("topOnActivity", plan.currentIsTop);
call.resolve(result);
} catch (Exception exception) {
closePresentation();
call.reject("Unable to force both Thor displays.", exception);
}
});
}
@Override
protected void handleOnResume() {
if (forceEnabled) scheduleEnsurePresentation(0L);
}
@Override
protected void handleOnDestroy() {
forceEnabled = false;
if (displayManager != null) displayManager.unregisterDisplayListener(this);
handler.removeCallbacksAndMessages(null);
closePresentation();
}
@Override
public void onDisplayAdded(int displayId) {
if (forceEnabled) scheduleEnsurePresentation(100L);
}
@Override
public void onDisplayRemoved(int displayId) {
if (presentation != null && presentation.getDisplay().getDisplayId() == displayId) {
closePresentation();
}
if (forceEnabled) scheduleEnsurePresentation(250L);
}
@Override
public void onDisplayChanged(int displayId) {
if (forceEnabled) scheduleEnsurePresentation(100L);
}
private void scheduleEnsurePresentation(long delayMs) {
handler.removeCallbacks(ensurePresentation);
handler.postDelayed(ensurePresentation, delayMs);
}
private final Runnable ensurePresentation = () -> {
if (!forceEnabled || getActivity() == null || getActivity().isFinishing()) return;
DisplayPlan plan = createPlan();
if (plan.target == null) return;
if (
presentation != null
&& presentation.isShowing()
&& presentation.getDisplay().getDisplayId() == plan.target.getDisplayId()
) return;
openPlan(plan);
};
private void openPlan(DisplayPlan plan) {
String baseUrl = bridge.getLocalUrl();
String targetUrl = baseUrl + "/?display=" + (plan.targetIsTop ? "top" : "bottom") + "&role=presentation";
String currentUrl = baseUrl + "/?display=" + (plan.currentIsTop ? "top" : "bottom") + "&role=activity";
closePresentation();
presentation = new ThorPresentation(
getActivity(),
plan.target,
targetUrl
);
presentation.setOnDismissListener(dialog -> {
presentation = null;
notifyListeners("displayDisconnected", new JSObject());
if (forceEnabled) scheduleEnsurePresentation(300L);
});
presentation.show();
bridge.getWebView().loadUrl(currentUrl);
}
private DisplayPlan createPlan() {
Display current = currentDisplay();
int currentId = current == null ? -1 : current.getDisplayId();
Display[] displays = displayManager.getDisplays();
Display top = largestDisplay(displays);
boolean currentIsTop = top != null && top.getDisplayId() == currentId;
Display target = currentIsTop
? smallestOtherDisplay(displays, currentId)
: top != null && top.getDisplayId() != currentId ? top : largestOtherDisplay(displays, currentId);
boolean targetIsTop = target != null && top != null && target.getDisplayId() == top.getDisplayId();
return new DisplayPlan(target, targetIsTop, currentIsTop);
}
private Display currentDisplay() {
return getActivity().getWindow().getDecorView().getDisplay();
}
private Display largestDisplay(Display[] displays) {
Display selected = null;
long pixels = -1L;
for (Display display : displays) {
if (display.getState() == Display.STATE_OFF) continue;
long candidate = pixelCount(display);
if (candidate > pixels) {
selected = display;
pixels = candidate;
}
}
return selected;
}
private Display largestOtherDisplay(Display[] displays, int currentId) {
Display selected = null;
long pixels = -1L;
for (Display display : displays) {
if (display.getDisplayId() == currentId || display.getState() == Display.STATE_OFF) continue;
long candidate = pixelCount(display);
if (candidate > pixels) {
selected = display;
pixels = candidate;
}
}
return selected;
}
private Display smallestOtherDisplay(Display[] displays, int currentId) {
Display selected = null;
long pixels = Long.MAX_VALUE;
for (Display display : displays) {
if (display.getDisplayId() == currentId || display.getState() == Display.STATE_OFF) continue;
long candidate = pixelCount(display);
if (candidate < pixels) {
selected = display;
pixels = candidate;
}
}
return selected;
}
private long pixelCount(Display display) {
Display.Mode mode = display.getMode();
return (long) mode.getPhysicalWidth() * mode.getPhysicalHeight();
}
private JSObject describeDisplay(Display display, Display current) {
Display.Mode mode = display.getMode();
JSObject result = new JSObject();
result.put("id", display.getDisplayId());
result.put("name", display.getName());
result.put("width", mode.getPhysicalWidth());
result.put("height", mode.getPhysicalHeight());
result.put("refreshRate", mode.getRefreshRate());
result.put("isCurrent", current != null && current.getDisplayId() == display.getDisplayId());
return result;
}
private void closePresentation() {
if (presentation == null) return;
ThorPresentation closing = presentation;
presentation = null;
closing.setOnDismissListener(null);
closing.dismiss();
}
private final class ThorPresentation extends Presentation {
private final String initialUrl;
private WebView webView;
ThorPresentation(Context context, Display display, String initialUrl) {
super(context, display);
this.initialUrl = initialUrl;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setBackgroundDrawableResource(android.R.color.black);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
WindowManager.LayoutParams attributes = getWindow().getAttributes();
attributes.preferredRefreshRate = 60.0f;
getWindow().setAttributes(attributes);
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
);
FrameLayout container = new FrameLayout(getContext());
container.setBackgroundColor(Color.BLACK);
webView = new WebView(getContext());
configureWebView(webView);
container.addView(webView, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
));
setContentView(container);
webView.loadUrl(initialUrl);
}
@Override
protected void onStop() {
super.onStop();
if (webView != null) {
webView.stopLoading();
webView.destroy();
webView = null;
}
}
}
private void configureWebView(WebView webView) {
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setDatabaseEnabled(true);
settings.setMediaPlaybackRequiresUserGesture(false);
settings.setAllowFileAccess(true);
settings.setAllowContentAccess(true);
webView.setBackgroundColor(Color.BLACK);
webView.setOverScrollMode(View.OVER_SCROLL_NEVER);
webView.setWebViewClient(new WebViewClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
return bridge.getLocalServer().shouldInterceptRequest(request);
}
});
}
private static final class DisplayPlan {
private final Display target;
private final boolean targetIsTop;
private final boolean currentIsTop;
DisplayPlan(Display target, boolean targetIsTop, boolean currentIsTop) {
this.target = target;
this.targetIsTop = targetIsTop;
this.currentIsTop = currentIsTop;
}
}
}
+5 -2
View File
@@ -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
@@ -11,5 +13,6 @@ services:
- "4173:4173" - "4173:4173"
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- /mnt/usbssds/apps/iwanttoheal/app:/app - /mnt/usbssds/apps/iwanttoheal-mmo/app:/app
- /mnt/usbssds/apps/iwanttoheal-mmo/data:/app/data
working_dir: /app working_dir: /app
+142
View File
@@ -0,0 +1,142 @@
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);
CREATE TABLE IF NOT EXISTS hockey_healing_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_returns INTEGER NOT NULL DEFAULT 0 CHECK (highest_returns >= 0),
duration_seconds REAL NOT NULL DEFAULT 0 CHECK (duration_seconds >= 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 hockey_healing_rank_idx
ON hockey_healing_records (highest_returns DESC, duration_seconds DESC, updated_at ASC);
CREATE TABLE IF NOT EXISTS hockey_pvp_records (
account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
slot_id INTEGER NOT NULL CHECK (slot_id BETWEEN 1 AND 3),
wins INTEGER NOT NULL DEFAULT 0 CHECK (wins >= 0),
losses INTEGER NOT NULL DEFAULT 0 CHECK (losses >= 0),
boss_kills INTEGER NOT NULL DEFAULT 0 CHECK (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 hockey_pvp_wins_rank_idx
ON hockey_pvp_records (wins DESC, losses ASC, updated_at ASC);
CREATE INDEX IF NOT EXISTS hockey_pvp_boss_kills_rank_idx
ON hockey_pvp_records (boss_kills DESC, wins DESC, updated_at ASC);
CREATE TABLE IF NOT EXISTS blockbreaker_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_bricks INTEGER NOT NULL DEFAULT 0 CHECK (highest_bricks >= 0),
bricks_achieved_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
longest_seconds REAL NOT NULL DEFAULT 0 CHECK (longest_seconds >= 0),
time_achieved_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
highest_score INTEGER NOT NULL DEFAULT 0 CHECK (highest_score >= 0),
score_achieved_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
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 blockbreaker_bricks_rank_idx
ON blockbreaker_records (highest_bricks DESC, bricks_achieved_at ASC, account_id ASC, slot_id ASC);
CREATE INDEX IF NOT EXISTS blockbreaker_time_rank_idx
ON blockbreaker_records (longest_seconds DESC, time_achieved_at ASC, account_id ASC, slot_id ASC);
CREATE INDEX IF NOT EXISTS blockbreaker_score_rank_idx
ON blockbreaker_records (highest_score DESC, score_achieved_at ASC, account_id ASC, slot_id ASC);
CREATE TABLE IF NOT EXISTS aether_assault_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_score INTEGER NOT NULL DEFAULT 0 CHECK (highest_score >= 0),
wave_at_best INTEGER NOT NULL DEFAULT 0 CHECK (wave_at_best >= 0),
duration_at_best REAL NOT NULL DEFAULT 0 CHECK (duration_at_best >= 0),
score_achieved_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
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 aether_assault_rank_idx
ON aether_assault_records (
highest_score DESC,
wave_at_best DESC,
score_achieved_at ASC,
account_id ASC,
slot_id ASC
);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

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
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 769 KiB

Binary file not shown.

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"
}
]
}
@@ -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_green"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_green",
"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_green.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_patternA_blue"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_patternA_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" : 220,
"max" : [
0.7500076293945312,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-0.749992847442627,
0.5313930511474609,
0.3776381015777588
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 220,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 220,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 330,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 2640,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 1760,
"byteOffset" : 2640,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 2640,
"byteOffset" : 4400,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 660,
"byteOffset" : 7040,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 7700,
"uri" : "banner_patternA_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_patternA_brown"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_patternA_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" : 220,
"max" : [
0.7500076293945312,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-0.749992847442627,
0.5313930511474609,
0.3776381015777588
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 220,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 220,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 330,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 2640,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 1760,
"byteOffset" : 2640,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 2640,
"byteOffset" : 4400,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 660,
"byteOffset" : 7040,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 7700,
"uri" : "banner_patternA_brown.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_patternA_green"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_patternA_green",
"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" : 220,
"max" : [
0.7500076293945312,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-0.749992847442627,
0.5313930511474609,
0.3776381015777588
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 220,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 220,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 330,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 2640,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 1760,
"byteOffset" : 2640,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 2640,
"byteOffset" : 4400,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 660,
"byteOffset" : 7040,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 7700,
"uri" : "banner_patternA_green.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_patternA_red"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_patternA_red",
"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" : 220,
"max" : [
0.7500076293945312,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-0.749992847442627,
0.5313930511474609,
0.3776381015777588
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 220,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 220,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 330,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 2640,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 1760,
"byteOffset" : 2640,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 2640,
"byteOffset" : 4400,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 660,
"byteOffset" : 7040,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 7700,
"uri" : "banner_patternA_red.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_patternA_white"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_patternA_white",
"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" : 220,
"max" : [
0.7500076293945312,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-0.749992847442627,
0.5313930511474609,
0.3776381015777588
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 220,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 220,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 330,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 2640,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 1760,
"byteOffset" : 2640,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 2640,
"byteOffset" : 4400,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 660,
"byteOffset" : 7040,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 7700,
"uri" : "banner_patternA_white.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_patternA_yellow"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_patternA_yellow",
"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" : 220,
"max" : [
0.7500076293945312,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-0.749992847442627,
0.5313930511474609,
0.3776381015777588
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 220,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 220,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 330,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 2640,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 1760,
"byteOffset" : 2640,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 2640,
"byteOffset" : 4400,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 660,
"byteOffset" : 7040,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 7700,
"uri" : "banner_patternA_yellow.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_patternB_blue"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_patternB_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" : 197,
"max" : [
0.7500076293945312,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-0.749992847442627,
0.5313930511474609,
0.3776381015777588
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 197,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 197,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 291,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 1576,
"byteOffset" : 2364,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 3940,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 582,
"byteOffset" : 6304,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 6888,
"uri" : "banner_patternB_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_patternB_brown"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_patternB_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" : 197,
"max" : [
0.7500076293945312,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-0.749992847442627,
0.5313930511474609,
0.3776381015777588
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 197,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 197,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 291,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 1576,
"byteOffset" : 2364,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 3940,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 582,
"byteOffset" : 6304,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 6888,
"uri" : "banner_patternB_brown.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_patternB_green"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_patternB_green",
"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" : 197,
"max" : [
0.7500076293945312,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-0.749992847442627,
0.5313930511474609,
0.3776381015777588
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 197,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 197,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 291,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 1576,
"byteOffset" : 2364,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 3940,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 582,
"byteOffset" : 6304,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 6888,
"uri" : "banner_patternB_green.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_patternB_red"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_patternB_red",
"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" : 197,
"max" : [
0.7500076293945312,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-0.749992847442627,
0.5313930511474609,
0.3776381015777588
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 197,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 197,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 291,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 1576,
"byteOffset" : 2364,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 3940,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 582,
"byteOffset" : 6304,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 6888,
"uri" : "banner_patternB_red.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_patternB_white"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_patternB_white",
"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" : 197,
"max" : [
0.7500076293945312,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-0.749992847442627,
0.5313930511474609,
0.3776381015777588
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 197,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 197,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 291,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 1576,
"byteOffset" : 2364,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 3940,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 582,
"byteOffset" : 6304,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 6888,
"uri" : "banner_patternB_white.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_patternB_yellow"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_patternB_yellow",
"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" : 197,
"max" : [
0.7500076293945312,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-0.749992847442627,
0.5313930511474609,
0.3776381015777588
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 197,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 197,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 291,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 1576,
"byteOffset" : 2364,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 3940,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 582,
"byteOffset" : 6304,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 6888,
"uri" : "banner_patternB_yellow.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_patternC_blue"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_patternC_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" : 197,
"max" : [
0.7500076293945312,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-0.749992847442627,
0.5313930511474609,
0.3776381015777588
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 197,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 197,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 291,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 1576,
"byteOffset" : 2364,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 3940,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 582,
"byteOffset" : 6304,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 6888,
"uri" : "banner_patternC_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_patternC_brown"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_patternC_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" : 197,
"max" : [
0.7500076293945312,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-0.749992847442627,
0.5313930511474609,
0.3776381015777588
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 197,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 197,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 291,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 1576,
"byteOffset" : 2364,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 3940,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 582,
"byteOffset" : 6304,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 6888,
"uri" : "banner_patternC_brown.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_patternC_green"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_patternC_green",
"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" : 197,
"max" : [
0.7500076293945312,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-0.749992847442627,
0.5313930511474609,
0.3776381015777588
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 197,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 197,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 291,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 1576,
"byteOffset" : 2364,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 3940,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 582,
"byteOffset" : 6304,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 6888,
"uri" : "banner_patternC_green.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_patternC_red"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_patternC_red",
"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" : 197,
"max" : [
0.7500076293945312,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-0.749992847442627,
0.5313930511474609,
0.3776381015777588
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 197,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 197,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 291,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 1576,
"byteOffset" : 2364,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 3940,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 582,
"byteOffset" : 6304,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 6888,
"uri" : "banner_patternC_red.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_patternC_white"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_patternC_white",
"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" : 197,
"max" : [
0.7500076293945312,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-0.749992847442627,
0.5313930511474609,
0.3776381015777588
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 197,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 197,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 291,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 1576,
"byteOffset" : 2364,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 3940,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 582,
"byteOffset" : 6304,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 6888,
"uri" : "banner_patternC_white.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_patternC_yellow"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_patternC_yellow",
"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" : 197,
"max" : [
0.7500076293945312,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-0.749992847442627,
0.5313930511474609,
0.3776381015777588
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 197,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 197,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 291,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 1576,
"byteOffset" : 2364,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 2364,
"byteOffset" : 3940,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 582,
"byteOffset" : 6304,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 6888,
"uri" : "banner_patternC_yellow.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_red"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_red",
"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_red.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_shield_blue"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_shield_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" : 1022,
"max" : [
1.116003155708313,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-1.1159993410110474,
0.5313930511474609,
0.31542640924453735
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 1022,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 1022,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 1917,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 12264,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 8176,
"byteOffset" : 12264,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 12264,
"byteOffset" : 20440,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 3834,
"byteOffset" : 32704,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 36540,
"uri" : "banner_shield_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_shield_brown"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_shield_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" : 1022,
"max" : [
1.116003155708313,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-1.1159993410110474,
0.5313930511474609,
0.31542640924453735
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 1022,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 1022,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 1917,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 12264,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 8176,
"byteOffset" : 12264,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 12264,
"byteOffset" : 20440,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 3834,
"byteOffset" : 32704,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 36540,
"uri" : "banner_shield_brown.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_shield_green"
}
],
"materials" : [
{
"name" : "texture",
"pbrMetallicRoughness" : {
"baseColorTexture" : {
"index" : 0
},
"metallicFactor" : 0,
"roughnessFactor" : 0.44999998807907104
}
}
],
"meshes" : [
{
"name" : "banner_shield_green",
"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" : 1022,
"max" : [
1.116003155708313,
3.7274699211120605,
0.6895275712013245
],
"min" : [
-1.1159993410110474,
0.5313930511474609,
0.31542640924453735
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 1022,
"type" : "VEC2"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 1022,
"type" : "VEC3"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 1917,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 12264,
"byteOffset" : 0,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 8176,
"byteOffset" : 12264,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 12264,
"byteOffset" : 20440,
"target" : 34962
},
{
"buffer" : 0,
"byteLength" : 3834,
"byteOffset" : 32704,
"target" : 34963
}
],
"samplers" : [
{
"magFilter" : 9729,
"minFilter" : 9987
}
],
"buffers" : [
{
"byteLength" : 36540,
"uri" : "banner_shield_green.bin"
}
]
}

Some files were not shown because too many files have changed in this diff Show More