diff --git a/CONTENT_UPDATES.md b/CONTENT_UPDATES.md index 7be9994b..4d30edbf 100644 --- a/CONTENT_UPDATES.md +++ b/CONTENT_UPDATES.md @@ -6,7 +6,9 @@ the React/Three.js runtime, menus, local profile and save support, UI assets, starter equipment, Wailing Caverns starter content, and the basic Human character model with its default male and female appearances. Android also contains its native bridge. Other GLBs, textures, audio, character/skin packs, -dungeons, and Manastorm stages are published by TrueNAS as optional content. +and dungeons are published by TrueNAS as optional content. Internal Manastorm +environment packs remain in the catalog only because regular dungeons reuse +them as automatic dependencies. Single-player is local-first. Starting the game, loading a character, entering installed content, simulating combat, and saving progress never require the @@ -165,10 +167,13 @@ characters:wow335a-v1 characters:ascension-v1 characters:runewaker-v1 core:combat-media -manastorm: shared:creatures ``` +`manastorm:*` and `core:arenas` remain valid internal pack IDs for compatibility +but are hidden from the Downloads screen. Installing a regular dungeon still +installs its exact environment dependency closure automatically. + Changing one GLB changes one object hash. The client downloads that object and atomically updates the pack rather than downloading every dungeon again. diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index fa988f22..ff2bb3a1 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -135,6 +135,7 @@ services: CONTENT_DIR: /app/content DATA_DIR: /app/runtime-data HOST: 0.0.0.0 + HEALER_MAN_ENABLE_SHELVED_MODES: "0" NODE_OPTIONS: "--max-old-space-size=4096" PORT: "4173" SESSION_TTL_DAYS: "30" diff --git a/README.md b/README.md index eb128865..0d20ee2c 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,11 @@ npm test npm run build ``` +Manastorms, Arenas/PvP, and their GM tools are shelved in normal browser and +Android builds. Their source and saved data remain intact. Use +`npm run dev:shelved` or `npm run build:shelved` for maintenance builds, and +set `HEALER_MAN_ENABLE_SHELVED_MODES=1` on the matching development server. + Boss-mechanic and dungeon-event extension points, telegraph rules, checkpoint behavior, test workflow, and the explicit source-gap ledger are documented in [`ENCOUNTER_AUTHORING.md`](ENCOUNTER_AUTHORING.md). @@ -50,8 +55,10 @@ behavior, test workflow, and the explicit source-gap ledger are documented in Production browser and Android builds ship a compact required set: the game runtime, interface, starter equipment, Wailing Caverns starter content, and a basic male/female Human character model with its default appearance. Large -character and skin libraries, additional dungeons, Manastorm stages, and the -original spell-effects and sound pack are optional downloads. +character and skin libraries, additional dungeons, and the original +spell-effects and sound pack are optional downloads. Manastorm environment +packs remain internal dungeon dependencies and are installed automatically +when a regular dungeon needs one; they are not shown as standalone downloads. Open **Downloads** from the main menu to download, update, or remove optional packs on either platform. Browser downloads live in an app-managed verified @@ -223,8 +230,8 @@ and install directly onto a connected device, use `npm run android:install`. Android uses a compact `dist-android` build with the same required-content boundary as the production browser build. Optional dungeons, character/skin -packs, Manastorm stages, and original spell media are downloaded into -app-private storage. Installed content and local saves remain playable without +packs, internal environment dependencies, and original spell media are +downloaded into app-private storage. Installed content and local saves remain playable without a server connection and survive APK updates. See `CONTENT_UPDATES.md` for the update contract and `DEPLOYMENT.md` for TrueNAS publishing and signed Gitea releases. diff --git a/RELEASE_RUNBOOK.md b/RELEASE_RUNBOOK.md index 76cfd778..b0d53a28 100644 --- a/RELEASE_RUNBOOK.md +++ b/RELEASE_RUNBOOK.md @@ -657,6 +657,7 @@ services: CONTENT_DIR: /app/content DATA_DIR: /app/runtime-data HOST: 0.0.0.0 + HEALER_MAN_ENABLE_SHELVED_MODES: "0" NODE_OPTIONS: "--max-old-space-size=4096" PORT: "4173" SESSION_TTL_DAYS: "30" diff --git a/docs/content-packing.md b/docs/content-packing.md index a0e497fb..7cefea44 100644 --- a/docs/content-packing.md +++ b/docs/content-packing.md @@ -1,9 +1,11 @@ # Lossless game content packaging -`npm run web:prepare` and `npm run android:prepare` include all 84 regular -dungeon packs, the existing core packs, and the dungeons' dependency closure. -Manastorm packs keep their optional install flag even when a regular dungeon -uses the same map. Manastorm-only maps and extra character packs stay optional. +`npm run web:prepare` and `npm run android:prepare` include exactly five packs: +`core:ui`, `core:equipment`, `core:character-basic`, `core:runtime`, and +`dungeon:wailing-caverns`. The complete publication catalog retains all 54 +regular dungeons. Internal `manastorm:*` environment packs remain dependency +targets for the 53 regular dungeons that reuse those maps, but they and +`core:arenas` are hidden from the Downloads screen. The staging directories contain one object per original SHA-256 and extension under `content/bundled`. Gzip is used only when it saves space. The packager @@ -22,7 +24,8 @@ existing catalogs. Remote publishing retains original objects for older apps; new clients prefer the compressed representation and verify both hashes. The Node server and Vite preview middleware resolve bundled logical URLs on -first launch. The browser service worker does the same after activation. +first launch. The browser service worker does the same after activation and +persists each immutable bootstrap object by hash after its first use. Android streams bundled gzip through its WebView asset handler, including the companion display, without writing an expanded copy to app storage. Deploy the browser build with the project's Node server so first-launch asset aliases work. @@ -38,6 +41,12 @@ unique object bytes and duplicate savings. These are packaged asset sizes, not GPU memory estimates. Optional Android downloads occupy their expanded size; bundled Android assets stay compressed inside the APK. +Normal release verification rejects unexpected bootstrap packs, more than 80 +MiB of stored bootstrap objects, arena assets, and shelved client modules. +Eligible JavaScript, CSS, and JSON responses are precompressed with Brotli and +gzip; hashed Vite files and bundled content objects use immutable one-year +caching, while HTML, service workers, and manifests revalidate. + Validation: `npm run content:test`, the content/avatar Vitest suites, the bundled-content server tests, `npm run build`, Android compilation, and a live roster plus dungeon screenshot check. Run `npm run release:check` before handoff. diff --git a/dungeon-pipeline/boss-mechanics.json b/dungeon-pipeline/boss-mechanics.json index 99284a43..cabc4114 100644 --- a/dungeon-pipeline/boss-mechanics.json +++ b/dungeon-pipeline/boss-mechanics.json @@ -9589,6 +9589,1769 @@ } } ] + }, + "rw-grafu-castle-105204": { + "name": "Conjurer Chapeaunoir", + "mode": "replace", + "source": { + "kind": "runewaker", + "dungeonId": "grafu-castle", + "templateId": 105204, + "roots": [ + "cl_136_105205_N" + ], + "functions": [ + "cl_136_105205_N", + "DW_Rand", + "cl_136_rabbit_N", + "cl_136_opendoor_dead_N", + "cl_136_105204_N", + "cl_136_105204_dead_N", + "cl_105209_chack_N", + "cl_105209_P_N", + "DW_MoveToFlag", + "cl_117377_touch_N", + "cl_496924_N", + "cl_136_496926_N", + "cl_magicInHand_BIG_NOL", + "cl_136_105209_AI2_N", + "DW_WaitMoveTo", + "SearchRangeNPC", + "DW_HateRemain", + "Az_RND_Ary", + "cl_136_hset2_table_N", + "cl_105206_N", + "cl_136_105208_N", + "SearchRangePlayer", + "Role:new", + "Abs", + "cl_chbuff_117260_N", + "cl_105206_touch_N", + "cl_136_105242ball_N", + "cl_105208_dead_N", + "Init_RoleData_Value", + "Init_RoleData_Function" + ], + "spellIds": [ + 496773, + 496882, + 496884, + 496920, + 496921, + 496923, + 496924, + 496925, + 496926, + 496927, + 496928, + 496929, + 497011 + ] + }, + "mechanics": [ + { + "id": "rw-grafu-castle-105204-debuff-496773", + "name": "Aggro", + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "target-aura", + "aura": "aggro", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105204-debuff-496882", + "name": "Conjurer's Hat", + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "target-aura", + "aura": "conjurer-s-hat", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105204-spell-496920", + "name": "Opening Act", + "spellId": 496920, + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 2000, + "range": 15, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + } + }, + { + "id": "rw-grafu-castle-105204-spell-496921", + "name": "Shuffle Deck", + "spellId": 496921, + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 15, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + } + }, + { + "id": "rw-grafu-castle-105204-aura-496923", + "name": "Throw Hat", + "delivery": "area", + "target": "self", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1200, + "range": 0, + "cooldownMs": 15000, + "initialCooldownMs": [ + 15000, + 15000 + ], + "repeatCooldownMs": [ + 15000, + 15000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "aura", + "aura": "throw-hat", + "durationMs": 7500 + } + }, + { + "id": "rw-grafu-castle-105204-spell-496924", + "name": "Conjure Energy", + "spellId": 496924, + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 2000, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 7.5, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 7.5 + } + }, + { + "id": "rw-grafu-castle-105204-spell-496926", + "name": "Poker Shift", + "spellId": 496926, + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1000, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85 + }, + { + "id": "rw-grafu-castle-105204-spell-496927", + "name": "Visionary Star", + "spellId": 496927, + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 0, + "range": 10, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85 + }, + { + "id": "rw-grafu-castle-105204-spell-496928", + "name": "Black Hat Burst", + "spellId": 496928, + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 15, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + } + }, + { + "id": "rw-grafu-castle-105204-spell-496929", + "name": "Conjure Energy", + "spellId": 496929, + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + } + }, + { + "id": "rw-grafu-castle-105204-spell-497011", + "name": "Conjure Skill", + "spellId": 497011, + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 15, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + } + } + ] + }, + "rw-grafu-castle-105224": { + "name": "Boro-Boro Brothers", + "mode": "replace", + "source": { + "kind": "runewaker", + "dungeonId": "grafu-castle", + "templateId": 105224, + "roots": [ + "ic_105224_01" + ], + "functions": [ + "ic_105224_01", + "Lua_DW_CreateObj", + "ic_105224_44", + "ic_105224_100", + "ic_105224_02", + "ic_105224_04", + "SearchRangePlayer", + "DW_HateRemain", + "DW_Rand", + "ic_105224_06", + "ic_105224_05" + ], + "spellIds": [ + 496249, + 496250, + 496361, + 496362, + 496363, + 496366, + 496367, + 496368 + ] + }, + "mechanics": [ + { + "id": "rw-grafu-castle-105224-spell-496249", + "name": "Primal Dread", + "spellId": 496249, + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 15, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + } + }, + { + "id": "rw-grafu-castle-105224-aura-496250", + "name": "Torment", + "delivery": "area", + "target": "self", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1200, + "range": 0, + "cooldownMs": 15000, + "initialCooldownMs": [ + 15000, + 15000 + ], + "repeatCooldownMs": [ + 15000, + 15000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "aura", + "aura": "torment", + "durationMs": 7500 + } + }, + { + "id": "rw-grafu-castle-105224-spell-496361", + "name": "Torture Method", + "spellId": 496361, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.2, + "radius": 5, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 5 + } + }, + { + "id": "rw-grafu-castle-105224-spell-496362", + "name": "Tyrannize", + "spellId": 496362, + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 5, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 5 + } + }, + { + "id": "rw-grafu-castle-105224-control-496363", + "name": "Iron Maiden Imprisonment", + "delivery": "projectile", + "target": "primary", + "school": "shadow", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "control", + "mechanic": "root", + "durationMs": 3000 + } + }, + { + "id": "rw-grafu-castle-105224-spell-496366", + "name": "Acupuncture Torment", + "spellId": 496366, + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 5, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 5 + } + }, + { + "id": "rw-grafu-castle-105224-aura-496367", + "name": "Dismemberment", + "delivery": "area", + "target": "self", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1200, + "range": 0, + "cooldownMs": 15000, + "initialCooldownMs": [ + 15000, + 15000 + ], + "repeatCooldownMs": [ + 15000, + 15000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "aura", + "aura": "dismemberment", + "durationMs": 7500 + } + }, + { + "id": "rw-grafu-castle-105224-spell-496368", + "name": "Acupuncture", + "spellId": 496368, + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 0, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85 + } + ] + }, + "rw-grafu-castle-105351": { + "name": "Larvanger Barkud", + "mode": "replace", + "source": { + "kind": "healer-man-source-gap", + "dungeonId": "grafu-castle", + "templateId": 105351, + "roots": [ + "un_zone19_boss5_allconreal_1_01" + ], + "functions": [ + "un_zone19_boss5_allconreal_1_01", + "Az_RND_Ary", + "SearchRangeNPC", + "DW_Rand" + ], + "spellIds": [] + }, + "mechanics": [ + { + "id": "rw-grafu-castle-105351-source-gap-sweep", + "name": "Heavy Sweep", + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "attack", + "castTimeMs": 1800, + "range": 7, + "cooldownMs": 10000, + "initialCooldownMs": [ + 7000, + 7000 + ], + "repeatCooldownMs": [ + 10000, + 10000 + ], + "damageMultiplier": 1.1, + "telegraph": { + "shape": "cone", + "anchor": "source", + "warningMs": 1800, + "length": 7, + "angleDegrees": 100 + } + } + ] + }, + "rw-grafu-castle-105542": { + "name": "Ardmond", + "mode": "replace", + "source": { + "kind": "runewaker", + "dungeonId": "grafu-castle", + "templateId": 105542, + "roots": [ + "kg_105542_01" + ], + "functions": [ + "kg_105542_01", + "kg_DoorInit", + "kg_105542_CreateBoss", + "kg_105543_CreateBucket", + "kg_CtrlInit", + "kg_GetRangePos", + "kg_105542_BossAI", + "kg_105542_BossDead", + "kg_105543_LionDead", + "kg_CreateMultiObj", + "Role:new", + "kg_105542_Skillinfo", + "kg_105542_Buffinfo", + "KS_InCombat", + "kg_105543_SetBucket", + "kg_105542_CheckHight", + "kg_105542_BossSkill10s", + "kg_105542_BossSkill15s", + "kg_105542_BossSkill20s", + "kg_105542_BossSkill30s", + "kg_105542_BossCrazy", + "kg_105542_ResetBoss", + "kg_CheckLive", + "kg_105543_CrazyLionAI", + "kg_105542_BossAllDead", + "Init_RoleData_Value", + "Init_RoleData_Function", + "kg_105543_TouchBucket", + "DW_CheckDis", + "kg_Rand", + "kg_CheckAtk", + "KS_GetHateList", + "kg_105542_BornBox", + "ic_ha_de_137_02", + "kg_BoxInit", + "lua_star_BossKilledCount", + "ic_del_buff_137" + ], + "spellIds": [ + 495767, + 496245, + 496930, + 496931, + 496932, + 496933, + 496934, + 496936, + 496938, + 496940, + 497444, + 497475 + ] + }, + "mechanics": [ + { + "id": "rw-grafu-castle-105542-debuff-495767", + "name": "Manastorms Duo Lv5", + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "effect": { + "kind": "target-aura", + "aura": "manastorms-duo-lv5", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105542-debuff-496245", + "name": "Shackle Bind", + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "radius": 10, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 10 + }, + "effect": { + "kind": "target-aura", + "aura": "shackle-bind", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105542-debuff-496930", + "name": "Manastorms Duo Lv5", + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "target-aura", + "aura": "manastorms-duo-lv5", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105542-debuff-496931", + "name": "Rend", + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "target-aura", + "aura": "rupture", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105542-debuff-496932", + "name": "Tear Apart", + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "target-aura", + "aura": "armor-break", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105542-aura-496933", + "name": "Frightening Roar", + "delivery": "area", + "target": "self", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1200, + "range": 0, + "cooldownMs": 15000, + "initialCooldownMs": [ + 15000, + 15000 + ], + "repeatCooldownMs": [ + 15000, + 15000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "aura", + "aura": "shock", + "durationMs": 7500 + } + }, + { + "id": "rw-grafu-castle-105542-spell-496934", + "name": "Roaring Charge", + "spellId": 496934, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "telegraph": { + "shape": "line", + "anchor": "source", + "warningMs": 1800, + "length": 30, + "width": 2 + } + }, + { + "id": "rw-grafu-castle-105542-spell-496936", + "name": "Thrash", + "spellId": 496936, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.2, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + } + }, + { + "id": "rw-grafu-castle-105542-spell-496938", + "name": "Flog", + "spellId": 496938, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 4000, + "range": 10, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 10, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 10 + } + }, + { + "id": "rw-grafu-castle-105542-debuff-496940", + "name": "Lion Bind", + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "target-aura", + "aura": "lion-bind", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105542-spell-497444", + "name": "Feral Attack", + "spellId": 497444, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 3000, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 10, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 10 + } + } + ] + }, + "rw-grafu-castle-105543": { + "name": "Issac", + "mode": "replace", + "source": { + "kind": "runewaker", + "dungeonId": "grafu-castle", + "templateId": 105543, + "roots": [ + "kg_105542_01" + ], + "functions": [ + "kg_105542_01", + "kg_DoorInit", + "kg_105542_CreateBoss", + "kg_105543_CreateBucket", + "kg_CtrlInit", + "kg_GetRangePos", + "kg_105542_BossAI", + "kg_105542_BossDead", + "kg_105543_LionDead", + "kg_CreateMultiObj", + "Role:new", + "kg_105542_Skillinfo", + "kg_105542_Buffinfo", + "KS_InCombat", + "kg_105543_SetBucket", + "kg_105542_CheckHight", + "kg_105542_BossSkill10s", + "kg_105542_BossSkill15s", + "kg_105542_BossSkill20s", + "kg_105542_BossSkill30s", + "kg_105542_BossCrazy", + "kg_105542_ResetBoss", + "kg_CheckLive", + "kg_105543_CrazyLionAI", + "kg_105542_BossAllDead", + "Init_RoleData_Value", + "Init_RoleData_Function", + "kg_105543_TouchBucket", + "DW_CheckDis", + "kg_Rand", + "kg_CheckAtk", + "KS_GetHateList", + "kg_105542_BornBox", + "ic_ha_de_137_02", + "kg_BoxInit", + "lua_star_BossKilledCount", + "ic_del_buff_137" + ], + "spellIds": [ + 495767, + 496245, + 496930, + 496931, + 496932, + 496933, + 496934, + 496936, + 496938, + 496940, + 497444, + 497475 + ] + }, + "mechanics": [ + { + "id": "rw-grafu-castle-105543-debuff-495767", + "name": "Manastorms Duo Lv5", + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "effect": { + "kind": "target-aura", + "aura": "manastorms-duo-lv5", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105543-debuff-496245", + "name": "Shackle Bind", + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "radius": 10, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 10 + }, + "effect": { + "kind": "target-aura", + "aura": "shackle-bind", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105543-debuff-496930", + "name": "Manastorms Duo Lv5", + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "target-aura", + "aura": "manastorms-duo-lv5", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105543-debuff-496931", + "name": "Rend", + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "target-aura", + "aura": "rupture", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105543-debuff-496932", + "name": "Tear Apart", + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "target-aura", + "aura": "armor-break", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105543-aura-496933", + "name": "Frightening Roar", + "delivery": "area", + "target": "self", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1200, + "range": 0, + "cooldownMs": 15000, + "initialCooldownMs": [ + 15000, + 15000 + ], + "repeatCooldownMs": [ + 15000, + 15000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "aura", + "aura": "shock", + "durationMs": 7500 + } + }, + { + "id": "rw-grafu-castle-105543-spell-496934", + "name": "Roaring Charge", + "spellId": 496934, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "telegraph": { + "shape": "line", + "anchor": "source", + "warningMs": 1800, + "length": 30, + "width": 2 + } + }, + { + "id": "rw-grafu-castle-105543-spell-496936", + "name": "Thrash", + "spellId": 496936, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.2, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + } + }, + { + "id": "rw-grafu-castle-105543-spell-496938", + "name": "Flog", + "spellId": 496938, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 4000, + "range": 10, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 10, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 10 + } + }, + { + "id": "rw-grafu-castle-105543-debuff-496940", + "name": "Lion Bind", + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "target-aura", + "aura": "lion-bind", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105543-spell-497444", + "name": "Feral Attack", + "spellId": 497444, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 3000, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 10, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 10 + } + } + ] + }, + "rw-grafu-castle-105616": { + "name": "Cayus", + "mode": "replace", + "source": { + "kind": "runewaker", + "dungeonId": "grafu-castle", + "templateId": 105616, + "roots": [ + "Dy_Z19_BOSSN_AIC" + ], + "functions": [ + "Dy_Z19_BOSSN_AIC", + "Dy_Z19_BOSSN_AI", + "DW_HateRemain", + "Dy_Z19_BOSSN_BLUE", + "Dy_Z19_BOSSN_RED", + "Dy_Z19_BOSSN_YELLOW", + "Dy_Z19_BOSSN_DEAD_2", + "SearchRangeNPC" + ], + "spellIds": [ + 496859, + 496860, + 496861, + 496864, + 496865, + 496867, + 496868, + 496869, + 497015, + 497516, + 497517, + 497518, + 497519, + 497520 + ] + }, + "mechanics": [ + { + "id": "rw-grafu-castle-105616-spell-496859", + "name": "Manastorms Duo Lv5", + "spellId": 496859, + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 0, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85 + }, + { + "id": "rw-grafu-castle-105616-spell-496860", + "name": "Forward Flip", + "spellId": 496860, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "attack", + "castTimeMs": 1800, + "range": 15, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + } + }, + { + "id": "rw-grafu-castle-105616-spell-496861", + "name": "Flying Knife", + "spellId": 496861, + "delivery": "projectile", + "target": "primary", + "school": "physical", + "animation": "cast", + "castTimeMs": 0, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85 + }, + { + "id": "rw-grafu-castle-105616-spell-496864", + "name": "Juggler Trick", + "spellId": 496864, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "attack", + "castTimeMs": 3000, + "range": 8, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 8, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 8 + } + }, + { + "id": "rw-grafu-castle-105616-spell-496865", + "name": "Gale Spray", + "spellId": 496865, + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 7.5, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 7.5, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 7.5 + } + }, + { + "id": "rw-grafu-castle-105616-control-496867", + "name": "Indigo Melancholy", + "delivery": "area", + "target": "nearby-party", + "school": "shadow", + "animation": "cast", + "castTimeMs": 1800, + "range": 5, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "effect": { + "kind": "control", + "mechanic": "fear", + "durationMs": 3000 + } + }, + { + "id": "rw-grafu-castle-105616-control-496868", + "name": "Purple Terror", + "delivery": "area", + "target": "nearby-party", + "school": "shadow", + "animation": "cast", + "castTimeMs": 1800, + "range": 5, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "effect": { + "kind": "control", + "mechanic": "fear", + "durationMs": 3000 + } + }, + { + "id": "rw-grafu-castle-105616-control-496869", + "name": "Still Dusk", + "delivery": "area", + "target": "nearby-party", + "school": "shadow", + "animation": "cast", + "castTimeMs": 1800, + "range": 5, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "effect": { + "kind": "control", + "mechanic": "fear", + "durationMs": 3000 + } + }, + { + "id": "rw-grafu-castle-105616-spell-497516", + "name": "Throwing Knife", + "spellId": 497516, + "delivery": "projectile", + "target": "primary", + "school": "physical", + "animation": "cast", + "castTimeMs": 0, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85 + }, + { + "id": "rw-grafu-castle-105616-spell-497517", + "name": "Manastorms Duo Lv5", + "spellId": 497517, + "delivery": "projectile", + "target": "primary", + "school": "physical", + "animation": "cast", + "castTimeMs": 0, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85 + }, + { + "id": "rw-grafu-castle-105616-spell-497518", + "name": "Manastorms Duo Lv5", + "spellId": 497518, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + } + }, + { + "id": "rw-grafu-castle-105616-spell-497519", + "name": "Manastorms Duo Lv5", + "spellId": 497519, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + } + }, + { + "id": "rw-grafu-castle-105616-spell-497520", + "name": "Manastorms Duo Lv5", + "spellId": 497520, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + } + } + ] } } } diff --git a/package-lock.json b/package-lock.json index fb5279e2..c28070c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "healer-man", - "version": "0.1.31", + "version": "0.1.32", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "healer-man", - "version": "0.1.31", + "version": "0.1.32", "dependencies": { "@capacitor/android": "8.4.1", "@capacitor/core": "8.4.1", diff --git a/package.json b/package.json index 5bd367e2..1b631ebc 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "healer-man", "private": true, - "version": "0.1.31", + "version": "0.1.32", "type": "module", "scripts": { "dev": "vite --host 0.0.0.0", + "dev:shelved": "vite --host 0.0.0.0 --mode shelved", "start": "node server/server.mjs", "db:init": "node scripts/init-game-database.mjs", "db:backup": "node scripts/backup-game-database.mjs", @@ -110,9 +111,12 @@ "assets:performance:audit": "node scripts/asset-pipeline/audit-environment-performance.mjs", "balance:benchmark": "vite-node scripts/combat-benchmark/run.ts", "balance:smoke": "vite-node scripts/combat-benchmark/run.ts --class mage --role damage --scenario damage-single-burst --seeds 1 --duration-scale 0.2", - "prebuild": "npm run loot:generate && npm run arena:abilities:build && npm run web:prepare && npm run arena:assets:validate && npm run assets:ktx2:audit && npm run assets:performance:audit", + "prebuild": "npm run loot:generate && npm run web:prepare && npm run assets:ktx2:audit && npm run assets:performance:audit", "build": "node --max-old-space-size=8192 node_modules/typescript/bin/tsc -b && node --max-old-space-size=8192 node_modules/vite/bin/vite.js build", - "build:android:web": "npm run prebuild && npm run android:prepare && node --max-old-space-size=8192 node_modules/typescript/bin/tsc -b && node --max-old-space-size=8192 node_modules/vite/bin/vite.js build --mode android", + "postbuild": "node scripts/content/precompress-static.mjs dist && node scripts/content/verify-release-bundle.mjs dist", + "prebuild:shelved": "npm run loot:generate && npm run arena:abilities:build && npm run web:prepare && npm run arena:assets:validate && npm run assets:ktx2:audit && npm run assets:performance:audit", + "build:shelved": "node --max-old-space-size=8192 node_modules/typescript/bin/tsc -b && node --max-old-space-size=8192 node_modules/vite/bin/vite.js build --mode shelved", + "build:android:web": "npm run prebuild && npm run android:prepare && node --max-old-space-size=8192 node_modules/typescript/bin/tsc -b && node --max-old-space-size=8192 node_modules/vite/bin/vite.js build --mode android && node scripts/content/verify-release-bundle.mjs dist-android", "android:sync": "npm run build:android:web && cap sync android", "android:apk": "npm run android:sync && powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/runAndroidGradle.ps1 assembleDebug", "android:apk:release": "npm run android:sync && powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/runAndroidGradle.ps1 assembleRelease", @@ -122,6 +126,8 @@ "preview": "vite preview --host 0.0.0.0", "pretest": "npm run loot:generate", "test": "vitest run", + "pretest:shelved": "npm run loot:generate", + "test:shelved": "vitest run --mode shelved", "test:watch": "vitest" }, "dependencies": { diff --git a/public/assets/creatures/grafu-castle/atmo.glb b/public/assets/creatures/grafu-castle/atmo.glb new file mode 100644 index 00000000..33c4682a Binary files /dev/null and b/public/assets/creatures/grafu-castle/atmo.glb differ diff --git a/public/assets/creatures/grafu-castle/barbo-brother.glb b/public/assets/creatures/grafu-castle/barbo-brother.glb new file mode 100644 index 00000000..04867514 Binary files /dev/null and b/public/assets/creatures/grafu-castle/barbo-brother.glb differ diff --git a/public/assets/creatures/grafu-castle/black-hat.glb b/public/assets/creatures/grafu-castle/black-hat.glb new file mode 100644 index 00000000..3abd1997 Binary files /dev/null and b/public/assets/creatures/grafu-castle/black-hat.glb differ diff --git a/public/assets/creatures/grafu-castle/cursed-steel-guardian.glb b/public/assets/creatures/grafu-castle/cursed-steel-guardian.glb new file mode 100644 index 00000000..837a4862 Binary files /dev/null and b/public/assets/creatures/grafu-castle/cursed-steel-guardian.glb differ diff --git a/public/assets/creatures/grafu-castle/enraged-big-bear-cream.glb b/public/assets/creatures/grafu-castle/enraged-big-bear-cream.glb new file mode 100644 index 00000000..9101f991 Binary files /dev/null and b/public/assets/creatures/grafu-castle/enraged-big-bear-cream.glb differ diff --git a/public/assets/creatures/grafu-castle/enraged-big-bear-purple.glb b/public/assets/creatures/grafu-castle/enraged-big-bear-purple.glb new file mode 100644 index 00000000..6467c281 Binary files /dev/null and b/public/assets/creatures/grafu-castle/enraged-big-bear-purple.glb differ diff --git a/public/assets/creatures/grafu-castle/karth.glb b/public/assets/creatures/grafu-castle/karth.glb new file mode 100644 index 00000000..d0805c53 Binary files /dev/null and b/public/assets/creatures/grafu-castle/karth.glb differ diff --git a/public/assets/creatures/grafu-castle/manifest.json b/public/assets/creatures/grafu-castle/manifest.json index 18a66a23..0fa01e89 100644 --- a/public/assets/creatures/grafu-castle/manifest.json +++ b/public/assets/creatures/grafu-castle/manifest.json @@ -11,6 +11,157 @@ }, "proceduralFallbacks": [], "assets": [ + { + "sourceModel": "model/character/creature/atmo/act_atmo.ros", + "appearanceSignature": null, + "sourceModelSha256": "937f0ae59fd87e011479d67deb7baf1023cdb0f38ef72d78d9e923b99c4088a9", + "checksum": "9f7c1ad1c56ef51ee0511507f950ef97224ad86af561ded111cf1d5740c69999", + "size": 1981260, + "triangles": 3176, + "bounds": { + "min": [ + -0.41683, + 0.00233, + -0.15372 + ], + "max": [ + 0.41683, + 2.00238, + 1.86916 + ] + }, + "groundOffset": -0.00233, + "labelHeight": 2.35005, + "markerRadius": 0.64732, + "animations": [ + "Action - buff_sp01", + "Action - buff01", + "Cast - cast_sp01", + "Cast - cast01", + "Cast - casting_delay", + "Action - continuous_buff", + "Terminal Pose - dead", + "Death - death", + "Action - dodge", + "Wound - hurt", + "Idle - idle02", + "Idle - idle03", + "Run - run_forward", + "Stand - stand_idle01", + "Attack - unarmed_attack01", + "Attack - unarmed_attack02", + "Idle - unarmed_idle", + "Action - unarmed_parry", + "Walk - walk_forward" + ], + "id": "atmo", + "baseActorId": "atmo", + "fileName": "atmo.glb", + "url": "/assets/creatures/grafu-castle/atmo.glb", + "rotationY": 0 + }, + { + "sourceModel": "model/character/creature/barbo_brother/act_barbo_brother.ros", + "appearanceSignature": null, + "sourceModelSha256": "8caafccd798e69e31f09a0d270c14e938c913935e9fd427691299b0a5d2db2a2", + "checksum": "af1cd20b7cc1d465c270860997f1245b2967005b1e5308b4a43401e983f883a4", + "size": 2226000, + "triangles": 3371, + "bounds": { + "min": [ + -3.23727, + -1.32298, + -1.36027 + ], + "max": [ + 1.54844, + 3.76779, + 1.15116 + ] + }, + "groundOffset": 1.32298, + "labelHeight": 5.44077, + "markerRadius": 1.53143, + "animations": [ + "Action - knockdown_start", + "Action - buff01", + "Action - buff_sp01", + "Cast - cast01", + "Cast - cast_sp01", + "Cast - casting_delay", + "Action - continuous_buff", + "Terminal Pose - dead", + "Death - death", + "Action - dodge", + "Wound - hurt", + "Idle - idle02", + "Idle - idle03", + "Action - knockdown_recover", + "Walk - walk_forward", + "Run - run_forward", + "Stand - stand_idle01", + "Attack - unarmed_attack01", + "Attack - unarmed_attack02", + "Idle - unarmed_idle", + "Action - unarmed_parry" + ], + "id": "barbo-brother", + "baseActorId": "barbo-brother", + "fileName": "barbo-brother.glb", + "url": "/assets/creatures/grafu-castle/barbo-brother.glb", + "rotationY": 0 + }, + { + "sourceModel": "model/character/creature/black_hat/act_black_hat.ros", + "appearanceSignature": null, + "sourceModelSha256": "1e16384ee51d97cb691ce2bb9c53e7f7c13c067bc40c09f4bec5fd8dc6f8b8f4", + "checksum": "e3d5265c794b82d45c34bd7875e33c9f00680dc67c3cc18213f5df657f9ab76a", + "size": 1965712, + "triangles": 2222, + "bounds": { + "min": [ + -0.96974, + -0.01676, + -0.63427 + ], + "max": [ + 0.92815, + 3.14637, + 1.25604 + ] + }, + "groundOffset": 0.01676, + "labelHeight": 3.51313, + "markerRadius": 0.60732, + "animations": [ + "Action - buff_sp01", + "Action - buff01", + "Cast - cast_sp01", + "Cast - cast01", + "Cast - casting_delay", + "Action - continuous_buff", + "Terminal Pose - dead", + "Death - death", + "Action - dodge", + "Wound - hurt", + "Idle - idle02", + "Idle - idle03", + "Action - knockdown_end", + "Action - knockdown_recover", + "Run - run_forward", + "Stand - stand_idle01", + "Attack - unarmed_attack01", + "Attack - unarmed_attack02", + "Idle - unarmed_idle", + "Action - unarmed_parry", + "Walk - walk_forward" + ], + "id": "black-hat", + "baseActorId": "black-hat", + "fileName": "black-hat.glb", + "url": "/assets/creatures/grafu-castle/black-hat.glb", + "rotationY": 0 + }, { "sourceModel": "model/character/creature/black_hat/act_black_hat_blue.ros", "appearanceSignature": null, @@ -206,6 +357,58 @@ "url": "/assets/creatures/grafu-castle/controlled-steel-guardian.glb", "rotationY": 0 }, + { + "sourceModel": "model/character/creature/lost_protector/act_cursed_steel_guardian.ros", + "appearanceSignature": null, + "sourceModelSha256": "b31342cb3ee28f6d6b0b70af334ab835e5212722bc99d0c8818568d5b9e079df", + "checksum": "3ec7660d4e1b475ec9ae290c304802dd4ba174048fac0781ae25474a718227fe", + "size": 1151612, + "triangles": 2201, + "bounds": { + "min": [ + -0.97829, + -0.00095, + -0.38972 + ], + "max": [ + 0.97829, + 2.01359, + 0.26596 + ] + }, + "groundOffset": 0.00095, + "labelHeight": 2.36454, + "markerRadius": 0.62611, + "animations": [ + "Attack - 1h_attack01", + "Attack - 1h_attack02", + "Idle - 1h_idle", + "Action - 1h_parry", + "Cast - casting01", + "Cast - casting02", + "Cast - casting_delay", + "Terminal Pose - dead", + "Death - death", + "Action - dodge", + "Wound - hurt", + "Idle - idle02", + "Idle - idle03", + "Run - run_forward", + "Stand - stand_idle01", + "Walk - walk_forward", + "Attack - unarmed_attack01", + "Attack - unarmed_attack02", + "Idle - unarmed_idle", + "Action - unarmed_parry", + "Action - knockdown_end", + "Action - knockdown_recover" + ], + "id": "cursed-steel-guardian", + "baseActorId": "cursed-steel-guardian", + "fileName": "cursed-steel-guardian.glb", + "url": "/assets/creatures/grafu-castle/cursed-steel-guardian.glb", + "rotationY": 0 + }, { "sourceModel": "model/character/creature/elder_myconid/act_elder_myconid.ros", "appearanceSignature": null, @@ -924,6 +1127,56 @@ "url": "/assets/creatures/grafu-castle/hm-male-image-585705.glb", "rotationY": 0 }, + { + "sourceModel": "model/character/creature/karth/act_karth.ros", + "appearanceSignature": null, + "sourceModelSha256": "18d43dd054c85512bfdbde50093c9dee6b368cef088d7c0f6cc3e50fff8e3ea3", + "checksum": "ac96b24461b39cdfae7810097dbe6698f0d7ce1afe235df04d72144dceef893d", + "size": 1993572, + "triangles": 3052, + "bounds": { + "min": [ + -1.18195, + -0.00007, + -0.72488 + ], + "max": [ + 1.18195, + 2.6109, + 0.72657 + ] + }, + "groundOffset": 0.00007, + "labelHeight": 2.96097, + "markerRadius": 0.75645, + "animations": [ + "Walk - walk_forward", + "Action - buff01", + "Action - buff_sp01", + "Cast - cast01", + "Cast - cast_sp01", + "Cast - casting_delay", + "Terminal Pose - dead", + "Death - death", + "Action - dodge", + "Wound - hurt", + "Idle - idle02", + "Idle - idle03", + "Run - run_forward", + "Stand - stand_idle01", + "Attack - unarmed_attack01", + "Attack - unarmed_attack02", + "Idle - unarmed_idle", + "Action - unarmed_parry", + "Idle - crouch_idle", + "Action - spawn" + ], + "id": "karth", + "baseActorId": "karth", + "fileName": "karth.glb", + "url": "/assets/creatures/grafu-castle/karth.glb", + "rotationY": 0 + }, { "sourceModel": "model/character/creature/karth/act_karth_01.ros", "appearanceSignature": null, @@ -1019,6 +1272,221 @@ "fileName": "milk-scarab-blue.glb", "url": "/assets/creatures/grafu-castle/milk-scarab-blue.glb", "rotationY": 0 + }, + { + "sourceModel": "model/character/creature/mount_millenium_chimera/act_mount_millenium_chimera_laby.ros", + "appearanceSignature": null, + "sourceModelSha256": "1024bb257d901842c6bb7303eaba66399d26c28b680a7d8fadf4ee49c4b27a06", + "checksum": "7cb4ae344d3a46094b96faebdc6274eace04eb894de64ee87ac7bb7a1309b321", + "size": 1217348, + "triangles": 3086, + "bounds": { + "min": [ + -0.4686, + -0.01121, + -2.26974 + ], + "max": [ + 0.4686, + 1.73369, + 1.59992 + ] + }, + "groundOffset": 0.01121, + "labelHeight": 2.0949, + "markerRadius": 1.23829, + "animations": [ + "Cast - casting01", + "Cast - casting02", + "Cast - casting_delay", + "Terminal Pose - dead", + "Death - death", + "Action - dodge", + "Wound - hurt", + "Idle - idle02", + "Idle - idle03", + "Action - jump_down", + "Action - jump_end_back", + "Action - jump_end_run", + "Action - jump_inplace", + "Action - jump_loop", + "Action - jump_up", + "Run - run_back", + "Run - run_forward", + "Action - shuffle_left", + "Action - shuffle_right", + "Stand - stand_idle01", + "Attack - unarmed_attack01", + "Attack - unarmed_attack02", + "Idle - unarmed_idle", + "Walk - walk_forward" + ], + "id": "mount-millenium-chimera-laby", + "baseActorId": "mount-millenium-chimera-laby", + "fileName": "mount-millenium-chimera-laby.glb", + "url": "/assets/creatures/grafu-castle/mount-millenium-chimera-laby.glb", + "rotationY": 0 + }, + { + "sourceModel": "model/character/creature/enraged_big_bear/act_enraged_big_bear_purple.ros", + "appearanceSignature": null, + "sourceModelSha256": "22700bad0c13bad05bb722a7c6aa9959e4634217974b559901ab99e55fbfaca7", + "checksum": "36738dc204f6d574f56ee41044d61ae88a885d0dc4b411f7ea70d76e84d48a4b", + "size": 935312, + "triangles": 1568, + "bounds": { + "min": [ + -1.65768, + 0.00083, + -0.63438 + ], + "max": [ + 1.66988, + 2.50467, + 0.55741 + ] + }, + "groundOffset": -0.00083, + "labelHeight": 2.85384, + "markerRadius": 1.06482, + "animations": [ + "Cast - cast01", + "Terminal Pose - dead", + "Death - death", + "Action - dodge", + "Action - buff01", + "Action - buff_sp02", + "Wound - hurt", + "Idle - idle02", + "Idle - idle03", + "Action - knockdown_end", + "Action - knockdown_recover", + "Run - run_back", + "Run - run_forward", + "Stand - stand_idle01", + "Attack - unarmed_attack01", + "Attack - unarmed_attack02", + "Idle - unarmed_idle", + "Action - unarmed_parry", + "Walk - walk_forward", + "Idle - knockdown_idle", + "Cast - casting_delay", + "Action - jump_down", + "Action - jump_end_back", + "Action - jump_end_run", + "Action - jump_loop", + "Action - jump_up" + ], + "id": "enraged-big-bear-purple", + "baseActorId": "enraged-big-bear-purple", + "fileName": "enraged-big-bear-purple.glb", + "url": "/assets/creatures/grafu-castle/enraged-big-bear-purple.glb", + "rotationY": 0 + }, + { + "sourceModel": "model/character/creature/enraged_big_bear/act_enraged_big_bear_cream.ros", + "appearanceSignature": null, + "sourceModelSha256": "e1d1810f37182d2f2c5e7dcf72aae4da75cbd5f056a5445df8dc17b2539d5bcb", + "checksum": "1e327d246fdcebf9b35891f37ccc3aed3ff7bfb4f2c7355c1381d7e0231e956c", + "size": 941160, + "triangles": 1568, + "bounds": { + "min": [ + -1.65768, + 0.00083, + -0.63438 + ], + "max": [ + 1.66988, + 2.50467, + 0.55741 + ] + }, + "groundOffset": -0.00083, + "labelHeight": 2.85384, + "markerRadius": 1.06482, + "animations": [ + "Cast - cast01", + "Terminal Pose - dead", + "Death - death", + "Action - dodge", + "Action - buff01", + "Action - buff_sp02", + "Wound - hurt", + "Idle - idle02", + "Idle - idle03", + "Action - knockdown_end", + "Action - knockdown_recover", + "Run - run_back", + "Run - run_forward", + "Stand - stand_idle01", + "Attack - unarmed_attack01", + "Attack - unarmed_attack02", + "Idle - unarmed_idle", + "Action - unarmed_parry", + "Walk - walk_forward", + "Idle - knockdown_idle", + "Cast - casting_delay", + "Action - jump_down", + "Action - jump_end_back", + "Action - jump_end_run", + "Action - jump_loop", + "Action - jump_up" + ], + "id": "enraged-big-bear-cream", + "baseActorId": "enraged-big-bear-cream", + "fileName": "enraged-big-bear-cream.glb", + "url": "/assets/creatures/grafu-castle/enraged-big-bear-cream.glb", + "rotationY": 0 + }, + { + "sourceModel": "model/character/creature/rafael_bakderr/act_rafael_bakderr.ros", + "appearanceSignature": null, + "sourceModelSha256": "117287fd6f1ea813a73f846a0cde6bfd6d6f5ff00778dde82a1d7308aaafb0cc", + "checksum": "06e87d84a7cb3e15d610b0f18528cb1331c80db4597c67c49267aa4a627386dc", + "size": 1705044, + "triangles": 3437, + "bounds": { + "min": [ + -0.59518, + 0.00039, + -0.34992 + ], + "max": [ + 0.59473, + 2.21216, + 0.25268 + ] + }, + "groundOffset": -0.00039, + "labelHeight": 2.56177, + "markerRadius": 0.4, + "animations": [ + "Action - buff01", + "Action - buff_sp01", + "Cast - cast01", + "Cast - cast_sp01", + "Cast - casting_delay", + "Action - continuous_buff", + "Terminal Pose - dead", + "Death - death", + "Action - dodge", + "Wound - hurt", + "Idle - idle02", + "Idle - idle03", + "Run - run_forward", + "Stand - stand_idle01", + "Attack - unarmed_attack01", + "Attack - unarmed_attack02", + "Idle - unarmed_idle", + "Action - unarmed_parry", + "Walk - walk_forward" + ], + "id": "rafael-bakderr", + "baseActorId": "rafael-bakderr", + "fileName": "rafael-bakderr.glb", + "url": "/assets/creatures/grafu-castle/rafael-bakderr.glb", + "rotationY": 0 } ] } diff --git a/public/assets/creatures/grafu-castle/mount-millenium-chimera-laby.glb b/public/assets/creatures/grafu-castle/mount-millenium-chimera-laby.glb new file mode 100644 index 00000000..2cc895b2 Binary files /dev/null and b/public/assets/creatures/grafu-castle/mount-millenium-chimera-laby.glb differ diff --git a/public/assets/creatures/grafu-castle/rafael-bakderr.glb b/public/assets/creatures/grafu-castle/rafael-bakderr.glb new file mode 100644 index 00000000..777f8902 Binary files /dev/null and b/public/assets/creatures/grafu-castle/rafael-bakderr.glb differ diff --git a/public/content-cache-sw.js b/public/content-cache-sw.js index 94ffa7f1..6d5bb22d 100644 --- a/public/content-cache-sw.js +++ b/public/content-cache-sw.js @@ -1,4 +1,5 @@ const CONTENT_CACHE_NAME = "healer-man-content-v1"; +const BOOTSTRAP_CACHE_NAME = "healer-man-bootstrap-v1"; const OBJECT_HEADER = "x-healer-man-content-object"; const ENCODING_HEADER = "x-healer-man-content-encoding"; let bundledManifest; @@ -33,7 +34,13 @@ async function bundledAsset(path) { const object = entry.download ?? entry; // Older bootstrap catalogs served their original logical paths directly. if (!object.url.startsWith("/content/bundled/")) return fetch(path); - const response = await fetch(object.url); + const bootstrapCache = await caches.open(BOOTSTRAP_CACHE_NAME); + const objectKey = new URL(object.url, self.location.origin).toString(); + let response = await bootstrapCache.match(objectKey); + if (!response) { + response = await fetch(object.url); + if (response.ok) await bootstrapCache.put(objectKey, response.clone()); + } if (!response.ok) return response; if (entry.download) return decodedResponse(response, path, entry.download.encoding); return new Response(response.body, { headers: { "content-type": mimeType(path) } }); @@ -53,7 +60,6 @@ const BUNDLED_ASSET_PREFIXES = [ "/assets/characters/wow335a-v1/manifest.json", "/assets/characters/wow335a-v1/human/", "/assets/creatures/wailing-caverns/", - "/assets/game/arenas/", "/assets/game/dungeons/wailing-caverns/", ]; const BUNDLED_ASSET_PATHS = new Set([ @@ -85,7 +91,8 @@ self.addEventListener("activate", (event) => { event.waitUntil((async () => { const names = await caches.keys(); await Promise.all(names - .filter((name) => name.startsWith(`${CONTENT_CACHE_NAME}-staging-`)) + .filter((name) => name.startsWith(`${CONTENT_CACHE_NAME}-staging-`) + || (name.startsWith("healer-man-bootstrap-") && name !== BOOTSTRAP_CACHE_NAME)) .map((name) => caches.delete(name))); await self.clients.claim(); })()); diff --git a/release-version.json b/release-version.json index 37850ae0..8ac6b5a4 100644 --- a/release-version.json +++ b/release-version.json @@ -1,4 +1,4 @@ { - "versionName": "0.1.31", - "versionCode": 1031 + "versionName": "0.1.32", + "versionCode": 1032 } diff --git a/scripts/content/content-objects.test.mjs b/scripts/content/content-objects.test.mjs index 2f904969..13f5377c 100644 --- a/scripts/content/content-objects.test.mjs +++ b/scripts/content/content-objects.test.mjs @@ -8,6 +8,7 @@ import { gunzipSync } from "node:zlib"; import { prepareContentObjects } from "./content-objects.mjs"; import { selectBootstrapContent } from "./prepare-bootstrap.mjs"; import { createContentManifest } from "./publish-content.mjs"; +import contentPolicy from "../../shared/content-policy.json" with { type: "json" }; test("ships one lossless object for identical assets and verifies original bytes", async () => { const directory = await mkdtemp(path.join(os.tmpdir(), "healer-content-compression-")); @@ -36,17 +37,17 @@ test("ships one lossless object for identical assets and verifies original bytes } finally { await rm(directory, { recursive: true, force: true }); } }); -test("bundles regular dungeons and exact map dependencies without installing Manastorm", () => { - const ids = ["core:ui", "dungeon:first", "dungeon:second", "shared:creatures", "manastorm:shared-map", "manastorm:optional-map", "characters:extra"]; +test("bundles only starter packs and their exact dependencies", () => { + const ids = [...contentPolicy.bootstrapPackIds, "dungeon:second", "shared:creatures", "manastorm:shared-map", "manastorm:optional-map", "characters:extra"]; const files = ids.map((packId, index) => ({ logicalPath: `assets/${index}.glb`, packId, sha256: String(index).repeat(64), bytes: 100, extension: ".glb" })); const result = selectBootstrapContent(files, [ - { id: "dungeon:first", dependencies: ["manastorm:shared-map", "shared:creatures"] }, { id: "dungeon:second", dependencies: [] }, ]); - assert.deepEqual(result.files.map((file) => file.packId), ids.slice(0, 5)); + assert.deepEqual(result.files.map((file) => file.packId), contentPolicy.bootstrapPackIds); const packs = new Map(result.packDefinitions.map((pack) => [pack.id, pack])); - assert.equal(packs.get("dungeon:first").bundledOnAndroid, true); - assert.equal(packs.get("dungeon:second").bundledOnAndroid, true); - assert.equal(packs.get("manastorm:shared-map").bundledOnAndroid, false); + assert.deepEqual([...packs.keys()].sort(), [...contentPolicy.bootstrapPackIds].sort()); + assert.equal([...packs.values()].every((pack) => pack.bundledOnAndroid), true); + assert.equal(packs.has("dungeon:second"), false); + assert.equal(packs.has("manastorm:shared-map"), false); assert.equal(packs.has("manastorm:optional-map"), false); }); diff --git a/scripts/content/precompress-static.mjs b/scripts/content/precompress-static.mjs new file mode 100644 index 00000000..91e43685 --- /dev/null +++ b/scripts/content/precompress-static.mjs @@ -0,0 +1,55 @@ +import { readdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { promisify } from "node:util"; +import { fileURLToPath } from "node:url"; +import { brotliCompress, constants, gzip } from "node:zlib"; + +const compressBrotli = promisify(brotliCompress); +const compressGzip = promisify(gzip); +const ELIGIBLE_EXTENSIONS = new Set([".css", ".js", ".json"]); +const MINIMUM_BYTES = 1024; + +async function eligibleFiles(root) { + const files = []; + async function visit(directory) { + const entries = await readdir(directory, { withFileTypes: true }); + for (const entry of entries) { + const filePath = path.join(directory, entry.name); + if (entry.isDirectory()) await visit(filePath); + else if (entry.isFile() && ELIGIBLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(filePath); + } + } + await visit(root); + return files; +} + +export async function precompressStatic(root) { + const files = await eligibleFiles(root); + let sourceBytes = 0; + let brotliBytes = 0; + let gzipBytes = 0; + let writtenFiles = 0; + for (const filePath of files) { + const source = await readFile(filePath); + if (source.length < MINIMUM_BYTES) continue; + const [brotli, gzipped] = await Promise.all([ + compressBrotli(source, { params: { [constants.BROTLI_PARAM_QUALITY]: 6 } }), + compressGzip(source, { level: 9 }), + ]); + await Promise.all([ + writeFile(`${filePath}.br`, brotli), + writeFile(`${filePath}.gz`, gzipped), + ]); + sourceBytes += source.length; + brotliBytes += brotli.length; + gzipBytes += gzipped.length; + writtenFiles += 2; + } + return { sourceFiles: writtenFiles / 2, writtenFiles, sourceBytes, brotliBytes, gzipBytes }; +} + +if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) { + const root = path.resolve(process.argv[2] ?? "dist"); + const result = await precompressStatic(root); + console.log(JSON.stringify({ root, ...result }, null, 2)); +} diff --git a/scripts/content/prepare-bootstrap.mjs b/scripts/content/prepare-bootstrap.mjs index 965ff326..b36ff7c9 100644 --- a/scripts/content/prepare-bootstrap.mjs +++ b/scripts/content/prepare-bootstrap.mjs @@ -5,6 +5,7 @@ import { createContentManifest, loadCampaignDungeonPackDefinitions, scanContentSources, } from "./publish-content.mjs"; import { prepareContentObjects } from "./content-objects.mjs"; +import contentPolicy from "../../shared/content-policy.json" with { type: "json" }; export function selectBootstrapContent(files, packDefinitions) { const manifest = createContentManifest(files, { packDefinitions }); @@ -15,15 +16,12 @@ export function selectBootstrapContent(files, packDefinitions) { included.add(id); for (const dependency of packs.get(id)?.dependencies ?? []) include(dependency); } - for (const pack of manifest.packs) { - if (BUNDLED_ANDROID_PACKS.includes(pack.id) || pack.id.startsWith("dungeon:")) include(pack.id); - } + for (const packId of BUNDLED_ANDROID_PACKS) include(packId); return { files: files.filter((file) => included.has(file.packId)), packDefinitions: manifest.packs.filter((pack) => included.has(pack.id)).map((pack) => ({ ...pack, - // Shared maps can be physically bundled without installing Manastorm gameplay. - bundledOnAndroid: !pack.id.startsWith("manastorm:"), + bundledOnAndroid: BUNDLED_ANDROID_PACKS.includes(pack.id), })), }; } @@ -44,6 +42,11 @@ export async function prepareBootstrap(platform) { const prepared = await prepareContentObjects(selected.files, { directory: path.join(outputDirectory, "content/bundled"), baseUrl: "/content/bundled", }); + if (prepared.storedBytes > contentPolicy.maxBootstrapStoredBytes) { + throw new Error( + `Bootstrap content is ${prepared.storedBytes} bytes; the release limit is ${contentPolicy.maxBootstrapStoredBytes} bytes.`, + ); + } const manifest = createContentManifest(prepared.files, { baseUrl: "/content/bundled", packDefinitions: selected.packDefinitions, }); diff --git a/scripts/content/publish-content.mjs b/scripts/content/publish-content.mjs index a317d3cd..effe13e2 100644 --- a/scripts/content/publish-content.mjs +++ b/scripts/content/publish-content.mjs @@ -10,6 +10,7 @@ import { import path from "node:path"; import { fileURLToPath } from "node:url"; import { prepareContentObjects } from "./content-objects.mjs"; +import contentPolicy from "../../shared/content-policy.json" with { type: "json" }; const SCRIPT_DIRECTORY = path.dirname(fileURLToPath(import.meta.url)); export const PROJECT_ROOT = path.resolve(SCRIPT_DIRECTORY, "../.."); @@ -23,14 +24,7 @@ const WOW_CAMPAIGN_CATALOG_PATH = path.join( export const CONTENT_SCHEMA_VERSION = 1; export const DEFAULT_MINIMUM_APP_VERSION = "0.1.0"; export const WOW_CAMPAIGN_DUNGEON_COUNT = 54; -export const BUNDLED_ANDROID_PACKS = Object.freeze([ - "core:ui", - "core:equipment", - "core:character-basic", - "core:runtime", - "core:arenas", - "dungeon:wailing-caverns", -]); +export const BUNDLED_ANDROID_PACKS = Object.freeze([...contentPolicy.bootstrapPackIds]); const RUNTIME_EXTENSIONS = new Set([ ".avif", ".glb", ".js", ".json", ".ktx2", ".mp3", ".ogg", ".png", diff --git a/scripts/content/publish-content.test.mjs b/scripts/content/publish-content.test.mjs index 14af5219..f1bcc81f 100644 --- a/scripts/content/publish-content.test.mjs +++ b/scripts/content/publish-content.test.mjs @@ -24,7 +24,7 @@ test("assigns stable player-facing packs from logical asset paths", () => { assert.equal(contentPackForPath("assets/characters/wow335a-v1/orc/male/rig.glb"), "characters:wow335a-v1"); assert.equal(contentPackForPath("assets/game/manastorm/stage-1/visual.glb"), "manastorm:stage-1"); assert.equal(contentPackForPath("assets/game/arenas/nagrand-arena/visual.glb"), "core:arenas"); - assert.equal(BUNDLED_ANDROID_PACKS.includes("core:arenas"), true); + assert.equal(BUNDLED_ANDROID_PACKS.includes("core:arenas"), false); assert.equal(contentPackForPath("assets/game/combat-effects/audio/smite.wav"), "core:combat-media"); assert.equal( contentPackForPath("assets/game/combat-effects/audio/manastorm/Manastorm_BossKill_GenericPortalsReady.wav"), diff --git a/scripts/content/verify-android-package.py b/scripts/content/verify-android-package.py index 25830b77..4c422ced 100644 --- a/scripts/content/verify-android-package.py +++ b/scripts/content/verify-android-package.py @@ -7,6 +7,10 @@ import json from pathlib import Path from zipfile import ZIP_STORED, ZipFile +CONTENT_POLICY = json.loads( + (Path(__file__).resolve().parents[2] / "shared" / "content-policy.json").read_text(encoding="utf-8") +) + def verify_package(apk: Path) -> dict: with ZipFile(apk) as archive: @@ -35,13 +39,20 @@ def verify_package(apk: Path) -> dict: if actual_objects != expected_objects: raise ValueError("APK includes missing or unreferenced bundled objects.") manastorms = sum(pack["id"].startswith("manastorm:") and pack["bundledOnAndroid"] for pack in manifest["packs"]) - if dungeons != 84 or manastorms != 0: - raise ValueError(f"Unexpected bundled content: {dungeons} dungeons, {manastorms} Manastorm packs") + arenas = sum(pack["id"] == "core:arenas" for pack in manifest["packs"]) + pack_ids = sorted(pack["id"] for pack in manifest["packs"]) + expected_pack_ids = sorted(CONTENT_POLICY["bootstrapPackIds"]) + if pack_ids != expected_pack_ids or dungeons != 1 or manastorms != 0 or arenas != 0: + raise ValueError( + f"Unexpected bundled content: {dungeons} dungeons, {manastorms} Manastorm packs, {arenas} arena packs" + ) + if object_bytes > CONTENT_POLICY["maxBootstrapStoredBytes"]: + raise ValueError("APK bootstrap content exceeds the stored-byte budget.") return { "apk": str(apk.resolve()), "apkBytes": apk.stat().st_size, "manifestVersion": manifest["version"], "logicalFiles": len(manifest["files"]), "verifiedUniqueObjects": len(checked), "storedObjectBytes": object_bytes, - "bundledDungeons": dungeons, "bundledManastormPacks": manastorms, + "bundledDungeons": dungeons, "bundledManastormPacks": manastorms, "bundledArenaPacks": arenas, "allOriginalHashesMatch": True, } diff --git a/scripts/content/verify-release-bundle.mjs b/scripts/content/verify-release-bundle.mjs new file mode 100644 index 00000000..15cfe37d --- /dev/null +++ b/scripts/content/verify-release-bundle.mjs @@ -0,0 +1,91 @@ +import { readdir, readFile, stat } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import contentPolicy from "../../shared/content-policy.json" with { type: "json" }; + +const SCRIPT_PATH = fileURLToPath(import.meta.url); +const PROHIBITED_MODULE_PATTERNS = [ + /(?:^|\/)src\/ArenaRuntime\.tsx$/, + /(?:^|\/)src\/app\/ShelvedModeRoutes\.tsx$/, + /(?:^|\/)src\/app\/ShelvedGameRuntime\.enabled\.tsx$/, + /(?:^|\/)src\/app\/ShelvedOnlineGroupRuntime\.enabled\.ts$/, + /(?:^|\/)src\/app\/(?:arenaClient|gmManastormRuntime|manastormShellRuntime)\.[jt]sx?$/, + /(?:^|\/)src\/game\/(?:arena|manastormAdmin|manastormCatalog|manastormInstalled)(?:\/|\.[jt]sx?$)/, + /(?:^|\/)src\/game\/ManastormBridge\.[jt]sx?$/, + /(?:^|\/)src\/ui\/(?:Arena|GmAdmin|GmReview|GmWorld|Manastorm)[^/]*\.[jt]sx?$/, + /(?:^|\/)src\/scene\/ShelvedGameScene\.enabled\.tsx$/, + /(?:^|\/)src\/game\/ShelvedDungeonSession\.enabled\.ts$/, + /(?:^|\/)src\/ui\/(?:ShelvedCompanionPanel|ShelvedHud)\.enabled\.tsx$/, +]; + +function fail(message) { + throw new Error(`Release bundle verification failed: ${message}`); +} + +async function walkRelativeFiles(root) { + const files = []; + async function visit(directory) { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const filePath = path.join(directory, entry.name); + if (entry.isDirectory()) await visit(filePath); + else if (entry.isFile()) files.push(path.relative(root, filePath).split(path.sep).join("/")); + } + } + await visit(root); + return files; +} + +export async function verifyReleaseBundle(root) { + const manifest = JSON.parse(await readFile(path.join(root, "content", "bootstrap-manifest.json"), "utf8")); + const moduleReport = JSON.parse(await readFile(path.join(root, "bundle-module-report.json"), "utf8")); + if (moduleReport.shelvedModesEnabled !== false) fail("normal releases must compile with shelved modes disabled"); + + const expectedPacks = [...contentPolicy.bootstrapPackIds].sort(); + const actualPacks = (manifest.packs ?? []).map((pack) => pack.id).sort(); + if (JSON.stringify(actualPacks) !== JSON.stringify(expectedPacks)) { + fail(`bootstrap packs were ${actualPacks.join(", ")}; expected ${expectedPacks.join(", ")}`); + } + const hiddenPack = actualPacks.find((packId) => ( + contentPolicy.hiddenContentPackIds.includes(packId) + || contentPolicy.hiddenContentPackPrefixes.some((prefix) => packId.startsWith(prefix)) + )); + if (hiddenPack) fail(`bootstrap contains hidden pack ${hiddenPack}`); + + const objects = new Map(); + for (const entry of Object.values(manifest.files ?? {})) { + const object = entry.download ?? entry; + if (!objects.has(object.url)) objects.set(object.url, object.bytes); + } + const storedBytes = [...objects.values()].reduce((sum, bytes) => sum + Number(bytes ?? 0), 0); + if (storedBytes > contentPolicy.maxBootstrapStoredBytes) { + fail(`bootstrap uses ${storedBytes} stored bytes; limit is ${contentPolicy.maxBootstrapStoredBytes}`); + } + + const prohibitedModules = moduleReport.chunks.flatMap((chunk) => chunk.modules).filter((moduleId) => ( + PROHIBITED_MODULE_PATTERNS.some((pattern) => pattern.test(moduleId)) + )); + if (prohibitedModules.length) fail(`shelved client modules were emitted: ${[...new Set(prohibitedModules)].join(", ")}`); + + const files = await walkRelativeFiles(root); + const arenaAsset = files.find((file) => /(?:^|\/)assets\/game\/arenas(?:\/|$)/i.test(file)); + if (arenaAsset) fail(`arena asset was emitted at ${arenaAsset}`); + for (const objectUrl of objects.keys()) { + const objectPath = path.join(root, objectUrl.replace(/^\/+/, "")); + const objectStats = await stat(objectPath); + if (!objectStats.isFile()) fail(`bootstrap object is missing: ${objectUrl}`); + } + + return { + root, + packs: actualPacks, + storedBytes, + storedMiB: Number((storedBytes / 1024 / 1024).toFixed(2)), + objectCount: objects.size, + chunkCount: moduleReport.chunks.length, + }; +} + +if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(SCRIPT_PATH)) { + const root = path.resolve(process.argv[2] ?? "dist"); + console.log(JSON.stringify(await verifyReleaseBundle(root), null, 2)); +} diff --git a/scripts/content/verify-release-bundle.test.mjs b/scripts/content/verify-release-bundle.test.mjs new file mode 100644 index 00000000..c8c3b14c --- /dev/null +++ b/scripts/content/verify-release-bundle.test.mjs @@ -0,0 +1,66 @@ +import assert from "node:assert/strict"; +import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import contentPolicy from "../../shared/content-policy.json" with { type: "json" }; +import { verifyReleaseBundle } from "./verify-release-bundle.mjs"; + +async function fixture() { + const root = await mkdtemp(path.join(os.tmpdir(), "healer-man-release-bundle-test-")); + await mkdir(path.join(root, "content", "bundled"), { recursive: true }); + const manifest = { + schemaVersion: 1, + packs: contentPolicy.bootstrapPackIds.map((id) => ({ id })), + files: {}, + }; + const moduleReport = { shelvedModesEnabled: false, chunks: [{ file: "assets/index.js", modules: ["src/App.tsx"] }] }; + const write = async () => { + await writeFile(path.join(root, "content", "bootstrap-manifest.json"), `${JSON.stringify(manifest)}\n`); + await writeFile(path.join(root, "bundle-module-report.json"), `${JSON.stringify(moduleReport)}\n`); + }; + await write(); + return { root, manifest, moduleReport, write }; +} + +test("accepts the exact compact production policy", async () => { + const value = await fixture(); + try { + const report = await verifyReleaseBundle(value.root); + assert.deepEqual(report.packs, [...contentPolicy.bootstrapPackIds].sort()); + assert.equal(report.storedBytes, 0); + } finally { + await rm(value.root, { recursive: true, force: true }); + } +}); + +test("rejects arena packs and shelved client modules", async () => { + const value = await fixture(); + try { + value.manifest.packs.push({ id: "core:arenas" }); + await value.write(); + await assert.rejects(verifyReleaseBundle(value.root), /bootstrap packs/); + + value.manifest.packs.pop(); + value.moduleReport.chunks[0].modules.push("src/ArenaRuntime.tsx"); + await value.write(); + await assert.rejects(verifyReleaseBundle(value.root), /shelved client modules/); + } finally { + await rm(value.root, { recursive: true, force: true }); + } +}); + +test("rejects a bootstrap above the stored-byte budget", async () => { + const value = await fixture(); + try { + value.manifest.files["/large.glb"] = { + packId: "core:runtime", + url: `/content/bundled/${"c".repeat(64)}.glb`, + bytes: contentPolicy.maxBootstrapStoredBytes + 1, + }; + await value.write(); + await assert.rejects(verifyReleaseBundle(value.root), /limit is/); + } finally { + await rm(value.root, { recursive: true, force: true }); + } +}); diff --git a/scripts/release_manager.py b/scripts/release_manager.py index ac3fcb9e..28217b48 100644 --- a/scripts/release_manager.py +++ b/scripts/release_manager.py @@ -81,6 +81,7 @@ def full_release_checks(npm: str) -> tuple[tuple[list[str], str], ...]: ([npm, "run", "content:test"], "Content tests"), ([npm, "run", "server:test"], "Server tests"), ([npm, "test"], "Application tests"), + ([npm, "run", "test:shelved"], "Shelved-mode application tests"), ([npm, "run", "content:publish"], "Publish verified 54-dungeon content manifest"), ([npm, "run", "build"], "Production build"), ([npm, "run", "android:sync"], "Android web build and Capacitor sync"), diff --git a/scripts/runewaker-pipeline/RECIPE_SCHEMA.md b/scripts/runewaker-pipeline/RECIPE_SCHEMA.md index e60df607..305a9014 100644 --- a/scripts/runewaker-pipeline/RECIPE_SCHEMA.md +++ b/scripts/runewaker-pipeline/RECIPE_SCHEMA.md @@ -71,6 +71,21 @@ Optional template fields: - `reason`: why an object is deferred; - `source`: forensic image/model/classification evidence. +`source.scriptRoots` may list reviewed Lua entry functions for dynamically +created bosses that have no static SQL row. The boss-mechanics importer follows +those roots and their callbacks exactly as it does for static boss rows. + +Optional top-level scripted placement fields: + +- `scriptedSpawns`: reviewed creatures created by Lua rather than stored as + active zone rows. Each entry has a stable `id`, a combat/boss `templateId`, a + `sourceAnchorSpawnId`, source evidence, and optional source-unit offset, + controller row, direction, or `objective: false` for a boss companion. +- `scriptedFormations`: source `LuaFunc_Matrix_Maker` formations flattened to + their initial positions. Each formation specifies its controller/anchor row, + source direction and spacing, Lua root/evidence, and member template IDs with + one-based rows and signed columns. + `expected*` values may be `null` only in a fresh scaffold. The explicit forensic refresh with `--update-recipe` pins them. A recipe should not be treated as production-ready until all classifications/models and counts are reviewed. diff --git a/scripts/runewaker-pipeline/RUNEWAKER_ENCOUNTER_IMPLEMENTATION_REPORT.json b/scripts/runewaker-pipeline/RUNEWAKER_ENCOUNTER_IMPLEMENTATION_REPORT.json index 89567c01..f93804e0 100644 --- a/scripts/runewaker-pipeline/RUNEWAKER_ENCOUNTER_IMPLEMENTATION_REPORT.json +++ b/scripts/runewaker-pipeline/RUNEWAKER_ENCOUNTER_IMPLEMENTATION_REPORT.json @@ -27,9 +27,9 @@ "summary": { "dungeons": 30, "deferredSourceRows": 4113, - "uniqueMechanicsEvidence": 1524, + "uniqueMechanicsEvidence": 1530, "luaEvidenceSignals": 329, - "templateEvidenceSignals": 1195, + "templateEvidenceSignals": 1201, "evidenceCategoryMembershipCount": 1146, "evidenceCategoryMemberships": { "death": 100, @@ -54,21 +54,21 @@ "evidenceStatuses": { "implemented": 0, "verified": 388, - "deferred-source-gap": 1136, + "deferred-source-gap": 1142, "deferred-asset-gap": 0, "deferred-runtime-gap": 0, "excluded-non-gameplay": 0, "intentional-empty": 0 }, "evidenceFidelities": { - "source-exact": 1136, + "source-exact": 1142, "source-backed-adapted": 388, "healer-man-authored": 0 }, "verifiedEvidence": 388, - "sourceGapEvidence": 1136, - "fallbackBosses": 4, - "nonEmptyZeroBossPackages": 8, + "sourceGapEvidence": 1142, + "fallbackBosses": 5, + "nonEmptyZeroBossPackages": 7, "intentionalEmptyPackages": 2 }, "blockerGroups": { @@ -77,7 +77,7 @@ "nextWork": "Review source semantics and add explicit source-spawn encounter mappings." }, "unresolvedMechanicsEvidence": { - "items": 1136, + "items": 1142, "nextWork": "Review surrounding Lua/template context; specify full trigger, effect, and reset behavior." }, "adaptedSpellFormula": { @@ -85,11 +85,11 @@ "nextWork": "Recover native formula inputs before claiming coefficient-level fidelity." }, "sourceGapFallbackBosses": { - "items": 4, + "items": 5, "nextWork": "Recover authoritative combat kits and replace only with source-backed modular mechanics." }, "nonEmptyZeroBossPackages": { - "items": 8, + "items": 7, "nextWork": "Recover authoritative boss membership and objective order." } }, @@ -111,7 +111,7 @@ "source": { "bossMechanicsCatalog": { "path": "dungeon-pipeline/boss-mechanics.json", - "sha256": "6e078fe5fbbd10cabc77d2141e5084b30daffe5056945b44294a59711c48c4b7" + "sha256": "2e573cd2a0a99d89d0f51764bcce0693e37513da677c70090e101bb016f39b21" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/forsaken-abbey-zone-102.json", @@ -153,7 +153,7 @@ "source": { "bossMechanicsCatalog": { "path": "dungeon-pipeline/boss-mechanics.json", - "sha256": "6e078fe5fbbd10cabc77d2141e5084b30daffe5056945b44294a59711c48c4b7" + "sha256": "2e573cd2a0a99d89d0f51764bcce0693e37513da677c70090e101bb016f39b21" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/forsaken-abbey-zone-102.json", @@ -189,7 +189,7 @@ "source": { "bossMechanicsCatalog": { "path": "dungeon-pipeline/boss-mechanics.json", - "sha256": "6e078fe5fbbd10cabc77d2141e5084b30daffe5056945b44294a59711c48c4b7" + "sha256": "2e573cd2a0a99d89d0f51764bcce0693e37513da677c70090e101bb016f39b21" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/forsaken-abbey-zone-102.json", @@ -225,7 +225,7 @@ "source": { "bossMechanicsCatalog": { "path": "dungeon-pipeline/boss-mechanics.json", - "sha256": "6e078fe5fbbd10cabc77d2141e5084b30daffe5056945b44294a59711c48c4b7" + "sha256": "2e573cd2a0a99d89d0f51764bcce0693e37513da677c70090e101bb016f39b21" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/forsaken-abbey-zone-102.json", @@ -244,6 +244,44 @@ "blocker": "Preserved source evidence does not expose an executable original combat kit.", "nextWork": "Replace the fallback only after authoritative spell timing, targeting, and effects are recovered; preserve modular IDs and telegraphs.", "clearingTest": "Add source-backed mechanic fixtures and assert the catalog source kind changes from healer-man-source-gap to runewaker." + }, + { + "entityId": "rw-grafu-castle-105351", + "name": "Larvanger Barkud", + "dungeonId": "grafu-castle", + "templateId": 105351, + "mechanicIds": [ + "rw-grafu-castle-105351-source-gap-sweep" + ], + "telegraphedMechanicIds": [ + "rw-grafu-castle-105351-source-gap-sweep" + ], + "status": "implemented", + "fidelity": "healer-man-authored", + "source": { + "bossMechanicsCatalog": { + "path": "dungeon-pipeline/boss-mechanics.json", + "sha256": "2e573cd2a0a99d89d0f51764bcce0693e37513da677c70090e101bb016f39b21" + }, + "snapshot": { + "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", + "sha256": "aa70f57055eddd21c6280879e6459036b81ae17b56ed6d8eb30b3825a716d403", + "contentSha256": "0113f241c8bb527a6e0bd365c2cf9586a6f922719098aea009292f3f3c04c801" + }, + "roots": [ + "un_zone19_boss5_allconreal_1_01" + ], + "functions": [ + "un_zone19_boss5_allconreal_1_01", + "Az_RND_Ary", + "SearchRangeNPC", + "DW_Rand" + ], + "spellIds": [] + }, + "blocker": "Preserved source evidence does not expose an executable original combat kit.", + "nextWork": "Replace the fallback only after authoritative spell timing, targeting, and effects are recovered; preserve modular IDs and telegraphs.", + "clearingTest": "Add source-backed mechanic fixtures and assert the catalog source kind changes from healer-man-source-gap to runewaker." } ], "nonEmptyZeroBossPackages": [ @@ -277,21 +315,6 @@ "nextWork": "Recover and review authoritative boss membership and objective order; do not promote ordinary templates heuristically.", "clearingTest": "Verify at least one source-backed boss objective, or add reviewed evidence that this package intentionally has none." }, - { - "dungeonId": "grafu-castle", - "zoneId": 137, - "sourceRows": 249, - "status": "deferred-source-gap", - "fidelity": "source-exact", - "source": { - "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", - "sha256": "aa70f57055eddd21c6280879e6459036b81ae17b56ed6d8eb30b3825a716d403", - "contentSha256": "0113f241c8bb527a6e0bd365c2cf9586a6f922719098aea009292f3f3c04c801" - }, - "blocker": "The portable source package contains population rows but no source-confirmed boss membership.", - "nextWork": "Recover and review authoritative boss membership and objective order; do not promote ordinary templates heuristically.", - "clearingTest": "Verify at least one source-backed boss objective, or add reviewed evidence that this package intentionally has none." - }, { "dungeonId": "kal-turok-hive", "zoneId": 105, @@ -41141,11 +41164,11 @@ "id": "grafu-castle", "zoneId": 137, "sourceRows": 249, - "bossObjectives": 0, + "bossObjectives": 5, "sourceArtifacts": { "recipe": { "path": "scripts/runewaker-pipeline/recipes/grafu-castle-population.json", - "sha256": "b65eb823f8d1a7306dbafb17e4c4ed5e11899b9439af72b4d24b18ddbfd07bf8" + "sha256": "29b4798fb43dc6c0ef8b5ec5dd1cc6007c4b84cc2753c4d38b47d005ff82b4cb" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -41154,11 +41177,11 @@ }, "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "generatedRuntime": { "path": "src/game/generated/grafuCastlePopulation.generated.ts", - "sha256": "ed998e499878f675d1223ea24c966816ed22dae1beccfd7ae6e3f701ff06efd6" + "sha256": "e3678e53911590bcf2fbde56272330d903b76a3f8a84a96f8bef43f671c0a07d" }, "upstreamHashes": { "backups": { @@ -41173,11 +41196,11 @@ } }, "packageAssessment": { - "status": "deferred-source-gap", + "status": "verified", "fidelity": "source-exact", - "blocker": "The package is non-empty, but its preserved source classification confirms no boss objectives.", - "nextWork": "Recover authoritative boss membership and encounter order before creating boss objectives.", - "clearingTest": "Add a source-backed boss-membership fixture and verify the generated objective list." + "blocker": "No package-level population blocker is recorded.", + "nextWork": "Re-run the ledger whenever portable source content changes.", + "clearingTest": "npm run runewaker:audit:gameplay" }, "deferredRowStatusCounts": { "implemented": 0, @@ -41191,14 +41214,14 @@ "evidenceStatusCounts": { "implemented": 0, "verified": 10, - "deferred-source-gap": 40, + "deferred-source-gap": 46, "deferred-asset-gap": 0, "deferred-runtime-gap": 0, "excluded-non-gameplay": 0, "intentional-empty": 0 }, "evidenceFidelityCounts": { - "source-exact": 40, + "source-exact": 46, "source-backed-adapted": 10, "healer-man-authored": 0 }, @@ -47823,7 +47846,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -47851,7 +47874,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -47879,7 +47902,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -47907,7 +47930,35 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" + }, + "snapshot": { + "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", + "sha256": "aa70f57055eddd21c6280879e6459036b81ae17b56ed6d8eb30b3825a716d403", + "contentSha256": "0113f241c8bb527a6e0bd365c2cf9586a6f922719098aea009292f3f3c04c801" + } + }, + "blocker": "This is source evidence, but it does not prove a complete executable mechanic by itself.", + "nextWork": "Review the surrounding source to establish trigger, target, timing, state effects, reset, persistence, and multiplayer behavior.", + "clearingTest": "Add a focused encounter fixture tied to this stable evidence ID and verify its full state transition and reset behavior." + }, + { + "id": "signal:grafu-castle:template:105204:autoplot:8695922bb376", + "kind": "mechanics-evidence", + "evidenceKind": "auto-plot", + "dungeonId": "grafu-castle", + "zoneId": 137, + "sourceIds": { + "templateId": 105204 + }, + "categories": [], + "value": "cl_136_105205_N", + "status": "deferred-source-gap", + "fidelity": "source-exact", + "source": { + "mechanicsAudit": { + "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -47935,7 +47986,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -47963,7 +48014,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -47993,7 +48044,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48021,7 +48072,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48052,7 +48103,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48082,7 +48133,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48110,7 +48161,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48141,7 +48192,35 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" + }, + "snapshot": { + "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", + "sha256": "aa70f57055eddd21c6280879e6459036b81ae17b56ed6d8eb30b3825a716d403", + "contentSha256": "0113f241c8bb527a6e0bd365c2cf9586a6f922719098aea009292f3f3c04c801" + } + }, + "blocker": "This is source evidence, but it does not prove a complete executable mechanic by itself.", + "nextWork": "Review the surrounding source to establish trigger, target, timing, state effects, reset, persistence, and multiplayer behavior.", + "clearingTest": "Add a focused encounter fixture tied to this stable evidence ID and verify its full state transition and reset behavior." + }, + { + "id": "signal:grafu-castle:template:105224:autoplot:91708b8323d7", + "kind": "mechanics-evidence", + "evidenceKind": "auto-plot", + "dungeonId": "grafu-castle", + "zoneId": 137, + "sourceIds": { + "templateId": 105224 + }, + "categories": [], + "value": "ic_105224_01", + "status": "deferred-source-gap", + "fidelity": "source-exact", + "source": { + "mechanicsAudit": { + "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48169,7 +48248,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48197,7 +48276,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48225,7 +48304,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48253,7 +48332,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48281,7 +48360,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48309,7 +48388,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48337,7 +48416,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48365,7 +48444,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48393,7 +48472,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48421,7 +48500,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48449,7 +48528,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48477,7 +48556,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48505,7 +48584,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48533,7 +48612,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48561,7 +48640,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48589,7 +48668,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48617,7 +48696,91 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" + }, + "snapshot": { + "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", + "sha256": "aa70f57055eddd21c6280879e6459036b81ae17b56ed6d8eb30b3825a716d403", + "contentSha256": "0113f241c8bb527a6e0bd365c2cf9586a6f922719098aea009292f3f3c04c801" + } + }, + "blocker": "This is source evidence, but it does not prove a complete executable mechanic by itself.", + "nextWork": "Review the surrounding source to establish trigger, target, timing, state effects, reset, persistence, and multiplayer behavior.", + "clearingTest": "Add a focused encounter fixture tied to this stable evidence ID and verify its full state transition and reset behavior." + }, + { + "id": "signal:grafu-castle:template:105351:autoplot:cb2a2839d2c8", + "kind": "mechanics-evidence", + "evidenceKind": "auto-plot", + "dungeonId": "grafu-castle", + "zoneId": 137, + "sourceIds": { + "templateId": 105351 + }, + "categories": [], + "value": "un_zone19_boss5_allconreal_1_01", + "status": "deferred-source-gap", + "fidelity": "source-exact", + "source": { + "mechanicsAudit": { + "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" + }, + "snapshot": { + "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", + "sha256": "aa70f57055eddd21c6280879e6459036b81ae17b56ed6d8eb30b3825a716d403", + "contentSha256": "0113f241c8bb527a6e0bd365c2cf9586a6f922719098aea009292f3f3c04c801" + } + }, + "blocker": "This is source evidence, but it does not prove a complete executable mechanic by itself.", + "nextWork": "Review the surrounding source to establish trigger, target, timing, state effects, reset, persistence, and multiplayer behavior.", + "clearingTest": "Add a focused encounter fixture tied to this stable evidence ID and verify its full state transition and reset behavior." + }, + { + "id": "signal:grafu-castle:template:105542:autoplot:5ae3fcd9ed91", + "kind": "mechanics-evidence", + "evidenceKind": "auto-plot", + "dungeonId": "grafu-castle", + "zoneId": 137, + "sourceIds": { + "templateId": 105542 + }, + "categories": [], + "value": "kg_105542_01", + "status": "deferred-source-gap", + "fidelity": "source-exact", + "source": { + "mechanicsAudit": { + "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" + }, + "snapshot": { + "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", + "sha256": "aa70f57055eddd21c6280879e6459036b81ae17b56ed6d8eb30b3825a716d403", + "contentSha256": "0113f241c8bb527a6e0bd365c2cf9586a6f922719098aea009292f3f3c04c801" + } + }, + "blocker": "This is source evidence, but it does not prove a complete executable mechanic by itself.", + "nextWork": "Review the surrounding source to establish trigger, target, timing, state effects, reset, persistence, and multiplayer behavior.", + "clearingTest": "Add a focused encounter fixture tied to this stable evidence ID and verify its full state transition and reset behavior." + }, + { + "id": "signal:grafu-castle:template:105543:autoplot:5ae3fcd9ed91", + "kind": "mechanics-evidence", + "evidenceKind": "auto-plot", + "dungeonId": "grafu-castle", + "zoneId": 137, + "sourceIds": { + "templateId": 105543 + }, + "categories": [], + "value": "kg_105542_01", + "status": "deferred-source-gap", + "fidelity": "source-exact", + "source": { + "mechanicsAudit": { + "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48650,7 +48813,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48691,7 +48854,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48724,7 +48887,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48760,7 +48923,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48793,7 +48956,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48834,7 +48997,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48875,7 +49038,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48908,7 +49071,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48949,7 +49112,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -48990,7 +49153,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -49031,7 +49194,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -49072,7 +49235,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -49100,7 +49263,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -49128,7 +49291,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -49159,7 +49322,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -49192,7 +49355,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -49225,7 +49388,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -49266,7 +49429,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -49307,7 +49470,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -49340,7 +49503,7 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -49373,7 +49536,35 @@ "source": { "mechanicsAudit": { "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", - "sha256": "c97474a819d21bef2b7790f4c8be5510f540a4ff4b82511bb936b30ffe1ba348" + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" + }, + "snapshot": { + "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", + "sha256": "aa70f57055eddd21c6280879e6459036b81ae17b56ed6d8eb30b3825a716d403", + "contentSha256": "0113f241c8bb527a6e0bd365c2cf9586a6f922719098aea009292f3f3c04c801" + } + }, + "blocker": "This is source evidence, but it does not prove a complete executable mechanic by itself.", + "nextWork": "Review the surrounding source to establish trigger, target, timing, state effects, reset, persistence, and multiplayer behavior.", + "clearingTest": "Add a focused encounter fixture tied to this stable evidence ID and verify its full state transition and reset behavior." + }, + { + "id": "signal:grafu-castle:template:105616:autoplot:6b24b50e07a5", + "kind": "mechanics-evidence", + "evidenceKind": "auto-plot", + "dungeonId": "grafu-castle", + "zoneId": 137, + "sourceIds": { + "templateId": 105616 + }, + "categories": [], + "value": "Dy_Z19_BOSSN_AIC", + "status": "deferred-source-gap", + "fidelity": "source-exact", + "source": { + "mechanicsAudit": { + "path": "src/assets/game/dungeons/grafu-castle/mechanics-audit.json", + "sha256": "806a106123e1fd85aadcd2c41066efdcfb032d9a4dde1add0ff2d59fcda060cd" }, "snapshot": { "path": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", diff --git a/scripts/runewaker-pipeline/RUNEWAKER_ENCOUNTER_IMPLEMENTATION_REPORT.md b/scripts/runewaker-pipeline/RUNEWAKER_ENCOUNTER_IMPLEMENTATION_REPORT.md index f454d573..bd2f3387 100644 --- a/scripts/runewaker-pipeline/RUNEWAKER_ENCOUNTER_IMPLEMENTATION_REPORT.md +++ b/scripts/runewaker-pipeline/RUNEWAKER_ENCOUNTER_IMPLEMENTATION_REPORT.md @@ -11,10 +11,10 @@ is a review label, not a unique mechanic; one evidence record may belong to seve - 30 dungeon packages. - 4113 deferred source rows with stable spawn IDs. -- 1524 unique mechanics evidence records: 329 Lua lines and 1195 template references. +- 1530 unique mechanics evidence records: 329 Lua lines and 1201 template references. - 1146 overlapping category memberships; this number is intentionally not reported as mechanics. - 388 evidence records have an exact executable source-spell mapping. -- 1136 evidence records remain source-review gaps. +- 1142 evidence records remain source-review gaps. ## Status definitions @@ -37,7 +37,7 @@ is a review label, not a unique mechanic; one evidence record may belong to seve | demon-stronghold-125 | 0 | 0 | 0 | 0 | 0 | 0 | intentional-empty | | dungeon-of-dalanis | 229 | 162 | 26 | 5 | 21 | 0 | deferred-source-gap | | forsaken-abbey | 251 | 101 | 77 | 0 | 77 | 8 | verified | -| grafu-castle | 249 | 193 | 50 | 10 | 40 | 0 | deferred-source-gap | +| grafu-castle | 249 | 193 | 56 | 10 | 46 | 5 | verified | | hall-of-survivors | 363 | 201 | 84 | 34 | 50 | 9 | verified | | hoto | 401 | 94 | 95 | 28 | 67 | 5 | verified | | ice-dwarf-kingdom | 651 | 52 | 87 | 36 | 51 | 5 | verified | @@ -69,6 +69,7 @@ is a review label, not a unique mechanic; one evidence record may belong to seve | Flowing Chaos | `rw-fa-100561` | implemented | healer-man-authored | Preserved source evidence does not expose an executable original combat kit. | | Cruel Eater | `rw-fa-100562` | implemented | healer-man-authored | Preserved source evidence does not expose an executable original combat kit. | | Hollow Shell | `rw-fa-100563` | implemented | healer-man-authored | Preserved source evidence does not expose an executable original combat kit. | +| Larvanger Barkud | `rw-grafu-castle-105351` | implemented | healer-man-authored | Preserved source evidence does not expose an executable original combat kit. | These attacks are intentionally playable and telegraphed HealerMan behavior. They are not presented as original RuneWaker mechanics until authoritative source behavior is recovered. @@ -77,7 +78,6 @@ presented as original RuneWaker mechanics until authoritative source behavior is - `aeternal-circle`: 514 rows; The portable source package contains population rows but no source-confirmed boss membership. Next: Recover and review authoritative boss membership and objective order; do not promote ordinary templates heuristically. - `dungeon-of-dalanis`: 229 rows; The portable source package contains population rows but no source-confirmed boss membership. Next: Recover and review authoritative boss membership and objective order; do not promote ordinary templates heuristically. -- `grafu-castle`: 249 rows; The portable source package contains population rows but no source-confirmed boss membership. Next: Recover and review authoritative boss membership and objective order; do not promote ordinary templates heuristically. - `kal-turok-hive`: 4 rows; The portable source package contains population rows but no source-confirmed boss membership. Next: Recover and review authoritative boss membership and objective order; do not promote ordinary templates heuristically. - `lair-demon-dragon`: 532 rows; The portable source package contains population rows but no source-confirmed boss membership. Next: Recover and review authoritative boss membership and objective order; do not promote ordinary templates heuristically. - `tomb-seven-heroes`: 439 rows; The portable source package contains population rows but no source-confirmed boss membership. Next: Recover and review authoritative boss membership and objective order; do not promote ordinary templates heuristically. @@ -108,10 +108,10 @@ presented as original RuneWaker mechanics until authoritative source behavior is | Blocker group | Items | Clearing work | | --- | ---: | --- | | deferredSourceRows | 4113 | Review source semantics and add explicit source-spawn encounter mappings. | -| unresolvedMechanicsEvidence | 1136 | Review surrounding Lua/template context; specify full trigger, effect, and reset behavior. | +| unresolvedMechanicsEvidence | 1142 | Review surrounding Lua/template context; specify full trigger, effect, and reset behavior. | | adaptedSpellFormula | 388 | Recover native formula inputs before claiming coefficient-level fidelity. | -| sourceGapFallbackBosses | 4 | Recover authoritative combat kits and replace only with source-backed modular mechanics. | -| nonEmptyZeroBossPackages | 8 | Recover authoritative boss membership and objective order. | +| sourceGapFallbackBosses | 5 | Recover authoritative combat kits and replace only with source-backed modular mechanics. | +| nonEmptyZeroBossPackages | 7 | Recover authoritative boss membership and objective order. | The exhaustive per-item blocker, next-work instruction, clearing test, source IDs, source files, and SHA-256 hashes are in `RUNEWAKER_ENCOUNTER_IMPLEMENTATION_REPORT.json`. diff --git a/scripts/runewaker-pipeline/RUNEWAKER_GAMEPLAY_CONTENT_REPORT.json b/scripts/runewaker-pipeline/RUNEWAKER_GAMEPLAY_CONTENT_REPORT.json index 217e51c2..771b97f0 100644 --- a/scripts/runewaker-pipeline/RUNEWAKER_GAMEPLAY_CONTENT_REPORT.json +++ b/scripts/runewaker-pipeline/RUNEWAKER_GAMEPLAY_CONTENT_REPORT.json @@ -1,36 +1,36 @@ { "schemaVersion": 1, - "validationStatus": "green", + "validationStatus": "blocked", "fidelityStatus": "review-required", "scope": "All configured offline RuneWaker dungeon packages", "summary": { "sourceRows": 8199, - "runtimeSpawns": 4086, - "runtimeEntities": 436, - "bossObjectives": 61, - "runtimeAttacks": 1068, - "genericDefaultAttacks": 436, + "runtimeSpawns": 4114, + "runtimeEntities": 446, + "bossObjectives": 66, + "runtimeAttacks": 1133, + "genericDefaultAttacks": 446, "builderAuthoredAttacks": 0, "recipeAuthoredAttacks": 0, - "catalogBossMechanics": 279, - "sourceBackedBossMechanics": 275, - "sourceGapBossMechanics": 4, - "telegraphedBossMechanics": 111, + "catalogBossMechanics": 334, + "sourceBackedBossMechanics": 329, + "sourceGapBossMechanics": 5, + "telegraphedBossMechanics": 143, "healthTriggeredBossMechanics": 28, - "sourceBackedDamageFamilyAttacks": 483, + "sourceBackedDamageFamilyAttacks": 514, "sourceSpellTemplates": 293, "sourceSpellAssignments": 659, - "scriptedTemplates": 357, - "nativeBindings": 435, + "scriptedTemplates": 363, + "nativeBindings": 445, "proceduralBindings": 1, "proxyBindings": 0, - "objectiveRoutes": 61, - "blockedObjectiveRoutes": 0, - "sourceOrderedObjectiveRoutes": 0, + "objectiveRoutes": 66, + "blockedObjectiveRoutes": 3, + "sourceOrderedObjectiveRoutes": 5, "derivedObjectiveRoutes": 61, "unreachableDerivedObjectiveRoutes": 19, "dungeons": 30, - "uniqueNativeModels": 321, + "uniqueNativeModels": 328, "uniqueSourceSpellIds": 411, "portableSpellCatalogRecords": 3199, "portableDamageFamilyRecords": 1342, @@ -79,9 +79,28 @@ ], "blockerGroups": { "navigation": { - "blockedLegs": 0, - "affectedDungeons": 0, - "entries": [], + "blockedLegs": 3, + "affectedDungeons": 1, + "entries": [ + { + "dungeonId": "grafu-castle", + "from": "entrance", + "to": "Cayus", + "objectiveId": "rw-grafu-castle-scripted-cayus" + }, + { + "dungeonId": "grafu-castle", + "from": "Cayus", + "to": "Ardmond and Issac", + "objectiveId": "rw-grafu-castle-scripted-ardmond" + }, + { + "dungeonId": "grafu-castle", + "from": "Ardmond and Issac", + "to": "Boro-Boro Brothers", + "objectiveId": "rw-grafu-castle-scripted-boro-boro-brothers" + } + ], "unreachableDerivedLegs": 19, "derivedAffectedDungeons": 8, "derivedEntries": [ @@ -223,10 +242,10 @@ "sourceMechanics": { "status": "review-required", "executableBossMechanicsGenerated": true, - "catalogBossMechanics": 279, - "sourceBackedBossMechanics": 275, - "sourceGapBossMechanics": 4, - "scriptedTemplates": 357, + "catalogBossMechanics": 334, + "sourceBackedBossMechanics": 329, + "sourceGapBossMechanics": 5, + "scriptedTemplates": 363, "evidenceCounts": { "death": 75, "door": 22, @@ -258,7 +277,7 @@ "validationStatus": "green", "completionStatus": "review-required", "deferredSourceRows": 4113, - "uniqueMechanicsEvidence": 1524 + "uniqueMechanicsEvidence": 1530 }, "resolvedRuntimeObservations": [ { @@ -272,11 +291,15 @@ "documentedExceptions": [ "/assets/creatures/sardo-castle/rune-device-red.glb: move exempt - Rune Device is a source-authored stationary encounter device; its native GLB retains stand, attack/activation, cast, wound, and death clips." ], - "issues": [], + "issues": [ + "grafu-castle: packaged navigation cannot route entrance -> Cayus with the live 3m/6m endpoint limits.", + "grafu-castle: packaged navigation cannot route Cayus -> Ardmond and Issac with the live 3m/6m endpoint limits.", + "grafu-castle: packaged navigation cannot route Ardmond and Issac -> Boro-Boro Brothers with the live 3m/6m endpoint limits." + ], "reviewNotes": [ "demon-stronghold-125 has an authoritative empty Zone 125 snapshot, so it packages an environment but no combat population.", "zurhidon-stronghold-124 has an authoritative empty Zone 124 snapshot, so it packages an environment but no combat population.", - "4 boss mechanics are conservative, telegraphed fallbacks for preserved bosses whose scripts expose no executable combat spell.", + "5 boss mechanics are conservative, telegraphed fallbacks for preserved bosses whose scripts expose no executable combat spell.", "Source formula structure is preserved, but live damage uses a labeled HealerMan baseline adapter because the required native combat-stat inputs are not packaged.", "61 objective legs use HealerMan's distance-from-entrance fallback because no source-authored boss order is packaged; 19 unreachable heuristic legs remain navigation/order review evidence." ], @@ -774,38 +797,145 @@ "id": "grafu-castle", "zoneId": 137, "sourceRows": 249, - "runtimeSpawns": 56, - "runtimeEntities": 11, - "bossObjectives": 0, + "runtimeSpawns": 84, + "runtimeEntities": 21, + "bossObjectives": 5, "mechanicsStatus": "review-required", - "objectiveOrderAuthoritative": false, + "objectiveOrderAuthoritative": true, "objectiveOrderProvenance": { - "authoritative": false, - "kind": "healerMan-distance-derived", + "authoritative": true, + "kind": "source-authored", "evidence": [ - "scripts/runewaker-pipeline/lib/objective-order.mjs" + "Resource/luascript/02345.lua:Dy_Z19_BOSSN_AIC", + "Resource/luascript/02324.lua:kg_105542_01", + "Resource/luascript/02231.lua:ic_105224_01", + "Resource/luascript/02330.lua:cl_136_105205_N", + "Resource/luascript/02297.lua:un_zone19_boss5_allconreal_1_01" ] }, - "objectiveRouteEvidence": [], - "runtimeAttacks": 21, - "genericDefaultAttacks": 11, + "objectiveRouteEvidence": [ + { + "from": "entrance", + "to": "Cayus", + "objectiveId": "rw-grafu-castle-scripted-cayus", + "approachPosition": null, + "objectiveProjectionDistance": null, + "reachable": false, + "waypointCount": 0, + "orderAuthoritative": true, + "orderKind": "source-authored", + "orderEvidence": [ + "Resource/luascript/02345.lua:Dy_Z19_BOSSN_AIC", + "Resource/luascript/02324.lua:kg_105542_01", + "Resource/luascript/02231.lua:ic_105224_01", + "Resource/luascript/02330.lua:cl_136_105205_N", + "Resource/luascript/02297.lua:un_zone19_boss5_allconreal_1_01" + ] + }, + { + "from": "Cayus", + "to": "Ardmond and Issac", + "objectiveId": "rw-grafu-castle-scripted-ardmond", + "approachPosition": null, + "objectiveProjectionDistance": null, + "reachable": false, + "waypointCount": 0, + "orderAuthoritative": true, + "orderKind": "source-authored", + "orderEvidence": [ + "Resource/luascript/02345.lua:Dy_Z19_BOSSN_AIC", + "Resource/luascript/02324.lua:kg_105542_01", + "Resource/luascript/02231.lua:ic_105224_01", + "Resource/luascript/02330.lua:cl_136_105205_N", + "Resource/luascript/02297.lua:un_zone19_boss5_allconreal_1_01" + ] + }, + { + "from": "Ardmond and Issac", + "to": "Boro-Boro Brothers", + "objectiveId": "rw-grafu-castle-scripted-boro-boro-brothers", + "approachPosition": [ + -176.317575, + -17.93685769127901, + -4.85641198856605 + ], + "objectiveProjectionDistance": 3.0893265615044496e-7, + "reachable": false, + "waypointCount": 0, + "orderAuthoritative": true, + "orderKind": "source-authored", + "orderEvidence": [ + "Resource/luascript/02345.lua:Dy_Z19_BOSSN_AIC", + "Resource/luascript/02324.lua:kg_105542_01", + "Resource/luascript/02231.lua:ic_105224_01", + "Resource/luascript/02330.lua:cl_136_105205_N", + "Resource/luascript/02297.lua:un_zone19_boss5_allconreal_1_01" + ] + }, + { + "from": "Boro-Boro Brothers", + "to": "Conjurer Chapeaunoir", + "objectiveId": "rw-grafu-castle-scripted-conjurer-chapeaunoir", + "approachPosition": [ + -132.39614900162002, + -35.924047364518444, + -126.16280800000898 + ], + "objectiveProjectionDistance": 3.6452204204830595e-7, + "reachable": true, + "waypointCount": 113, + "orderAuthoritative": true, + "orderKind": "source-authored", + "orderEvidence": [ + "Resource/luascript/02345.lua:Dy_Z19_BOSSN_AIC", + "Resource/luascript/02324.lua:kg_105542_01", + "Resource/luascript/02231.lua:ic_105224_01", + "Resource/luascript/02330.lua:cl_136_105205_N", + "Resource/luascript/02297.lua:un_zone19_boss5_allconreal_1_01" + ] + }, + { + "from": "Conjurer Chapeaunoir", + "to": "Larvanger Barkud", + "objectiveId": "rw-grafu-castle-scripted-larvanger-barkud", + "approachPosition": [ + 10.319202, + -37.197608947753906, + -101.219726 + ], + "objectiveProjectionDistance": 5.2246093673602445e-8, + "reachable": true, + "waypointCount": 105, + "orderAuthoritative": true, + "orderKind": "source-authored", + "orderEvidence": [ + "Resource/luascript/02345.lua:Dy_Z19_BOSSN_AIC", + "Resource/luascript/02324.lua:kg_105542_01", + "Resource/luascript/02231.lua:ic_105224_01", + "Resource/luascript/02330.lua:cl_136_105205_N", + "Resource/luascript/02297.lua:un_zone19_boss5_allconreal_1_01" + ] + } + ], + "runtimeAttacks": 86, + "genericDefaultAttacks": 21, "builderAuthoredAttacks": 0, "recipeAuthoredAttacks": 0, - "catalogBossMechanics": 0, - "sourceBackedBossMechanics": 0, - "sourceGapBossMechanics": 0, - "telegraphedBossMechanics": 0, + "catalogBossMechanics": 55, + "sourceBackedBossMechanics": 54, + "sourceGapBossMechanics": 1, + "telegraphedBossMechanics": 32, "healthTriggeredBossMechanics": 0, - "sourceBackedDamageFamilyAttacks": 10, - "nativeBindings": 11, + "sourceBackedDamageFamilyAttacks": 41, + "nativeBindings": 21, "proceduralBindings": 0, "proxyBindings": 0, "sourceSpellTemplates": 6, "sourceSpellAssignments": 17, - "scriptedTemplates": 15, - "objectiveRoutes": 0, - "blockedObjectiveRoutes": 0, - "sourceOrderedObjectiveRoutes": 0, + "scriptedTemplates": 21, + "objectiveRoutes": 5, + "blockedObjectiveRoutes": 3, + "sourceOrderedObjectiveRoutes": 5, "derivedObjectiveRoutes": 0, "unreachableDerivedObjectiveRoutes": 0 }, diff --git a/scripts/runewaker-pipeline/RUNEWAKER_GAMEPLAY_CONTENT_REPORT.md b/scripts/runewaker-pipeline/RUNEWAKER_GAMEPLAY_CONTENT_REPORT.md index ee8110cc..b7d3cd4b 100644 --- a/scripts/runewaker-pipeline/RUNEWAKER_GAMEPLAY_CONTENT_REPORT.md +++ b/scripts/runewaker-pipeline/RUNEWAKER_GAMEPLAY_CONTENT_REPORT.md @@ -1,6 +1,6 @@ # RuneWaker gameplay content audit -Structural validation: **green** +Structural validation: **blocked** Original-mechanics fidelity: **review-required** This offline audit reads packaged recipes, snapshots, generated TypeScript, reports, @@ -8,15 +8,15 @@ mechanics evidence, manifests, and creature GLBs. It never connects to SQL Serve ## Coverage -- 30 dungeons, 8199 source rows, 4086 static spawns, and 436 combat entities. -- 61 boss objectives and 1068 executable attacks. -- Source-authored objective navigation: 0/0 ordered entrance/boss legs are mesh-reachable. +- 30 dungeons, 8199 source rows, 4114 static spawns, and 446 combat entities. +- 66 boss objectives and 1133 executable attacks. +- Source-authored objective navigation: 2/5 ordered entrance/boss legs are mesh-reachable. - HealerMan-derived objective diagnostics: 42/61 distance-ordered legs are mesh-reachable; unreachable derived legs are review evidence, not structural blockers. -- Animation entity bindings: 435 native, 1 static procedural, and 0 proxy. -- 293 templates preserve source spell evidence (659 assignments; 411 unique IDs); 357 preserve AutoPlot/Lua hook evidence. -- 279 modular boss mechanics are active: 275 source-backed and 4 explicitly flagged source-gap fallbacks. -- Boss signaling includes 111 spatial telegraphs and 28 visible health-triggered mechanics. -- The exhaustive encounter implementation ledger tracks 4113 deferred source rows and 1524 unique mechanics evidence records without inflating overlapping regex labels. +- Animation entity bindings: 445 native, 1 static procedural, and 0 proxy. +- 293 templates preserve source spell evidence (659 assignments; 411 unique IDs); 363 preserve AutoPlot/Lua hook evidence. +- 334 modular boss mechanics are active: 329 source-backed and 5 explicitly flagged source-gap fallbacks. +- Boss signaling includes 143 spatial telegraphs and 28 visible health-triggered mechanics. +- The exhaustive encounter implementation ledger tracks 4113 deferred source rows and 1530 unique mechanics evidence records without inflating overlapping regex labels. ## Authentic source data currently used @@ -28,9 +28,9 @@ mechanics evidence, manifests, and creature GLBs. It never connects to SQL Serve ## HealerMan-generic or deferred -- 436 entities use generated Basic Attack/Corrosive Slam. +- 446 entities use generated Basic Attack/Corrosive Slam. - 0 attacks are builder exceptions outside the modular boss catalog. -- 483 executable attacks carry a conservative source-backed direct-damage family mapping. +- 514 executable attacks carry a conservative source-backed direct-damage family mapping. - Source-backed mapping preserves spell identity, damage family, school, range, cooldown, source power fields, and the exact native formula shape/provenance. - Damage execution uses a labeled HealerMan baseline adapter because native ATK/MATK, @@ -53,7 +53,7 @@ Animation columns are native/procedural/proxy entity bindings. | demon-stronghold-125 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0/0/0 | 0/0 derived | review-required | | dungeon-of-dalanis | 229 | 67 | 17 | 0 | 22 | 9 | 13 | 17/0/0 | 0/0 derived | review-required | | forsaken-abbey | 251 | 150 | 15 | 8 | 26 | 0 | 11 | 15/0/0 | 8/8 derived | review-required | -| grafu-castle | 249 | 56 | 11 | 0 | 21 | 6 | 15 | 11/0/0 | 0/0 derived | review-required | +| grafu-castle | 249 | 84 | 21 | 5 | 86 | 6 | 21 | 21/0/0 | 2/5 source | review-required | | hall-of-survivors | 363 | 162 | 40 | 9 | 122 | 23 | 25 | 39/1/0 | 9/9 derived | review-required | | hoto | 401 | 307 | 26 | 5 | 75 | 19 | 15 | 26/0/0 | 2/5 derived | review-required | | ice-dwarf-kingdom | 651 | 599 | 32 | 5 | 80 | 25 | 11 | 32/0/0 | 3/5 derived | review-required | @@ -91,8 +91,11 @@ Animation columns are native/procedural/proxy entity bindings. ## Remaining blocker groups -Navigation: 0 blocked source-authored ordered legs across 0 dungeons. +Navigation: 3 blocked source-authored ordered legs across 1 dungeons. +- grafu-castle: entrance -> Cayus +- grafu-castle: Cayus -> Ardmond and Issac +- grafu-castle: Ardmond and Issac -> Boro-Boro Brothers Derived-order navigation review: 19 unreachable heuristic legs across 8 dungeons. @@ -116,7 +119,7 @@ Derived-order navigation review: 19 unreachable heuristic legs across 8 dungeons - ystra-labyrinth-boss1: Specter Guardian Creator -> Raichika the Destroyer (healerMan-distance-derived) - ystra-labyrinth-boss1: Raichika the Destroyer -> Rune Guardian Leader (healerMan-distance-derived) -Source-mechanics review: 357 templates preserve AutoPlot/Lua evidence; supported boss combat behavior is compiled into the modular catalog. +Source-mechanics review: 363 templates preserve AutoPlot/Lua evidence; supported boss combat behavior is compiled into the modular catalog. - death: 75 - door: 22 @@ -130,7 +133,9 @@ Source-mechanics review: 357 templates preserve AutoPlot/Lua evidence; supported ## Validation issues -- None. +- grafu-castle: packaged navigation cannot route entrance -> Cayus with the live 3m/6m endpoint limits. +- grafu-castle: packaged navigation cannot route Cayus -> Ardmond and Issac with the live 3m/6m endpoint limits. +- grafu-castle: packaged navigation cannot route Ardmond and Issac -> Boro-Boro Brothers with the live 3m/6m endpoint limits. ## Documented exceptions @@ -144,6 +149,6 @@ Source-mechanics review: 357 templates preserve AutoPlot/Lua evidence; supported - demon-stronghold-125 has an authoritative empty Zone 125 snapshot, so it packages an environment but no combat population. - zurhidon-stronghold-124 has an authoritative empty Zone 124 snapshot, so it packages an environment but no combat population. -- 4 boss mechanics are conservative, telegraphed fallbacks for preserved bosses whose scripts expose no executable combat spell. +- 5 boss mechanics are conservative, telegraphed fallbacks for preserved bosses whose scripts expose no executable combat spell. - Source formula structure is preserved, but live damage uses a labeled HealerMan baseline adapter because the required native combat-stat inputs are not packaged. - 61 objective legs use HealerMan's distance-from-entrance fallback because no source-authored boss order is packaged; 19 unreachable heuristic legs remain navigation/order review evidence. diff --git a/scripts/runewaker-pipeline/audit-gameplay-content.mjs b/scripts/runewaker-pipeline/audit-gameplay-content.mjs index a51273e4..575c9572 100644 --- a/scripts/runewaker-pipeline/audit-gameplay-content.mjs +++ b/scripts/runewaker-pipeline/audit-gameplay-content.mjs @@ -437,7 +437,8 @@ for (const recipeName of recipeNames) { sourceTemplateIds.add(row.templateId); const template = templateById.get(row.templateId); if (!template) issue(issues, dungeonId, "source template " + row.templateId + " is unmapped."); - else if (template.classification !== row.classification) { + else if (template.classification !== row.classification + && template.source?.classificationRule !== "reviewed Lua controller override") { issue(issues, dungeonId, "classification drift for template " + row.templateId + "."); } } @@ -651,8 +652,13 @@ for (const recipeName of recipeNames) { } if (!boss.name?.trim()) issue(issues, dungeonId, boss.id + " objective has no name."); } + const runtimePrefix = recipe.runtimeIdPrefix ?? "rw-" + dungeonId; + const nonObjectiveBossIds = new Set((recipe.scriptedSpawns ?? []) + .filter((placement) => placement.objective === false) + .map((placement) => runtimePrefix + "-scripted-" + placement.id)); for (const spawn of spawns) { - if (entities[spawn.entityId]?.kind === "boss" && !bossIds.has(spawn.id)) { + if (entities[spawn.entityId]?.kind === "boss" && !bossIds.has(spawn.id) + && !nonObjectiveBossIds.has(spawn.id)) { issue(issues, dungeonId, spawn.id + " boss has no objective."); } } @@ -702,8 +708,15 @@ for (const recipeName of recipeNames) { !route.orderAuthoritative && !route.reachable )).length; + const scriptedTemplateIds = new Set([ + ...(recipe.scriptedSpawns ?? []).map((placement) => placement.templateId), + ...(recipe.scriptedFormations ?? []).flatMap((formation) => ( + formation.members.map((member) => member.templateId) + )), + ]); const expectedEntities = recipe.templates.filter((template) => ( - ["combat", "boss"].includes(template.classification) && sourceTemplateIds.has(template.id) + ["combat", "boss"].includes(template.classification) + && (sourceTemplateIds.has(template.id) || scriptedTemplateIds.has(template.id)) )).length; if (expectedEntities !== Object.keys(entities).length) { issue(issues, dungeonId, "combat source templates and entity counts disagree."); diff --git a/scripts/runewaker-pipeline/build-population.mjs b/scripts/runewaker-pipeline/build-population.mjs index ce758e97..e4f46974 100644 --- a/scripts/runewaker-pipeline/build-population.mjs +++ b/scripts/runewaker-pipeline/build-population.mjs @@ -431,6 +431,35 @@ function yawFromDirection(direction) { return Number(((((direction % turn) + turn) % turn) * Math.PI * 2 / turn).toFixed(9)); } +function scriptedPlacements() { + const placements = [...(recipe.scriptedSpawns ?? [])]; + for (const formation of recipe.scriptedFormations ?? []) { + const radians = formation.direction * Math.PI / 180; + for (const [index, member] of formation.members.entries()) { + const rowDistance = (member.row - 1) * formation.spacingSourceUnits; + const columnDistance = member.column * formation.spacingSourceUnits; + placements.push({ + id: formation.id + "-" + (index + 1), + templateId: member.templateId, + sourceAnchorSpawnId: formation.sourceAnchorSpawnId, + controllerSpawnId: formation.controllerSpawnId ?? formation.sourceAnchorSpawnId, + sourceOffset: [ + -rowDistance * Math.cos(radians) - columnDistance * Math.sin(radians), + 0, + rowDistance * Math.sin(radians) - columnDistance * Math.cos(radians), + ], + direction: formation.direction, + sourceScriptRoot: formation.sourceScriptRoot, + evidence: formation.evidence, + }); + } + } + if (new Set(placements.map((placement) => placement.id)).size !== placements.length) { + throw new Error("Scripted placement ids must be unique after formation expansion."); + } + return placements; +} + function attacksFor(template, sourceSpells, spellCatalog, bossMechanics) { const ooze = template.archetype === "ooze"; const basicAttack = { @@ -564,7 +593,8 @@ function validateSnapshot(snapshot, templates) { const template = templates.get(row.templateId); if (!template) throw new Error("Unmapped RuneWaker template " + row.templateId + "."); if (template.classification === "unclassified") throw new Error("Unreviewed template " + row.templateId + "."); - if (row.classification !== template.classification) { + if (row.classification !== template.classification + && template.source?.classificationRule !== "reviewed Lua controller override") { throw new Error("Classification drift for template " + row.templateId + "."); } const actor = recipe.actors.find((entry) => entry.id === template.actor); @@ -636,6 +666,8 @@ async function generatePopulation() { const records = []; const rawBosses = []; const countsByTemplate = {}; + const rowsBySpawnId = new Map(snapshot.rows.map((row) => [row.spawnId, row])); + const scripted = scriptedPlacements(); try { for (const row of snapshot.rows) { const template = templates.get(row.templateId); @@ -648,7 +680,8 @@ async function generatePopulation() { sourceSpawnId: row.spawnId, sourceTemplateId: row.templateId, name: row.name, - classification: row.classification, + classification: template.classification, + sourceClassification: row.classification, sourcePosition: row.sourcePosition, sourceDirection: row.direction, localPosition: local.map((value) => Number(value.toFixed(6))), @@ -687,6 +720,72 @@ async function generatePopulation() { }); } } + for (const placementDefinition of scripted) { + const anchor = rowsBySpawnId.get(placementDefinition.sourceAnchorSpawnId); + if (!anchor) { + throw new Error( + "Scripted placement " + placementDefinition.id + " has no source anchor row " + + placementDefinition.sourceAnchorSpawnId + ".", + ); + } + if (placementDefinition.controllerSpawnId !== undefined + && !rowsBySpawnId.has(placementDefinition.controllerSpawnId)) { + throw new Error("Scripted placement " + placementDefinition.id + " has no controller row " + placementDefinition.controllerSpawnId + "."); + } + const template = templates.get(placementDefinition.templateId); + const offset = placementDefinition.sourceOffset ?? [0, 0, 0]; + const sourcePosition = anchor.sourcePosition.map((value, index) => value + offset[index]); + const direction = placementDefinition.direction ?? anchor.direction; + const local = sourceToLocal(sourcePosition, placement); + const projected = projector.project(local); + if (projected.distance > recipe.coordinateSystem.maximumProjectionDistance) { + throw new Error("Scripted placement " + placementDefinition.id + " is " + projected.distance + "m from navigation."); + } + const entityId = runtimePrefix + "-" + template.id; + if (!entities[entityId]) { + entities[entityId] = entityFor( + template, + actorForTemplate(template), + [...(sourceSpellsByTemplate.get(template.id)?.values() ?? [])], + spellCatalog, + bossMechanics, + ); + } + const spawn = { + id: runtimePrefix + "-scripted-" + placementDefinition.id, + entityId, + position: projected.position, + yaw: yawFromDirection(direction), + spawnMask: template.spawnMask ?? 1, + }; + staticSpawns.push(spawn); + records.push({ + sourceSpawnId: anchor.spawnId, + sourceTemplateId: template.id, + scriptedPlacementId: placementDefinition.id, + sourceControllerSpawnId: placementDefinition.controllerSpawnId ?? null, + sourceScriptRoot: placementDefinition.sourceScriptRoot ?? null, + evidence: placementDefinition.evidence, + name: template.name, + classification: template.classification, + sourcePosition, + sourceDirection: direction, + localPosition: local.map((value) => Number(value.toFixed(6))), + projectedPosition: projected.position, + projectionDistance: projected.distance, + modelPath: template.source?.modelPath ?? null, + }); + countsByTemplate[template.id] = (countsByTemplate[template.id] ?? 0) + 1; + if (template.classification === "boss" && placementDefinition.objective !== false) { + rawBosses.push({ + id: spawn.id, + sourceSpawnId: anchor.spawnId, + templateId: template.id, + name: template.name, + position: spawn.position, + }); + } + } } finally { projector.dispose(); } @@ -720,6 +819,7 @@ async function generatePopulation() { zoneId: recipe.zoneId, sourceRowCount: snapshot.rows.length, importedSpawnCount: staticSpawns.length, + scriptedSpawnCount: scripted.length, excludedRowCount: excluded.length, bossSpawnCount: bosses.length, objectiveOrder: objectiveOrder.provenance, diff --git a/scripts/runewaker-pipeline/encounter-implementation-report.test.mjs b/scripts/runewaker-pipeline/encounter-implementation-report.test.mjs index 57ec44bd..8d51d791 100644 --- a/scripts/runewaker-pipeline/encounter-implementation-report.test.mjs +++ b/scripts/runewaker-pipeline/encounter-implementation-report.test.mjs @@ -12,9 +12,9 @@ test("encounter ledger exhaustively classifies portable RuneWaker evidence", asy assert.equal(report.validationStatus, "green", report.issues.join("\n")); assert.equal(report.summary.dungeons, 30); assert.equal(report.summary.deferredSourceRows, 4_113); - assert.equal(report.summary.uniqueMechanicsEvidence, 1_524); + assert.equal(report.summary.uniqueMechanicsEvidence, 1_530); assert.equal(report.summary.luaEvidenceSignals, 329); - assert.equal(report.summary.templateEvidenceSignals, 1_195); + assert.equal(report.summary.templateEvidenceSignals, 1_201); const inventory = report.dungeons.flatMap((dungeon) => [ ...dungeon.deferredSourceRows, @@ -54,7 +54,13 @@ test("source gaps, fallbacks, zero-boss packages, and empty variants stay explic const report = await buildEncounterImplementationReport(); assert.deepEqual( report.specialCases.fallbackBosses.map((boss) => boss.entityId), - ["rw-fa-100070", "rw-fa-100561", "rw-fa-100562", "rw-fa-100563"], + [ + "rw-fa-100070", + "rw-fa-100561", + "rw-fa-100562", + "rw-fa-100563", + "rw-grafu-castle-105351", + ], ); assert.ok(report.specialCases.fallbackBosses.every((boss) => ( boss.status === "implemented" && boss.fidelity === "healer-man-authored" @@ -68,7 +74,6 @@ test("source gaps, fallbacks, zero-boss packages, and empty variants stay explic [ "aeternal-circle", "dungeon-of-dalanis", - "grafu-castle", "kal-turok-hive", "lair-demon-dragon", "tomb-seven-heroes", diff --git a/scripts/runewaker-pipeline/extract-mechanics.mjs b/scripts/runewaker-pipeline/extract-mechanics.mjs index 7d5f799e..e6a27c39 100644 --- a/scripts/runewaker-pipeline/extract-mechanics.mjs +++ b/scripts/runewaker-pipeline/extract-mechanics.mjs @@ -44,6 +44,8 @@ const config = await readJson(configFile); const dungeon = (config.dungeons ?? []).find((entry) => entry.id === dungeonConfigId); if (!dungeon) throw new Error("Missing dungeon_config entry " + dungeonConfigId + "."); const snapshot = await readJson(snapshotFile); +const templatesById = new Map(recipe.templates.map((template) => [template.id, template])); +const rowsBySpawnId = new Map((snapshot.rows ?? []).map((row) => [row.spawnId, row])); const SIGNALS = [ { kind: "summon", pattern: /summon|spawn.*npc|create.*npc|create.*monster|add.*npc/i }, @@ -115,10 +117,11 @@ for (const fileName of dungeon.lua_files ?? []) { const templateMetadata = new Map(); for (const row of snapshot.rows ?? []) { + const template = templatesById.get(row.templateId); const metadata = templateMetadata.get(row.templateId) ?? { templateId: row.templateId, - name: row.name, - classification: row.classification, + name: template?.name ?? row.name, + classification: template?.classification ?? row.classification, spawnCount: 0, autoPlots: new Set(), plotClassNames: new Set(), @@ -139,6 +142,19 @@ for (const row of snapshot.rows ?? []) { } templateMetadata.set(row.templateId, metadata); } +for (const template of recipe.templates) { + if (templateMetadata.has(template.id)) continue; + templateMetadata.set(template.id, { + templateId: template.id, + name: template.name, + classification: template.classification, + spawnCount: 0, + autoPlots: new Set(template.source?.scriptRoots ?? []), + plotClassNames: new Set(), + templateScripts: new Map(), + spells: new Map(), + }); +} const templateCandidates = [...templateMetadata.values()] .map((entry) => ({ @@ -155,7 +171,7 @@ const templateCandidates = [...templateMetadata.values()] .sort((left, right) => left.templateId - right.templateId); const bossRows = (snapshot.rows ?? []) - .filter((row) => row.classification === "boss") + .filter((row) => templatesById.get(row.templateId)?.classification === "boss") .map((row) => ({ spawnId: row.spawnId, templateId: row.templateId, @@ -164,6 +180,26 @@ const bossRows = (snapshot.rows ?? []) autoPlot: clean(row.source?.autoPlot) || null, plotClassName: clean(row.source?.plotClassName) || null, })); +const scriptedBossSpawns = (recipe.scriptedSpawns ?? []) + .filter((spawn) => templatesById.get(spawn.templateId)?.classification === "boss") + .map((spawn) => { + const template = templatesById.get(spawn.templateId); + const anchor = rowsBySpawnId.get(spawn.sourceAnchorSpawnId); + const offset = spawn.sourceOffset ?? [0, 0, 0]; + return { + scriptedPlacementId: spawn.id, + sourceAnchorSpawnId: spawn.sourceAnchorSpawnId, + controllerSpawnId: spawn.controllerSpawnId ?? null, + templateId: spawn.templateId, + name: template.name, + sourcePosition: anchor + ? anchor.sourcePosition.map((value, index) => value + offset[index]) + : null, + sourceScriptRoot: spawn.sourceScriptRoot ?? template.source?.scriptRoots?.[0] ?? null, + objective: spawn.objective !== false, + evidence: spawn.evidence, + }; + }); const categories = Object.fromEntries(SIGNALS.map(({ kind }) => [ kind, luaSignals.filter((signal) => signal.kinds.includes(kind)).length @@ -186,8 +222,12 @@ const report = { dungeonConfigId, }, configuredBossTemplateIds: dungeon.boss_guids ?? [], + reviewedBossTemplateIds: recipe.templates + .filter((template) => template.classification === "boss") + .map((template) => template.id), configuredBossPositions: dungeon.boss_positions ?? {}, bossRows, + scriptedBossSpawns, luaSources, luaSignals, templateCandidates, @@ -225,6 +265,7 @@ const generated = [ status: report.status, categories: report.categories, bossRows: report.bossRows, + scriptedBossSpawns: report.scriptedBossSpawns, templateCandidates: report.templateCandidates, reviewQueue: report.reviewQueue, sourceReport: projectPath(reportFile), diff --git a/scripts/runewaker-pipeline/import-boss-mechanics.mjs b/scripts/runewaker-pipeline/import-boss-mechanics.mjs index f941f06e..486a6457 100644 --- a/scripts/runewaker-pipeline/import-boss-mechanics.mjs +++ b/scripts/runewaker-pipeline/import-boss-mechanics.mjs @@ -2,6 +2,7 @@ import { readdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { + optionValue, pipelineDirectory, projectPath, projectRoot, @@ -16,9 +17,12 @@ const spellCatalogFile = path.join( "snapshots", "runewaker-npc-spell-catalog.json", ); -const recipeNames = (await readdir(recipesDirectory)) - .filter((name) => name.endsWith("-population.json")) - .sort(); +const requestedRecipe = optionValue(process.argv.slice(2), "--recipe"); +const recipeNames = requestedRecipe + ? [path.basename(requestedRecipe)] + : (await readdir(recipesDirectory)) + .filter((name) => name.endsWith("-population.json")) + .sort(); function slugify(value) { return String(value ?? "") @@ -407,6 +411,7 @@ const spellCatalog = await readJson(spellCatalogFile); const spellsById = new Map(spellCatalog.spells.map((spell) => [Number(spell.spellId), spell])); const catalog = await readJson(mechanicCatalogFile); const runewakerBosses = {}; +const replacedRuntimePrefixes = new Set(); let sourceBacked = 0; let sourceGap = 0; const firstRecipe = await readJson(path.join(recipesDirectory, recipeNames[0])); @@ -423,6 +428,7 @@ const sharedFunctions = functionIndex(sharedLuaSources); for (const recipeName of recipeNames) { const recipe = await readJson(path.join(recipesDirectory, recipeName)); + replacedRuntimePrefixes.add(recipe.runtimeIdPrefix ?? "rw-" + recipe.dungeonId); const snapshot = await readJson(path.resolve(projectRoot, recipe.files.snapshot)); const rowsByTemplate = new Map(); for (const row of snapshot.rows ?? []) { @@ -434,12 +440,15 @@ for (const recipeName of recipeNames) { for (const template of recipe.templates.filter((entry) => entry.classification === "boss")) { const rows = rowsByTemplate.get(Number(template.id)) ?? []; - if (!rows.length) continue; - const roots = [...new Set(rows.flatMap((row) => [ - rootName(row.source?.autoPlot), - rootName(row.source?.plotClassName), - ...Object.values(row.source?.templateScripts ?? {}).map(rootName), - ]).filter(Boolean))]; + const roots = [...new Set([ + ...(template.source?.scriptRoots ?? []).map(rootName), + ...rows.flatMap((row) => [ + rootName(row.source?.autoPlot), + rootName(row.source?.plotClassName), + ...Object.values(row.source?.templateScripts ?? {}).map(rootName), + ]), + ].filter(Boolean))]; + if (!rows.length && !roots.length) continue; const reachable = reachableFunctions(sharedFunctions, roots); const bodies = reachable.map((entry) => entry.body); const scriptSpellIds = [...new Set(bodies.flatMap((body) => ( @@ -500,7 +509,11 @@ for (const recipeName of recipeNames) { } catalog.bosses = Object.fromEntries([ - ...Object.entries(catalog.bosses).filter(([key]) => !key.startsWith("rw-")), + ...Object.entries(catalog.bosses).filter(([key]) => ( + requestedRecipe + ? ![...replacedRuntimePrefixes].some((prefix) => key.startsWith(prefix + "-")) + : !key.startsWith("rw-") + )), ...Object.entries(runewakerBosses).sort(([left], [right]) => left.localeCompare(right)), ]); await writeFile(mechanicCatalogFile, JSON.stringify(catalog, null, 2) + "\n", "utf8"); diff --git a/scripts/runewaker-pipeline/lib/encounter-implementation-report.mjs b/scripts/runewaker-pipeline/lib/encounter-implementation-report.mjs index 839d828a..da7c1cb9 100644 --- a/scripts/runewaker-pipeline/lib/encounter-implementation-report.mjs +++ b/scripts/runewaker-pipeline/lib/encounter-implementation-report.mjs @@ -34,11 +34,11 @@ const EXPECTED_FALLBACK_BOSSES = Object.freeze([ "rw-fa-100561", "rw-fa-100562", "rw-fa-100563", + "rw-grafu-castle-105351", ]); const EXPECTED_NON_EMPTY_ZERO_BOSS_PACKAGES = Object.freeze([ "aeternal-circle", "dungeon-of-dalanis", - "grafu-castle", "kal-turok-hive", "lair-demon-dragon", "tomb-seven-heroes", diff --git a/scripts/runewaker-pipeline/lib/recipe.mjs b/scripts/runewaker-pipeline/lib/recipe.mjs index 8bac885b..5f0e143b 100644 --- a/scripts/runewaker-pipeline/lib/recipe.mjs +++ b/scripts/runewaker-pipeline/lib/recipe.mjs @@ -148,6 +148,7 @@ export function validatePopulationRecipe(recipe, file = "population recipe") { actorIds.add(actor.id); } const templateIds = new Set(); + const templatesById = new Map(); const classifications = new Set(["combat", "boss", "deferred-object", "unclassified"]); for (const template of recipe.templates) { if (!Number.isInteger(template.id)) throw new Error("Template ids must be integers."); @@ -161,6 +162,74 @@ export function validatePopulationRecipe(recipe, file = "population recipe") { && template.proceduralFallback !== true) { throw new Error("Combat template " + template.id + " has no mapped actor or explicit procedural fallback."); } + if (template.source?.scriptRoots !== undefined + && (!Array.isArray(template.source.scriptRoots) + || template.source.scriptRoots.some((root) => typeof root !== "string" || !root.trim()))) { + throw new Error("Template " + template.id + " source.scriptRoots must contain non-empty strings."); + } + templatesById.set(template.id, template); + } + const scriptedIds = new Set(); + const validateScriptedBase = (entry, label) => { + assertObject(entry, label); + assertString(entry.id, label + ".id"); + if (scriptedIds.has(entry.id)) throw new Error("Duplicate scripted placement id " + entry.id + "."); + scriptedIds.add(entry.id); + if (!Number.isInteger(entry.sourceAnchorSpawnId) || entry.sourceAnchorSpawnId <= 0) { + throw new Error(label + ".sourceAnchorSpawnId must be a positive integer."); + } + if (entry.controllerSpawnId !== undefined + && (!Number.isInteger(entry.controllerSpawnId) || entry.controllerSpawnId <= 0)) { + throw new Error(label + ".controllerSpawnId must be a positive integer."); + } + if (!Array.isArray(entry.evidence) || !entry.evidence.length + || entry.evidence.some((value) => typeof value !== "string" || !value.trim())) { + throw new Error(label + ".evidence must contain non-empty source references."); + } + }; + for (const [index, spawn] of (recipe.scriptedSpawns ?? []).entries()) { + const label = file + ".scriptedSpawns[" + index + "]"; + validateScriptedBase(spawn, label); + const template = templatesById.get(spawn.templateId); + if (!template || !["combat", "boss"].includes(template.classification)) { + throw new Error(label + ".templateId must reference a combat or boss template."); + } + if (spawn.sourceOffset !== undefined + && (!Array.isArray(spawn.sourceOffset) || spawn.sourceOffset.length !== 3 + || spawn.sourceOffset.some((value) => !Number.isFinite(value)))) { + throw new Error(label + ".sourceOffset must contain three finite source-unit values."); + } + if (spawn.direction !== undefined && !Number.isFinite(spawn.direction)) { + throw new Error(label + ".direction must be finite."); + } + if (spawn.objective !== undefined && typeof spawn.objective !== "boolean") { + throw new Error(label + ".objective must be boolean when provided."); + } + } + for (const [index, formation] of (recipe.scriptedFormations ?? []).entries()) { + const label = file + ".scriptedFormations[" + index + "]"; + validateScriptedBase(formation, label); + assertString(formation.sourceScriptRoot, label + ".sourceScriptRoot"); + if (!(Number(formation.spacingSourceUnits) > 0)) { + throw new Error(label + ".spacingSourceUnits must be positive."); + } + if (!Number.isFinite(formation.direction)) { + throw new Error(label + ".direction must be finite."); + } + if (!Array.isArray(formation.members) || !formation.members.length) { + throw new Error(label + ".members must be a non-empty array."); + } + for (const [memberIndex, member] of formation.members.entries()) { + const memberLabel = label + ".members[" + memberIndex + "]"; + assertObject(member, memberLabel); + const template = templatesById.get(member.templateId); + if (!template || !["combat", "boss"].includes(template.classification)) { + throw new Error(memberLabel + ".templateId must reference a combat or boss template."); + } + if (!Number.isInteger(member.row) || member.row < 1 || !Number.isInteger(member.column)) { + throw new Error(memberLabel + " requires a positive integer row and integer column."); + } + } } return recipe; } diff --git a/scripts/runewaker-pipeline/recipes/grafu-castle-population.json b/scripts/runewaker-pipeline/recipes/grafu-castle-population.json index 4f016a8f..452f7e9c 100644 --- a/scripts/runewaker-pipeline/recipes/grafu-castle-population.json +++ b/scripts/runewaker-pipeline/recipes/grafu-castle-population.json @@ -4,13 +4,273 @@ "sourceDungeonConfigId": "grafu_castle", "zoneId": 137, "expectedActiveRows": 249, - "expectedBossSpawns": 0, + "expectedBossSpawns": 5, "runtimeIdPrefix": "rw-grafu-castle", "coordinateSystem": { "metersPerSourceUnit": 0.1, "directionUnitsPerTurn": 360, "maximumProjectionDistance": 4 }, + "bossObjectiveOrder": { + "kind": "source-authored", + "spawnIds": [ + 41927224, + 41926939, + 41926924, + 41926979, + 41927071 + ], + "evidence": [ + "Resource/luascript/02345.lua:Dy_Z19_BOSSN_AIC", + "Resource/luascript/02324.lua:kg_105542_01", + "Resource/luascript/02231.lua:ic_105224_01", + "Resource/luascript/02330.lua:cl_136_105205_N", + "Resource/luascript/02297.lua:un_zone19_boss5_allconreal_1_01" + ] + }, + "scriptedSpawns": [ + { + "id": "cayus", + "templateId": 105616, + "sourceAnchorSpawnId": 41927224, + "controllerSpawnId": 41928351, + "sourceScriptRoot": "Dy_Z19_BOSSN_AIC", + "evidence": [ + "Resource/luascript/02345.lua:CreateObjByFlag(105616,780696,1,1)" + ] + }, + { + "id": "ardmond", + "templateId": 105542, + "sourceAnchorSpawnId": 41926939, + "controllerSpawnId": 41928345, + "sourceOffset": [ + 0, + 0, + -20 + ], + "sourceScriptRoot": "kg_105542_01", + "evidence": [ + "Resource/luascript/02324.lua:kg_105542_CreateBoss" + ] + }, + { + "id": "issac", + "templateId": 105543, + "sourceAnchorSpawnId": 41926939, + "controllerSpawnId": 41928345, + "sourceOffset": [ + 0, + 0, + 20 + ], + "objective": false, + "sourceScriptRoot": "kg_105542_01", + "evidence": [ + "Resource/luascript/02324.lua:kg_105542_CreateBoss" + ] + }, + { + "id": "boro-boro-brothers", + "templateId": 105224, + "sourceAnchorSpawnId": 41926924, + "controllerSpawnId": 41927347, + "sourceScriptRoot": "ic_105224_01", + "evidence": [ + "Resource/luascript/02231.lua:CreateObjByFlag(105224,780775,1,1)" + ] + }, + { + "id": "conjurer-chapeaunoir", + "templateId": 105204, + "sourceAnchorSpawnId": 41926979, + "controllerSpawnId": 41927358, + "sourceScriptRoot": "cl_136_105205_N", + "evidence": [ + "Resource/luascript/02330.lua:CreateObjByFlag(105204,780767,0,1)" + ] + }, + { + "id": "larvanger-barkud", + "templateId": 105351, + "sourceAnchorSpawnId": 41927071, + "controllerSpawnId": 41927369, + "sourceScriptRoot": "un_zone19_boss5_allconreal_1_01", + "evidence": [ + "Resource/luascript/02297.lua:CreateObjByFlag(105351,780838,1,1)" + ] + } + ], + "scriptedFormations": [ + { + "id": "castle-patrol-41927381", + "sourceAnchorSpawnId": 41927381, + "controllerSpawnId": 41927381, + "sourceScriptRoot": "cl_136_01_01_N", + "direction": 179.87330627441406, + "spacingSourceUnits": 20, + "members": [ + { "templateId": 105598, "row": 1, "column": 0 }, + { "templateId": 105229, "row": 2, "column": 1 }, + { "templateId": 105229, "row": 2, "column": -1 }, + { "templateId": 105230, "row": 3, "column": 0 } + ], + "evidence": [ + "Resource/luascript/02334.lua:cl_136_01_01_N" + ] + }, + { + "id": "castle-patrol-41927382", + "sourceAnchorSpawnId": 41927382, + "controllerSpawnId": 41927382, + "sourceScriptRoot": "cl_136_01_01_N", + "direction": 269.88189697265625, + "spacingSourceUnits": 20, + "members": [ + { "templateId": 105598, "row": 1, "column": 0 }, + { "templateId": 105229, "row": 2, "column": 1 }, + { "templateId": 105229, "row": 2, "column": -1 }, + { "templateId": 105230, "row": 3, "column": 0 } + ], + "evidence": [ + "Resource/luascript/02334.lua:cl_136_01_01_N" + ] + }, + { + "id": "castle-patrol-41927384", + "sourceAnchorSpawnId": 41927384, + "controllerSpawnId": 41927384, + "sourceScriptRoot": "cl_136_03_01_N", + "direction": 89.27565002441406, + "spacingSourceUnits": 20, + "members": [ + { "templateId": 105596, "row": 1, "column": 0 }, + { "templateId": 105597, "row": 2, "column": 1 }, + { "templateId": 105597, "row": 2, "column": -1 }, + { "templateId": 105601, "row": 3, "column": 0 } + ], + "evidence": [ + "Resource/luascript/02334.lua:cl_136_03_01_N" + ] + }, + { + "id": "castle-patrol-41927385", + "sourceAnchorSpawnId": 41927385, + "controllerSpawnId": 41927385, + "sourceScriptRoot": "cl_136_03_02_N", + "direction": 180.14939880371094, + "spacingSourceUnits": 20, + "members": [ + { "templateId": 105599, "row": 1, "column": 0 }, + { "templateId": 105598, "row": 2, "column": 1 }, + { "templateId": 105598, "row": 2, "column": -1 } + ], + "evidence": [ + "Resource/luascript/02334.lua:cl_136_03_02_N" + ] + }, + { + "id": "castle-patrol-41927386", + "sourceAnchorSpawnId": 41927386, + "controllerSpawnId": 41927386, + "sourceScriptRoot": "cl_136_03_04_N", + "direction": 0.1327040046453476, + "spacingSourceUnits": 20, + "members": [ + { "templateId": 105599, "row": 1, "column": 0 }, + { "templateId": 105598, "row": 2, "column": 1 }, + { "templateId": 105598, "row": 2, "column": -1 }, + { "templateId": 105601, "row": 3, "column": 0 } + ], + "evidence": [ + "Resource/luascript/02334.lua:cl_136_03_04_N" + ] + }, + { + "id": "castle-patrol-41927387", + "sourceAnchorSpawnId": 41927387, + "controllerSpawnId": 41927387, + "sourceScriptRoot": "cl_136_03_042_N", + "direction": 86.2711410522461, + "spacingSourceUnits": 20, + "members": [ + { "templateId": 105599, "row": 1, "column": 0 }, + { "templateId": 105601, "row": 2, "column": 1 }, + { "templateId": 105601, "row": 2, "column": -1 } + ], + "evidence": [ + "Resource/luascript/02334.lua:cl_136_03_042_N" + ] + }, + { + "id": "castle-patrol-41927391", + "sourceAnchorSpawnId": 41927391, + "controllerSpawnId": 41927391, + "sourceScriptRoot": "cl_136_04_052", + "direction": 89, + "spacingSourceUnits": 20, + "members": [ + { "templateId": 105237, "row": 1, "column": 0 }, + { "templateId": 105236, "row": 2, "column": 1 }, + { "templateId": 105236, "row": 2, "column": -1 }, + { "templateId": 105237, "row": 3, "column": 0 }, + { "templateId": 105236, "row": 3, "column": -1 }, + { "templateId": 105236, "row": 3, "column": 1 } + ], + "evidence": [ + "Resource/luascript/02214.lua:cl_136_04_052" + ] + }, + { + "id": "castle-patrol-41927392", + "sourceAnchorSpawnId": 41927392, + "controllerSpawnId": 41927392, + "sourceScriptRoot": "cl_136_01_01_N", + "direction": 269.99481201171875, + "spacingSourceUnits": 20, + "members": [ + { "templateId": 105598, "row": 1, "column": 0 }, + { "templateId": 105229, "row": 2, "column": 1 }, + { "templateId": 105229, "row": 2, "column": -1 }, + { "templateId": 105230, "row": 3, "column": 0 } + ], + "evidence": [ + "Resource/luascript/02334.lua:cl_136_01_01_N" + ] + }, + { + "id": "castle-patrol-41927395", + "sourceAnchorSpawnId": 41927395, + "controllerSpawnId": 41927395, + "sourceScriptRoot": "cl_136_03_042", + "direction": 181, + "spacingSourceUnits": 20, + "members": [ + { "templateId": 105233, "row": 1, "column": 0 }, + { "templateId": 105235, "row": 2, "column": 1 }, + { "templateId": 105235, "row": 2, "column": -1 } + ], + "evidence": [ + "Resource/luascript/02214.lua:cl_136_03_042" + ] + }, + { + "id": "castle-patrol-41927396", + "sourceAnchorSpawnId": 41927396, + "controllerSpawnId": 41927396, + "sourceScriptRoot": "cl_136_03_042", + "direction": 4.999985218048096, + "spacingSourceUnits": 20, + "members": [ + { "templateId": 105233, "row": 1, "column": 0 }, + { "templateId": 105235, "row": 2, "column": 1 }, + { "templateId": 105235, "row": 2, "column": -1 } + ], + "evidence": [ + "Resource/luascript/02214.lua:cl_136_03_042" + ] + } + ], "files": { "environmentRecipe": "scripts/runewaker-pipeline/recipes/grafu-castle.json", "snapshot": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -43,6 +303,21 @@ "serverDefault": ".\\HEALERMAN_RW" }, "actors": [ + { + "id": "atmo", + "sourceModel": "model/character/creature/atmo/act_atmo.ros", + "runtimeRotationY": 0 + }, + { + "id": "barbo-brother", + "sourceModel": "model/character/creature/barbo_brother/act_barbo_brother.ros", + "runtimeRotationY": 0 + }, + { + "id": "black-hat", + "sourceModel": "model/character/creature/black_hat/act_black_hat.ros", + "runtimeRotationY": 0 + }, { "id": "black-hat-blue", "sourceModel": "model/character/creature/black_hat/act_black_hat_blue.ros", @@ -63,6 +338,11 @@ "sourceModel": "model/character/creature/lost_protector/act_controlled_steel_guardian.ros", "runtimeRotationY": 0 }, + { + "id": "cursed-steel-guardian", + "sourceModel": "model/character/creature/lost_protector/act_cursed_steel_guardian.ros", + "runtimeRotationY": 0 + }, { "id": "elder-myconid", "sourceModel": "model/character/creature/elder_myconid/act_elder_myconid.ros", @@ -83,6 +363,11 @@ "sourceModel": "model/character/pc/human/male/act_hm_male.ros", "runtimeRotationY": 0 }, + { + "id": "karth", + "sourceModel": "model/character/creature/karth/act_karth.ros", + "runtimeRotationY": 0 + }, { "id": "karth-01", "sourceModel": "model/character/creature/karth/act_karth_01.ros", @@ -92,6 +377,26 @@ "id": "milk-scarab-blue", "sourceModel": "model/character/creature/milk_scarab/act_milk_scarab_blue.ros", "runtimeRotationY": 0 + }, + { + "id": "mount-millenium-chimera-laby", + "sourceModel": "model/character/creature/mount_millenium_chimera/act_mount_millenium_chimera_laby.ros", + "runtimeRotationY": 0 + }, + { + "id": "enraged-big-bear-purple", + "sourceModel": "model/character/creature/enraged_big_bear/act_enraged_big_bear_purple.ros", + "runtimeRotationY": 0 + }, + { + "id": "enraged-big-bear-cream", + "sourceModel": "model/character/creature/enraged_big_bear/act_enraged_big_bear_cream.ros", + "runtimeRotationY": 0 + }, + { + "id": "rafael-bakderr", + "sourceModel": "model/character/creature/rafael_bakderr/act_rafael_bakderr.ros", + "runtimeRotationY": 0 } ], "templates": [ @@ -202,6 +507,25 @@ "classificationRule": "noncombat/dynamic-display source object" } }, + { + "primaryColor": "hsl(348 22% 38%)", + "accentColor": "hsl(16 38% 64%)", + "id": 105204, + "name": "Conjurer Chapeaunoir", + "title": "Conjurer Chapeaunoir", + "level": 68, + "classification": "boss", + "archetype": "humanoid", + "actor": "black-hat", + "source": { + "imageId": 584594, + "modelPath": "model/character/creature/black_hat/act_black_hat.ros", + "classificationRule": "NPCObjectDB.sex=3 plus reviewed Lua dynamic creation", + "scriptRoots": [ + "cl_136_105205_N" + ] + } + }, { "primaryColor": "hsl(35 22% 38%)", "accentColor": "hsl(63 38% 64%)", @@ -223,13 +547,13 @@ "id": 105210, "name": "Template 105210", "level": 10, - "classification": "combat", + "classification": "deferred-object", "archetype": "humanoid", - "actor": "cereus-01", + "reason": "Source boss controller for Ardmond and Issac; the controller is not a combat mob.", "source": { "imageId": 583949, "modelPath": "model/character/creature/cereus/act_cereus_01.ros", - "classificationRule": "positive native level plus static model/character ROS" + "classificationRule": "reviewed Lua controller override" } }, { @@ -337,19 +661,128 @@ "classificationRule": "noncombat/dynamic-display source object" } }, + { + "primaryColor": "hsl(208 22% 38%)", + "accentColor": "hsl(236 38% 64%)", + "id": 105224, + "name": "Boro-Boro Brothers", + "title": "Boro-Boro Brothers", + "level": 68, + "classification": "boss", + "archetype": "humanoid", + "actor": "barbo-brother", + "source": { + "imageId": 584595, + "modelPath": "model/character/creature/barbo_brother/act_barbo_brother.ros", + "classificationRule": "NPCObjectDB.sex=3 plus reviewed Lua dynamic creation", + "scriptRoots": [ + "ic_105224_01" + ] + } + }, + { + "primaryColor": "hsl(83 22% 38%)", + "accentColor": "hsl(111 38% 64%)", + "id": 105229, + "name": "Smirking Castle Clown", + "level": 63, + "classification": "combat", + "archetype": "humanoid", + "actor": "grinning-clown", + "source": { + "imageId": 584630, + "modelPath": "model/character/creature/grinning_clown/act_grinning_clown.ros", + "classificationRule": "reviewed Lua matrix member" + } + }, + { + "primaryColor": "hsl(130 22% 38%)", + "accentColor": "hsl(158 38% 64%)", + "id": 105230, + "name": "Odd Castle Clown", + "level": 63, + "classification": "combat", + "archetype": "humanoid", + "actor": "ghastly-clown-red", + "source": { + "imageId": 584631, + "modelPath": "model/character/creature/ghastly_clown/act_ghastly_clown_red.ros", + "classificationRule": "reviewed Lua matrix member" + } + }, + { + "primaryColor": "hsl(271 22% 38%)", + "accentColor": "hsl(299 38% 64%)", + "id": 105233, + "name": "Cursed Steel Soldier", + "level": 63, + "classification": "combat", + "archetype": "humanoid", + "actor": "cursed-steel-guardian", + "source": { + "imageId": 584634, + "modelPath": "model/character/creature/lost_protector/act_cursed_steel_guardian.ros", + "classificationRule": "reviewed Lua matrix member" + } + }, + { + "primaryColor": "hsl(5 22% 38%)", + "accentColor": "hsl(33 38% 64%)", + "id": 105235, + "name": "Conjurer's Apprentice", + "level": 63, + "classification": "combat", + "archetype": "humanoid", + "actor": "black-hat-blue", + "source": { + "imageId": 584636, + "modelPath": "model/character/creature/black_hat/act_black_hat_blue.ros", + "classificationRule": "reviewed Lua matrix member" + } + }, + { + "primaryColor": "hsl(52 22% 38%)", + "accentColor": "hsl(80 38% 64%)", + "id": 105236, + "name": "Giant Enraged Bear", + "level": 63, + "classification": "combat", + "archetype": "humanoid", + "actor": "enraged-big-bear-purple", + "source": { + "imageId": 584637, + "modelPath": "model/character/creature/enraged_big_bear/act_enraged_big_bear_purple.ros", + "classificationRule": "reviewed Lua matrix member" + } + }, + { + "primaryColor": "hsl(99 22% 38%)", + "accentColor": "hsl(127 38% 64%)", + "id": 105237, + "name": "Manipulated Bear", + "level": 63, + "classification": "combat", + "archetype": "humanoid", + "actor": "enraged-big-bear-cream", + "source": { + "imageId": 584664, + "modelPath": "model/character/creature/enraged_big_bear/act_enraged_big_bear_cream.ros", + "classificationRule": "reviewed Lua matrix member" + } + }, { "primaryColor": "hsl(178 22% 38%)", "accentColor": "hsl(206 38% 64%)", "id": 105254, "name": "Template 105254", "level": 1, - "classification": "combat", + "classification": "deferred-object", "archetype": "plant", - "actor": "elder-myconid", + "reason": "Source Lua patrol controller; it creates the real combat formations and must not be rendered as a mob.", "source": { "imageId": 576437, "modelPath": "model/character/creature/elder_myconid/act_elder_myconid.ros", - "classificationRule": "positive native level plus static model/character ROS" + "classificationRule": "reviewed Lua controller override" } }, { @@ -358,13 +791,13 @@ "id": 105258, "name": "Template 105258", "level": 1, - "classification": "combat", + "classification": "deferred-object", "archetype": "plant", - "actor": "elder-myconid", + "reason": "Source proximity and ambient-rabbit trigger; it is not a combat mob.", "source": { "imageId": 576437, "modelPath": "model/character/creature/elder_myconid/act_elder_myconid.ros", - "classificationRule": "positive native level plus static model/character ROS" + "classificationRule": "reviewed Lua controller override" } }, { @@ -399,6 +832,64 @@ "classificationRule": "noncombat/dynamic-display source object" } }, + { + "primaryColor": "hsl(57 22% 38%)", + "accentColor": "hsl(85 38% 64%)", + "id": 105351, + "name": "Larvanger Barkud", + "title": "Larvanger Barkud", + "level": 68, + "classification": "boss", + "archetype": "humanoid", + "actor": "rafael-bakderr", + "source": { + "imageId": 584614, + "modelPath": "model/character/creature/rafael_bakderr/act_rafael_bakderr.ros", + "classificationRule": "NPCObjectDB.sex=3 plus reviewed Lua dynamic creation", + "scriptRoots": [ + "un_zone19_boss5_allconreal_1_01" + ] + } + }, + { + "primaryColor": "hsl(34 22% 38%)", + "accentColor": "hsl(62 38% 64%)", + "id": 105542, + "name": "Ardmond", + "title": "Ardmond", + "objectiveNamePattern": "Ardmond and Issac", + "level": 68, + "classification": "boss", + "archetype": "humanoid", + "actor": "atmo", + "source": { + "imageId": 584588, + "modelPath": "model/character/creature/atmo/act_atmo.ros", + "classificationRule": "NPCObjectDB.sex=3 plus reviewed Lua dynamic creation", + "scriptRoots": [ + "kg_105542_01" + ] + } + }, + { + "primaryColor": "hsl(81 22% 38%)", + "accentColor": "hsl(109 38% 64%)", + "id": 105543, + "name": "Issac", + "title": "Issac", + "level": 68, + "classification": "boss", + "archetype": "humanoid", + "actor": "mount-millenium-chimera-laby", + "source": { + "imageId": 584587, + "modelPath": "model/character/creature/mount_millenium_chimera/act_mount_millenium_chimera_laby.ros", + "classificationRule": "NPCObjectDB.sex=3 plus reviewed Lua dynamic creation", + "scriptRoots": [ + "kg_105542_01" + ] + } + }, { "primaryColor": "hsl(52 22% 38%)", "accentColor": "hsl(80 38% 64%)", @@ -444,6 +935,21 @@ "classificationRule": "positive native level plus static model/character ROS" } }, + { + "primaryColor": "hsl(193 22% 38%)", + "accentColor": "hsl(221 38% 64%)", + "id": 105599, + "name": "Cursed Steel Soldier", + "level": 63, + "classification": "combat", + "archetype": "humanoid", + "actor": "cursed-steel-guardian", + "source": { + "imageId": 584634, + "modelPath": "model/character/creature/lost_protector/act_cursed_steel_guardian.ros", + "classificationRule": "reviewed Lua matrix member" + } + }, { "primaryColor": "hsl(240 22% 38%)", "accentColor": "hsl(268 38% 64%)", @@ -519,6 +1025,25 @@ "classificationRule": "noncombat/dynamic-display source object" } }, + { + "primaryColor": "hsl(272 22% 38%)", + "accentColor": "hsl(300 38% 64%)", + "id": 105616, + "name": "Cayus", + "title": "Cayus", + "level": 68, + "classification": "boss", + "archetype": "humanoid", + "actor": "karth", + "source": { + "imageId": 585676, + "modelPath": "model/character/creature/karth/act_karth.ros", + "classificationRule": "NPCObjectDB.sex=3 plus reviewed Lua dynamic creation", + "scriptRoots": [ + "Dy_Z19_BOSSN_AIC" + ] + } + }, { "primaryColor": "hsl(149 22% 38%)", "accentColor": "hsl(177 38% 64%)", diff --git a/scripts/runewaker-pipeline/validate-instance.mjs b/scripts/runewaker-pipeline/validate-instance.mjs index 8ab196f4..73828c52 100644 --- a/scripts/runewaker-pipeline/validate-instance.mjs +++ b/scripts/runewaker-pipeline/validate-instance.mjs @@ -79,8 +79,24 @@ if (errors.length === 0) { check("unique source spawn ids", new Set(snapshot.rows.map((row) => row.spawnId)).size === snapshot.rows.length, "no duplicates"); check("environment id", environment.dungeonId === recipe.dungeonId, String(environment.dungeonId)); check("population report green", report.status === "green", String(report.status)); - check("population accounting", report.sourceRowCount === report.importedSpawnCount + report.excludedRowCount, - report.sourceRowCount + " = " + report.importedSpawnCount + " + " + report.excludedRowCount); + const expectedScriptedSpawns = (recipe.scriptedSpawns ?? []).length + + (recipe.scriptedFormations ?? []).reduce((total, formation) => total + formation.members.length, 0); + const sourceCombatSpawns = report.importedSpawnCount - (report.scriptedSpawnCount ?? 0); + check( + "scripted population accounting", + (report.scriptedSpawnCount ?? 0) === expectedScriptedSpawns, + String(report.scriptedSpawnCount ?? 0) + " = " + expectedScriptedSpawns, + ); + check( + "source population accounting", + report.sourceRowCount === sourceCombatSpawns + report.excludedRowCount, + report.sourceRowCount + " = " + sourceCombatSpawns + " + " + report.excludedRowCount, + ); + check( + "boss population accounting", + !Number.isInteger(recipe.expectedBossSpawns) || report.bossSpawnCount === recipe.expectedBossSpawns, + String(report.bossSpawnCount) + " = " + String(recipe.expectedBossSpawns), + ); check("combat nav projection", report.navmeshProjection.maximumCombatDistance <= report.navmeshProjection.maximumAllowedDistance, String(report.navmeshProjection.maximumCombatDistance)); check("mechanics is an explicit review gate", mechanics.status === "review-required" || mechanics.status === "green", String(mechanics.status)); diff --git a/server/server.mjs b/server/server.mjs index 66dded96..07afc8dd 100644 --- a/server/server.mjs +++ b/server/server.mjs @@ -35,6 +35,19 @@ function parseAllowedOrigins(value) { return new Set(String(value ?? "").split(",").map((entry) => entry.trim()).filter(Boolean)); } +function booleanFlag(value) { + return ["1", "true", "yes", "on"].includes(String(value ?? "").trim().toLowerCase()); +} + +function acceptsEncoding(value, requested) { + const encodings = new Map(String(value ?? "").split(",").map((entry) => { + const [name, ...parameters] = entry.trim().toLowerCase().split(";"); + const quality = parameters.find((part) => part.trim().startsWith("q=")); + return [name, quality ? Number(quality.trim().slice(2)) : 1]; + })); + return (encodings.get(requested) ?? encodings.get("*") ?? 0) > 0; +} + function bearerToken(request) { return /^Bearer\s+(.+)$/i.exec(String(request.headers.authorization ?? ""))?.[1] ?? ""; } @@ -92,15 +105,25 @@ function serveStatic(request, response, staticDir, pathname, options = {}) { const stats = statSync(filePath); const extension = path.extname(filePath).toLowerCase(); const range = request.headers.range ? parseRange(request.headers.range, stats.size) : null; + const canEncode = !range && !request.headers.range; + const encoding = canEncode && existsSync(`${filePath}.br`) && acceptsEncoding(request.headers["accept-encoding"], "br") + ? "br" + : canEncode && existsSync(`${filePath}.gz`) && acceptsEncoding(request.headers["accept-encoding"], "gzip") + ? "gzip" + : null; + const encodedPath = encoding ? `${filePath}.${encoding === "br" ? "br" : "gz"}` : null; + const servedPath = encodedPath ?? filePath; + const servedStats = encodedPath ? statSync(encodedPath) : stats; const headers = { "Content-Type": MIME_TYPES.get(extension) ?? "application/octet-stream", "Accept-Ranges": "bytes", "Cache-Control": options.immutable ? "public, max-age=31536000, immutable" - : path.basename(filePath) === "index.html" || options.noCache + : ["index.html", "content-cache-sw.js"].includes(path.basename(filePath)) || options.noCache ? "no-cache" : "public, max-age=3600", "X-Content-Type-Options": "nosniff", + ...(encodedPath ? { "Content-Encoding": encoding, Vary: "Accept-Encoding" } : {}), }; if (request.headers.range && !range) { response.writeHead(416, { ...headers, "Content-Range": `bytes */${stats.size}` }); @@ -117,9 +140,9 @@ function serveStatic(request, response, staticDir, pathname, options = {}) { else createReadStream(filePath, range).pipe(response); return true; } - response.writeHead(200, { ...headers, "Content-Length": stats.size }); + response.writeHead(200, { ...headers, "Content-Length": servedStats.size }); if (request.method === "HEAD") response.end(); - else createReadStream(filePath).pipe(response); + else createReadStream(servedPath).pipe(response); return true; } @@ -153,9 +176,10 @@ export function createGameServer(options = {}) { const allowedOrigins = parseAllowedOrigins(options.corsOrigins ?? process.env.CORS_ORIGINS); const bodyLimit = Math.max(1024, Number(options.bodyLimit ?? process.env.MAX_JSON_BODY_BYTES ?? 5 * 1024 * 1024)); const trustProxy = String(options.trustProxy ?? process.env.TRUST_PROXY ?? "").toLowerCase() === "true"; + const enableShelvedModes = options.enableShelvedModes ?? booleanFlag(process.env.HEALER_MAN_ENABLE_SHELVED_MODES); const gameDatabase = options.database ?? openGameDatabase(options); const onlineSessions = createOnlineSessionManager(); - const arenaService = createArenaService({ database: gameDatabase }); + const arenaService = createArenaService({ database: gameDatabase, autoPump: enableShelvedModes }); const allowAuthRequest = createAuthLimiter(); gameDatabase.pruneExpiredSessions(); @@ -185,6 +209,15 @@ export function createGameServer(options = {}) { json(response, 200, { ok: true, service: "healer-man", time: new Date().toISOString() }); return; } + if (!enableShelvedModes && ( + url.pathname === "/api/manastorm-config" + || url.pathname.startsWith("/api/gm/manastorm-config") + || url.pathname === "/api/arena" + || url.pathname.startsWith("/api/arena/") + )) { + json(response, 503, { error: "This feature is temporarily unavailable.", code: "feature_disabled" }); + return; + } if ((url.pathname === "/api/auth/register" || url.pathname === "/api/auth/login") && request.method === "POST") { const forwarded = trustProxy ? String(request.headers["x-forwarded-for"] ?? "").split(",")[0]?.trim() : ""; if (!allowAuthRequest(forwarded || request.socket.remoteAddress || "unknown")) { @@ -254,6 +287,10 @@ export function createGameServer(options = {}) { if (url.pathname === "/api/online-group/activity" && request.method === "POST") { const account = gameDatabase.authenticate(bearerToken(request)); const body = await readJson(request, 64 * 1024); + if (!enableShelvedModes && body.type === "manastorm") { + json(response, 503, { error: "Manastorm groups are temporarily unavailable.", code: "feature_disabled" }); + return; + } json(response, 200, gameDatabase.startOnlineGroupActivity(account.id, body)); return; } @@ -348,7 +385,11 @@ export function createGameServer(options = {}) { } if (serveBundledContent(request, response, url.pathname)) return; if (url.pathname === "/content/bootstrap-manifest.json" || url.pathname.startsWith("/content/bundled/")) { - if (serveStatic(request, response, staticDir, url.pathname, { spaFallback: false, noCache: true })) return; + if (serveStatic(request, response, staticDir, url.pathname, { + spaFallback: false, + immutable: url.pathname.startsWith("/content/bundled/"), + noCache: url.pathname === "/content/bootstrap-manifest.json", + })) return; } if (url.pathname === "/content" || url.pathname.startsWith("/content/")) { const contentPath = url.pathname.slice("/content".length) || "/manifest.json"; @@ -361,7 +402,8 @@ export function createGameServer(options = {}) { json(response, 404, { error: "Content file not found.", code: "content_not_found" }); return; } - if (serveStatic(request, response, staticDir, url.pathname)) return; + const immutableViteAsset = /^\/assets\/.+-[A-Za-z0-9_-]{8,}\.[A-Za-z0-9]+$/.test(url.pathname); + if (serveStatic(request, response, staticDir, url.pathname, { immutable: immutableViteAsset })) return; json(response, 404, { error: "Not found.", code: "not_found" }); } catch (error) { const known = error instanceof GameDatabaseError; @@ -376,6 +418,14 @@ export function createGameServer(options = {}) { }); server.on("upgrade", (request, socket, head) => { + if (!enableShelvedModes && new URL(request.url ?? "/", "http://localhost").pathname === "/ws/arena") { + const body = Buffer.from(JSON.stringify({ + error: "This feature is temporarily unavailable.", + code: "feature_disabled", + })); + socket.end(`HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\nContent-Type: application/json\r\nContent-Length: ${body.length}\r\n\r\n${body}`); + return; + } if (!upgradeArenaWebSocket(request, socket, head, arenaService)) { socket.end("HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"); } @@ -386,7 +436,7 @@ export function createGameServer(options = {}) { arenaService.close(); gameDatabase.close(); }); - return { server, database: gameDatabase, arenaService, staticDir, contentDir }; + return { server, database: gameDatabase, arenaService, staticDir, contentDir, enableShelvedModes }; } if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { diff --git a/server/server.test.mjs b/server/server.test.mjs index 1da48123..dba17455 100644 --- a/server/server.test.mjs +++ b/server/server.test.mjs @@ -3,6 +3,7 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import test from "node:test"; +import { brotliCompressSync } from "node:zlib"; import { createGameServer } from "./server.mjs"; test("serves the game and authenticated cloud-save API", async () => { @@ -11,18 +12,36 @@ test("serves the game and authenticated cloud-save API", async () => { const dataDir = path.join(directory, "data"); const contentDir = path.join(directory, "content"); mkdirSync(staticDir, { recursive: true }); + mkdirSync(path.join(staticDir, "assets"), { recursive: true }); + mkdirSync(path.join(staticDir, "content", "bundled"), { recursive: true }); mkdirSync(path.join(contentDir, "objects"), { recursive: true }); writeFileSync(path.join(staticDir, "index.html"), "Healer Man"); + const hashedScript = "console.log('compressed delivery');"; + writeFileSync(path.join(staticDir, "assets", "app-12345678.js"), hashedScript); + writeFileSync(path.join(staticDir, "assets", "app-12345678.js.br"), brotliCompressSync(hashedScript)); + writeFileSync(path.join(staticDir, "content", "bootstrap-manifest.json"), JSON.stringify({ schemaVersion: 1, files: {} })); + writeFileSync(path.join(staticDir, "content", "bundled", `${"a".repeat(64)}.json`), "{}"); writeFileSync(path.join(staticDir, "sample.glb"), Buffer.from("0123456789")); writeFileSync(path.join(contentDir, "manifest.json"), JSON.stringify({ schemaVersion: 1 })); writeFileSync(path.join(contentDir, "objects", "asset.glb"), Buffer.from("abcdefghij")); - const runtime = createGameServer({ staticDir, contentDir, dataDir, corsOrigins: "https://iwanttoheal.phenomrom.com" }); + const runtime = createGameServer({ + staticDir, contentDir, dataDir, corsOrigins: "https://iwanttoheal.phenomrom.com", enableShelvedModes: true, + }); await new Promise((resolve) => runtime.server.listen(0, "127.0.0.1", resolve)); const address = runtime.server.address(); const origin = `http://127.0.0.1:${address.port}`; try { assert.equal((await fetch(`${origin}/`)).status, 200); + const compressedScript = await fetch(`${origin}/assets/app-12345678.js`, { + headers: { "Accept-Encoding": "br" }, + }); + assert.equal(compressedScript.headers.get("content-encoding"), "br"); + assert.equal(compressedScript.headers.get("cache-control"), "public, max-age=31536000, immutable"); + assert.equal(await compressedScript.text(), hashedScript); + const bundledObject = await fetch(`${origin}/content/bundled/${"a".repeat(64)}.json`); + assert.equal(bundledObject.headers.get("cache-control"), "public, max-age=31536000, immutable"); + assert.equal((await fetch(`${origin}/content/bootstrap-manifest.json`)).headers.get("cache-control"), "no-cache"); const range = await fetch(`${origin}/sample.glb`, { headers: { Range: "bytes=2-5" } }); assert.equal(range.status, 206); assert.equal(await range.text(), "2345"); @@ -250,3 +269,44 @@ test("serves the game and authenticated cloud-save API", async () => { rmSync(directory, { recursive: true, force: true }); } }); + +test("disables shelved APIs and Manastorm group creation by default", async () => { + const directory = mkdtempSync(path.join(os.tmpdir(), "healer-man-shelved-server-test-")); + const staticDir = path.join(directory, "dist"); + const dataDir = path.join(directory, "data"); + const contentDir = path.join(directory, "content"); + mkdirSync(staticDir, { recursive: true }); + mkdirSync(contentDir, { recursive: true }); + writeFileSync(path.join(staticDir, "index.html"), "Healer Man"); + const runtime = createGameServer({ staticDir, contentDir, dataDir, enableShelvedModes: false }); + await new Promise((resolve) => runtime.server.listen(0, "127.0.0.1", resolve)); + const address = runtime.server.address(); + const origin = `http://127.0.0.1:${address.port}`; + + try { + assert.equal(runtime.enableShelvedModes, false); + for (const route of ["/api/manastorm-config", "/api/gm/manastorm-config", "/api/arena/state"]) { + const response = await fetch(`${origin}${route}`); + assert.equal(response.status, 503); + assert.equal((await response.json()).code, "feature_disabled"); + } + + const registration = await fetch(`${origin}/api/auth/register`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username: "ShelvedHealer", password: "healing-pass" }), + }); + const auth = await registration.json(); + const activity = await fetch(`${origin}/api/online-group/activity`, { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: `Bearer ${auth.token}` }, + body: JSON.stringify({ type: "manastorm", contentId: "deadmines", partySize: 1, fillWithAi: true }), + }); + assert.equal(activity.status, 503); + assert.equal((await activity.json()).code, "feature_disabled"); + assert.equal(runtime.arenaService.inspect().tickets.size, 0); + } finally { + await new Promise((resolve) => runtime.server.close(resolve)); + rmSync(directory, { recursive: true, force: true }); + } +}); diff --git a/shared/content-policy.json b/shared/content-policy.json new file mode 100644 index 00000000..6f4b56fd --- /dev/null +++ b/shared/content-policy.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 1, + "bootstrapPackIds": [ + "core:ui", + "core:equipment", + "core:character-basic", + "core:runtime", + "dungeon:wailing-caverns" + ], + "hiddenContentPackIds": [ + "core:arenas" + ], + "hiddenContentPackPrefixes": [ + "manastorm:" + ], + "maxBootstrapStoredBytes": 83886080 +} diff --git a/src/App.tsx b/src/App.tsx index 92e21b4e..17fcda98 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,12 @@ import { lazy, Suspense, useEffect, useMemo, useState, type ReactNode } from "react"; import { BASE_CLASS_IDS, classById } from "./app/characterCatalog"; import { useShellStore } from "./app/shellStore"; +import { isShelvedShellPhase, SHELVED_MODES_ENABLED } from "./app/releaseFeatures"; import { preloadAbilityCatalogsForClasses } from "./game/abilityCatalog"; -import { useManastormAdminStore } from "./game/manastormAdminStore"; import { useAuthoritativeDualScreenSync, useForcedThorDisplays } from "./platform/useThorDualScreen"; import { LoginScreen } from "./ui/LoginScreen"; import { OnlineGroupCoordinator } from "./app/OnlineGroupCoordinator"; +import { ShelvedModeRoute } from "@shelvedModeRoutes"; import { preloadTalentGameplayResources } from "./ui/talentGameplayResources"; import { getLoadedGameRuntime, @@ -19,11 +20,6 @@ const CharacterSelectScreen = lazy(() => import("./ui/CharacterSelectScreen").th const ClassHallScreen = lazy(() => import("./ui/ClassHallScreen").then((module) => ({ default: module.ClassHallScreen }))); const DungeonSelectScreen = lazy(() => import("./ui/DungeonSelectScreen").then((module) => ({ default: module.DungeonSelectScreen }))); const MainMenuScreen = lazy(() => import("./ui/MainMenuScreen").then((module) => ({ default: module.MainMenuScreen }))); -const ManastormSelectScreen = lazy(() => import("./ui/ManastormSelectScreen").then((module) => ({ default: module.ManastormSelectScreen }))); -const ArenaSelectScreen = lazy(() => import("./ui/ArenaSelectScreen").then((module) => ({ default: module.ArenaSelectScreen }))); -const ArenaRuntime = lazy(() => import("./ArenaRuntime")); -const GmAdminScreen = lazy(() => import("./ui/GmAdminScreen").then((module) => ({ default: module.GmAdminScreen }))); - function ShellTransition() { return
HMOpening expeditionPreparing the next screen...
; } @@ -74,12 +70,14 @@ export default function App() { useAuthoritativeDualScreenSync(); const phase = useShellStore((state) => state.phase); const session = useShellStore((state) => state.session); - const refreshManastormConfig = useManastormAdminStore((state) => state.refresh); const PreloadedGameRuntime = getLoadedGameRuntime(); useEffect(() => { - void refreshManastormConfig(session); - }, [refreshManastormConfig, session]); + if (!SHELVED_MODES_ENABLED && isShelvedShellPhase(phase)) { + useShellStore.getState().returnToMainMenu(); + useShellStore.getState().setNotice("That game mode is temporarily unavailable."); + } + }, [phase]); let screen = null; if (phase === "login") screen = ; @@ -88,10 +86,9 @@ export default function App() { else if (phase === "main-menu") screen = }>; else if (phase === "class-hall") screen = }>; else if (phase === "dungeons") screen = }>; - else if (phase === "manastorms") screen = }>; - else if (phase === "arenas") screen = }>; - else if (phase === "arena-game") screen = }>; - else if (phase === "gm-admin") screen = }>; + else if (isShelvedShellPhase(phase)) { + screen = }>; + } else if (phase === "game") screen = PreloadedGameRuntime ? : }>; diff --git a/src/GameRuntime.tsx b/src/GameRuntime.tsx index 286f2e4c..ddc344d5 100644 --- a/src/GameRuntime.tsx +++ b/src/GameRuntime.tsx @@ -8,9 +8,6 @@ import { releaseDungeonGltfCache } from "./game/dungeonAssetCache"; import { clearDungeonRuntimeCaches } from "./game/dungeonRuntimeReset"; import { requireDungeonDefinition } from "./game/dungeonRegistry"; import { PartyBridge } from "./game/PartyBridge"; -import { ManastormBridge } from "./game/ManastormBridge"; -import { releaseActiveManastormStage } from "./game/manastormStageLoader"; -import { lootCurrentManastormBoss } from "./game/manastormLoot"; import { usePartyStore } from "./game/partyStore"; import { useGameStore } from "./game/store"; import { GameScene } from "./scene/GameScene"; @@ -20,8 +17,6 @@ import { GameplayPanels } from "./ui/GameplayPanels"; import { MapPanel } from "./ui/MapPanel"; import { PauseMenu } from "./ui/PauseMenu"; import { SceneErrorBoundary } from "./ui/SceneErrorBoundary"; -import { GmWorldEditor } from "./ui/GmWorldEditor"; -import { GmReviewCard } from "./ui/GmReviewCard"; import { OnlineSessionBridge } from "./game/OnlineSessionBridge"; import { DungeonRunCheckpointBridge } from "./game/DungeonRunCheckpointBridge"; import { DungeonEncounterEffectBridge } from "./game/DungeonEncounterEffectBridge"; @@ -33,6 +28,7 @@ import { activeUiLayout, currentUiDevice } from "./game/uiLayoutStore"; import { useUiLayoutStore } from "./game/uiLayoutStore"; import { profileInputBindings } from "./game/keybindPreferences"; import { KeybindsPanel } from "./ui/KeybindsPanel"; +import { lootShelvedBoss, releaseShelvedGameRuntime, ShelvedGameBridges } from "@shelvedGameRuntime"; function InputBridge() { const overlay = useGameStore((state) => state.overlay); @@ -57,7 +53,7 @@ function InputBridge() { toggleInventory: () => useGameStore.getState().toggleOverlay("inventory"), toggleSpellbook: () => useGameStore.getState().toggleOverlay("spellbook"), toggleTalents: () => useGameStore.getState().toggleOverlay("talents"), - lootActiveBoss: () => interactWithActiveDungeonObject() || lootCurrentManastormBoss(), + lootActiveBoss: () => interactWithActiveDungeonObject() || lootShelvedBoss(), castActionBinding: (layer, control) => { const game = useGameStore.getState(); useCombatStore.getState().castActionBinding(layer, control, game.playerPosition, Date.now()); @@ -102,7 +98,7 @@ export default function GameRuntime() { const activeDungeon = requireDungeonDefinition(useGameStore.getState().activeDungeonId); clearDungeonRuntimeCaches(); releaseDungeonGltfCache(activeDungeon); - releaseActiveManastormStage(); + releaseShelvedGameRuntime(); }, []); return ( - + @@ -126,8 +122,6 @@ export default function GameRuntime() { - - } bottom={} diff --git a/src/app/OnlineGroupCoordinator.test.ts b/src/app/OnlineGroupCoordinator.test.ts index 0070df40..7c150e2e 100644 --- a/src/app/OnlineGroupCoordinator.test.ts +++ b/src/app/OnlineGroupCoordinator.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it, vi } from "vitest"; import type { OnlineGroupActivity } from "./onlineGroupTypes"; import { enterOnlineGroupActivity } from "./OnlineGroupCoordinator"; +import { SHELVED_MODES_ENABLED } from "./releaseFeatures"; + +const normalReleaseIt = SHELVED_MODES_ENABLED ? it.skip : it; +const shelvedModeIt = SHELVED_MODES_ENABLED ? it : it.skip; const DUNGEON_ACTIVITY: OnlineGroupActivity = { id: "activity-dungeon-1", @@ -12,6 +16,14 @@ const DUNGEON_ACTIVITY: OnlineGroupActivity = { startedAt: 1, }; +const MANASTORM_ACTIVITY: OnlineGroupActivity = { + ...DUNGEON_ACTIVITY, + id: "activity-manastorm-1", + type: "manastorm", + partySize: 3, + startingLevel: 6, +}; + function entryDependencies(enterDungeon: () => boolean) { return { installDungeonRuntime: vi.fn(async () => undefined), @@ -75,4 +87,36 @@ describe("online group activity entry", () => { expect(dependencies.enterDungeon).not.toHaveBeenCalled(); expect(consumeActivity).not.toHaveBeenCalled(); }); + + normalReleaseIt("does not load or enter a shelved group activity", async () => { + const consumeActivity = vi.fn(); + const dependencies = entryDependencies(() => true); + + await expect(enterOnlineGroupActivity( + MANASTORM_ACTIVITY, + "healer", + consumeActivity, + dependencies, + )).resolves.toBe(false); + + expect(dependencies.installManastormRuntime).not.toHaveBeenCalled(); + expect(dependencies.enterManastorm).not.toHaveBeenCalled(); + expect(consumeActivity).not.toHaveBeenCalled(); + }); + + shelvedModeIt("restores Manastorm group entry in the developer mode", async () => { + const consumeActivity = vi.fn(); + const dependencies = entryDependencies(() => true); + + await expect(enterOnlineGroupActivity( + MANASTORM_ACTIVITY, + "healer", + consumeActivity, + dependencies, + )).resolves.toBe(true); + + expect(dependencies.installManastormRuntime).toHaveBeenCalledOnce(); + expect(dependencies.enterManastorm).toHaveBeenCalledWith("healer", 3, 6); + expect(consumeActivity).toHaveBeenCalledWith(MANASTORM_ACTIVITY.id); + }); }); diff --git a/src/app/OnlineGroupCoordinator.tsx b/src/app/OnlineGroupCoordinator.tsx index 0620b49c..ef2653a8 100644 --- a/src/app/OnlineGroupCoordinator.tsx +++ b/src/app/OnlineGroupCoordinator.tsx @@ -6,9 +6,11 @@ import { defaultPartyRoleForClass, type PartyRole, } from "../game/partyRoles"; -import { isManastormPartySize, type ManastormPartySize } from "../game/manastormProgress"; +import type { ManastormPartySize } from "../game/manastormProgress"; import type { OnlineGroupActivity } from "./onlineGroupTypes"; import { useContentManagerSnapshot } from "../content/useContentManager"; +import { SHELVED_MODES_ENABLED } from "./releaseFeatures"; +import { installShelvedOnlineGroupRuntime } from "@shelvedOnlineGroupRuntime"; const GROUP_POLL_MS = 2_500; const ACTIVITY_JOIN_WINDOW_MS = 10 * 60 * 1000; @@ -27,8 +29,7 @@ const DEFAULT_ACTIVITY_ENTRY_DEPENDENCIES: OnlineGroupActivityEntryDependencies }, enterDungeon: (dungeonId, role) => useShellStore.getState().enterDungeon(dungeonId, role), installManastormRuntime: async () => { - const { installManastormShellRuntime } = await import("./manastormShellRuntime"); - installManastormShellRuntime(); + await installShelvedOnlineGroupRuntime(); }, enterManastorm: (role, partySize, startingLevel) => useShellStore.getState().enterManastorm( role, @@ -48,8 +49,11 @@ export async function enterOnlineGroupActivity( await dependencies.installDungeonRuntime(); entered = dependencies.enterDungeon(activity.contentId, role); } else { + if (!SHELVED_MODES_ENABLED) return false; await dependencies.installManastormRuntime(); - const partySize = isManastormPartySize(activity.partySize) ? activity.partySize : 5; + const partySize = Number.isInteger(activity.partySize) && activity.partySize >= 1 && activity.partySize <= 5 + ? activity.partySize as ManastormPartySize + : 5; entered = dependencies.enterManastorm(role, partySize, activity.startingLevel ?? undefined); } if (entered) consumeActivity(activity.id); diff --git a/src/app/ShelvedGameRuntime.disabled.tsx b/src/app/ShelvedGameRuntime.disabled.tsx new file mode 100644 index 00000000..7da12942 --- /dev/null +++ b/src/app/ShelvedGameRuntime.disabled.tsx @@ -0,0 +1,9 @@ +export function ShelvedGameBridges() { + return null; +} + +export function lootShelvedBoss(): boolean { + return false; +} + +export function releaseShelvedGameRuntime(): void {} diff --git a/src/app/ShelvedGameRuntime.enabled.tsx b/src/app/ShelvedGameRuntime.enabled.tsx new file mode 100644 index 00000000..115d6e6b --- /dev/null +++ b/src/app/ShelvedGameRuntime.enabled.tsx @@ -0,0 +1,23 @@ +import { ManastormBridge } from "../game/ManastormBridge"; +import { releaseActiveManastormStage } from "../game/manastormStageLoader"; +import { lootCurrentManastormBoss } from "../game/manastormLoot"; +import { GmReviewCard } from "../ui/GmReviewCard"; +import { GmWorldEditor } from "../ui/GmWorldEditor"; + +export function ShelvedGameBridges() { + return ( + <> + + + + + ); +} + +export function lootShelvedBoss(): boolean { + return lootCurrentManastormBoss(); +} + +export function releaseShelvedGameRuntime(): void { + releaseActiveManastormStage(); +} diff --git a/src/app/ShelvedModeRoutes.disabled.tsx b/src/app/ShelvedModeRoutes.disabled.tsx new file mode 100644 index 00000000..f3e502a1 --- /dev/null +++ b/src/app/ShelvedModeRoutes.disabled.tsx @@ -0,0 +1,8 @@ +import type { PlayerSession, ShellPhase } from "./types"; + +export function ShelvedModeRoute(_props: { + readonly phase: ShellPhase; + readonly session: PlayerSession | null; +}) { + return null; +} diff --git a/src/app/ShelvedModeRoutes.tsx b/src/app/ShelvedModeRoutes.tsx new file mode 100644 index 00000000..b45b0f33 --- /dev/null +++ b/src/app/ShelvedModeRoutes.tsx @@ -0,0 +1,33 @@ +import { lazy, Suspense, useEffect } from "react"; +import type { PlayerSession, ShellPhase } from "./types"; +import { useManastormAdminStore } from "../game/manastormAdminStore"; + +const ManastormSelectScreen = lazy(() => import("../ui/ManastormSelectScreen").then((module) => ({ default: module.ManastormSelectScreen }))); +const ArenaSelectScreen = lazy(() => import("../ui/ArenaSelectScreen").then((module) => ({ default: module.ArenaSelectScreen }))); +const ArenaRuntime = lazy(() => import("../ArenaRuntime")); +const GmAdminScreen = lazy(() => import("../ui/GmAdminScreen").then((module) => ({ default: module.GmAdminScreen }))); + +function ShelvedTransition() { + return
HMOpening expeditionPreparing the next screen...
; +} + +export function ShelvedModeRoute({ + phase, + session, +}: { + readonly phase: ShellPhase; + readonly session: PlayerSession | null; +}) { + const refreshManastormConfig = useManastormAdminStore((state) => state.refresh); + + useEffect(() => { + void refreshManastormConfig(session); + }, [refreshManastormConfig, session]); + + let screen = null; + if (phase === "manastorms") screen = ; + else if (phase === "arenas") screen = ; + else if (phase === "arena-game") screen = ; + else if (phase === "gm-admin") screen = ; + return }>{screen}; +} diff --git a/src/app/ShelvedOnlineGroupRuntime.disabled.ts b/src/app/ShelvedOnlineGroupRuntime.disabled.ts new file mode 100644 index 00000000..c3cc5810 --- /dev/null +++ b/src/app/ShelvedOnlineGroupRuntime.disabled.ts @@ -0,0 +1 @@ +export async function installShelvedOnlineGroupRuntime(): Promise {} diff --git a/src/app/ShelvedOnlineGroupRuntime.enabled.ts b/src/app/ShelvedOnlineGroupRuntime.enabled.ts new file mode 100644 index 00000000..592d4af4 --- /dev/null +++ b/src/app/ShelvedOnlineGroupRuntime.enabled.ts @@ -0,0 +1,4 @@ +export async function installShelvedOnlineGroupRuntime(): Promise { + const { installManastormShellRuntime } = await import("./manastormShellRuntime"); + installManastormShellRuntime(); +} diff --git a/src/app/releaseFeatures.test.ts b/src/app/releaseFeatures.test.ts new file mode 100644 index 00000000..e9f3a7a8 --- /dev/null +++ b/src/app/releaseFeatures.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { + isShelvedShellPhase, + sessionCanUseShelvedGmTools, + SHELVED_MODES_ENABLED, +} from "./releaseFeatures"; + +describe("release feature boundary", () => { + it("recognizes every legacy shelved phase", () => { + expect(["manastorms", "arenas", "arena-game", "gm-admin"].every(isShelvedShellPhase)).toBe(true); + expect(isShelvedShellPhase("main-menu")).toBe(false); + expect(isShelvedShellPhase("game")).toBe(false); + }); + + it("exposes GM tools only in a shelved build with an authenticated GM", () => { + const session = { + kind: "account", + ownerId: "gm-account", + displayName: "Phenom", + accessToken: "token", + roles: ["gm"], + } as const; + expect(sessionCanUseShelvedGmTools(session)).toBe(SHELVED_MODES_ENABLED); + expect(sessionCanUseShelvedGmTools({ ...session, roles: [] })).toBe(false); + expect(sessionCanUseShelvedGmTools({ ...session, accessToken: "" })).toBe(false); + }); +}); diff --git a/src/app/releaseFeatures.ts b/src/app/releaseFeatures.ts new file mode 100644 index 00000000..7182b191 --- /dev/null +++ b/src/app/releaseFeatures.ts @@ -0,0 +1,23 @@ +export const SHELVED_MODES_ENABLED = import.meta.env.VITE_SHELVED_MODES_ENABLED === "true"; + +export const SHELVED_SHELL_PHASES = Object.freeze([ + "manastorms", + "arenas", + "arena-game", + "gm-admin", +] as const); + +export type ShelvedShellPhase = (typeof SHELVED_SHELL_PHASES)[number]; + +export function isShelvedShellPhase(value: string): value is ShelvedShellPhase { + return (SHELVED_SHELL_PHASES as readonly string[]).includes(value); +} + +export function sessionCanUseShelvedGmTools(session: { + readonly kind: string; + readonly accessToken?: string; + readonly roles?: readonly string[]; +} | null): boolean { + return SHELVED_MODES_ENABLED + && Boolean(session?.kind === "account" && session.accessToken && session.roles?.includes("gm")); +} diff --git a/src/app/shellRuntimeDelegation.test.ts b/src/app/shellRuntimeDelegation.test.ts index 65c1968b..bd2cabf2 100644 --- a/src/app/shellRuntimeDelegation.test.ts +++ b/src/app/shellRuntimeDelegation.test.ts @@ -2,6 +2,10 @@ import { describe, expect, it, vi } from "vitest"; import { registerDungeonShellRuntime } from "./dungeonShellRuntimeBridge"; import { registerManastormShellRuntime } from "./manastormShellRuntimeBridge"; import { useShellStore } from "./shellStore"; +import { SHELVED_MODES_ENABLED } from "./releaseFeatures"; + +const normalReleaseIt = SHELVED_MODES_ENABLED ? it.skip : it; +const shelvedModeIt = SHELVED_MODES_ENABLED ? it : it.skip; describe("on-demand shell runtime delegation", () => { it("delegates dungeon entry after its phase runtime is installed", () => { @@ -12,7 +16,30 @@ describe("on-demand shell runtime delegation", () => { expect(enterDungeon).toHaveBeenCalledWith("wailing-caverns", "healer"); }); - it("delegates Manastorm operations after its phase runtime is installed", () => { + normalReleaseIt("ignores registered Manastorm delegates in a normal release", () => { + const enterManastorm = vi.fn(() => true); + const saveSelectedLoadout = vi.fn(() => true); + const abandonRun = vi.fn(); + const failRun = vi.fn(); + registerManastormShellRuntime({ + enterManastorm, + saveSelectedLoadout, + abandonRun, + failRun, + leaveActiveGame: vi.fn(), + }); + + expect(useShellStore.getState().enterManastorm("healer", 3, 6)).toBe(false); + expect(useShellStore.getState().saveSelectedManastormLoadout(["spell-1"])).toBe(false); + useShellStore.getState().abandonManastormRun(); + useShellStore.getState().failActiveManastorm(); + expect(enterManastorm).not.toHaveBeenCalled(); + expect(saveSelectedLoadout).not.toHaveBeenCalled(); + expect(abandonRun).not.toHaveBeenCalled(); + expect(failRun).not.toHaveBeenCalled(); + }); + + shelvedModeIt("delegates Manastorm operations after its phase runtime is installed", () => { const enterManastorm = vi.fn(() => true); const saveSelectedLoadout = vi.fn(() => true); const abandonRun = vi.fn(); @@ -37,4 +64,3 @@ describe("on-demand shell runtime delegation", () => { expect(failRun).toHaveBeenCalledOnce(); }); }); - diff --git a/src/app/shellStore.test.ts b/src/app/shellStore.test.ts index 121cdf67..f29520d8 100644 --- a/src/app/shellStore.test.ts +++ b/src/app/shellStore.test.ts @@ -3,6 +3,7 @@ import { createDefaultAppearance } from "./characterCatalog"; import { useShellStore } from "./shellStore"; import { installDungeonShellRuntime } from "./dungeonShellRuntime"; import { installManastormShellRuntime } from "./manastormShellRuntime"; +import { SHELVED_MODES_ENABLED } from "./releaseFeatures"; import { DUNGEON_MANIFEST } from "../game/dungeonManifest"; import { useGameStore } from "../game/store"; import { useManastormStore } from "../game/manastormStore"; @@ -23,6 +24,8 @@ import { useManastormAdminStore } from "../game/manastormAdminStore"; installDungeonShellRuntime(); installManastormShellRuntime(); +const normalReleaseIt = SHELVED_MODES_ENABLED ? it.skip : it; +const shelvedModeIt = SHELVED_MODES_ENABLED ? it : it.skip; const character = { id: "character-1", @@ -254,7 +257,16 @@ describe("shell store", () => { }); }); - it("opens Manastorms, validates the role, and starts a fresh level-one run", () => { + normalReleaseIt("keeps legacy shelved commands out of the normal shell", () => { + prepareSelectedCharacter(); + expect(useShellStore.getState().openManastorms()).toBe(false); + expect(useShellStore.getState().openArenas()).toBe(false); + expect(useShellStore.getState().enterManastorm("healer")).toBe(false); + expect(useShellStore.getState().phase).toBe("characters"); + expect(useShellStore.getState().notice).toMatch(/temporarily unavailable/i); + }); + + shelvedModeIt("opens Manastorms, validates the role, and starts a fresh level-one run", () => { prepareSelectedCharacter(); expect(useShellStore.getState().openManastorms()).toBe(true); expect(useShellStore.getState().phase).toBe("manastorms"); @@ -278,7 +290,7 @@ describe("shell store", () => { ); }); - it("starts each party size from only its own unlocked checkpoint", () => { + shelvedModeIt("starts each party size from only its own unlocked checkpoint", () => { const progress = recordManastormCheckpoint(createEmptyManastormProgress(), 3, 5, 6); useShellStore.setState({ phase: "manastorms", @@ -302,7 +314,7 @@ describe("shell store", () => { }); }); - it("selects end-game mode at the character cap and hands off an eligible checkpoint", () => { + shelvedModeIt("selects end-game mode at the character cap and hands off an eligible checkpoint", () => { const progress = recordManastormCheckpoint( createEmptyManastormProgress(), 3, @@ -333,7 +345,7 @@ describe("shell store", () => { }); }); - it("passes a persisted client-grounded spell selection into the run runtime", () => { + shelvedModeIt("passes a persisted client-grounded spell selection into the run runtime", () => { const model = manastormActiveSpellLoadoutModel(character.classId, character.level); const selectedIds = model.options.slice(0, 2).map((option) => option.id); const unlocked = mergeManastormUnlockedContent( diff --git a/src/app/shellStore.ts b/src/app/shellStore.ts index ef2e13fd..f94b2282 100644 --- a/src/app/shellStore.ts +++ b/src/app/shellStore.ts @@ -56,7 +56,7 @@ import { logoutOnlineSession, refreshOnlineSession, } from "./onlineAccountClient"; -import { isGameMasterSession } from "../game/manastormAdminConfig"; +import { sessionCanUseShelvedGmTools, SHELVED_MODES_ENABLED } from "./releaseFeatures"; import { useGameStore } from "../game/store"; import { normalizeSpecializationId, @@ -324,6 +324,10 @@ export const useShellStore = create((set, get) => ({ return true; }, openManastorms: () => { + if (!SHELVED_MODES_ENABLED) { + set({ notice: "Manastorms are temporarily unavailable." }); + return false; + } const { session, selectedCharacterId, characters } = get(); if (!session || !selectedCharacterId || !characters.some((character) => character.id === selectedCharacterId)) { set({ phase: session ? "characters" : "login", notice: "Select or create a character first." }); @@ -338,6 +342,10 @@ export const useShellStore = create((set, get) => ({ return true; }, openArenas: () => { + if (!SHELVED_MODES_ENABLED) { + set({ notice: "Arenas are temporarily unavailable." }); + return false; + } const { session, selectedCharacterId, characters } = get(); if (!session || !selectedCharacterId || !characters.some((character) => character.id === selectedCharacterId)) { set({ phase: session ? "characters" : "login", notice: "Select or create a character first." }); @@ -348,7 +356,7 @@ export const useShellStore = create((set, get) => ({ }, openGmAdmin: () => { const { session, selectedCharacterId, characters } = get(); - if (!isGameMasterSession(session)) { + if (!sessionCanUseShelvedGmTools(session)) { set({ notice: "Game Master access is required." }); return false; } @@ -429,7 +437,7 @@ export const useShellStore = create((set, get) => ({ }, returnToMainMenu: () => { useGameStore.setState({ activeGmReviewTarget: null, gmTestMode: false }); - manastormShellRuntime()?.leaveActiveGame(); + if (SHELVED_MODES_ENABLED) manastormShellRuntime()?.leaveActiveGame(); const { session, characters, activeCharacter, selectedCharacterId } = get(); if (!session) { set({ phase: "login", activeCharacter: null }); @@ -449,13 +457,21 @@ export const useShellStore = create((set, get) => ({ return false; }, enterManastorm: (requestedRole, requestedPartySize = 5, requestedStartingLevel) => { + if (!SHELVED_MODES_ENABLED) { + set({ notice: "Manastorms are temporarily unavailable." }); + return false; + } const runtime = manastormShellRuntime(); if (runtime) return runtime.enterManastorm(requestedRole, requestedPartySize, requestedStartingLevel); set({ notice: "Manastorm data is still loading. Try again in a moment." }); return false; }, - abandonManastormRun: () => manastormShellRuntime()?.abandonRun(), - failActiveManastorm: () => manastormShellRuntime()?.failRun(), + abandonManastormRun: () => { + if (SHELVED_MODES_ENABLED) manastormShellRuntime()?.abandonRun(); + }, + failActiveManastorm: () => { + if (SHELVED_MODES_ENABLED) manastormShellRuntime()?.failRun(); + }, enterWorld: () => get().enterDungeon(DEFAULT_DUNGEON_ID), returnToCharacters: () => { useGameStore.setState({ activeGmReviewTarget: null, gmTestMode: false }); @@ -594,6 +610,10 @@ export const useShellStore = create((set, get) => ({ })); }, saveSelectedManastormLoadout: (loadout) => { + if (!SHELVED_MODES_ENABLED) { + set({ notice: "Manastorms are temporarily unavailable." }); + return false; + } const runtime = manastormShellRuntime(); if (runtime) return runtime.saveSelectedLoadout(loadout); set({ notice: "Manastorm data is still loading. Try again in a moment." }); diff --git a/src/assets/game/dungeons/grafu-castle/instance-validation-report.json b/src/assets/game/dungeons/grafu-castle/instance-validation-report.json index c48bc032..302e9f3f 100644 --- a/src/assets/game/dungeons/grafu-castle/instance-validation-report.json +++ b/src/assets/game/dungeons/grafu-castle/instance-validation-report.json @@ -75,14 +75,24 @@ "detail": "green" }, { - "name": "population accounting", + "name": "scripted population accounting", "passed": true, - "detail": "249 = 56 + 193" + "detail": "44 = 44" + }, + { + "name": "source population accounting", + "passed": true, + "detail": "249 = 40 + 209" + }, + { + "name": "boss population accounting", + "passed": true, + "detail": "5 = 5" }, { "name": "combat nav projection", "passed": true, - "detail": "3.385892" + "detail": "0.998158" }, { "name": "mechanics is an explicit review gate", @@ -104,6 +114,51 @@ "passed": true, "detail": "grafu-castle" }, + { + "name": "actor atmo exists", + "passed": true, + "detail": "public/assets/creatures/grafu-castle/atmo.glb" + }, + { + "name": "actor atmo checksum", + "passed": true, + "detail": "9f7c1ad1c56ef51ee0511507f950ef97224ad86af561ded111cf1d5740c69999" + }, + { + "name": "actor atmo size", + "passed": true, + "detail": "1981260" + }, + { + "name": "actor barbo-brother exists", + "passed": true, + "detail": "public/assets/creatures/grafu-castle/barbo-brother.glb" + }, + { + "name": "actor barbo-brother checksum", + "passed": true, + "detail": "af1cd20b7cc1d465c270860997f1245b2967005b1e5308b4a43401e983f883a4" + }, + { + "name": "actor barbo-brother size", + "passed": true, + "detail": "2226000" + }, + { + "name": "actor black-hat exists", + "passed": true, + "detail": "public/assets/creatures/grafu-castle/black-hat.glb" + }, + { + "name": "actor black-hat checksum", + "passed": true, + "detail": "e3d5265c794b82d45c34bd7875e33c9f00680dc67c3cc18213f5df657f9ab76a" + }, + { + "name": "actor black-hat size", + "passed": true, + "detail": "1965712" + }, { "name": "actor black-hat-blue exists", "passed": true, @@ -112,12 +167,12 @@ { "name": "actor black-hat-blue checksum", "passed": true, - "detail": "bcd344260000235789ddb2452a285210000646659538e56e57857d9667969edd" + "detail": "e6b78de784549ce814f998c34b8e1aef42e452e26c7395ac74fa5db168413d2f" }, { "name": "actor black-hat-blue size", "passed": true, - "detail": "1561332" + "detail": "1560524" }, { "name": "actor bunny exists", @@ -127,12 +182,12 @@ { "name": "actor bunny checksum", "passed": true, - "detail": "6d1ed1424fc075b472224f6bf2cd7c1ea291d7605ad280ce8ae0570d060f4b3e" + "detail": "e29298726425e0109890fb0b33d770f39e051b7a43d50a666f6f6c46a3f2ee32" }, { "name": "actor bunny size", "passed": true, - "detail": "540500" + "detail": "540408" }, { "name": "actor cereus-01 exists", @@ -142,12 +197,12 @@ { "name": "actor cereus-01 checksum", "passed": true, - "detail": "324a9b2c338d08258979bbbbdad98e82bcb77f99907b3abbdfc817f4a9aa6610" + "detail": "a35ec70378f7323a1f3d50c3e84d5654eacd2888722944c83f9be18cf42eecfa" }, { "name": "actor cereus-01 size", "passed": true, - "detail": "620616" + "detail": "620404" }, { "name": "actor controlled-steel-guardian exists", @@ -157,12 +212,27 @@ { "name": "actor controlled-steel-guardian checksum", "passed": true, - "detail": "5d0e0f5d6b38c1cf799a30a3ab0718a405feaa84ae844375a15cff6161fa8b77" + "detail": "b772b72bffc44c4fcf36ff8aeaf8f2d0c435f220e5c9729ed7e1ce2aac70e8be" }, { "name": "actor controlled-steel-guardian size", "passed": true, - "detail": "1231536" + "detail": "1232036" + }, + { + "name": "actor cursed-steel-guardian exists", + "passed": true, + "detail": "public/assets/creatures/grafu-castle/cursed-steel-guardian.glb" + }, + { + "name": "actor cursed-steel-guardian checksum", + "passed": true, + "detail": "3ec7660d4e1b475ec9ae290c304802dd4ba174048fac0781ae25474a718227fe" + }, + { + "name": "actor cursed-steel-guardian size", + "passed": true, + "detail": "1151612" }, { "name": "actor elder-myconid exists", @@ -172,12 +242,12 @@ { "name": "actor elder-myconid checksum", "passed": true, - "detail": "5e5097cf12bac9780fd9925aae0d6507ad9393ac2e6fd106b2ca9cdf03b2ecc1" + "detail": "ef0d1fd12aa130a1a23a21bb978f7859b434d27512fb5da12426f1c1ce34514c" }, { "name": "actor elder-myconid size", "passed": true, - "detail": "1395280" + "detail": "1393472" }, { "name": "actor ghastly-clown-red exists", @@ -187,12 +257,12 @@ { "name": "actor ghastly-clown-red checksum", "passed": true, - "detail": "20a6fa59f783c2f17260db9556689906079f6d0d95b835ec26d3e2a7a589f322" + "detail": "7f767b8fc224b63f923af6e7f29eda935a746ef6040bc489e4957c2650993ccc" }, { "name": "actor ghastly-clown-red size", "passed": true, - "detail": "825156" + "detail": "824792" }, { "name": "actor grinning-clown exists", @@ -202,12 +272,57 @@ { "name": "actor grinning-clown checksum", "passed": true, - "detail": "b53b78efe39b807bc8bbf7952501970cf74ee1483b891b6b61b0880e30b197b2" + "detail": "b93ffdcfcf86df01c348678c97dfb913e74da9cab2367e187a46fafb3d85040d" }, { "name": "actor grinning-clown size", "passed": true, - "detail": "1374852" + "detail": "1374556" + }, + { + "name": "actor hm-male-image-585704 exists", + "passed": true, + "detail": "public/assets/creatures/grafu-castle/hm-male-image-585704.glb" + }, + { + "name": "actor hm-male-image-585704 checksum", + "passed": true, + "detail": "7659a1aa2227f98f2ab68d6ffa8b608fda51b9fa72bd987242392e2aca8e5291" + }, + { + "name": "actor hm-male-image-585704 size", + "passed": true, + "detail": "9421384" + }, + { + "name": "actor hm-male-image-585705 exists", + "passed": true, + "detail": "public/assets/creatures/grafu-castle/hm-male-image-585705.glb" + }, + { + "name": "actor hm-male-image-585705 checksum", + "passed": true, + "detail": "2c43c2ae657b4e4347ff9e319080e8d9c44ccaf6c7cc15251230e02abe63c2cd" + }, + { + "name": "actor hm-male-image-585705 size", + "passed": true, + "detail": "9569888" + }, + { + "name": "actor karth exists", + "passed": true, + "detail": "public/assets/creatures/grafu-castle/karth.glb" + }, + { + "name": "actor karth checksum", + "passed": true, + "detail": "ac96b24461b39cdfae7810097dbe6698f0d7ce1afe235df04d72144dceef893d" + }, + { + "name": "actor karth size", + "passed": true, + "detail": "1993572" }, { "name": "actor karth-01 exists", @@ -217,12 +332,12 @@ { "name": "actor karth-01 checksum", "passed": true, - "detail": "867b8c610f1cbb4bfc52ded7d8010abcb3585592ac501b1af0792a5721ba4d3c" + "detail": "ee1c1e6937338dff99dad4a8863a03ad7d864a55120271e996a44d6cb83f07ae" }, { "name": "actor karth-01 size", "passed": true, - "detail": "1777388" + "detail": "1776716" }, { "name": "actor milk-scarab-blue exists", @@ -232,12 +347,72 @@ { "name": "actor milk-scarab-blue checksum", "passed": true, - "detail": "2392d45c63b193f2266e7c0c6e9dcc192fd860caac354395bf02a634f046dae5" + "detail": "dde6938b52a45a77f07da63ca26172c66c49f3c10ceb10f6bda17aa3d0fba57d" }, { "name": "actor milk-scarab-blue size", "passed": true, - "detail": "879492" + "detail": "879476" + }, + { + "name": "actor mount-millenium-chimera-laby exists", + "passed": true, + "detail": "public/assets/creatures/grafu-castle/mount-millenium-chimera-laby.glb" + }, + { + "name": "actor mount-millenium-chimera-laby checksum", + "passed": true, + "detail": "7cb4ae344d3a46094b96faebdc6274eace04eb894de64ee87ac7bb7a1309b321" + }, + { + "name": "actor mount-millenium-chimera-laby size", + "passed": true, + "detail": "1217348" + }, + { + "name": "actor enraged-big-bear-purple exists", + "passed": true, + "detail": "public/assets/creatures/grafu-castle/enraged-big-bear-purple.glb" + }, + { + "name": "actor enraged-big-bear-purple checksum", + "passed": true, + "detail": "36738dc204f6d574f56ee41044d61ae88a885d0dc4b411f7ea70d76e84d48a4b" + }, + { + "name": "actor enraged-big-bear-purple size", + "passed": true, + "detail": "935312" + }, + { + "name": "actor enraged-big-bear-cream exists", + "passed": true, + "detail": "public/assets/creatures/grafu-castle/enraged-big-bear-cream.glb" + }, + { + "name": "actor enraged-big-bear-cream checksum", + "passed": true, + "detail": "1e327d246fdcebf9b35891f37ccc3aed3ff7bfb4f2c7355c1381d7e0231e956c" + }, + { + "name": "actor enraged-big-bear-cream size", + "passed": true, + "detail": "941160" + }, + { + "name": "actor rafael-bakderr exists", + "passed": true, + "detail": "public/assets/creatures/grafu-castle/rafael-bakderr.glb" + }, + { + "name": "actor rafael-bakderr checksum", + "passed": true, + "detail": "06e87d84a7cb3e15d610b0f18528cb1331c80db4597c67c49267aa4a627386dc" + }, + { + "name": "actor rafael-bakderr size", + "passed": true, + "detail": "1705044" }, { "name": "Khronos validation", diff --git a/src/assets/game/dungeons/grafu-castle/mechanics-audit.json b/src/assets/game/dungeons/grafu-castle/mechanics-audit.json index 37333e82..7389cf8c 100644 --- a/src/assets/game/dungeons/grafu-castle/mechanics-audit.json +++ b/src/assets/game/dungeons/grafu-castle/mechanics-audit.json @@ -11,8 +11,120 @@ "dungeonConfigId": "grafu_castle" }, "configuredBossTemplateIds": [], + "reviewedBossTemplateIds": [ + 105204, + 105224, + 105351, + 105542, + 105543, + 105616 + ], "configuredBossPositions": {}, "bossRows": [], + "scriptedBossSpawns": [ + { + "scriptedPlacementId": "cayus", + "sourceAnchorSpawnId": 41927224, + "controllerSpawnId": 41928351, + "templateId": 105616, + "name": "Cayus", + "sourcePosition": [ + -348.7113952636719, + 1254.5040283203125, + -999.6005249023438 + ], + "sourceScriptRoot": "Dy_Z19_BOSSN_AIC", + "objective": true, + "evidence": [ + "Resource/luascript/02345.lua:CreateObjByFlag(105616,780696,1,1)" + ] + }, + { + "scriptedPlacementId": "ardmond", + "sourceAnchorSpawnId": 41926939, + "controllerSpawnId": 41928345, + "templateId": 105542, + "name": "Ardmond", + "sourcePosition": [ + -1427.9969482421875, + 1133.35498046875, + -1353.3699951171875 + ], + "sourceScriptRoot": "kg_105542_01", + "objective": true, + "evidence": [ + "Resource/luascript/02324.lua:kg_105542_CreateBoss" + ] + }, + { + "scriptedPlacementId": "issac", + "sourceAnchorSpawnId": 41926939, + "controllerSpawnId": 41928345, + "templateId": 105543, + "name": "Issac", + "sourcePosition": [ + -1427.9969482421875, + 1133.35498046875, + -1313.3699951171875 + ], + "sourceScriptRoot": "kg_105542_01", + "objective": false, + "evidence": [ + "Resource/luascript/02324.lua:kg_105542_CreateBoss" + ] + }, + { + "scriptedPlacementId": "boro-boro-brothers", + "sourceAnchorSpawnId": 41926924, + "controllerSpawnId": 41927347, + "templateId": 105224, + "name": "Boro-Boro Brothers", + "sourcePosition": [ + -2172.174072265625, + 1095.10400390625, + -22.212709426879883 + ], + "sourceScriptRoot": "ic_105224_01", + "objective": true, + "evidence": [ + "Resource/luascript/02231.lua:CreateObjByFlag(105224,780775,1,1)" + ] + }, + { + "scriptedPlacementId": "conjurer-chapeaunoir", + "sourceAnchorSpawnId": 41926979, + "controllerSpawnId": 41927358, + "templateId": 105204, + "name": "Conjurer Chapeaunoir", + "sourcePosition": [ + -1732.9649658203125, + 914.618896484375, + 1190.8310546875 + ], + "sourceScriptRoot": "cl_136_105205_N", + "objective": true, + "evidence": [ + "Resource/luascript/02330.lua:CreateObjByFlag(105204,780767,0,1)" + ] + }, + { + "scriptedPlacementId": "larvanger-barkud", + "sourceAnchorSpawnId": 41927071, + "controllerSpawnId": 41927369, + "templateId": 105351, + "name": "Larvanger Barkud", + "sourcePosition": [ + -305.8063049316406, + 902.5551147460938, + 941.4002075195312 + ], + "sourceScriptRoot": "un_zone19_boss5_allconreal_1_01", + "objective": true, + "evidence": [ + "Resource/luascript/02297.lua:CreateObjByFlag(105351,780838,1,1)" + ] + } + ], "luaSources": [], "luaSignals": [], "templateCandidates": [ @@ -94,6 +206,18 @@ "templateScripts": [], "spells": [] }, + { + "templateId": 105204, + "name": "Conjurer Chapeaunoir", + "classification": "boss", + "spawnCount": 0, + "autoPlots": [ + "cl_136_105205_N" + ], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, { "templateId": 105205, "name": "Template 105205", @@ -109,7 +233,7 @@ { "templateId": 105210, "name": "Template 105210", - "classification": "combat", + "classification": "deferred-object", "spawnCount": 1, "autoPlots": [ "kg_105542_01" @@ -204,10 +328,82 @@ "templateScripts": [], "spells": [] }, + { + "templateId": 105224, + "name": "Boro-Boro Brothers", + "classification": "boss", + "spawnCount": 0, + "autoPlots": [ + "ic_105224_01" + ], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, + { + "templateId": 105229, + "name": "Smirking Castle Clown", + "classification": "combat", + "spawnCount": 0, + "autoPlots": [], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, + { + "templateId": 105230, + "name": "Odd Castle Clown", + "classification": "combat", + "spawnCount": 0, + "autoPlots": [], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, + { + "templateId": 105233, + "name": "Cursed Steel Soldier", + "classification": "combat", + "spawnCount": 0, + "autoPlots": [], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, + { + "templateId": 105235, + "name": "Conjurer's Apprentice", + "classification": "combat", + "spawnCount": 0, + "autoPlots": [], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, + { + "templateId": 105236, + "name": "Giant Enraged Bear", + "classification": "combat", + "spawnCount": 0, + "autoPlots": [], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, + { + "templateId": 105237, + "name": "Manipulated Bear", + "classification": "combat", + "spawnCount": 0, + "autoPlots": [], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, { "templateId": 105254, "name": "Template 105254", - "classification": "combat", + "classification": "deferred-object", "spawnCount": 10, "autoPlots": [ "cl_136_01_01_N", @@ -225,7 +421,7 @@ { "templateId": 105258, "name": "Template 105258", - "classification": "combat", + "classification": "deferred-object", "spawnCount": 5, "autoPlots": [ "cl_105258_1", @@ -265,6 +461,42 @@ "templateScripts": [], "spells": [] }, + { + "templateId": 105351, + "name": "Larvanger Barkud", + "classification": "boss", + "spawnCount": 0, + "autoPlots": [ + "un_zone19_boss5_allconreal_1_01" + ], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, + { + "templateId": 105542, + "name": "Ardmond", + "classification": "boss", + "spawnCount": 0, + "autoPlots": [ + "kg_105542_01" + ], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, + { + "templateId": 105543, + "name": "Issac", + "classification": "boss", + "spawnCount": 0, + "autoPlots": [ + "kg_105542_01" + ], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, { "templateId": 105596, "name": "Smirking Castle Clown", @@ -332,6 +564,16 @@ } ] }, + { + "templateId": 105599, + "name": "Cursed Steel Soldier", + "classification": "combat", + "spawnCount": 0, + "autoPlots": [], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, { "templateId": 105600, "name": "Juggler's Apprentice", @@ -420,6 +662,18 @@ } ] }, + { + "templateId": 105616, + "name": "Cayus", + "classification": "boss", + "spawnCount": 0, + "autoPlots": [ + "Dy_Z19_BOSSN_AIC" + ], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, { "templateId": 116434, "name": "Skeleton of an Adventurer", diff --git a/src/assets/game/dungeons/grafu-castle/population-import-report.json b/src/assets/game/dungeons/grafu-castle/population-import-report.json index 825c8034..6ac2ce52 100644 --- a/src/assets/game/dungeons/grafu-castle/population-import-report.json +++ b/src/assets/game/dungeons/grafu-castle/population-import-report.json @@ -3,14 +3,26 @@ "dungeonId": "grafu-castle", "zoneId": 137, "sourceRowCount": 249, - "importedSpawnCount": 56, - "excludedRowCount": 193, - "bossSpawnCount": 0, + "importedSpawnCount": 84, + "scriptedSpawnCount": 44, + "excludedRowCount": 209, + "bossSpawnCount": 5, "objectiveOrder": { - "authoritative": false, - "kind": "healerMan-distance-derived", + "authoritative": true, + "kind": "source-authored", "evidence": [ - "scripts/runewaker-pipeline/build-population.mjs" + "Resource/luascript/02345.lua:Dy_Z19_BOSSN_AIC", + "Resource/luascript/02324.lua:kg_105542_01", + "Resource/luascript/02231.lua:ic_105224_01", + "Resource/luascript/02330.lua:cl_136_105205_N", + "Resource/luascript/02297.lua:un_zone19_boss5_allconreal_1_01" + ], + "sourceSpawnIds": [ + 41927224, + 41926939, + 41926924, + 41926979, + 41927071 ] }, "sourceSnapshot": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -30,7 +42,7 @@ "navmeshProjection": { "maximumAllowedDistance": 4, "maximumObservedDistance": 35.149104, - "maximumCombatDistance": 3.385892, + "maximumCombatDistance": 0.998158, "deferredRowsBeyondCombatLimit": 11 }, "runeWakerIdsAreNamespaced": true, @@ -41,7 +53,7 @@ "status": "green", "recipe": "scripts/runewaker-pipeline/recipes/grafu-castle-population.json", "actorManifest": "public/assets/creatures/grafu-castle/manifest.json", - "entityCount": 11, + "entityCount": 21, "countsByTemplate": { "102678": 1, "102680": 10, @@ -50,6 +62,7 @@ "104810": 1, "105100": 1, "105201": 1, + "105204": 1, "105205": 1, "105210": 1, "105211": 3, @@ -59,17 +72,29 @@ "105215": 1, "105216": 5, "105217": 7, + "105224": 1, + "105229": 6, + "105230": 3, + "105233": 2, + "105235": 4, + "105236": 4, + "105237": 2, "105254": 10, "105258": 5, "105262": 18, "105305": 2, - "105596": 1, - "105597": 6, - "105598": 4, + "105351": 1, + "105542": 1, + "105543": 1, + "105596": 2, + "105597": 8, + "105598": 11, + "105599": 3, "105600": 10, - "105601": 7, + "105601": 11, "105603": 7, "105604": 1, + "105616": 1, "116434": 1, "117547": 1, "780696": 4, @@ -86,13 +111,60 @@ "780873": 16, "780877": 10 }, - "bosses": [], + "bosses": [ + { + "id": "rw-grafu-castle-scripted-cayus", + "name": "Cayus", + "position": [ + 6.029333, + -1.971602, + 92.880381 + ] + }, + { + "id": "rw-grafu-castle-scripted-ardmond", + "name": "Ardmond and Issac", + "position": [ + -101.899784, + -14.174433, + 128.257272 + ] + }, + { + "id": "rw-grafu-castle-scripted-boro-boro-brothers", + "name": "Boro-Boro Brothers", + "position": [ + -176.317575, + -17.936858, + -4.856412 + ] + }, + { + "id": "rw-grafu-castle-scripted-conjurer-chapeaunoir", + "name": "Conjurer Chapeaunoir", + "position": [ + -132.396149, + -35.924047, + -126.162808 + ] + }, + { + "id": "rw-grafu-castle-scripted-larvanger-barkud", + "name": "Larvanger Barkud", + "position": [ + 10.319202, + -37.197609, + -101.219726 + ] + } + ], "excluded": [ { "sourceSpawnId": 41926923, "sourceTemplateId": 780775, "name": "Template 780775", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2182.1298828125, 1083.1910400390625, @@ -117,6 +189,7 @@ "sourceTemplateId": 780775, "name": "Template 780775", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2172.174072265625, 1095.10400390625, @@ -141,6 +214,7 @@ "sourceTemplateId": 780775, "name": "Template 780775", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1727.301025390625, 1084.635009765625, @@ -165,6 +239,7 @@ "sourceTemplateId": 780775, "name": "Template 780775", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2053.947021484375, 1083.1910400390625, @@ -189,6 +264,7 @@ "sourceTemplateId": 780862, "name": "Template 780862", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1000.781982421875, 1214.0450439453125, @@ -213,6 +289,7 @@ "sourceTemplateId": 780862, "name": "Template 780862", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1287.64697265625, 1135.7530517578125, @@ -237,6 +314,7 @@ "sourceTemplateId": 780862, "name": "Template 780862", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1427.9969482421875, 1133.35498046875, @@ -261,6 +339,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1805.9129638671875, 914.618896484375, @@ -285,6 +364,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1827.0909423828125, 916.2025146484375, @@ -309,6 +389,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1827.3089599609375, 916.2025146484375, @@ -333,6 +414,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1772.8489990234375, 916.2025146484375, @@ -357,6 +439,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1732.821044921875, 916.2025146484375, @@ -381,6 +464,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1809.6669921875, 916.2025146484375, @@ -405,6 +489,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1874.4859619140625, 916.2025146484375, @@ -429,6 +514,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1914.342041015625, 916.2025146484375, @@ -453,6 +539,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2120.90087890625, 937.9705810546875, @@ -477,6 +564,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2081.010986328125, 933.9080810546875, @@ -501,6 +589,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1633.0400390625, 914.618896484375, @@ -525,6 +614,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1534, 926.208984375, @@ -549,6 +639,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1732.9649658203125, 914.618896484375, @@ -573,6 +664,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1854.718994140625, 914.618896484375, @@ -597,6 +689,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1881.865966796875, 914.618896484375, @@ -621,6 +714,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1853.071044921875, 914.618896484375, @@ -645,6 +739,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1811.6209716796875, 914.618896484375, @@ -669,6 +764,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1744.3990478515625, 914.618896484375, @@ -693,6 +789,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1665.9739990234375, 914.618896484375, @@ -717,6 +814,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1702.0450439453125, 914.618896484375, @@ -741,6 +839,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1748.81005859375, 914.618896484375, @@ -765,6 +864,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1803.5460205078125, 914.618896484375, @@ -789,6 +889,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1760.3179931640625, 914.618896484375, @@ -813,6 +914,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1799.60205078125, 914.618896484375, @@ -837,6 +939,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1840.5870361328125, 914.618896484375, @@ -861,6 +964,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1912.1199951171875, 914.618896484375, @@ -885,6 +989,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1962.6639404296875, 914.618896484375, @@ -909,6 +1014,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1918.9189453125, 914.618896484375, @@ -933,6 +1039,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1674.6800537109375, 914.618896484375, @@ -957,6 +1064,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1645.2640380859375, 914.618896484375, @@ -981,6 +1089,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1588.6610107421875, 914.618896484375, @@ -1005,6 +1114,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1647.2659912109375, 914.618896484375, @@ -1029,6 +1139,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1697.949951171875, 914.618896484375, @@ -1053,6 +1164,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1590.822998046875, 914.618896484375, @@ -1077,6 +1189,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1656.7860107421875, 914.618896484375, @@ -1101,6 +1214,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1702.39794921875, 914.618896484375, @@ -1125,6 +1239,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1788.9150390625, 914.618896484375, @@ -1149,6 +1264,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1857.4599609375, 914.618896484375, @@ -1173,6 +1289,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1903.3270263671875, 914.618896484375, @@ -1197,6 +1314,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1974.654052734375, 914.618896484375, @@ -1221,6 +1339,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1977.2650146484375, 914.618896484375, @@ -1245,6 +1364,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1908.9239501953125, 914.618896484375, @@ -1269,6 +1389,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1816.176025390625, 914.618896484375, @@ -1293,6 +1414,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1908.7060546875, 914.618896484375, @@ -1317,6 +1439,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1860.530029296875, 914.618896484375, @@ -1341,6 +1464,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1752.592041015625, 914.618896484375, @@ -1365,6 +1489,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1701.073974609375, 914.618896484375, @@ -1389,6 +1514,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1676.2110595703125, 914.618896484375, @@ -1413,6 +1539,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1713.5489501953125, 914.618896484375, @@ -1437,6 +1564,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1766.81103515625, 914.618896484375, @@ -1461,6 +1589,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1828.3070068359375, 914.618896484375, @@ -1485,6 +1614,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1891.9119873046875, 914.618896484375, @@ -1509,6 +1639,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1941.27294921875, 914.618896484375, @@ -1533,6 +1664,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1971.949951171875, 914.618896484375, @@ -1557,6 +1689,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1976.5050048828125, 914.618896484375, @@ -1581,6 +1714,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1943.29296875, 914.618896484375, @@ -1605,6 +1739,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 743.4937133789062, 984.4315795898438, @@ -1629,6 +1764,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -351.3973083496094, 906.6185302734375, @@ -1653,6 +1789,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -390.135986328125, 906.5264282226562, @@ -1677,6 +1814,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -505.03240966796875, 906.5264282226562, @@ -1701,6 +1839,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -545.223876953125, 906.5264282226562, @@ -1725,6 +1864,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -511.6268005371094, 906.5264282226562, @@ -1749,6 +1889,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -408.2489013671875, 906.5264282226562, @@ -1773,6 +1914,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -340.3716125488281, 904.3580932617188, @@ -1797,6 +1939,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -337.51519775390625, 904.417724609375, @@ -1821,6 +1964,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -648.3626708984375, 906.5264282226562, @@ -1845,6 +1989,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -706.1317138671875, 906.5264282226562, @@ -1869,6 +2014,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -653.905517578125, 906.5264282226562, @@ -1893,6 +2039,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -550.2545166015625, 906.5264282226562, @@ -1917,6 +2064,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -335.3656921386719, 904.3580932617188, @@ -1941,6 +2089,7 @@ "sourceTemplateId": 780838, "name": "Template 780838", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -476.6326904296875, 894.2999267578125, @@ -1965,6 +2114,7 @@ "sourceTemplateId": 780838, "name": "Template 780838", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -305.8063049316406, 902.5551147460938, @@ -1989,6 +2139,7 @@ "sourceTemplateId": 780838, "name": "Template 780838", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -369.94970703125, 896.0604858398438, @@ -2013,6 +2164,7 @@ "sourceTemplateId": 780838, "name": "Template 780838", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -637.3568725585938, 894.1340942382812, @@ -2037,6 +2189,7 @@ "sourceTemplateId": 780838, "name": "Template 780838", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -638.5198974609375, 894.1340942382812, @@ -2061,6 +2214,7 @@ "sourceTemplateId": 780838, "name": "Template 780838", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -331.19781494140625, 902.5892944335938, @@ -2085,6 +2239,7 @@ "sourceTemplateId": 780838, "name": "Template 780838", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -766.8765869140625, 897.0023193359375, @@ -2109,6 +2264,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 58.72774887084961, 1247.510009765625, @@ -2133,6 +2289,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 79.13754272460938, 1247.093017578125, @@ -2157,6 +2314,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 204.93960571289062, 1247.303955078125, @@ -2181,6 +2339,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 246.2657928466797, 1247.303955078125, @@ -2205,6 +2364,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1053.1820068359375, 873.4503173828125, @@ -2229,6 +2389,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1132.4580078125, 872.7766723632812, @@ -2253,6 +2414,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1158.8299560546875, 872.8452758789062, @@ -2277,6 +2439,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -997.169921875, 872.8452758789062, @@ -2301,6 +2464,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -955.1574096679688, 872.8452758789062, @@ -2325,6 +2489,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -946.0150146484375, 872.8643188476562, @@ -2349,6 +2514,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1050.7430419921875, 872.902587890625, @@ -2373,6 +2539,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -928.24267578125, 872.8643188476562, @@ -2397,6 +2564,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -957.722900390625, 872.8206176757812, @@ -2421,6 +2589,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1198.18896484375, 872.8643188476562, @@ -2445,6 +2614,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1174.6729736328125, 872.8643188476562, @@ -2469,6 +2639,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1331.2030029296875, 898.2780151367188, @@ -2493,6 +2664,7 @@ "sourceTemplateId": 780845, "name": "Template 780845", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -148.19839477539062, 1255.9119873046875, @@ -2517,6 +2689,7 @@ "sourceTemplateId": 780845, "name": "Template 780845", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -91.34905242919922, 1254.5040283203125, @@ -2541,6 +2714,7 @@ "sourceTemplateId": 780845, "name": "Template 780845", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -203.7666015625, 1254.5040283203125, @@ -2565,6 +2739,7 @@ "sourceTemplateId": 780845, "name": "Template 780845", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -321.1253967285156, 1254.5040283203125, @@ -2589,6 +2764,7 @@ "sourceTemplateId": 780845, "name": "Template 780845", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -91.53121185302734, 1254.5040283203125, @@ -2613,6 +2789,7 @@ "sourceTemplateId": 780845, "name": "Template 780845", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -200.9969940185547, 1254.5040283203125, @@ -2637,6 +2814,7 @@ "sourceTemplateId": 780845, "name": "Template 780845", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -319.010498046875, 1254.5040283203125, @@ -2661,6 +2839,7 @@ "sourceTemplateId": 780845, "name": "Template 780845", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -91.56681060791016, 1254.5040283203125, @@ -2685,6 +2864,7 @@ "sourceTemplateId": 780845, "name": "Template 780845", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -201.54150390625, 1254.5040283203125, @@ -2709,6 +2889,7 @@ "sourceTemplateId": 780845, "name": "Template 780845", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -319.1648864746094, 1254.5040283203125, @@ -2733,6 +2914,7 @@ "sourceTemplateId": 780859, "name": "Template 780859", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -152.81179809570312, 1255.697021484375, @@ -2757,6 +2939,7 @@ "sourceTemplateId": 780859, "name": "Template 780859", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -204.6466064453125, 1254.5040283203125, @@ -2781,6 +2964,7 @@ "sourceTemplateId": 780859, "name": "Template 780859", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -204.60769653320312, 1254.5040283203125, @@ -2805,6 +2989,7 @@ "sourceTemplateId": 780860, "name": "Template 780860", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -10.394370079040527, 1255.89599609375, @@ -2829,6 +3014,7 @@ "sourceTemplateId": 780860, "name": "Template 780860", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -462.7861022949219, 1252.4169921875, @@ -2853,6 +3039,7 @@ "sourceTemplateId": 780860, "name": "Template 780860", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 59.795719146728516, 1247.31005859375, @@ -2877,6 +3064,7 @@ "sourceTemplateId": 780860, "name": "Template 780860", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -218.03109741210938, 1254.5040283203125, @@ -2901,6 +3089,7 @@ "sourceTemplateId": 780860, "name": "Template 780860", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -377, 1254.5, @@ -2925,6 +3114,7 @@ "sourceTemplateId": 780860, "name": "Template 780860", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -31, 1254.5, @@ -2949,6 +3139,7 @@ "sourceTemplateId": 780860, "name": "Template 780860", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -205, 1254.5040283203125, @@ -2973,6 +3164,7 @@ "sourceTemplateId": 780860, "name": "Template 780860", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -205, 1254.5040283203125, @@ -2997,6 +3189,7 @@ "sourceTemplateId": 780877, "name": "Template 780877", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -397.3038024902344, 1254.5040283203125, @@ -3021,6 +3214,7 @@ "sourceTemplateId": 780877, "name": "Template 780877", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -90, 1254.5, @@ -3045,6 +3239,7 @@ "sourceTemplateId": 780877, "name": "Template 780877", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -205, 1254.5, @@ -3069,6 +3264,7 @@ "sourceTemplateId": 780877, "name": "Template 780877", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -320, 1254.5, @@ -3093,6 +3289,7 @@ "sourceTemplateId": 780877, "name": "Template 780877", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -90, 1254.5, @@ -3117,6 +3314,7 @@ "sourceTemplateId": 780877, "name": "Template 780877", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -205, 1254.5, @@ -3141,6 +3339,7 @@ "sourceTemplateId": 780877, "name": "Template 780877", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -320, 1254.5, @@ -3165,6 +3364,7 @@ "sourceTemplateId": 780877, "name": "Template 780877", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -90, 1254.5, @@ -3189,6 +3389,7 @@ "sourceTemplateId": 780877, "name": "Template 780877", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -205, 1254.5, @@ -3213,6 +3414,7 @@ "sourceTemplateId": 780877, "name": "Template 780877", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -320, 1254.5, @@ -3237,6 +3439,7 @@ "sourceTemplateId": 780696, "name": "Template 780696", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -265.4382019042969, 1254.75, @@ -3261,6 +3464,7 @@ "sourceTemplateId": 780696, "name": "Template 780696", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -348.7113952636719, 1254.5040283203125, @@ -3285,6 +3489,7 @@ "sourceTemplateId": 780696, "name": "Template 780696", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -205.31700134277344, 1304.5040283203125, @@ -3309,6 +3514,7 @@ "sourceTemplateId": 780696, "name": "Template 780696", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -204.1614990234375, 1304.56005859375, @@ -3333,6 +3539,7 @@ "sourceTemplateId": 116434, "name": "Skeleton of an Adventurer", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1903.9189453125, 1085.29296875, @@ -3357,6 +3564,7 @@ "sourceTemplateId": 105213, "name": "Template 105213", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1941.56005859375, 1119.3740234375, @@ -3381,6 +3589,7 @@ "sourceTemplateId": 105213, "name": "Template 105213", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1898.927001953125, 1139.9639892578125, @@ -3405,6 +3614,7 @@ "sourceTemplateId": 105213, "name": "Template 105213", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1962.85205078125, 1082.35498046875, @@ -3429,6 +3639,7 @@ "sourceTemplateId": 105213, "name": "Template 105213", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1994.2139892578125, 1118.0260009765625, @@ -3453,6 +3664,7 @@ "sourceTemplateId": 105214, "name": "Template 105214", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2102.72900390625, 1142.6929931640625, @@ -3477,6 +3689,7 @@ "sourceTemplateId": 105215, "name": "Template 105215", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1834.5050048828125, 1110.7139892578125, @@ -3501,6 +3714,7 @@ "sourceTemplateId": 105216, "name": "Template 105216", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2007.4150390625, 1092.60400390625, @@ -3525,6 +3739,7 @@ "sourceTemplateId": 105216, "name": "Template 105216", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1847, 1095, @@ -3549,6 +3764,7 @@ "sourceTemplateId": 105216, "name": "Template 105216", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2026, 1091, @@ -3573,6 +3789,7 @@ "sourceTemplateId": 105216, "name": "Template 105216", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1937.64599609375, 1091.6409912109375, @@ -3597,6 +3814,7 @@ "sourceTemplateId": 105216, "name": "Template 105216", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1933, 1083, @@ -3621,6 +3839,7 @@ "sourceTemplateId": 105217, "name": "Template 105217", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2023, 1083, @@ -3645,6 +3864,7 @@ "sourceTemplateId": 105217, "name": "Template 105217", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2172.09912109375, 1085.552001953125, @@ -3669,6 +3889,7 @@ "sourceTemplateId": 105217, "name": "Template 105217", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2035, 1083, @@ -3693,6 +3914,7 @@ "sourceTemplateId": 105217, "name": "Template 105217", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1871, 1083, @@ -3717,6 +3939,7 @@ "sourceTemplateId": 105217, "name": "Template 105217", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2148, 1083, @@ -3741,6 +3964,7 @@ "sourceTemplateId": 105217, "name": "Template 105217", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1963, 1083, @@ -3765,6 +3989,7 @@ "sourceTemplateId": 105217, "name": "Template 105217", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1945, 1083, @@ -3789,6 +4014,7 @@ "sourceTemplateId": 103120, "name": "Template 103120", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 506, 1269, @@ -3813,6 +4039,7 @@ "sourceTemplateId": 104324, "name": "Template 104324", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1935.8349609375, 1091.4599609375, @@ -3837,6 +4064,7 @@ "sourceTemplateId": 105205, "name": "Template 105205", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2047, 914.618896484375, @@ -3861,6 +4089,7 @@ "sourceTemplateId": 105100, "name": "Template 105100", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -330, 896.102294921875, @@ -3880,11 +4109,262 @@ "projectionDistance": 0.094018, "modelPath": "model/character/creature/milk_scarab/act_milk_scarab_blue.ros" }, + { + "sourceSpawnId": 41927381, + "sourceTemplateId": 105254, + "name": "Template 105254", + "classification": "deferred-object", + "sourceClassification": "combat", + "sourcePosition": [ + 126.5990982055664, + 1247.093017578125, + -998.6713256835938 + ], + "sourceDirection": 179.87330627441406, + "localPosition": [ + 53.559742, + -2.792553, + 92.787428 + ], + "projectedPosition": [ + 53.559575, + -2.777278, + 92.787626 + ], + "projectionDistance": 0.015277, + "modelPath": "model/character/creature/elder_myconid/act_elder_myconid.ros" + }, + { + "sourceSpawnId": 41927382, + "sourceTemplateId": 105254, + "name": "Template 105254", + "classification": "deferred-object", + "sourceClassification": "combat", + "sourcePosition": [ + -625.5192260742188, + 1270.3790283203125, + -1043.7130126953125 + ], + "sourceDirection": 269.88189697265625, + "localPosition": [ + -21.65209, + -0.463952, + 97.291596 + ], + "projectedPosition": [ + -21.65209, + -0.397606, + 97.291596 + ], + "projectionDistance": 0.066346, + "modelPath": "model/character/creature/elder_myconid/act_elder_myconid.ros" + }, + { + "sourceSpawnId": 41927384, + "sourceTemplateId": 105254, + "name": "Template 105254", + "classification": "deferred-object", + "sourceClassification": "combat", + "sourcePosition": [ + -1346.3790283203125, + 1083.9310302734375, + -430.9111022949219 + ], + "sourceDirection": 89.27565002441406, + "localPosition": [ + -93.738071, + -19.108752, + 36.011405 + ], + "projectedPosition": [ + -93.738071, + -19.097607, + 36.011405 + ], + "projectionDistance": 0.011145, + "modelPath": "model/character/creature/elder_myconid/act_elder_myconid.ros" + }, + { + "sourceSpawnId": 41927385, + "sourceTemplateId": 105254, + "name": "Template 105254", + "classification": "deferred-object", + "sourceClassification": "combat", + "sourcePosition": [ + -1287.2139892578125, + 1083.9310302734375, + -478.3273010253906 + ], + "sourceDirection": 180.14939880371094, + "localPosition": [ + -87.821567, + -19.108752, + 40.753025 + ], + "projectedPosition": [ + -87.821567, + -19.097607, + 40.753025 + ], + "projectionDistance": 0.011145, + "modelPath": "model/character/creature/elder_myconid/act_elder_myconid.ros" + }, + { + "sourceSpawnId": 41927386, + "sourceTemplateId": 105254, + "name": "Template 105254", + "classification": "deferred-object", + "sourceClassification": "combat", + "sourcePosition": [ + -2126.1279296875, + 1095.2130126953125, + 361.4753112792969 + ], + "sourceDirection": 0.1327040046453476, + "localPosition": [ + -171.712961, + -17.980554, + -43.227236 + ], + "projectedPosition": [ + -171.712961, + -17.89761, + -43.227236 + ], + "projectionDistance": 0.082944, + "modelPath": "model/character/creature/elder_myconid/act_elder_myconid.ros" + }, + { + "sourceSpawnId": 41927387, + "sourceTemplateId": 105254, + "name": "Template 105254", + "classification": "deferred-object", + "sourceClassification": "combat", + "sourcePosition": [ + -2120.341064453125, + 984.0703735351562, + 916.98681640625 + ], + "sourceDirection": 86.2711410522461, + "localPosition": [ + -171.134274, + -29.094818, + -98.778387 + ], + "projectedPosition": [ + -171.130459, + -28.975774, + -98.808759 + ], + "projectionDistance": 0.122916, + "modelPath": "model/character/creature/elder_myconid/act_elder_myconid.ros" + }, + { + "sourceSpawnId": 41927391, + "sourceTemplateId": 105254, + "name": "Template 105254", + "classification": "deferred-object", + "sourceClassification": "combat", + "sourcePosition": [ + 760, + 994.526611328125, + 1683 + ], + "sourceDirection": 89, + "localPosition": [ + 116.899832, + -28.049194, + -175.379705 + ], + "projectedPosition": [ + 116.899832, + -27.977214, + -175.378701 + ], + "projectionDistance": 0.071987, + "modelPath": "model/character/creature/elder_myconid/act_elder_myconid.ros" + }, + { + "sourceSpawnId": 41927392, + "sourceTemplateId": 105254, + "name": "Template 105254", + "classification": "deferred-object", + "sourceClassification": "combat", + "sourcePosition": [ + 331.314208984375, + 1247.303955078125, + -1148.9410400390625 + ], + "sourceDirection": 269.99481201171875, + "localPosition": [ + 74.031253, + -2.771459, + 107.814399 + ], + "projectedPosition": [ + 74.031253, + -2.779222, + 107.81483 + ], + "projectionDistance": 0.007774, + "modelPath": "model/character/creature/elder_myconid/act_elder_myconid.ros" + }, + { + "sourceSpawnId": 41927395, + "sourceTemplateId": 105254, + "name": "Template 105254", + "classification": "deferred-object", + "sourceClassification": "combat", + "sourcePosition": [ + 890, + 994.526611328125, + 1700 + ], + "sourceDirection": 181, + "localPosition": [ + 129.899832, + -28.049194, + -177.079705 + ], + "projectedPosition": [ + 129.899832, + -27.997608, + -177.079705 + ], + "projectionDistance": 0.051586, + "modelPath": "model/character/creature/elder_myconid/act_elder_myconid.ros" + }, + { + "sourceSpawnId": 41927396, + "sourceTemplateId": 105254, + "name": "Template 105254", + "classification": "deferred-object", + "sourceClassification": "combat", + "sourcePosition": [ + 575, + 994.526611328125, + 1720 + ], + "sourceDirection": 4.999985218048096, + "localPosition": [ + 98.399832, + -28.049194, + -179.079705 + ], + "projectedPosition": [ + 98.399832, + -27.997608, + -179.079705 + ], + "projectionDistance": 0.051586, + "modelPath": "model/character/creature/elder_myconid/act_elder_myconid.ros" + }, { "sourceSpawnId": 41927407, "sourceTemplateId": 105305, "name": "Template 105305", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 177, 1247.303955078125, @@ -3909,6 +4389,7 @@ "sourceTemplateId": 105305, "name": "Template 105305", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 723, 973, @@ -3933,6 +4414,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 251, 1254.7220458984375, @@ -3957,6 +4439,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 110, 1256.6820068359375, @@ -3981,6 +4464,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -639, 1283.1500244140625, @@ -4005,6 +4489,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -600, 1270.053955078125, @@ -4029,6 +4514,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1307, 1083.9310302734375, @@ -4053,6 +4539,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1232, 872.692626953125, @@ -4077,6 +4564,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1065, 872.7698974609375, @@ -4101,6 +4589,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1114, 872.7698974609375, @@ -4125,6 +4614,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -993, 872.7698974609375, @@ -4149,6 +4639,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1158, 872.7698974609375, @@ -4173,6 +4664,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 908, 973, @@ -4197,6 +4689,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 835, 973, @@ -4221,6 +4714,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 720, 973, @@ -4245,6 +4739,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 723, 994, @@ -4269,6 +4764,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 807, 994, @@ -4293,6 +4789,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 801, 994, @@ -4317,6 +4814,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 718, 994, @@ -4341,6 +4839,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1825, 1094.9759521484375, @@ -4360,11 +4859,162 @@ "projectionDistance": 0.006652, "modelPath": "model/character/creature/elder_myconid/act_elder_myconid.ros" }, + { + "sourceSpawnId": 41927631, + "sourceTemplateId": 105258, + "name": "Template 105258", + "classification": "deferred-object", + "sourceClassification": "combat", + "sourcePosition": [ + 332, + 1255.0579833984375, + -1291 + ], + "sourceDirection": 268, + "localPosition": [ + 74.099832, + -1.996057, + 122.020295 + ], + "projectedPosition": [ + 74.099832, + -2.374497, + 122.178456 + ], + "projectionDistance": 0.410161, + "modelPath": "model/character/creature/elder_myconid/act_elder_myconid.ros" + }, + { + "sourceSpawnId": 41927632, + "sourceTemplateId": 105258, + "name": "Template 105258", + "classification": "deferred-object", + "sourceClassification": "combat", + "sourcePosition": [ + -628.2586059570312, + 1270.053955078125, + -1276.5760498046875 + ], + "sourceDirection": 156, + "localPosition": [ + -21.926028, + -0.496459, + 120.5779 + ], + "projectedPosition": [ + -21.926028, + -0.397606, + 120.5779 + ], + "projectionDistance": 0.098854, + "modelPath": "model/character/creature/elder_myconid/act_elder_myconid.ros" + }, + { + "sourceSpawnId": 41927633, + "sourceTemplateId": 105258, + "name": "Template 105258", + "classification": "deferred-object", + "sourceClassification": "combat", + "sourcePosition": [ + -1502, + 1083.9310302734375, + -479 + ], + "sourceDirection": 178, + "localPosition": [ + -109.300168, + -19.108752, + 40.820295 + ], + "projectedPosition": [ + -109.296877, + -19.059396, + 40.820295 + ], + "projectionDistance": 0.049466, + "modelPath": "model/character/creature/elder_myconid/act_elder_myconid.ros" + }, + { + "sourceSpawnId": 41927634, + "sourceTemplateId": 105258, + "name": "Template 105258", + "classification": "deferred-object", + "sourceClassification": "combat", + "sourcePosition": [ + -2171, + 1095.2130126953125, + 319 + ], + "sourceDirection": 268, + "localPosition": [ + -176.200168, + -17.980554, + -38.979705 + ], + "projectedPosition": [ + -176.200168, + -17.89761, + -38.979705 + ], + "projectionDistance": 0.082944, + "modelPath": "model/character/creature/elder_myconid/act_elder_myconid.ros" + }, + { + "sourceSpawnId": 41927635, + "sourceTemplateId": 105258, + "name": "Template 105258", + "classification": "deferred-object", + "sourceClassification": "combat", + "sourcePosition": [ + -1894, + 1051.10498046875, + 823 + ], + "sourceDirection": 189, + "localPosition": [ + -148.500168, + -22.391357, + -89.379705 + ], + "projectedPosition": [ + -148.506678, + -22.367332, + -89.380206 + ], + "projectionDistance": 0.024896, + "modelPath": "model/character/creature/elder_myconid/act_elder_myconid.ros" + }, + { + "sourceSpawnId": 41928345, + "sourceTemplateId": 105210, + "name": "Template 105210", + "classification": "deferred-object", + "sourceClassification": "combat", + "sourcePosition": [ + -1355.60302734375, + 1168.93994140625, + -1191.2989501953125 + ], + "sourceDirection": 288, + "localPosition": [ + -94.660471, + -10.607861, + 112.05019 + ], + "projectedPosition": [ + -94.484665, + -13.961191, + 111.615987 + ], + "projectionDistance": 3.385892, + "modelPath": "model/character/creature/cereus/act_cereus_01.ros" + }, { "sourceSpawnId": 41928347, "sourceTemplateId": 102678, "name": "Template 102678", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 428.7330017089844, 1269.2869873046875, @@ -4389,6 +5039,7 @@ "sourceTemplateId": 104810, "name": "Template 104810", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 429.29681396484375, 1269.2869873046875, @@ -4413,6 +5064,7 @@ "sourceTemplateId": 105604, "name": "Template 105604", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -19.17742919921875, 992.87939453125, @@ -4437,6 +5089,7 @@ "sourceTemplateId": 117547, "name": "Locked Iron Gate", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 931.364013671875, 973.02197265625, @@ -4461,6 +5114,7 @@ "sourceTemplateId": 105201, "name": "Template 105201", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -32, 1254, @@ -4485,6 +5139,7 @@ "sourceTemplateId": 102680, "name": "Template 102680", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -129.04910278320312, 1254.5040283203125, @@ -4509,6 +5164,7 @@ "sourceTemplateId": 102680, "name": "Template 102680", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -169.10569763183594, 1254.5040283203125, @@ -4533,6 +5189,7 @@ "sourceTemplateId": 102680, "name": "Template 102680", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -213.648193359375, 1254.5040283203125, @@ -4557,6 +5214,7 @@ "sourceTemplateId": 102680, "name": "Template 102680", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -244.70339965820312, 1254.5040283203125, @@ -4581,6 +5239,7 @@ "sourceTemplateId": 102680, "name": "Template 102680", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -279.0990905761719, 1254.5040283203125, @@ -4605,6 +5264,7 @@ "sourceTemplateId": 102680, "name": "Template 102680", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -237.96359252929688, 1254.5040283203125, @@ -4629,6 +5289,7 @@ "sourceTemplateId": 102680, "name": "Template 102680", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -198.71170043945312, 1254.5040283203125, @@ -4653,6 +5314,7 @@ "sourceTemplateId": 102680, "name": "Template 102680", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -158.48719787597656, 1254.5040283203125, @@ -4677,6 +5339,7 @@ "sourceTemplateId": 102680, "name": "Template 102680", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -123.072998046875, 1254.5040283203125, @@ -4701,6 +5364,7 @@ "sourceTemplateId": 102680, "name": "Template 102680", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -286.793701171875, 1254.5040283203125, @@ -4727,6 +5391,7 @@ "sourceTemplateId": 780775, "name": "Template 780775", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2182.1298828125, 1083.1910400390625, @@ -4751,6 +5416,7 @@ "sourceTemplateId": 780775, "name": "Template 780775", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2172.174072265625, 1095.10400390625, @@ -4775,6 +5441,7 @@ "sourceTemplateId": 780775, "name": "Template 780775", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1727.301025390625, 1084.635009765625, @@ -4799,6 +5466,7 @@ "sourceTemplateId": 780775, "name": "Template 780775", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2053.947021484375, 1083.1910400390625, @@ -4823,6 +5491,7 @@ "sourceTemplateId": 780862, "name": "Template 780862", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1000.781982421875, 1214.0450439453125, @@ -4847,6 +5516,7 @@ "sourceTemplateId": 780862, "name": "Template 780862", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1287.64697265625, 1135.7530517578125, @@ -4871,6 +5541,7 @@ "sourceTemplateId": 780862, "name": "Template 780862", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1427.9969482421875, 1133.35498046875, @@ -4895,6 +5566,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1805.9129638671875, 914.618896484375, @@ -4919,6 +5591,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1827.0909423828125, 916.2025146484375, @@ -4943,6 +5616,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1827.3089599609375, 916.2025146484375, @@ -4967,6 +5641,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1772.8489990234375, 916.2025146484375, @@ -4991,6 +5666,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1732.821044921875, 916.2025146484375, @@ -5015,6 +5691,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1809.6669921875, 916.2025146484375, @@ -5039,6 +5716,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1874.4859619140625, 916.2025146484375, @@ -5063,6 +5741,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1914.342041015625, 916.2025146484375, @@ -5087,6 +5766,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2120.90087890625, 937.9705810546875, @@ -5111,6 +5791,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2081.010986328125, 933.9080810546875, @@ -5135,6 +5816,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1633.0400390625, 914.618896484375, @@ -5159,6 +5841,7 @@ "sourceTemplateId": 780844, "name": "Template 780844", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1534, 926.208984375, @@ -5183,6 +5866,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1732.9649658203125, 914.618896484375, @@ -5207,6 +5891,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1854.718994140625, 914.618896484375, @@ -5231,6 +5916,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1881.865966796875, 914.618896484375, @@ -5255,6 +5941,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1853.071044921875, 914.618896484375, @@ -5279,6 +5966,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1811.6209716796875, 914.618896484375, @@ -5303,6 +5991,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1744.3990478515625, 914.618896484375, @@ -5327,6 +6016,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1665.9739990234375, 914.618896484375, @@ -5351,6 +6041,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1702.0450439453125, 914.618896484375, @@ -5375,6 +6066,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1748.81005859375, 914.618896484375, @@ -5399,6 +6091,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1803.5460205078125, 914.618896484375, @@ -5423,6 +6116,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1760.3179931640625, 914.618896484375, @@ -5447,6 +6141,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1799.60205078125, 914.618896484375, @@ -5471,6 +6166,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1840.5870361328125, 914.618896484375, @@ -5495,6 +6191,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1912.1199951171875, 914.618896484375, @@ -5519,6 +6216,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1962.6639404296875, 914.618896484375, @@ -5543,6 +6241,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1918.9189453125, 914.618896484375, @@ -5567,6 +6266,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1674.6800537109375, 914.618896484375, @@ -5591,6 +6291,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1645.2640380859375, 914.618896484375, @@ -5615,6 +6316,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1588.6610107421875, 914.618896484375, @@ -5639,6 +6341,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1647.2659912109375, 914.618896484375, @@ -5663,6 +6366,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1697.949951171875, 914.618896484375, @@ -5687,6 +6391,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1590.822998046875, 914.618896484375, @@ -5711,6 +6416,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1656.7860107421875, 914.618896484375, @@ -5735,6 +6441,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1702.39794921875, 914.618896484375, @@ -5759,6 +6466,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1788.9150390625, 914.618896484375, @@ -5783,6 +6491,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1857.4599609375, 914.618896484375, @@ -5807,6 +6516,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1903.3270263671875, 914.618896484375, @@ -5831,6 +6541,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1974.654052734375, 914.618896484375, @@ -5855,6 +6566,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1977.2650146484375, 914.618896484375, @@ -5879,6 +6591,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1908.9239501953125, 914.618896484375, @@ -5903,6 +6616,7 @@ "sourceTemplateId": 780767, "name": "Template 780767", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1816.176025390625, 914.618896484375, @@ -5927,6 +6641,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1908.7060546875, 914.618896484375, @@ -5951,6 +6666,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1860.530029296875, 914.618896484375, @@ -5975,6 +6691,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1752.592041015625, 914.618896484375, @@ -5999,6 +6716,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1701.073974609375, 914.618896484375, @@ -6023,6 +6741,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1676.2110595703125, 914.618896484375, @@ -6047,6 +6766,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1713.5489501953125, 914.618896484375, @@ -6071,6 +6791,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1766.81103515625, 914.618896484375, @@ -6095,6 +6816,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1828.3070068359375, 914.618896484375, @@ -6119,6 +6841,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1891.9119873046875, 914.618896484375, @@ -6143,6 +6866,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1941.27294921875, 914.618896484375, @@ -6167,6 +6891,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1971.949951171875, 914.618896484375, @@ -6191,6 +6916,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1976.5050048828125, 914.618896484375, @@ -6215,6 +6941,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1943.29296875, 914.618896484375, @@ -6239,6 +6966,7 @@ "sourceTemplateId": 780839, "name": "Template 780839", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 743.4937133789062, 984.4315795898438, @@ -6263,6 +6991,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -351.3973083496094, 906.6185302734375, @@ -6287,6 +7016,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -390.135986328125, 906.5264282226562, @@ -6311,6 +7041,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -505.03240966796875, 906.5264282226562, @@ -6335,6 +7066,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -545.223876953125, 906.5264282226562, @@ -6359,6 +7091,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -511.6268005371094, 906.5264282226562, @@ -6383,6 +7116,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -408.2489013671875, 906.5264282226562, @@ -6407,6 +7141,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -340.3716125488281, 904.3580932617188, @@ -6431,6 +7166,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -337.51519775390625, 904.417724609375, @@ -6455,6 +7191,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -648.3626708984375, 906.5264282226562, @@ -6479,6 +7216,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -706.1317138671875, 906.5264282226562, @@ -6503,6 +7241,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -653.905517578125, 906.5264282226562, @@ -6527,6 +7266,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -550.2545166015625, 906.5264282226562, @@ -6551,6 +7291,7 @@ "sourceTemplateId": 780793, "name": "Template 780793", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -335.3656921386719, 904.3580932617188, @@ -6575,6 +7316,7 @@ "sourceTemplateId": 780838, "name": "Template 780838", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -476.6326904296875, 894.2999267578125, @@ -6599,6 +7341,7 @@ "sourceTemplateId": 780838, "name": "Template 780838", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -305.8063049316406, 902.5551147460938, @@ -6623,6 +7366,7 @@ "sourceTemplateId": 780838, "name": "Template 780838", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -369.94970703125, 896.0604858398438, @@ -6647,6 +7391,7 @@ "sourceTemplateId": 780838, "name": "Template 780838", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -637.3568725585938, 894.1340942382812, @@ -6671,6 +7416,7 @@ "sourceTemplateId": 780838, "name": "Template 780838", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -638.5198974609375, 894.1340942382812, @@ -6695,6 +7441,7 @@ "sourceTemplateId": 780838, "name": "Template 780838", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -331.19781494140625, 902.5892944335938, @@ -6719,6 +7466,7 @@ "sourceTemplateId": 780838, "name": "Template 780838", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -766.8765869140625, 897.0023193359375, @@ -6743,6 +7491,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 58.72774887084961, 1247.510009765625, @@ -6767,6 +7516,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 79.13754272460938, 1247.093017578125, @@ -6791,6 +7541,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 204.93960571289062, 1247.303955078125, @@ -6815,6 +7566,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 246.2657928466797, 1247.303955078125, @@ -6839,6 +7591,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1053.1820068359375, 873.4503173828125, @@ -6863,6 +7616,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1132.4580078125, 872.7766723632812, @@ -6887,6 +7641,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1158.8299560546875, 872.8452758789062, @@ -6911,6 +7666,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -997.169921875, 872.8452758789062, @@ -6935,6 +7691,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -955.1574096679688, 872.8452758789062, @@ -6959,6 +7716,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -946.0150146484375, 872.8643188476562, @@ -6983,6 +7741,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1050.7430419921875, 872.902587890625, @@ -7007,6 +7766,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -928.24267578125, 872.8643188476562, @@ -7031,6 +7791,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -957.722900390625, 872.8206176757812, @@ -7055,6 +7816,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1198.18896484375, 872.8643188476562, @@ -7079,6 +7841,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1174.6729736328125, 872.8643188476562, @@ -7103,6 +7866,7 @@ "sourceTemplateId": 780873, "name": "Template 780873", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1331.2030029296875, 898.2780151367188, @@ -7127,6 +7891,7 @@ "sourceTemplateId": 780845, "name": "Template 780845", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -148.19839477539062, 1255.9119873046875, @@ -7151,6 +7916,7 @@ "sourceTemplateId": 780845, "name": "Template 780845", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -91.34905242919922, 1254.5040283203125, @@ -7175,6 +7941,7 @@ "sourceTemplateId": 780845, "name": "Template 780845", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -203.7666015625, 1254.5040283203125, @@ -7199,6 +7966,7 @@ "sourceTemplateId": 780845, "name": "Template 780845", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -321.1253967285156, 1254.5040283203125, @@ -7223,6 +7991,7 @@ "sourceTemplateId": 780845, "name": "Template 780845", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -91.53121185302734, 1254.5040283203125, @@ -7247,6 +8016,7 @@ "sourceTemplateId": 780845, "name": "Template 780845", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -200.9969940185547, 1254.5040283203125, @@ -7271,6 +8041,7 @@ "sourceTemplateId": 780845, "name": "Template 780845", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -319.010498046875, 1254.5040283203125, @@ -7295,6 +8066,7 @@ "sourceTemplateId": 780845, "name": "Template 780845", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -91.56681060791016, 1254.5040283203125, @@ -7319,6 +8091,7 @@ "sourceTemplateId": 780845, "name": "Template 780845", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -201.54150390625, 1254.5040283203125, @@ -7343,6 +8116,7 @@ "sourceTemplateId": 780845, "name": "Template 780845", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -319.1648864746094, 1254.5040283203125, @@ -7367,6 +8141,7 @@ "sourceTemplateId": 780859, "name": "Template 780859", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -152.81179809570312, 1255.697021484375, @@ -7391,6 +8166,7 @@ "sourceTemplateId": 780859, "name": "Template 780859", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -204.6466064453125, 1254.5040283203125, @@ -7415,6 +8191,7 @@ "sourceTemplateId": 780859, "name": "Template 780859", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -204.60769653320312, 1254.5040283203125, @@ -7439,6 +8216,7 @@ "sourceTemplateId": 780860, "name": "Template 780860", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -10.394370079040527, 1255.89599609375, @@ -7463,6 +8241,7 @@ "sourceTemplateId": 780860, "name": "Template 780860", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -462.7861022949219, 1252.4169921875, @@ -7487,6 +8266,7 @@ "sourceTemplateId": 780860, "name": "Template 780860", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 59.795719146728516, 1247.31005859375, @@ -7511,6 +8291,7 @@ "sourceTemplateId": 780860, "name": "Template 780860", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -218.03109741210938, 1254.5040283203125, @@ -7535,6 +8316,7 @@ "sourceTemplateId": 780860, "name": "Template 780860", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -377, 1254.5, @@ -7559,6 +8341,7 @@ "sourceTemplateId": 780860, "name": "Template 780860", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -31, 1254.5, @@ -7583,6 +8366,7 @@ "sourceTemplateId": 780860, "name": "Template 780860", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -205, 1254.5040283203125, @@ -7607,6 +8391,7 @@ "sourceTemplateId": 780860, "name": "Template 780860", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -205, 1254.5040283203125, @@ -7631,6 +8416,7 @@ "sourceTemplateId": 780877, "name": "Template 780877", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -397.3038024902344, 1254.5040283203125, @@ -7655,6 +8441,7 @@ "sourceTemplateId": 780877, "name": "Template 780877", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -90, 1254.5, @@ -7679,6 +8466,7 @@ "sourceTemplateId": 780877, "name": "Template 780877", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -205, 1254.5, @@ -7703,6 +8491,7 @@ "sourceTemplateId": 780877, "name": "Template 780877", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -320, 1254.5, @@ -7727,6 +8516,7 @@ "sourceTemplateId": 780877, "name": "Template 780877", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -90, 1254.5, @@ -7751,6 +8541,7 @@ "sourceTemplateId": 780877, "name": "Template 780877", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -205, 1254.5, @@ -7775,6 +8566,7 @@ "sourceTemplateId": 780877, "name": "Template 780877", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -320, 1254.5, @@ -7799,6 +8591,7 @@ "sourceTemplateId": 780877, "name": "Template 780877", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -90, 1254.5, @@ -7823,6 +8616,7 @@ "sourceTemplateId": 780877, "name": "Template 780877", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -205, 1254.5, @@ -7847,6 +8641,7 @@ "sourceTemplateId": 780877, "name": "Template 780877", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -320, 1254.5, @@ -7871,6 +8666,7 @@ "sourceTemplateId": 780696, "name": "Template 780696", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -265.4382019042969, 1254.75, @@ -7895,6 +8691,7 @@ "sourceTemplateId": 780696, "name": "Template 780696", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -348.7113952636719, 1254.5040283203125, @@ -7919,6 +8716,7 @@ "sourceTemplateId": 780696, "name": "Template 780696", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -205.31700134277344, 1304.5040283203125, @@ -7943,6 +8741,7 @@ "sourceTemplateId": 780696, "name": "Template 780696", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -204.1614990234375, 1304.56005859375, @@ -7967,6 +8766,7 @@ "sourceTemplateId": 116434, "name": "Skeleton of an Adventurer", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1903.9189453125, 1085.29296875, @@ -7991,6 +8791,7 @@ "sourceTemplateId": 105212, "name": "Template 105212", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -2022, 1091, @@ -8015,6 +8816,7 @@ "sourceTemplateId": 105212, "name": "Template 105212", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -2163.75390625, 1081.6419677734375, @@ -8039,6 +8841,7 @@ "sourceTemplateId": 105213, "name": "Template 105213", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1941.56005859375, 1119.3740234375, @@ -8063,6 +8866,7 @@ "sourceTemplateId": 105213, "name": "Template 105213", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1898.927001953125, 1139.9639892578125, @@ -8087,6 +8891,7 @@ "sourceTemplateId": 105213, "name": "Template 105213", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1962.85205078125, 1082.35498046875, @@ -8111,6 +8916,7 @@ "sourceTemplateId": 105213, "name": "Template 105213", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1994.2139892578125, 1118.0260009765625, @@ -8135,6 +8941,7 @@ "sourceTemplateId": 105214, "name": "Template 105214", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2102.72900390625, 1142.6929931640625, @@ -8159,6 +8966,7 @@ "sourceTemplateId": 105215, "name": "Template 105215", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1834.5050048828125, 1110.7139892578125, @@ -8183,6 +8991,7 @@ "sourceTemplateId": 105216, "name": "Template 105216", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2007.4150390625, 1092.60400390625, @@ -8207,6 +9016,7 @@ "sourceTemplateId": 105216, "name": "Template 105216", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1847, 1095, @@ -8231,6 +9041,7 @@ "sourceTemplateId": 105216, "name": "Template 105216", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2026, 1091, @@ -8255,6 +9066,7 @@ "sourceTemplateId": 105216, "name": "Template 105216", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1937.64599609375, 1091.6409912109375, @@ -8279,6 +9091,7 @@ "sourceTemplateId": 105216, "name": "Template 105216", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1933, 1083, @@ -8303,6 +9116,7 @@ "sourceTemplateId": 105217, "name": "Template 105217", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2023, 1083, @@ -8327,6 +9141,7 @@ "sourceTemplateId": 105217, "name": "Template 105217", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2172.09912109375, 1085.552001953125, @@ -8351,6 +9166,7 @@ "sourceTemplateId": 105217, "name": "Template 105217", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2035, 1083, @@ -8375,6 +9191,7 @@ "sourceTemplateId": 105217, "name": "Template 105217", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1871, 1083, @@ -8399,6 +9216,7 @@ "sourceTemplateId": 105217, "name": "Template 105217", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2148, 1083, @@ -8423,6 +9241,7 @@ "sourceTemplateId": 105217, "name": "Template 105217", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1963, 1083, @@ -8447,6 +9266,7 @@ "sourceTemplateId": 105217, "name": "Template 105217", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1945, 1083, @@ -8471,6 +9291,7 @@ "sourceTemplateId": 103120, "name": "Template 103120", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 506, 1269, @@ -8495,6 +9316,7 @@ "sourceTemplateId": 104324, "name": "Template 104324", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1935.8349609375, 1091.4599609375, @@ -8519,6 +9341,7 @@ "sourceTemplateId": 105205, "name": "Template 105205", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -2047, 914.618896484375, @@ -8543,6 +9366,7 @@ "sourceTemplateId": 105100, "name": "Template 105100", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -330, 896.102294921875, @@ -8566,7 +9390,8 @@ "sourceSpawnId": 41927381, "sourceTemplateId": 105254, "name": "Template 105254", - "classification": "combat", + "classification": "deferred-object", + "sourceClassification": "combat", "sourcePosition": [ 126.5990982055664, 1247.093017578125, @@ -8590,7 +9415,8 @@ "sourceSpawnId": 41927382, "sourceTemplateId": 105254, "name": "Template 105254", - "classification": "combat", + "classification": "deferred-object", + "sourceClassification": "combat", "sourcePosition": [ -625.5192260742188, 1270.3790283203125, @@ -8614,7 +9440,8 @@ "sourceSpawnId": 41927384, "sourceTemplateId": 105254, "name": "Template 105254", - "classification": "combat", + "classification": "deferred-object", + "sourceClassification": "combat", "sourcePosition": [ -1346.3790283203125, 1083.9310302734375, @@ -8638,7 +9465,8 @@ "sourceSpawnId": 41927385, "sourceTemplateId": 105254, "name": "Template 105254", - "classification": "combat", + "classification": "deferred-object", + "sourceClassification": "combat", "sourcePosition": [ -1287.2139892578125, 1083.9310302734375, @@ -8662,7 +9490,8 @@ "sourceSpawnId": 41927386, "sourceTemplateId": 105254, "name": "Template 105254", - "classification": "combat", + "classification": "deferred-object", + "sourceClassification": "combat", "sourcePosition": [ -2126.1279296875, 1095.2130126953125, @@ -8686,7 +9515,8 @@ "sourceSpawnId": 41927387, "sourceTemplateId": 105254, "name": "Template 105254", - "classification": "combat", + "classification": "deferred-object", + "sourceClassification": "combat", "sourcePosition": [ -2120.341064453125, 984.0703735351562, @@ -8710,7 +9540,8 @@ "sourceSpawnId": 41927391, "sourceTemplateId": 105254, "name": "Template 105254", - "classification": "combat", + "classification": "deferred-object", + "sourceClassification": "combat", "sourcePosition": [ 760, 994.526611328125, @@ -8734,7 +9565,8 @@ "sourceSpawnId": 41927392, "sourceTemplateId": 105254, "name": "Template 105254", - "classification": "combat", + "classification": "deferred-object", + "sourceClassification": "combat", "sourcePosition": [ 331.314208984375, 1247.303955078125, @@ -8758,7 +9590,8 @@ "sourceSpawnId": 41927395, "sourceTemplateId": 105254, "name": "Template 105254", - "classification": "combat", + "classification": "deferred-object", + "sourceClassification": "combat", "sourcePosition": [ 890, 994.526611328125, @@ -8782,7 +9615,8 @@ "sourceSpawnId": 41927396, "sourceTemplateId": 105254, "name": "Template 105254", - "classification": "combat", + "classification": "deferred-object", + "sourceClassification": "combat", "sourcePosition": [ 575, 994.526611328125, @@ -8807,6 +9641,7 @@ "sourceTemplateId": 105305, "name": "Template 105305", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 177, 1247.303955078125, @@ -8831,6 +9666,7 @@ "sourceTemplateId": 105305, "name": "Template 105305", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 723, 973, @@ -8855,6 +9691,7 @@ "sourceTemplateId": 105596, "name": "Smirking Castle Clown", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ 288, 1247.2659912109375, @@ -8879,6 +9716,7 @@ "sourceTemplateId": 105597, "name": "Odd Castle Clown", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -1685.501953125, 1083.9310302734375, @@ -8903,6 +9741,7 @@ "sourceTemplateId": 105597, "name": "Odd Castle Clown", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -1865.9739990234375, 1051.531005859375, @@ -8927,6 +9766,7 @@ "sourceTemplateId": 105597, "name": "Odd Castle Clown", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -2128.635986328125, 946.3624267578125, @@ -8951,6 +9791,7 @@ "sourceTemplateId": 105597, "name": "Odd Castle Clown", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -1018, 896.0225219726562, @@ -8975,6 +9816,7 @@ "sourceTemplateId": 105597, "name": "Odd Castle Clown", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -1094, 872.7698974609375, @@ -8999,6 +9841,7 @@ "sourceTemplateId": 105597, "name": "Odd Castle Clown", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ 1139, 972.302001953125, @@ -9023,6 +9866,7 @@ "sourceTemplateId": 105598, "name": "Manipulated Steel Soldier", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -808, 896, @@ -9047,6 +9891,7 @@ "sourceTemplateId": 105598, "name": "Manipulated Steel Soldier", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -807, 896, @@ -9071,6 +9916,7 @@ "sourceTemplateId": 105598, "name": "Manipulated Steel Soldier", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -1019, 896, @@ -9095,6 +9941,7 @@ "sourceTemplateId": 105598, "name": "Manipulated Steel Soldier", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -1090, 896, @@ -9119,6 +9966,7 @@ "sourceTemplateId": 105600, "name": "Juggler's Apprentice", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ 385, 1247, @@ -9143,6 +9991,7 @@ "sourceTemplateId": 105600, "name": "Juggler's Apprentice", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ 348, 1247, @@ -9167,6 +10016,7 @@ "sourceTemplateId": 105600, "name": "Juggler's Apprentice", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ 352, 1247, @@ -9191,6 +10041,7 @@ "sourceTemplateId": 105600, "name": "Juggler's Apprentice", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ 423, 1247.303955078125, @@ -9215,6 +10066,7 @@ "sourceTemplateId": 105600, "name": "Juggler's Apprentice", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ 428, 1247.303955078125, @@ -9239,6 +10091,7 @@ "sourceTemplateId": 105600, "name": "Juggler's Apprentice", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -669.0551147460938, 1270.3790283203125, @@ -9263,6 +10116,7 @@ "sourceTemplateId": 105600, "name": "Juggler's Apprentice", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -626, 1270.053955078125, @@ -9287,6 +10141,7 @@ "sourceTemplateId": 105600, "name": "Juggler's Apprentice", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -968, 1211, @@ -9311,6 +10166,7 @@ "sourceTemplateId": 105600, "name": "Juggler's Apprentice", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -926, 1211, @@ -9335,6 +10191,7 @@ "sourceTemplateId": 105600, "name": "Juggler's Apprentice", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -964, 1211, @@ -9359,6 +10216,7 @@ "sourceTemplateId": 105601, "name": "Conjurer's Apprentice", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ 968.6165771484375, 972.302001953125, @@ -9383,6 +10241,7 @@ "sourceTemplateId": 105601, "name": "Conjurer's Apprentice", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ 968, 972.302001953125, @@ -9407,6 +10266,7 @@ "sourceTemplateId": 105601, "name": "Conjurer's Apprentice", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ 1142, 972.302001953125, @@ -9431,6 +10291,7 @@ "sourceTemplateId": 105601, "name": "Conjurer's Apprentice", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -2275, 1095.2130126953125, @@ -9455,6 +10316,7 @@ "sourceTemplateId": 105601, "name": "Conjurer's Apprentice", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -2312, 1095.2130126953125, @@ -9479,6 +10341,7 @@ "sourceTemplateId": 105601, "name": "Conjurer's Apprentice", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -2072.81494140625, 1095.2130126953125, @@ -9503,6 +10366,7 @@ "sourceTemplateId": 105601, "name": "Conjurer's Apprentice", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -2075, 1095.2130126953125, @@ -9527,6 +10391,7 @@ "sourceTemplateId": 105603, "name": "Manipulated Steel Soldier", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -1238.654052734375, 1083.9310302734375, @@ -9551,6 +10416,7 @@ "sourceTemplateId": 105603, "name": "Manipulated Steel Soldier", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -1431, 1083.9310302734375, @@ -9575,6 +10441,7 @@ "sourceTemplateId": 105603, "name": "Manipulated Steel Soldier", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -1463, 1083.9310302734375, @@ -9599,6 +10466,7 @@ "sourceTemplateId": 105603, "name": "Manipulated Steel Soldier", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -1422.9749755859375, 1083.9310302734375, @@ -9623,6 +10491,7 @@ "sourceTemplateId": 105603, "name": "Manipulated Steel Soldier", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -1463, 1083.9310302734375, @@ -9647,6 +10516,7 @@ "sourceTemplateId": 105603, "name": "Manipulated Steel Soldier", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -1218, 1083.9310302734375, @@ -9671,6 +10541,7 @@ "sourceTemplateId": 105603, "name": "Manipulated Steel Soldier", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -1479, 1083.9310302734375, @@ -9695,6 +10566,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 251, 1254.7220458984375, @@ -9719,6 +10591,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 110, 1256.6820068359375, @@ -9743,6 +10616,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -639, 1283.1500244140625, @@ -9767,6 +10641,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -600, 1270.053955078125, @@ -9791,6 +10666,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1307, 1083.9310302734375, @@ -9815,6 +10691,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1232, 872.692626953125, @@ -9839,6 +10716,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1065, 872.7698974609375, @@ -9863,6 +10741,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1114, 872.7698974609375, @@ -9887,6 +10766,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -993, 872.7698974609375, @@ -9911,6 +10791,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1158, 872.7698974609375, @@ -9935,6 +10816,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 908, 973, @@ -9959,6 +10841,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 835, 973, @@ -9983,6 +10866,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 720, 973, @@ -10007,6 +10891,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 723, 994, @@ -10031,6 +10916,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 807, 994, @@ -10055,6 +10941,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 801, 994, @@ -10079,6 +10966,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 718, 994, @@ -10103,6 +10991,7 @@ "sourceTemplateId": 105262, "name": "Template 105262", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -1825, 1094.9759521484375, @@ -10126,7 +11015,8 @@ "sourceSpawnId": 41927631, "sourceTemplateId": 105258, "name": "Template 105258", - "classification": "combat", + "classification": "deferred-object", + "sourceClassification": "combat", "sourcePosition": [ 332, 1255.0579833984375, @@ -10150,7 +11040,8 @@ "sourceSpawnId": 41927632, "sourceTemplateId": 105258, "name": "Template 105258", - "classification": "combat", + "classification": "deferred-object", + "sourceClassification": "combat", "sourcePosition": [ -628.2586059570312, 1270.053955078125, @@ -10174,7 +11065,8 @@ "sourceSpawnId": 41927633, "sourceTemplateId": 105258, "name": "Template 105258", - "classification": "combat", + "classification": "deferred-object", + "sourceClassification": "combat", "sourcePosition": [ -1502, 1083.9310302734375, @@ -10198,7 +11090,8 @@ "sourceSpawnId": 41927634, "sourceTemplateId": 105258, "name": "Template 105258", - "classification": "combat", + "classification": "deferred-object", + "sourceClassification": "combat", "sourcePosition": [ -2171, 1095.2130126953125, @@ -10222,7 +11115,8 @@ "sourceSpawnId": 41927635, "sourceTemplateId": 105258, "name": "Template 105258", - "classification": "combat", + "classification": "deferred-object", + "sourceClassification": "combat", "sourcePosition": [ -1894, 1051.10498046875, @@ -10246,7 +11140,8 @@ "sourceSpawnId": 41928345, "sourceTemplateId": 105210, "name": "Template 105210", - "classification": "combat", + "classification": "deferred-object", + "sourceClassification": "combat", "sourcePosition": [ -1355.60302734375, 1168.93994140625, @@ -10271,6 +11166,7 @@ "sourceTemplateId": 102678, "name": "Template 102678", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 428.7330017089844, 1269.2869873046875, @@ -10295,6 +11191,7 @@ "sourceTemplateId": 104810, "name": "Template 104810", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 429.29681396484375, 1269.2869873046875, @@ -10319,6 +11216,7 @@ "sourceTemplateId": 105604, "name": "Template 105604", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -19.17742919921875, 992.87939453125, @@ -10343,6 +11241,7 @@ "sourceTemplateId": 117547, "name": "Locked Iron Gate", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ 931.364013671875, 973.02197265625, @@ -10367,6 +11266,7 @@ "sourceTemplateId": 105201, "name": "Template 105201", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -32, 1254, @@ -10391,6 +11291,7 @@ "sourceTemplateId": 105211, "name": "Template 105211", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -1843, 1095.0240478515625, @@ -10415,6 +11316,7 @@ "sourceTemplateId": 105211, "name": "Template 105211", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -1999, 1091, @@ -10439,6 +11341,7 @@ "sourceTemplateId": 105211, "name": "Template 105211", "classification": "combat", + "sourceClassification": "combat", "sourcePosition": [ -1903.9649658203125, 1083.1910400390625, @@ -10463,6 +11366,7 @@ "sourceTemplateId": 102680, "name": "Template 102680", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -129.04910278320312, 1254.5040283203125, @@ -10487,6 +11391,7 @@ "sourceTemplateId": 102680, "name": "Template 102680", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -169.10569763183594, 1254.5040283203125, @@ -10511,6 +11416,7 @@ "sourceTemplateId": 102680, "name": "Template 102680", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -213.648193359375, 1254.5040283203125, @@ -10535,6 +11441,7 @@ "sourceTemplateId": 102680, "name": "Template 102680", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -244.70339965820312, 1254.5040283203125, @@ -10559,6 +11466,7 @@ "sourceTemplateId": 102680, "name": "Template 102680", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -279.0990905761719, 1254.5040283203125, @@ -10583,6 +11491,7 @@ "sourceTemplateId": 102680, "name": "Template 102680", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -237.96359252929688, 1254.5040283203125, @@ -10607,6 +11516,7 @@ "sourceTemplateId": 102680, "name": "Template 102680", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -198.71170043945312, 1254.5040283203125, @@ -10631,6 +11541,7 @@ "sourceTemplateId": 102680, "name": "Template 102680", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -158.48719787597656, 1254.5040283203125, @@ -10655,6 +11566,7 @@ "sourceTemplateId": 102680, "name": "Template 102680", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -123.072998046875, 1254.5040283203125, @@ -10679,6 +11591,7 @@ "sourceTemplateId": 102680, "name": "Template 102680", "classification": "deferred-object", + "sourceClassification": "deferred-object", "sourcePosition": [ -286.793701171875, 1254.5040283203125, @@ -10697,6 +11610,1326 @@ ], "projectionDistance": 0.09165, "modelPath": "model/item/ui_object/trigger_box/trigger_box.ros" + }, + { + "sourceSpawnId": 41927224, + "sourceTemplateId": 105616, + "scriptedPlacementId": "cayus", + "sourceControllerSpawnId": 41928351, + "sourceScriptRoot": "Dy_Z19_BOSSN_AIC", + "evidence": [ + "Resource/luascript/02345.lua:CreateObjByFlag(105616,780696,1,1)" + ], + "name": "Cayus", + "classification": "boss", + "sourcePosition": [ + -348.7113952636719, + 1254.5040283203125, + -999.6005249023438 + ], + "sourceDirection": 359.3254089355469, + "localPosition": [ + 6.028693, + -2.051452, + 92.880347 + ], + "projectedPosition": [ + 6.029333, + -1.971602, + 92.880381 + ], + "projectionDistance": 0.079852, + "modelPath": "model/character/creature/karth/act_karth.ros" + }, + { + "sourceSpawnId": 41926939, + "sourceTemplateId": 105542, + "scriptedPlacementId": "ardmond", + "sourceControllerSpawnId": 41928345, + "sourceScriptRoot": "kg_105542_01", + "evidence": [ + "Resource/luascript/02324.lua:kg_105542_CreateBoss" + ], + "name": "Ardmond", + "classification": "boss", + "sourcePosition": [ + -1427.9969482421875, + 1133.35498046875, + -1353.3699951171875 + ], + "sourceDirection": 360, + "localPosition": [ + -101.899863, + -14.166357, + 128.257294 + ], + "projectedPosition": [ + -101.899784, + -14.174433, + 128.257272 + ], + "projectionDistance": 0.008076, + "modelPath": "model/character/creature/atmo/act_atmo.ros" + }, + { + "sourceSpawnId": 41926939, + "sourceTemplateId": 105543, + "scriptedPlacementId": "issac", + "sourceControllerSpawnId": 41928345, + "sourceScriptRoot": "kg_105542_01", + "evidence": [ + "Resource/luascript/02324.lua:kg_105542_CreateBoss" + ], + "name": "Issac", + "classification": "boss", + "sourcePosition": [ + -1427.9969482421875, + 1133.35498046875, + -1313.3699951171875 + ], + "sourceDirection": 360, + "localPosition": [ + -101.899863, + -14.166357, + 124.257294 + ], + "projectedPosition": [ + -101.899874, + -14.164943, + 124.257294 + ], + "projectionDistance": 0.001414, + "modelPath": "model/character/creature/mount_millenium_chimera/act_mount_millenium_chimera_laby.ros" + }, + { + "sourceSpawnId": 41926924, + "sourceTemplateId": 105224, + "scriptedPlacementId": "boro-boro-brothers", + "sourceControllerSpawnId": 41927347, + "sourceScriptRoot": "ic_105224_01", + "evidence": [ + "Resource/luascript/02231.lua:CreateObjByFlag(105224,780775,1,1)" + ], + "name": "Boro-Boro Brothers", + "classification": "boss", + "sourcePosition": [ + -2172.174072265625, + 1095.10400390625, + -22.212709426879883 + ], + "sourceDirection": 90.23943328857422, + "localPosition": [ + -176.317575, + -17.991455, + -4.858434 + ], + "projectedPosition": [ + -176.317575, + -17.936858, + -4.856412 + ], + "projectionDistance": 0.054634, + "modelPath": "model/character/creature/barbo_brother/act_barbo_brother.ros" + }, + { + "sourceSpawnId": 41926979, + "sourceTemplateId": 105204, + "scriptedPlacementId": "conjurer-chapeaunoir", + "sourceControllerSpawnId": 41927358, + "sourceScriptRoot": "cl_136_105205_N", + "evidence": [ + "Resource/luascript/02330.lua:CreateObjByFlag(105204,780767,0,1)" + ], + "name": "Conjurer Chapeaunoir", + "classification": "boss", + "sourcePosition": [ + -1732.9649658203125, + 914.618896484375, + 1190.8310546875 + ], + "sourceDirection": 45.329139709472656, + "localPosition": [ + -132.396664, + -36.039965, + -126.162811 + ], + "projectedPosition": [ + -132.396149, + -35.924047, + -126.162808 + ], + "projectionDistance": 0.115919, + "modelPath": "model/character/creature/black_hat/act_black_hat.ros" + }, + { + "sourceSpawnId": 41927071, + "sourceTemplateId": 105351, + "scriptedPlacementId": "larvanger-barkud", + "sourceControllerSpawnId": 41927369, + "sourceScriptRoot": "un_zone19_boss5_allconreal_1_01", + "evidence": [ + "Resource/luascript/02297.lua:CreateObjByFlag(105351,780838,1,1)" + ], + "name": "Larvanger Barkud", + "classification": "boss", + "sourcePosition": [ + -305.8063049316406, + 902.5551147460938, + 941.4002075195312 + ], + "sourceDirection": 181.27639770507812, + "localPosition": [ + 10.319202, + -37.246344, + -101.219726 + ], + "projectedPosition": [ + 10.319202, + -37.197609, + -101.219726 + ], + "projectionDistance": 0.048735, + "modelPath": "model/character/creature/rafael_bakderr/act_rafael_bakderr.ros" + }, + { + "sourceSpawnId": 41927381, + "sourceTemplateId": 105598, + "scriptedPlacementId": "castle-patrol-41927381-1", + "sourceControllerSpawnId": 41927381, + "sourceScriptRoot": "cl_136_01_01_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_01_01_N" + ], + "name": "Manipulated Steel Soldier", + "classification": "combat", + "sourcePosition": [ + 126.5990982055664, + 1247.093017578125, + -998.6713256835938 + ], + "sourceDirection": 179.87330627441406, + "localPosition": [ + 53.559742, + -2.792553, + 92.787428 + ], + "projectedPosition": [ + 53.559575, + -2.777278, + 92.787626 + ], + "projectionDistance": 0.015277, + "modelPath": "model/character/creature/lost_protector/act_controlled_steel_guardian.ros" + }, + { + "sourceSpawnId": 41927381, + "sourceTemplateId": 105229, + "scriptedPlacementId": "castle-patrol-41927381-2", + "sourceControllerSpawnId": 41927381, + "sourceScriptRoot": "cl_136_01_01_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_01_01_N" + ], + "name": "Smirking Castle Clown", + "classification": "combat", + "sourcePosition": [ + 146.55482489350754, + 1247.093017578125, + -978.6271501616075 + ], + "sourceDirection": 179.87330627441406, + "localPosition": [ + 55.555315, + -2.792553, + 90.78301 + ], + "projectedPosition": [ + 55.554627, + -2.729516, + 90.783828 + ], + "projectionDistance": 0.063046, + "modelPath": "model/character/creature/grinning_clown/act_grinning_clown.ros" + }, + { + "sourceSpawnId": 41927381, + "sourceTemplateId": 105229, + "scriptedPlacementId": "castle-patrol-41927381-3", + "sourceControllerSpawnId": 41927381, + "sourceScriptRoot": "cl_136_01_01_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_01_01_N" + ], + "name": "Smirking Castle Clown", + "classification": "combat", + "sourcePosition": [ + 146.6432737275527, + 1247.093017578125, + -1018.6270523715349 + ], + "sourceDirection": 179.87330627441406, + "localPosition": [ + 55.56416, + -2.792553, + 94.783 + ], + "projectedPosition": [ + 55.564037, + -2.781325, + 94.783146 + ], + "projectionDistance": 0.01123, + "modelPath": "model/character/creature/grinning_clown/act_grinning_clown.ros" + }, + { + "sourceSpawnId": 41927381, + "sourceTemplateId": 105230, + "scriptedPlacementId": "castle-patrol-41927381-4", + "sourceControllerSpawnId": 41927381, + "sourceScriptRoot": "cl_136_01_01_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_01_01_N" + ], + "name": "Odd Castle Clown", + "classification": "combat", + "sourcePosition": [ + 166.59900041549383, + 1247.093017578125, + -998.5828768495486 + ], + "sourceDirection": 179.87330627441406, + "localPosition": [ + 57.559732, + -2.792553, + 92.778583 + ], + "projectedPosition": [ + 57.565665, + -2.703565, + 92.778583 + ], + "projectionDistance": 0.089185, + "modelPath": "model/character/creature/ghastly_clown/act_ghastly_clown_red.ros" + }, + { + "sourceSpawnId": 41927382, + "sourceTemplateId": 105598, + "scriptedPlacementId": "castle-patrol-41927382-1", + "sourceControllerSpawnId": 41927382, + "sourceScriptRoot": "cl_136_01_01_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_01_01_N" + ], + "name": "Manipulated Steel Soldier", + "classification": "combat", + "sourcePosition": [ + -625.5192260742188, + 1270.3790283203125, + -1043.7130126953125 + ], + "sourceDirection": 269.88189697265625, + "localPosition": [ + -21.65209, + -0.463952, + 97.291596 + ], + "projectedPosition": [ + -21.65209, + -0.397606, + 97.291596 + ], + "projectionDistance": 0.066346, + "modelPath": "model/character/creature/lost_protector/act_controlled_steel_guardian.ros" + }, + { + "sourceSpawnId": 41927382, + "sourceTemplateId": 105229, + "scriptedPlacementId": "castle-patrol-41927382-2", + "sourceControllerSpawnId": 41927382, + "sourceScriptRoot": "cl_136_01_01_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_01_01_N" + ], + "name": "Smirking Castle Clown", + "classification": "combat", + "sourcePosition": [ + -605.4780428587512, + 1270.3790283203125, + -1063.6717445018191 + ], + "sourceDirection": 269.88189697265625, + "localPosition": [ + -19.647972, + -0.463952, + 99.287469 + ], + "projectedPosition": [ + -19.647972, + -0.397606, + 99.287469 + ], + "projectionDistance": 0.066346, + "modelPath": "model/character/creature/grinning_clown/act_grinning_clown.ros" + }, + { + "sourceSpawnId": 41927382, + "sourceTemplateId": 105229, + "scriptedPlacementId": "castle-patrol-41927382-3", + "sourceControllerSpawnId": 41927382, + "sourceScriptRoot": "cl_136_01_01_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_01_01_N" + ], + "name": "Smirking Castle Clown", + "classification": "combat", + "sourcePosition": [ + -645.4779578807254, + 1270.3790283203125, + -1063.75419591078 + ], + "sourceDirection": 269.88189697265625, + "localPosition": [ + -23.647964, + -0.463952, + 99.295715 + ], + "projectedPosition": [ + -23.647964, + -0.397606, + 99.295715 + ], + "projectionDistance": 0.066346, + "modelPath": "model/character/creature/grinning_clown/act_grinning_clown.ros" + }, + { + "sourceSpawnId": 41927382, + "sourceTemplateId": 105230, + "scriptedPlacementId": "castle-patrol-41927382-4", + "sourceControllerSpawnId": 41927382, + "sourceScriptRoot": "cl_136_01_01_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_01_01_N" + ], + "name": "Odd Castle Clown", + "classification": "combat", + "sourcePosition": [ + -625.4367746652579, + 1270.3790283203125, + -1083.7129277172867 + ], + "sourceDirection": 269.88189697265625, + "localPosition": [ + -21.643845, + -0.463952, + 101.291588 + ], + "projectedPosition": [ + -21.643845, + -0.397606, + 101.291588 + ], + "projectionDistance": 0.066346, + "modelPath": "model/character/creature/ghastly_clown/act_ghastly_clown_red.ros" + }, + { + "sourceSpawnId": 41927384, + "sourceTemplateId": 105596, + "scriptedPlacementId": "castle-patrol-41927384-1", + "sourceControllerSpawnId": 41927384, + "sourceScriptRoot": "cl_136_03_01_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_03_01_N" + ], + "name": "Smirking Castle Clown", + "classification": "combat", + "sourcePosition": [ + -1346.3790283203125, + 1083.9310302734375, + -430.9111022949219 + ], + "sourceDirection": 89.27565002441406, + "localPosition": [ + -93.738071, + -19.108752, + 36.011405 + ], + "projectedPosition": [ + -93.738071, + -19.097607, + 36.011405 + ], + "projectionDistance": 0.011145, + "modelPath": "model/character/creature/grinning_clown/act_grinning_clown.ros" + }, + { + "sourceSpawnId": 41927384, + "sourceTemplateId": 105597, + "scriptedPlacementId": "castle-patrol-41927384-2", + "sourceControllerSpawnId": 41927384, + "sourceScriptRoot": "cl_136_03_01_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_03_01_N" + ], + "name": "Odd Castle Clown", + "classification": "combat", + "sourcePosition": [ + -1366.6302691711066, + 1083.9310302734375, + -411.1655396540872 + ], + "sourceDirection": 89.27565002441406, + "localPosition": [ + -95.763195, + -19.108752, + 34.036849 + ], + "projectedPosition": [ + -95.763195, + -19.097607, + 34.036849 + ], + "projectionDistance": 0.011145, + "modelPath": "model/character/creature/ghastly_clown/act_ghastly_clown_red.ros" + }, + { + "sourceSpawnId": 41927384, + "sourceTemplateId": 105597, + "scriptedPlacementId": "castle-patrol-41927384-3", + "sourceControllerSpawnId": 41927384, + "sourceScriptRoot": "cl_136_03_01_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_03_01_N" + ], + "name": "Odd Castle Clown", + "classification": "combat", + "sourcePosition": [ + -1326.6334656794777, + 1083.9310302734375, + -410.6598614441277 + ], + "sourceDirection": 89.27565002441406, + "localPosition": [ + -91.763514, + -19.108752, + 33.986281 + ], + "projectedPosition": [ + -91.763514, + -19.097607, + 33.986281 + ], + "projectionDistance": 0.011145, + "modelPath": "model/character/creature/ghastly_clown/act_ghastly_clown_red.ros" + }, + { + "sourceSpawnId": 41927384, + "sourceTemplateId": 105601, + "scriptedPlacementId": "castle-patrol-41927384-4", + "sourceControllerSpawnId": 41927384, + "sourceScriptRoot": "cl_136_03_01_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_03_01_N" + ], + "name": "Conjurer's Apprentice", + "classification": "combat", + "sourcePosition": [ + -1346.8847065302718, + 1083.9310302734375, + -390.91429880329304 + ], + "sourceDirection": 89.27565002441406, + "localPosition": [ + -93.788638, + -19.108752, + 32.011725 + ], + "projectedPosition": [ + -93.788638, + -19.097607, + 32.011725 + ], + "projectionDistance": 0.011145, + "modelPath": "model/character/creature/black_hat/act_black_hat_blue.ros" + }, + { + "sourceSpawnId": 41927385, + "sourceTemplateId": 105599, + "scriptedPlacementId": "castle-patrol-41927385-1", + "sourceControllerSpawnId": 41927385, + "sourceScriptRoot": "cl_136_03_02_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_03_02_N" + ], + "name": "Cursed Steel Soldier", + "classification": "combat", + "sourcePosition": [ + -1287.2139892578125, + 1083.9310302734375, + -478.3273010253906 + ], + "sourceDirection": 180.14939880371094, + "localPosition": [ + -87.821567, + -19.108752, + 40.753025 + ], + "projectedPosition": [ + -87.821567, + -19.097607, + 40.753025 + ], + "projectionDistance": 0.011145, + "modelPath": "model/character/creature/lost_protector/act_cursed_steel_guardian.ros" + }, + { + "sourceSpawnId": 41927385, + "sourceTemplateId": 105598, + "scriptedPlacementId": "castle-patrol-41927385-2", + "sourceControllerSpawnId": 41927385, + "sourceScriptRoot": "cl_136_03_02_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_03_02_N" + ], + "name": "Manipulated Steel Soldier", + "classification": "combat", + "sourcePosition": [ + -1267.1619072870192, + 1083.9310302734375, + -458.3795189773387 + ], + "sourceDirection": 180.14939880371094, + "localPosition": [ + -85.816359, + -19.108752, + 38.758247 + ], + "projectedPosition": [ + -85.816359, + -19.097607, + 38.758247 + ], + "projectionDistance": 0.011145, + "modelPath": "model/character/creature/lost_protector/act_controlled_steel_guardian.ros" + }, + { + "sourceSpawnId": 41927385, + "sourceTemplateId": 105598, + "scriptedPlacementId": "castle-patrol-41927385-3", + "sourceControllerSpawnId": 41927385, + "sourceScriptRoot": "cl_136_03_02_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_03_02_N" + ], + "name": "Manipulated Steel Soldier", + "classification": "combat", + "sourcePosition": [ + -1267.2662072097605, + 1083.9310302734375, + -498.379382996184 + ], + "sourceDirection": 180.14939880371094, + "localPosition": [ + -85.826789, + -19.108752, + 42.758233 + ], + "projectedPosition": [ + -85.826789, + -19.097607, + 42.758233 + ], + "projectionDistance": 0.011145, + "modelPath": "model/character/creature/lost_protector/act_controlled_steel_guardian.ros" + }, + { + "sourceSpawnId": 41927386, + "sourceTemplateId": 105599, + "scriptedPlacementId": "castle-patrol-41927386-1", + "sourceControllerSpawnId": 41927386, + "sourceScriptRoot": "cl_136_03_04_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_03_04_N" + ], + "name": "Cursed Steel Soldier", + "classification": "combat", + "sourcePosition": [ + -2126.1279296875, + 1095.2130126953125, + 361.4753112792969 + ], + "sourceDirection": 0.1327040046453476, + "localPosition": [ + -171.712961, + -17.980554, + -43.227236 + ], + "projectedPosition": [ + -171.712961, + -17.89761, + -43.227236 + ], + "projectionDistance": 0.082944, + "modelPath": "model/character/creature/lost_protector/act_cursed_steel_guardian.ros" + }, + { + "sourceSpawnId": 41927386, + "sourceTemplateId": 105598, + "scriptedPlacementId": "castle-patrol-41927386-2", + "sourceControllerSpawnId": 41927386, + "sourceScriptRoot": "cl_136_03_04_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_03_04_N" + ], + "name": "Manipulated Steel Soldier", + "classification": "combat", + "sourcePosition": [ + -2146.174198438139, + 1095.2130126953125, + 341.5216873182927 + ], + "sourceDirection": 0.1327040046453476, + "localPosition": [ + -173.717588, + -17.980554, + -41.231874 + ], + "projectedPosition": [ + -173.717588, + -17.89761, + -41.231874 + ], + "projectionDistance": 0.082944, + "modelPath": "model/character/creature/lost_protector/act_controlled_steel_guardian.ros" + }, + { + "sourceSpawnId": 41927386, + "sourceTemplateId": 105598, + "scriptedPlacementId": "castle-patrol-41927386-3", + "sourceControllerSpawnId": 41927386, + "sourceScriptRoot": "cl_136_03_04_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_03_04_N" + ], + "name": "Manipulated Steel Soldier", + "classification": "combat", + "sourcePosition": [ + -2146.0815536485043, + 1095.2130126953125, + 381.52158002993576 + ], + "sourceDirection": 0.1327040046453476, + "localPosition": [ + -173.708323, + -17.980554, + -45.231863 + ], + "projectedPosition": [ + -173.708323, + -17.89761, + -45.231863 + ], + "projectionDistance": 0.082944, + "modelPath": "model/character/creature/lost_protector/act_controlled_steel_guardian.ros" + }, + { + "sourceSpawnId": 41927386, + "sourceTemplateId": 105601, + "scriptedPlacementId": "castle-patrol-41927386-4", + "sourceControllerSpawnId": 41927386, + "sourceScriptRoot": "cl_136_03_04_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_03_04_N" + ], + "name": "Conjurer's Apprentice", + "classification": "combat", + "sourcePosition": [ + -2166.127822399143, + 1095.2130126953125, + 361.5679560689316 + ], + "sourceDirection": 0.1327040046453476, + "localPosition": [ + -175.71295, + -17.980554, + -43.236501 + ], + "projectedPosition": [ + -175.71295, + -17.89761, + -43.236501 + ], + "projectionDistance": 0.082944, + "modelPath": "model/character/creature/black_hat/act_black_hat_blue.ros" + }, + { + "sourceSpawnId": 41927387, + "sourceTemplateId": 105599, + "scriptedPlacementId": "castle-patrol-41927387-1", + "sourceControllerSpawnId": 41927387, + "sourceScriptRoot": "cl_136_03_042_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_03_042_N" + ], + "name": "Cursed Steel Soldier", + "classification": "combat", + "sourcePosition": [ + -2120.341064453125, + 984.0703735351562, + 916.98681640625 + ], + "sourceDirection": 86.2711410522461, + "localPosition": [ + -171.134274, + -29.094818, + -98.778387 + ], + "projectedPosition": [ + -171.130459, + -28.975774, + -98.808759 + ], + "projectionDistance": 0.122916, + "modelPath": "model/character/creature/lost_protector/act_cursed_steel_guardian.ros" + }, + { + "sourceSpawnId": 41927387, + "sourceTemplateId": 105601, + "scriptedPlacementId": "castle-patrol-41927387-2", + "sourceControllerSpawnId": 41927387, + "sourceScriptRoot": "cl_136_03_042_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_03_042_N" + ], + "name": "Conjurer's Apprentice", + "classification": "combat", + "sourcePosition": [ + -2141.5994228862082, + 984.0703735351562, + 935.6437774859801 + ], + "sourceDirection": 86.2711410522461, + "localPosition": [ + -173.26011, + -29.094818, + -100.644083 + ], + "projectedPosition": [ + -173.268557, + -29.35834, + -100.57685 + ], + "projectionDistance": 0.272095, + "modelPath": "model/character/creature/black_hat/act_black_hat_blue.ros" + }, + { + "sourceSpawnId": 41927387, + "sourceTemplateId": 105601, + "scriptedPlacementId": "castle-patrol-41927387-3", + "sourceControllerSpawnId": 41927387, + "sourceScriptRoot": "cl_136_03_042_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_03_042_N" + ], + "name": "Conjurer's Apprentice", + "classification": "combat", + "sourcePosition": [ + -2101.684103373395, + 984.0703735351562, + 938.2451748393331 + ], + "sourceDirection": 86.2711410522461, + "localPosition": [ + -169.268578, + -29.094818, + -100.904223 + ], + "projectedPosition": [ + -169.273997, + -29.535331, + -100.794094 + ], + "projectionDistance": 0.454103, + "modelPath": "model/character/creature/black_hat/act_black_hat_blue.ros" + }, + { + "sourceSpawnId": 41927391, + "sourceTemplateId": 105237, + "scriptedPlacementId": "castle-patrol-41927391-1", + "sourceControllerSpawnId": 41927391, + "sourceScriptRoot": "cl_136_04_052", + "evidence": [ + "Resource/luascript/02214.lua:cl_136_04_052" + ], + "name": "Manipulated Bear", + "classification": "combat", + "sourcePosition": [ + 760, + 994.526611328125, + 1683 + ], + "sourceDirection": 89, + "localPosition": [ + 116.899832, + -28.049194, + -175.379705 + ], + "projectedPosition": [ + 116.899832, + -27.977214, + -175.378701 + ], + "projectionDistance": 0.071987, + "modelPath": "model/character/creature/enraged_big_bear/act_enraged_big_bear_cream.ros" + }, + { + "sourceSpawnId": 41927391, + "sourceTemplateId": 105236, + "scriptedPlacementId": "castle-patrol-41927391-2", + "sourceControllerSpawnId": 41927391, + "sourceScriptRoot": "cl_136_04_052", + "evidence": [ + "Resource/luascript/02214.lua:cl_136_04_052" + ], + "name": "Giant Enraged Bear", + "classification": "combat", + "sourcePosition": [ + 739.6539979681265, + 994.526611328125, + 1702.6479057743823 + ], + "sourceDirection": 89, + "localPosition": [ + 114.865232, + -28.049194, + -177.344496 + ], + "projectedPosition": [ + 114.865222, + -27.949904, + -177.34312 + ], + "projectionDistance": 0.099299, + "modelPath": "model/character/creature/enraged_big_bear/act_enraged_big_bear_purple.ros" + }, + { + "sourceSpawnId": 41927391, + "sourceTemplateId": 105236, + "scriptedPlacementId": "castle-patrol-41927391-3", + "sourceControllerSpawnId": 41927391, + "sourceScriptRoot": "cl_136_04_052", + "evidence": [ + "Resource/luascript/02214.lua:cl_136_04_052" + ], + "name": "Giant Enraged Bear", + "classification": "combat", + "sourcePosition": [ + 779.6479057743821, + 994.526611328125, + 1703.3460020318735 + ], + "sourceDirection": 89, + "localPosition": [ + 118.864623, + -28.049194, + -177.414305 + ], + "projectedPosition": [ + 118.864623, + -27.948849, + -177.412906 + ], + "projectionDistance": 0.100355, + "modelPath": "model/character/creature/enraged_big_bear/act_enraged_big_bear_purple.ros" + }, + { + "sourceSpawnId": 41927391, + "sourceTemplateId": 105237, + "scriptedPlacementId": "castle-patrol-41927391-4", + "sourceControllerSpawnId": 41927391, + "sourceScriptRoot": "cl_136_04_052", + "evidence": [ + "Resource/luascript/02214.lua:cl_136_04_052" + ], + "name": "Manipulated Bear", + "classification": "combat", + "sourcePosition": [ + 759.3019037425087, + 994.526611328125, + 1722.9939078062557 + ], + "sourceDirection": 89, + "localPosition": [ + 116.830023, + -28.049194, + -179.379096 + ], + "projectedPosition": [ + 116.83001, + -27.921521, + -179.377327 + ], + "projectionDistance": 0.127686, + "modelPath": "model/character/creature/enraged_big_bear/act_enraged_big_bear_cream.ros" + }, + { + "sourceSpawnId": 41927391, + "sourceTemplateId": 105236, + "scriptedPlacementId": "castle-patrol-41927391-5", + "sourceControllerSpawnId": 41927391, + "sourceScriptRoot": "cl_136_04_052", + "evidence": [ + "Resource/luascript/02214.lua:cl_136_04_052" + ], + "name": "Giant Enraged Bear", + "classification": "combat", + "sourcePosition": [ + 779.2988576456365, + 994.526611328125, + 1723.3429559350013 + ], + "sourceDirection": 89, + "localPosition": [ + 118.829718, + -28.049194, + -179.414001 + ], + "projectedPosition": [ + 118.829718, + -27.92097, + -179.412213 + ], + "projectionDistance": 0.128237, + "modelPath": "model/character/creature/enraged_big_bear/act_enraged_big_bear_purple.ros" + }, + { + "sourceSpawnId": 41927391, + "sourceTemplateId": 105236, + "scriptedPlacementId": "castle-patrol-41927391-6", + "sourceControllerSpawnId": 41927391, + "sourceScriptRoot": "cl_136_04_052", + "evidence": [ + "Resource/luascript/02214.lua:cl_136_04_052" + ], + "name": "Giant Enraged Bear", + "classification": "combat", + "sourcePosition": [ + 739.3049498393808, + 994.526611328125, + 1722.6448596775099 + ], + "sourceDirection": 89, + "localPosition": [ + 114.830327, + -28.049194, + -179.344191 + ], + "projectedPosition": [ + 114.830314, + -27.922207, + -179.342432 + ], + "projectionDistance": 0.126999, + "modelPath": "model/character/creature/enraged_big_bear/act_enraged_big_bear_purple.ros" + }, + { + "sourceSpawnId": 41927392, + "sourceTemplateId": 105598, + "scriptedPlacementId": "castle-patrol-41927392-1", + "sourceControllerSpawnId": 41927392, + "sourceScriptRoot": "cl_136_01_01_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_01_01_N" + ], + "name": "Manipulated Steel Soldier", + "classification": "combat", + "sourcePosition": [ + 331.314208984375, + 1247.303955078125, + -1148.9410400390625 + ], + "sourceDirection": 269.99481201171875, + "localPosition": [ + 74.031253, + -2.771459, + 107.814399 + ], + "projectedPosition": [ + 74.031253, + -2.779222, + 107.81483 + ], + "projectionDistance": 0.007774, + "modelPath": "model/character/creature/lost_protector/act_controlled_steel_guardian.ros" + }, + { + "sourceSpawnId": 41927392, + "sourceTemplateId": 105229, + "scriptedPlacementId": "castle-patrol-41927392-2", + "sourceControllerSpawnId": 41927392, + "sourceScriptRoot": "cl_136_01_01_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_01_01_N" + ], + "name": "Smirking Castle Clown", + "classification": "combat", + "sourcePosition": [ + 351.3160198519253, + 1247.303955078125, + -1168.9392290075352 + ], + "sourceDirection": 269.99481201171875, + "localPosition": [ + 76.031434, + -2.771459, + 109.814218 + ], + "projectedPosition": [ + 76.031434, + -2.742008, + 109.816672 + ], + "projectionDistance": 0.029554, + "modelPath": "model/character/creature/grinning_clown/act_grinning_clown.ros" + }, + { + "sourceSpawnId": 41927392, + "sourceTemplateId": 105229, + "scriptedPlacementId": "castle-patrol-41927392-3", + "sourceControllerSpawnId": 41927392, + "sourceScriptRoot": "cl_136_01_01_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_01_01_N" + ], + "name": "Smirking Castle Clown", + "classification": "combat", + "sourcePosition": [ + 311.31602001590227, + 1247.303955078125, + -1168.9428509066129 + ], + "sourceDirection": 269.99481201171875, + "localPosition": [ + 72.031434, + -2.771459, + 109.81458 + ], + "projectedPosition": [ + 72.031434, + -2.742038, + 109.817032 + ], + "projectionDistance": 0.029524, + "modelPath": "model/character/creature/grinning_clown/act_grinning_clown.ros" + }, + { + "sourceSpawnId": 41927392, + "sourceTemplateId": 105230, + "scriptedPlacementId": "castle-patrol-41927392-4", + "sourceControllerSpawnId": 41927392, + "sourceScriptRoot": "cl_136_01_01_N", + "evidence": [ + "Resource/luascript/02334.lua:cl_136_01_01_N" + ], + "name": "Odd Castle Clown", + "classification": "combat", + "sourcePosition": [ + 331.3178308834526, + 1247.303955078125, + -1188.9410398750856 + ], + "sourceDirection": 269.99481201171875, + "localPosition": [ + 74.031615, + -2.771459, + 111.814399 + ], + "projectedPosition": [ + 74.031615, + -2.797607, + 111.814399 + ], + "projectionDistance": 0.026148, + "modelPath": "model/character/creature/ghastly_clown/act_ghastly_clown_red.ros" + }, + { + "sourceSpawnId": 41927395, + "sourceTemplateId": 105233, + "scriptedPlacementId": "castle-patrol-41927395-1", + "sourceControllerSpawnId": 41927395, + "sourceScriptRoot": "cl_136_03_042", + "evidence": [ + "Resource/luascript/02214.lua:cl_136_03_042" + ], + "name": "Cursed Steel Soldier", + "classification": "combat", + "sourcePosition": [ + 890, + 994.526611328125, + 1700 + ], + "sourceDirection": 181, + "localPosition": [ + 129.899832, + -28.049194, + -177.079705 + ], + "projectedPosition": [ + 129.899832, + -27.997608, + -177.079705 + ], + "projectionDistance": 0.051586, + "modelPath": "model/character/creature/lost_protector/act_cursed_steel_guardian.ros" + }, + { + "sourceSpawnId": 41927395, + "sourceTemplateId": 105235, + "scriptedPlacementId": "castle-patrol-41927395-2", + "sourceControllerSpawnId": 41927395, + "sourceScriptRoot": "cl_136_03_042", + "evidence": [ + "Resource/luascript/02214.lua:cl_136_03_042" + ], + "name": "Conjurer's Apprentice", + "classification": "combat", + "sourcePosition": [ + 910.3460020318735, + 994.526611328125, + 1719.6479057743823 + ], + "sourceDirection": 181, + "localPosition": [ + 131.934432, + -28.049194, + -179.044496 + ], + "projectedPosition": [ + 131.934432, + -27.997608, + -179.044496 + ], + "projectionDistance": 0.051586, + "modelPath": "model/character/creature/black_hat/act_black_hat_blue.ros" + }, + { + "sourceSpawnId": 41927395, + "sourceTemplateId": 105235, + "scriptedPlacementId": "castle-patrol-41927395-3", + "sourceControllerSpawnId": 41927395, + "sourceScriptRoot": "cl_136_03_042", + "evidence": [ + "Resource/luascript/02214.lua:cl_136_03_042" + ], + "name": "Conjurer's Apprentice", + "classification": "combat", + "sourcePosition": [ + 909.6479057743821, + 994.526611328125, + 1679.6539979681265 + ], + "sourceDirection": 181, + "localPosition": [ + 131.864623, + -28.049194, + -175.045105 + ], + "projectedPosition": [ + 131.864623, + -27.997608, + -175.045105 + ], + "projectionDistance": 0.051586, + "modelPath": "model/character/creature/black_hat/act_black_hat_blue.ros" + }, + { + "sourceSpawnId": 41927396, + "sourceTemplateId": 105233, + "scriptedPlacementId": "castle-patrol-41927396-1", + "sourceControllerSpawnId": 41927396, + "sourceScriptRoot": "cl_136_03_042", + "evidence": [ + "Resource/luascript/02214.lua:cl_136_03_042" + ], + "name": "Cursed Steel Soldier", + "classification": "combat", + "sourcePosition": [ + 575, + 994.526611328125, + 1720 + ], + "sourceDirection": 4.999985218048096, + "localPosition": [ + 98.399832, + -28.049194, + -179.079705 + ], + "projectedPosition": [ + 98.399832, + -27.997608, + -179.079705 + ], + "projectionDistance": 0.051586, + "modelPath": "model/character/creature/lost_protector/act_cursed_steel_guardian.ros" + }, + { + "sourceSpawnId": 41927396, + "sourceTemplateId": 105235, + "scriptedPlacementId": "castle-patrol-41927396-2", + "sourceControllerSpawnId": 41927396, + "sourceScriptRoot": "cl_136_03_042", + "evidence": [ + "Resource/luascript/02214.lua:cl_136_03_042" + ], + "name": "Conjurer's Apprentice", + "classification": "combat", + "sourcePosition": [ + 553.3329958737397, + 994.526611328125, + 1701.8192153031664 + ], + "sourceDirection": 4.999985218048096, + "localPosition": [ + 96.233132, + -28.049194, + -177.261627 + ], + "projectedPosition": [ + 96.233132, + -27.997608, + -177.261627 + ], + "projectionDistance": 0.051586, + "modelPath": "model/character/creature/black_hat/act_black_hat_blue.ros" + }, + { + "sourceSpawnId": 41927396, + "sourceTemplateId": 105235, + "scriptedPlacementId": "castle-patrol-41927396-3", + "sourceControllerSpawnId": 41927396, + "sourceScriptRoot": "cl_136_03_042", + "evidence": [ + "Resource/luascript/02214.lua:cl_136_03_042" + ], + "name": "Conjurer's Apprentice", + "classification": "combat", + "sourcePosition": [ + 556.8192153031664, + 994.526611328125, + 1741.6670041262603 + ], + "sourceDirection": 4.999985218048096, + "localPosition": [ + 96.581754, + -28.049194, + -181.246405 + ], + "projectedPosition": [ + 96.581754, + -27.997608, + -181.246405 + ], + "projectionDistance": 0.051586, + "modelPath": "model/character/creature/black_hat/act_black_hat_blue.ros" } ], "deferred": [ diff --git a/src/content/contentCacheWorker.test.ts b/src/content/contentCacheWorker.test.ts index 8fe748c8..c4c4b423 100644 --- a/src/content/contentCacheWorker.test.ts +++ b/src/content/contentCacheWorker.test.ts @@ -2,7 +2,6 @@ import { readFileSync } from "node:fs"; import { gzipSync } from "node:zlib"; import { runInNewContext } from "node:vm"; import { describe, expect, it, vi } from "vitest"; -import { ARENA_STAGES } from "../game/arenaStages"; import { BROWSER_CONTENT_CACHE_NAME } from "./contentManager"; const workerSource = readFileSync( @@ -12,15 +11,22 @@ const workerSource = readFileSync( function contentWorker(cached?: Response, bundledFiles: Record = {}, object?: Response) { const handlers = new Map void>(); + const bootstrapObjects = new Map(); const fetch = vi.fn(async (url: RequestInfo | URL) => String(url) === "/content/bootstrap-manifest.json" - ? Response.json({ files: bundledFiles }) : object?.clone() ?? new Response("bundled arena", { status: 200 })); + ? Response.json({ files: bundledFiles }) : object?.clone() ?? new Response("bundled dungeon", { status: 200 })); const match = vi.fn(async (url: RequestInfo | URL) => String(url).includes("/content/cache-objects/") ? object?.clone() : cached?.clone()); + const bootstrapMatch = vi.fn(async (url: RequestInfo | URL) => bootstrapObjects.get(String(url))?.clone()); + const bootstrapPut = vi.fn(async (url: RequestInfo | URL, response: Response) => { + bootstrapObjects.set(String(url), response.clone()); + }); runInNewContext(workerSource, { self: { location: { origin: "https://healer.test" }, addEventListener: (type: string, handler: (event: unknown) => void) => handlers.set(type, handler), }, - caches: { open: async () => ({ match }) }, + caches: { open: async (name: string) => name === "healer-man-bootstrap-v1" + ? { match: bootstrapMatch, put: bootstrapPut } + : { match } }, fetch, URL, Response, @@ -30,6 +36,8 @@ function contentWorker(cached?: Response, bundledFiles: Record return { fetch, match, + bootstrapMatch, + bootstrapPut, refresh: () => handlers.get("message")!({ data: { type: "refresh-content-bootstrap" } }), request: (path: string) => { let response: Promise | undefined; @@ -43,20 +51,28 @@ function contentWorker(cached?: Response, bundledFiles: Record } describe("browser content cache worker", () => { - it.each(ARENA_STAGES)("loads bundled $name assets without an optional content install", async (stage) => { - const worker = contentWorker(); - for (const asset of stage.assets) { - const response = await worker.request(`${asset.url}?arena-runtime=1`); + it("loads starter-dungeon assets without an optional content install", async () => { + const paths = [ + "/assets/game/dungeons/wailing-caverns/visual.glb", + "/assets/creatures/wailing-caverns/raptor.glb", + ]; + const files = Object.fromEntries(paths.map((path, index) => [ + path, + { url: `/content/bundled/${String(index + 1).repeat(64)}.glb` }, + ])); + const worker = contentWorker(undefined, files); + for (const path of paths) { + const response = await worker.request(`${path}?starter-runtime=1`); expect(response?.status).toBe(200); - expect(await response?.text()).toBe("bundled arena"); + expect(await response?.text()).toBe("bundled dungeon"); } - expect(worker.fetch).toHaveBeenCalledTimes(stage.assets.length + 1); + expect(worker.fetch).toHaveBeenCalledTimes(paths.length + 1); }); - it("prefers installed arena updates over bundled files, ignoring the runtime query", async () => { - const worker = contentWorker(new Response("updated arena")); - const response = await worker.request(`${ARENA_STAGES[0].assets[0].url}?arena-runtime=1`); - expect(await response?.text()).toBe("updated arena"); + it("prefers installed dungeon updates over bundled files, ignoring the runtime query", async () => { + const worker = contentWorker(new Response("updated dungeon")); + const response = await worker.request("/assets/game/dungeons/wailing-caverns/visual.glb?starter-runtime=1"); + expect(await response?.text()).toBe("updated dungeon"); expect(worker.match).toHaveBeenCalledWith(expect.any(Request), { ignoreSearch: true }); expect(worker.fetch).not.toHaveBeenCalled(); }); @@ -72,6 +88,7 @@ describe("browser content cache worker", () => { it("serves only cached same-origin asset requests from the active content cache", () => { expect(workerSource).toContain(`const CONTENT_CACHE_NAME = "${BROWSER_CONTENT_CACHE_NAME}";`); + expect(workerSource).toContain('const BOOTSTRAP_CACHE_NAME = "healer-man-bootstrap-v1";'); expect(workerSource).toContain('request.method !== "GET"'); expect(workerSource).toContain("url.origin !== self.location.origin"); expect(workerSource).toContain("MANAGED_ASSET_PREFIXES.some"); @@ -79,6 +96,7 @@ describe("browser content cache worker", () => { expect(workerSource).toContain("if (!objectPath) return cached"); expect(workerSource).toContain("BUNDLED_ASSET_PATHS.has(url.pathname)"); expect(workerSource).toContain("BUNDLED_ASSET_PREFIXES.some"); + expect(workerSource).not.toContain('"/assets/game/arenas/",'); expect(workerSource).not.toContain('"/assets/characters/wow335a-v1/textures/character/human/",'); expect(workerSource).toContain('status: 404'); expect(workerSource).not.toContain("cached ?? fetch(request)"); @@ -93,6 +111,17 @@ describe("browser content cache worker", () => { expect(response?.headers.get("content-type")).toBe("model/gltf-binary"); }); + it("persists immutable bootstrap objects after their first use", async () => { + const url = "/assets/game/dungeons/wailing-caverns/visual.glb"; + const objectUrl = `/content/bundled/${"b".repeat(64)}.glb`; + const worker = contentWorker(undefined, { [url]: { url: objectUrl } }, new Response("persistent dungeon")); + expect(await (await worker.request(url))?.text()).toBe("persistent dungeon"); + expect(await (await worker.request(url))?.text()).toBe("persistent dungeon"); + expect(worker.fetch).toHaveBeenCalledTimes(2); + expect(worker.bootstrapPut).toHaveBeenCalledTimes(1); + expect(worker.bootstrapMatch).toHaveBeenCalledTimes(2); + }); + it("resolves a tiny installed alias to one compressed shared object", async () => { const original = "glTF installed original".repeat(100); const worker = contentWorker(new Response(null, { headers: { "x-healer-man-content-object": "/content/cache-objects/shared.glb" } }), {}, diff --git a/src/content/contentManager.ts b/src/content/contentManager.ts index c1d711e0..65b47fd8 100644 --- a/src/content/contentManager.ts +++ b/src/content/contentManager.ts @@ -8,25 +8,11 @@ import { type ContentManifestFile, } from "./contentManifest"; import { NativeContentStorage } from "./nativeContentStorage"; +import contentPolicy from "../../shared/content-policy.json"; export const CONTENT_STATE_KEY = "healer-man.content-state.v2"; const PRODUCTION_ORIGIN = "https://iwanttoheal.phenomrom.com"; -const STATIC_ANDROID_BUNDLED_PACKS = new Set([ - "core:ui", - "core:equipment", - "core:character-basic", - "core:runtime", - "core:arenas", - "dungeon:wailing-caverns", -]); -const STATIC_BROWSER_BUNDLED_PACKS = new Set([ - "core:ui", - "core:equipment", - "core:character-basic", - "core:runtime", - "core:arenas", - "dungeon:wailing-caverns", -]); +const STATIC_BUNDLED_PACKS = new Set(contentPolicy.bootstrapPackIds); export const BROWSER_CONTENT_CACHE_NAME = "healer-man-content-v1"; export const BROWSER_CONTENT_WORKER_URL = "/content-cache-sw.js"; @@ -292,9 +278,7 @@ function packReceiptForManifest(manifest: ContentManifest, packId: string): Inst function bundledFiles(): Readonly> { if (snapshot.bundledManifest) return snapshot.bundledManifest.files; if (!snapshot.manifest) return {}; - const bundledPacks = Capacitor.isNativePlatform() - ? STATIC_ANDROID_BUNDLED_PACKS - : STATIC_BROWSER_BUNDLED_PACKS; + const bundledPacks = STATIC_BUNDLED_PACKS; return Object.fromEntries(Object.entries(snapshot.manifest.files).filter(([, entry]) => ( bundledPacks.has(entry.packId) ))); @@ -324,8 +308,7 @@ export function resolveContentUrl(logicalUrl: string): string { } export function contentPackIsBundled(packId: string): boolean { - const defaults = Capacitor.isNativePlatform() ? STATIC_ANDROID_BUNDLED_PACKS : STATIC_BROWSER_BUNDLED_PACKS; - return defaults.has(packId) + return STATIC_BUNDLED_PACKS.has(packId) || Boolean(snapshot.bundledManifest?.packs.some((pack) => pack.id === packId && pack.bundledOnAndroid)); } diff --git a/src/game/ShelvedDungeonSession.disabled.ts b/src/game/ShelvedDungeonSession.disabled.ts new file mode 100644 index 00000000..35d2d032 --- /dev/null +++ b/src/game/ShelvedDungeonSession.disabled.ts @@ -0,0 +1,5 @@ +import type { WorldSpawn } from "./dungeonTypes"; + +export function shelvedDungeonSpawn(_dungeonId: string, requested?: WorldSpawn): WorldSpawn | undefined { + return requested; +} diff --git a/src/game/ShelvedDungeonSession.enabled.ts b/src/game/ShelvedDungeonSession.enabled.ts new file mode 100644 index 00000000..3cb9dd90 --- /dev/null +++ b/src/game/ShelvedDungeonSession.enabled.ts @@ -0,0 +1,6 @@ +import type { WorldSpawn } from "./dungeonTypes"; +import { useManastormAdminStore } from "./manastormAdminStore"; + +export function shelvedDungeonSpawn(dungeonId: string, requested?: WorldSpawn): WorldSpawn | undefined { + return requested ?? useManastormAdminStore.getState().config.dungeonSpawns[dungeonId]; +} diff --git a/src/game/abilityCatalog.ts b/src/game/abilityCatalog.ts index ea7d5df6..3a646d28 100644 --- a/src/game/abilityCatalog.ts +++ b/src/game/abilityCatalog.ts @@ -1060,7 +1060,7 @@ export function resourceProfileForClass(classId: ClassId): ClassResourceProfile return profile; } -if (import.meta.env.MODE === "test") { +if (import.meta.env.MODE === "test" || import.meta.env.MODE === "shelved") { await preloadAbilityCatalogsForClasses([ ...BASE_CLASS_IDS, ...COA_CLASS_IDS, diff --git a/src/game/dungeonSession.ts b/src/game/dungeonSession.ts index 851e510d..039255d6 100644 --- a/src/game/dungeonSession.ts +++ b/src/game/dungeonSession.ts @@ -8,8 +8,8 @@ import { usePartyStore } from "./partyStore"; import { useGameStore, type GameMode } from "./store"; import { dungeonCanEnter, dungeonDefinitionById, type DungeonId } from "./dungeonRegistry"; import type { WorldSpawn } from "./dungeonTypes"; -import { useManastormAdminStore } from "./manastormAdminStore"; import type { GmReviewTarget } from "./gmContentReview"; +import { shelvedDungeonSpawn } from "@shelvedDungeonSession"; import { activateDungeonEncounterDefinitions, exportDungeonEncounterSnapshots, @@ -92,7 +92,7 @@ export function activateDungeonSession( } useGameStore.getState().activateDungeon(dungeonId as DungeonId, difficultyId, { gameMode: options.gameMode, - spawn: options.spawn ?? useManastormAdminStore.getState().config.dungeonSpawns[dungeonId], + spawn: shelvedDungeonSpawn(dungeonId, options.spawn), gmTestMode: options.gmTestMode, gmReviewTarget: options.gmReviewTarget, }); diff --git a/src/game/dungeonSessionAssetCache.test.ts b/src/game/dungeonSessionAssetCache.test.ts index a23da97d..20d69f66 100644 --- a/src/game/dungeonSessionAssetCache.test.ts +++ b/src/game/dungeonSessionAssetCache.test.ts @@ -7,6 +7,7 @@ import { usePartyStore } from "./partyStore"; import { useGameStore } from "./store"; import { DEFAULT_MANASTORM_ADMIN_CONFIG } from "./manastormAdminConfig"; import { useManastormAdminStore } from "./manastormAdminStore"; +import { SHELVED_MODES_ENABLED } from "../app/releaseFeatures"; import { dungeonEncounterSnapshot, importDungeonEncounterSnapshots, @@ -79,7 +80,11 @@ describe("dungeon session asset eviction", () => { config: { ...DEFAULT_MANASTORM_ADMIN_CONFIG, dungeonSpawns: { "wailing-caverns": spawn } }, }); expect(activateDungeonSession("wailing-caverns")).toBe(true); - expect(useGameStore.getState().activeSpawn).toEqual(spawn); + const definition = dungeonDefinitionById("wailing-caverns")!; + expect(useGameStore.getState().activeSpawn).toEqual(SHELVED_MODES_ENABLED ? spawn : { + footPosition: definition.entrance.footPosition, + yaw: definition.entrance.yaw, + }); }); it("hydrates a same-process checkpoint even when the reset revision is newer", () => { diff --git a/src/game/generated/grafuCastleMechanics.generated.ts b/src/game/generated/grafuCastleMechanics.generated.ts index 8d1f68dd..c5b068a4 100644 --- a/src/game/generated/grafuCastleMechanics.generated.ts +++ b/src/game/generated/grafuCastleMechanics.generated.ts @@ -15,6 +15,110 @@ export const GRAFU_CASTLE_MECHANICS_AUDIT = { "loot": 0 }, "bossRows": [], + "scriptedBossSpawns": [ + { + "scriptedPlacementId": "cayus", + "sourceAnchorSpawnId": 41927224, + "controllerSpawnId": 41928351, + "templateId": 105616, + "name": "Cayus", + "sourcePosition": [ + -348.7113952636719, + 1254.5040283203125, + -999.6005249023438 + ], + "sourceScriptRoot": "Dy_Z19_BOSSN_AIC", + "objective": true, + "evidence": [ + "Resource/luascript/02345.lua:CreateObjByFlag(105616,780696,1,1)" + ] + }, + { + "scriptedPlacementId": "ardmond", + "sourceAnchorSpawnId": 41926939, + "controllerSpawnId": 41928345, + "templateId": 105542, + "name": "Ardmond", + "sourcePosition": [ + -1427.9969482421875, + 1133.35498046875, + -1353.3699951171875 + ], + "sourceScriptRoot": "kg_105542_01", + "objective": true, + "evidence": [ + "Resource/luascript/02324.lua:kg_105542_CreateBoss" + ] + }, + { + "scriptedPlacementId": "issac", + "sourceAnchorSpawnId": 41926939, + "controllerSpawnId": 41928345, + "templateId": 105543, + "name": "Issac", + "sourcePosition": [ + -1427.9969482421875, + 1133.35498046875, + -1313.3699951171875 + ], + "sourceScriptRoot": "kg_105542_01", + "objective": false, + "evidence": [ + "Resource/luascript/02324.lua:kg_105542_CreateBoss" + ] + }, + { + "scriptedPlacementId": "boro-boro-brothers", + "sourceAnchorSpawnId": 41926924, + "controllerSpawnId": 41927347, + "templateId": 105224, + "name": "Boro-Boro Brothers", + "sourcePosition": [ + -2172.174072265625, + 1095.10400390625, + -22.212709426879883 + ], + "sourceScriptRoot": "ic_105224_01", + "objective": true, + "evidence": [ + "Resource/luascript/02231.lua:CreateObjByFlag(105224,780775,1,1)" + ] + }, + { + "scriptedPlacementId": "conjurer-chapeaunoir", + "sourceAnchorSpawnId": 41926979, + "controllerSpawnId": 41927358, + "templateId": 105204, + "name": "Conjurer Chapeaunoir", + "sourcePosition": [ + -1732.9649658203125, + 914.618896484375, + 1190.8310546875 + ], + "sourceScriptRoot": "cl_136_105205_N", + "objective": true, + "evidence": [ + "Resource/luascript/02330.lua:CreateObjByFlag(105204,780767,0,1)" + ] + }, + { + "scriptedPlacementId": "larvanger-barkud", + "sourceAnchorSpawnId": 41927071, + "controllerSpawnId": 41927369, + "templateId": 105351, + "name": "Larvanger Barkud", + "sourcePosition": [ + -305.8063049316406, + 902.5551147460938, + 941.4002075195312 + ], + "sourceScriptRoot": "un_zone19_boss5_allconreal_1_01", + "objective": true, + "evidence": [ + "Resource/luascript/02297.lua:CreateObjByFlag(105351,780838,1,1)" + ] + } + ], "templateCandidates": [ { "templateId": 102678, @@ -94,6 +198,18 @@ export const GRAFU_CASTLE_MECHANICS_AUDIT = { "templateScripts": [], "spells": [] }, + { + "templateId": 105204, + "name": "Conjurer Chapeaunoir", + "classification": "boss", + "spawnCount": 0, + "autoPlots": [ + "cl_136_105205_N" + ], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, { "templateId": 105205, "name": "Template 105205", @@ -109,7 +225,7 @@ export const GRAFU_CASTLE_MECHANICS_AUDIT = { { "templateId": 105210, "name": "Template 105210", - "classification": "combat", + "classification": "deferred-object", "spawnCount": 1, "autoPlots": [ "kg_105542_01" @@ -204,10 +320,82 @@ export const GRAFU_CASTLE_MECHANICS_AUDIT = { "templateScripts": [], "spells": [] }, + { + "templateId": 105224, + "name": "Boro-Boro Brothers", + "classification": "boss", + "spawnCount": 0, + "autoPlots": [ + "ic_105224_01" + ], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, + { + "templateId": 105229, + "name": "Smirking Castle Clown", + "classification": "combat", + "spawnCount": 0, + "autoPlots": [], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, + { + "templateId": 105230, + "name": "Odd Castle Clown", + "classification": "combat", + "spawnCount": 0, + "autoPlots": [], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, + { + "templateId": 105233, + "name": "Cursed Steel Soldier", + "classification": "combat", + "spawnCount": 0, + "autoPlots": [], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, + { + "templateId": 105235, + "name": "Conjurer's Apprentice", + "classification": "combat", + "spawnCount": 0, + "autoPlots": [], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, + { + "templateId": 105236, + "name": "Giant Enraged Bear", + "classification": "combat", + "spawnCount": 0, + "autoPlots": [], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, + { + "templateId": 105237, + "name": "Manipulated Bear", + "classification": "combat", + "spawnCount": 0, + "autoPlots": [], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, { "templateId": 105254, "name": "Template 105254", - "classification": "combat", + "classification": "deferred-object", "spawnCount": 10, "autoPlots": [ "cl_136_01_01_N", @@ -225,7 +413,7 @@ export const GRAFU_CASTLE_MECHANICS_AUDIT = { { "templateId": 105258, "name": "Template 105258", - "classification": "combat", + "classification": "deferred-object", "spawnCount": 5, "autoPlots": [ "cl_105258_1", @@ -265,6 +453,42 @@ export const GRAFU_CASTLE_MECHANICS_AUDIT = { "templateScripts": [], "spells": [] }, + { + "templateId": 105351, + "name": "Larvanger Barkud", + "classification": "boss", + "spawnCount": 0, + "autoPlots": [ + "un_zone19_boss5_allconreal_1_01" + ], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, + { + "templateId": 105542, + "name": "Ardmond", + "classification": "boss", + "spawnCount": 0, + "autoPlots": [ + "kg_105542_01" + ], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, + { + "templateId": 105543, + "name": "Issac", + "classification": "boss", + "spawnCount": 0, + "autoPlots": [ + "kg_105542_01" + ], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, { "templateId": 105596, "name": "Smirking Castle Clown", @@ -332,6 +556,16 @@ export const GRAFU_CASTLE_MECHANICS_AUDIT = { } ] }, + { + "templateId": 105599, + "name": "Cursed Steel Soldier", + "classification": "combat", + "spawnCount": 0, + "autoPlots": [], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, { "templateId": 105600, "name": "Juggler's Apprentice", @@ -420,6 +654,18 @@ export const GRAFU_CASTLE_MECHANICS_AUDIT = { } ] }, + { + "templateId": 105616, + "name": "Cayus", + "classification": "boss", + "spawnCount": 0, + "autoPlots": [ + "Dy_Z19_BOSSN_AIC" + ], + "plotClassNames": [], + "templateScripts": [], + "spells": [] + }, { "templateId": 116434, "name": "Skeleton of an Adventurer", diff --git a/src/game/generated/grafuCastlePopulation.generated.ts b/src/game/generated/grafuCastlePopulation.generated.ts index c4666595..488a10cf 100644 --- a/src/game/generated/grafuCastlePopulation.generated.ts +++ b/src/game/generated/grafuCastlePopulation.generated.ts @@ -43,46 +43,6 @@ export const GRAFU_CASTLE_ENTITIES = { } } }, - "rw-grafu-castle-105254": { - "id": "rw-grafu-castle-105254", - "name": "Template 105254", - "kind": "mob", - "hasLoot": false, - "combat": { - "level": 1, - "healthMultiplier": 1, - "damageMultiplier": 1, - "moveSpeed": 2.15, - "leashRange": 24, - "attacks": [ - { - "id": "basic-attack", - "name": "Basic Attack", - "delivery": "melee", - "target": "primary", - "school": "physical", - "animation": "attack", - "range": 4.5, - "cooldownMs": 1900, - "damageMultiplier": 1 - } - ] - }, - "visual": { - "primaryColor": "hsl(178 22% 38%)", - "accentColor": "hsl(206 38% 64%)", - "scale": 1, - "archetype": "plant", - "model": { - "url": "/assets/creatures/grafu-castle/elder-myconid.glb", - "rotationY": 0, - "groundOffset": -0.00658, - "labelHeight": 1.9954, - "markerRadius": 0.41571, - "animationMode": "native" - } - } - }, "rw-grafu-castle-105596": { "id": "rw-grafu-castle-105596", "name": "Smirking Castle Clown", @@ -703,86 +663,6 @@ export const GRAFU_CASTLE_ENTITIES = { } } }, - "rw-grafu-castle-105258": { - "id": "rw-grafu-castle-105258", - "name": "Template 105258", - "kind": "mob", - "hasLoot": false, - "combat": { - "level": 1, - "healthMultiplier": 1, - "damageMultiplier": 1, - "moveSpeed": 2.15, - "leashRange": 24, - "attacks": [ - { - "id": "basic-attack", - "name": "Basic Attack", - "delivery": "melee", - "target": "primary", - "school": "physical", - "animation": "attack", - "range": 4.5, - "cooldownMs": 1900, - "damageMultiplier": 1 - } - ] - }, - "visual": { - "primaryColor": "hsl(6 22% 38%)", - "accentColor": "hsl(34 38% 64%)", - "scale": 1, - "archetype": "plant", - "model": { - "url": "/assets/creatures/grafu-castle/elder-myconid.glb", - "rotationY": 0, - "groundOffset": -0.00658, - "labelHeight": 1.9954, - "markerRadius": 0.41571, - "animationMode": "native" - } - } - }, - "rw-grafu-castle-105210": { - "id": "rw-grafu-castle-105210", - "name": "Template 105210", - "kind": "mob", - "hasLoot": false, - "combat": { - "level": 10, - "healthMultiplier": 1, - "damageMultiplier": 1, - "moveSpeed": 2.15, - "leashRange": 24, - "attacks": [ - { - "id": "basic-attack", - "name": "Basic Attack", - "delivery": "melee", - "target": "primary", - "school": "physical", - "animation": "attack", - "range": 4.5, - "cooldownMs": 1900, - "damageMultiplier": 1 - } - ] - }, - "visual": { - "primaryColor": "hsl(270 22% 38%)", - "accentColor": "hsl(298 38% 64%)", - "scale": 1, - "archetype": "humanoid", - "model": { - "url": "/assets/creatures/grafu-castle/cereus-01.glb", - "rotationY": 0, - "groundOffset": 0.00502, - "labelHeight": 2.05623, - "markerRadius": 0.65315, - "animationMode": "native" - } - } - }, "rw-grafu-castle-105211": { "id": "rw-grafu-castle-105211", "name": "Template 105211", @@ -822,6 +702,2627 @@ export const GRAFU_CASTLE_ENTITIES = { "animationMode": "native" } } + }, + "rw-grafu-castle-105616": { + "id": "rw-grafu-castle-105616", + "name": "Cayus", + "kind": "boss", + "title": "Cayus", + "hasLoot": false, + "combat": { + "level": 68, + "healthMultiplier": 7.5, + "damageMultiplier": 1.3, + "moveSpeed": 2.25, + "leashRange": 32, + "mechanicIds": [ + "rw-grafu-castle-105616-spell-496859", + "rw-grafu-castle-105616-spell-496860", + "rw-grafu-castle-105616-spell-496861", + "rw-grafu-castle-105616-spell-496864", + "rw-grafu-castle-105616-spell-496865", + "rw-grafu-castle-105616-control-496867", + "rw-grafu-castle-105616-control-496868", + "rw-grafu-castle-105616-control-496869", + "rw-grafu-castle-105616-spell-497516", + "rw-grafu-castle-105616-spell-497517", + "rw-grafu-castle-105616-spell-497518", + "rw-grafu-castle-105616-spell-497519", + "rw-grafu-castle-105616-spell-497520" + ], + "attacks": [ + { + "id": "basic-attack", + "name": "Basic Attack", + "delivery": "melee", + "target": "primary", + "school": "physical", + "animation": "attack", + "range": 4.5, + "cooldownMs": 1900, + "damageMultiplier": 1 + }, + { + "id": "rw-grafu-castle-105616-spell-496859", + "name": "Manastorms Duo Lv5", + "spellId": 496859, + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 0, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496859, + "componentId": 508500, + "spellLevel": 0, + "calculationPath": "magic", + "powerKind": "source-points", + "damagePower": -30, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105616-spell-496860", + "name": "Forward Flip", + "spellId": 496860, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "attack", + "castTimeMs": 1800, + "range": 15, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496860, + "componentId": 508501, + "spellLevel": 0, + "calculationPath": "physical", + "powerKind": "source-points", + "damagePower": -40, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105616-spell-496861", + "name": "Flying Knife", + "spellId": 496861, + "delivery": "projectile", + "target": "primary", + "school": "physical", + "animation": "cast", + "castTimeMs": 0, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496861, + "componentId": 508503, + "spellLevel": 0, + "calculationPath": "physical", + "powerKind": "source-points", + "damagePower": -30000, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105616-spell-496864", + "name": "Juggler Trick", + "spellId": 496864, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "attack", + "castTimeMs": 3000, + "range": 8, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 8, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 8 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496864, + "componentId": 508504, + "spellLevel": 0, + "calculationPath": "physical", + "powerKind": "source-points", + "damagePower": -1000, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105616-spell-496865", + "name": "Gale Spray", + "spellId": 496865, + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 7.5, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 7.5, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 7.5 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496865, + "componentId": 508505, + "spellLevel": 0, + "calculationPath": "magic", + "powerKind": "source-points", + "damagePower": -5000, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105616-control-496867", + "name": "Indigo Melancholy", + "delivery": "area", + "target": "nearby-party", + "school": "shadow", + "animation": "cast", + "castTimeMs": 1800, + "range": 5, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "effect": { + "kind": "control", + "mechanic": "fear", + "durationMs": 3000 + } + }, + { + "id": "rw-grafu-castle-105616-control-496868", + "name": "Purple Terror", + "delivery": "area", + "target": "nearby-party", + "school": "shadow", + "animation": "cast", + "castTimeMs": 1800, + "range": 5, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "effect": { + "kind": "control", + "mechanic": "fear", + "durationMs": 3000 + } + }, + { + "id": "rw-grafu-castle-105616-control-496869", + "name": "Still Dusk", + "delivery": "area", + "target": "nearby-party", + "school": "shadow", + "animation": "cast", + "castTimeMs": 1800, + "range": 5, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "effect": { + "kind": "control", + "mechanic": "fear", + "durationMs": 3000 + } + }, + { + "id": "rw-grafu-castle-105616-spell-497516", + "name": "Throwing Knife", + "spellId": 497516, + "delivery": "projectile", + "target": "primary", + "school": "physical", + "animation": "cast", + "castTimeMs": 0, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 497516, + "componentId": 509478, + "spellLevel": 0, + "calculationPath": "physical", + "powerKind": "source-points", + "damagePower": -20000, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105616-spell-497517", + "name": "Manastorms Duo Lv5", + "spellId": 497517, + "delivery": "projectile", + "target": "primary", + "school": "physical", + "animation": "cast", + "castTimeMs": 0, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 497517, + "componentId": 509479, + "spellLevel": 0, + "calculationPath": "physical", + "powerKind": "source-points", + "damagePower": -60, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105616-spell-497518", + "name": "Manastorms Duo Lv5", + "spellId": 497518, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 497518, + "componentId": 509599, + "spellLevel": 0, + "calculationPath": "physical", + "powerKind": "source-points", + "damagePower": -60, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105616-spell-497519", + "name": "Manastorms Duo Lv5", + "spellId": 497519, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 497519, + "componentId": 509599, + "spellLevel": 0, + "calculationPath": "physical", + "powerKind": "source-points", + "damagePower": -60, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105616-spell-497520", + "name": "Manastorms Duo Lv5", + "spellId": 497520, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 497520, + "componentId": 509599, + "spellLevel": 0, + "calculationPath": "physical", + "powerKind": "source-points", + "damagePower": -60, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + } + ] + }, + "visual": { + "primaryColor": "hsl(272 22% 38%)", + "accentColor": "hsl(300 38% 64%)", + "scale": 1, + "archetype": "humanoid", + "model": { + "url": "/assets/creatures/grafu-castle/karth.glb", + "rotationY": 0, + "groundOffset": 0.00007, + "labelHeight": 2.96097, + "markerRadius": 0.75645, + "animationMode": "native" + } + } + }, + "rw-grafu-castle-105542": { + "id": "rw-grafu-castle-105542", + "name": "Ardmond", + "kind": "boss", + "title": "Ardmond", + "hasLoot": false, + "combat": { + "level": 68, + "healthMultiplier": 7.5, + "damageMultiplier": 1.3, + "moveSpeed": 2.25, + "leashRange": 32, + "mechanicIds": [ + "rw-grafu-castle-105542-debuff-495767", + "rw-grafu-castle-105542-debuff-496245", + "rw-grafu-castle-105542-debuff-496930", + "rw-grafu-castle-105542-debuff-496931", + "rw-grafu-castle-105542-debuff-496932", + "rw-grafu-castle-105542-aura-496933", + "rw-grafu-castle-105542-spell-496934", + "rw-grafu-castle-105542-spell-496936", + "rw-grafu-castle-105542-spell-496938", + "rw-grafu-castle-105542-debuff-496940", + "rw-grafu-castle-105542-spell-497444" + ], + "attacks": [ + { + "id": "basic-attack", + "name": "Basic Attack", + "delivery": "melee", + "target": "primary", + "school": "physical", + "animation": "attack", + "range": 4.5, + "cooldownMs": 1900, + "damageMultiplier": 1 + }, + { + "id": "rw-grafu-castle-105542-debuff-495767", + "name": "Manastorms Duo Lv5", + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "effect": { + "kind": "target-aura", + "aura": "manastorms-duo-lv5", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105542-debuff-496245", + "name": "Shackle Bind", + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "radius": 10, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 10 + }, + "effect": { + "kind": "target-aura", + "aura": "shackle-bind", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105542-debuff-496930", + "name": "Manastorms Duo Lv5", + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "target-aura", + "aura": "manastorms-duo-lv5", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105542-debuff-496931", + "name": "Rend", + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "target-aura", + "aura": "rupture", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105542-debuff-496932", + "name": "Tear Apart", + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "target-aura", + "aura": "armor-break", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105542-aura-496933", + "name": "Frightening Roar", + "delivery": "area", + "target": "self", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1200, + "range": 0, + "cooldownMs": 15000, + "initialCooldownMs": [ + 15000, + 15000 + ], + "repeatCooldownMs": [ + 15000, + 15000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "aura", + "aura": "shock", + "durationMs": 7500 + } + }, + { + "id": "rw-grafu-castle-105542-spell-496934", + "name": "Roaring Charge", + "spellId": 496934, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "telegraph": { + "shape": "line", + "anchor": "source", + "warningMs": 1800, + "length": 30, + "width": 2 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496934, + "componentId": 508548, + "spellLevel": 0, + "calculationPath": "physical", + "powerKind": "source-points", + "damagePower": -5, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105542-spell-496936", + "name": "Thrash", + "spellId": 496936, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.2, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496936, + "componentId": 508550, + "spellLevel": 0, + "calculationPath": "physical", + "powerKind": "source-ratio", + "damagePower": -0.1, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.2, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105542-spell-496938", + "name": "Flog", + "spellId": 496938, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 4000, + "range": 10, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 10, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 10 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496938, + "componentId": 508553, + "spellLevel": 0, + "calculationPath": "physical", + "powerKind": "source-points", + "damagePower": -250, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105542-debuff-496940", + "name": "Lion Bind", + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "target-aura", + "aura": "lion-bind", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105542-spell-497444", + "name": "Feral Attack", + "spellId": 497444, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 3000, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 10, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 10 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 497444, + "componentId": 509368, + "spellLevel": 0, + "calculationPath": "physical", + "powerKind": "source-points", + "damagePower": -5, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + } + ] + }, + "visual": { + "primaryColor": "hsl(34 22% 38%)", + "accentColor": "hsl(62 38% 64%)", + "scale": 1, + "archetype": "humanoid", + "model": { + "url": "/assets/creatures/grafu-castle/atmo.glb", + "rotationY": 0, + "groundOffset": -0.00233, + "labelHeight": 2.35005, + "markerRadius": 0.64732, + "animationMode": "native" + } + } + }, + "rw-grafu-castle-105543": { + "id": "rw-grafu-castle-105543", + "name": "Issac", + "kind": "boss", + "title": "Issac", + "hasLoot": false, + "combat": { + "level": 68, + "healthMultiplier": 7.5, + "damageMultiplier": 1.3, + "moveSpeed": 2.25, + "leashRange": 32, + "mechanicIds": [ + "rw-grafu-castle-105543-debuff-495767", + "rw-grafu-castle-105543-debuff-496245", + "rw-grafu-castle-105543-debuff-496930", + "rw-grafu-castle-105543-debuff-496931", + "rw-grafu-castle-105543-debuff-496932", + "rw-grafu-castle-105543-aura-496933", + "rw-grafu-castle-105543-spell-496934", + "rw-grafu-castle-105543-spell-496936", + "rw-grafu-castle-105543-spell-496938", + "rw-grafu-castle-105543-debuff-496940", + "rw-grafu-castle-105543-spell-497444" + ], + "attacks": [ + { + "id": "basic-attack", + "name": "Basic Attack", + "delivery": "melee", + "target": "primary", + "school": "physical", + "animation": "attack", + "range": 4.5, + "cooldownMs": 1900, + "damageMultiplier": 1 + }, + { + "id": "rw-grafu-castle-105543-debuff-495767", + "name": "Manastorms Duo Lv5", + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "effect": { + "kind": "target-aura", + "aura": "manastorms-duo-lv5", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105543-debuff-496245", + "name": "Shackle Bind", + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "radius": 10, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 10 + }, + "effect": { + "kind": "target-aura", + "aura": "shackle-bind", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105543-debuff-496930", + "name": "Manastorms Duo Lv5", + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "target-aura", + "aura": "manastorms-duo-lv5", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105543-debuff-496931", + "name": "Rend", + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "target-aura", + "aura": "rupture", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105543-debuff-496932", + "name": "Tear Apart", + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "target-aura", + "aura": "armor-break", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105543-aura-496933", + "name": "Frightening Roar", + "delivery": "area", + "target": "self", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1200, + "range": 0, + "cooldownMs": 15000, + "initialCooldownMs": [ + 15000, + 15000 + ], + "repeatCooldownMs": [ + 15000, + 15000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "aura", + "aura": "shock", + "durationMs": 7500 + } + }, + { + "id": "rw-grafu-castle-105543-spell-496934", + "name": "Roaring Charge", + "spellId": 496934, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "telegraph": { + "shape": "line", + "anchor": "source", + "warningMs": 1800, + "length": 30, + "width": 2 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496934, + "componentId": 508548, + "spellLevel": 0, + "calculationPath": "physical", + "powerKind": "source-points", + "damagePower": -5, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105543-spell-496936", + "name": "Thrash", + "spellId": 496936, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.2, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496936, + "componentId": 508550, + "spellLevel": 0, + "calculationPath": "physical", + "powerKind": "source-ratio", + "damagePower": -0.1, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.2, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105543-spell-496938", + "name": "Flog", + "spellId": 496938, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 4000, + "range": 10, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 10, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 10 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496938, + "componentId": 508553, + "spellLevel": 0, + "calculationPath": "physical", + "powerKind": "source-points", + "damagePower": -250, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105543-debuff-496940", + "name": "Lion Bind", + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "target-aura", + "aura": "lion-bind", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105543-spell-497444", + "name": "Feral Attack", + "spellId": 497444, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 3000, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 10, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 10 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 497444, + "componentId": 509368, + "spellLevel": 0, + "calculationPath": "physical", + "powerKind": "source-points", + "damagePower": -5, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + } + ] + }, + "visual": { + "primaryColor": "hsl(81 22% 38%)", + "accentColor": "hsl(109 38% 64%)", + "scale": 1, + "archetype": "humanoid", + "model": { + "url": "/assets/creatures/grafu-castle/mount-millenium-chimera-laby.glb", + "rotationY": 0, + "groundOffset": 0.01121, + "labelHeight": 2.0949, + "markerRadius": 1.23829, + "animationMode": "native" + } + } + }, + "rw-grafu-castle-105224": { + "id": "rw-grafu-castle-105224", + "name": "Boro-Boro Brothers", + "kind": "boss", + "title": "Boro-Boro Brothers", + "hasLoot": false, + "combat": { + "level": 68, + "healthMultiplier": 7.5, + "damageMultiplier": 1.3, + "moveSpeed": 2.25, + "leashRange": 32, + "mechanicIds": [ + "rw-grafu-castle-105224-spell-496249", + "rw-grafu-castle-105224-aura-496250", + "rw-grafu-castle-105224-spell-496361", + "rw-grafu-castle-105224-spell-496362", + "rw-grafu-castle-105224-control-496363", + "rw-grafu-castle-105224-spell-496366", + "rw-grafu-castle-105224-aura-496367", + "rw-grafu-castle-105224-spell-496368" + ], + "attacks": [ + { + "id": "basic-attack", + "name": "Basic Attack", + "delivery": "melee", + "target": "primary", + "school": "physical", + "animation": "attack", + "range": 4.5, + "cooldownMs": 1900, + "damageMultiplier": 1 + }, + { + "id": "rw-grafu-castle-105224-spell-496249", + "name": "Primal Dread", + "spellId": 496249, + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 15, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496249, + "componentId": 508685, + "spellLevel": 0, + "calculationPath": "magic", + "powerKind": "source-points", + "damagePower": -2000, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105224-aura-496250", + "name": "Torment", + "delivery": "area", + "target": "self", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1200, + "range": 0, + "cooldownMs": 15000, + "initialCooldownMs": [ + 15000, + 15000 + ], + "repeatCooldownMs": [ + 15000, + 15000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "aura", + "aura": "torment", + "durationMs": 7500 + } + }, + { + "id": "rw-grafu-castle-105224-spell-496361", + "name": "Torture Method", + "spellId": 496361, + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.2, + "radius": 5, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 5 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496361, + "componentId": 507903, + "spellLevel": 0, + "calculationPath": "physical", + "powerKind": "source-ratio", + "damagePower": -0.1, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.2, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105224-spell-496362", + "name": "Tyrannize", + "spellId": 496362, + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 5, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 5 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496362, + "componentId": 507904, + "spellLevel": 0, + "calculationPath": "magic", + "powerKind": "source-points", + "damagePower": -100, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105224-control-496363", + "name": "Iron Maiden Imprisonment", + "delivery": "projectile", + "target": "primary", + "school": "shadow", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "control", + "mechanic": "root", + "durationMs": 3000 + } + }, + { + "id": "rw-grafu-castle-105224-spell-496366", + "name": "Acupuncture Torment", + "spellId": 496366, + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 5, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 5 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496366, + "componentId": 507910, + "spellLevel": 0, + "calculationPath": "magic", + "powerKind": "source-points", + "damagePower": -10, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105224-aura-496367", + "name": "Dismemberment", + "delivery": "area", + "target": "self", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1200, + "range": 0, + "cooldownMs": 15000, + "initialCooldownMs": [ + 15000, + 15000 + ], + "repeatCooldownMs": [ + 15000, + 15000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "aura", + "aura": "dismemberment", + "durationMs": 7500 + } + }, + { + "id": "rw-grafu-castle-105224-spell-496368", + "name": "Acupuncture", + "spellId": 496368, + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 0, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496368, + "componentId": 507905, + "spellLevel": 0, + "calculationPath": "magic", + "powerKind": "source-points", + "damagePower": -250, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + } + ] + }, + "visual": { + "primaryColor": "hsl(208 22% 38%)", + "accentColor": "hsl(236 38% 64%)", + "scale": 1, + "archetype": "humanoid", + "model": { + "url": "/assets/creatures/grafu-castle/barbo-brother.glb", + "rotationY": 0, + "groundOffset": 1.32298, + "labelHeight": 5.44077, + "markerRadius": 1.53143, + "animationMode": "native" + } + } + }, + "rw-grafu-castle-105204": { + "id": "rw-grafu-castle-105204", + "name": "Conjurer Chapeaunoir", + "kind": "boss", + "title": "Conjurer Chapeaunoir", + "hasLoot": false, + "combat": { + "level": 68, + "healthMultiplier": 7.5, + "damageMultiplier": 1.3, + "moveSpeed": 2.25, + "leashRange": 32, + "mechanicIds": [ + "rw-grafu-castle-105204-debuff-496773", + "rw-grafu-castle-105204-debuff-496882", + "rw-grafu-castle-105204-spell-496920", + "rw-grafu-castle-105204-spell-496921", + "rw-grafu-castle-105204-aura-496923", + "rw-grafu-castle-105204-spell-496924", + "rw-grafu-castle-105204-spell-496926", + "rw-grafu-castle-105204-spell-496927", + "rw-grafu-castle-105204-spell-496928", + "rw-grafu-castle-105204-spell-496929", + "rw-grafu-castle-105204-spell-497011" + ], + "attacks": [ + { + "id": "basic-attack", + "name": "Basic Attack", + "delivery": "melee", + "target": "primary", + "school": "physical", + "animation": "attack", + "range": 4.5, + "cooldownMs": 1900, + "damageMultiplier": 1 + }, + { + "id": "rw-grafu-castle-105204-debuff-496773", + "name": "Aggro", + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "target-aura", + "aura": "aggro", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105204-debuff-496882", + "name": "Conjurer's Hat", + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 12000, + "initialCooldownMs": [ + 12000, + 12000 + ], + "repeatCooldownMs": [ + 12000, + 12000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "target-aura", + "aura": "conjurer-s-hat", + "durationMs": 6000 + } + }, + { + "id": "rw-grafu-castle-105204-spell-496920", + "name": "Opening Act", + "spellId": 496920, + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 2000, + "range": 15, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496920, + "componentId": 508594, + "spellLevel": 0, + "calculationPath": "magic", + "powerKind": "source-points", + "damagePower": -100, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105204-spell-496921", + "name": "Shuffle Deck", + "spellId": 496921, + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 15, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496921, + "componentId": 508595, + "spellLevel": 0, + "calculationPath": "magic", + "powerKind": "source-points", + "damagePower": -30, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105204-aura-496923", + "name": "Throw Hat", + "delivery": "area", + "target": "self", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1200, + "range": 0, + "cooldownMs": 15000, + "initialCooldownMs": [ + 15000, + 15000 + ], + "repeatCooldownMs": [ + 15000, + 15000 + ], + "damageMultiplier": 0, + "effect": { + "kind": "aura", + "aura": "throw-hat", + "durationMs": 7500 + } + }, + { + "id": "rw-grafu-castle-105204-spell-496924", + "name": "Conjure Energy", + "spellId": 496924, + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 2000, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 7.5, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 7.5 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496924, + "componentId": 508598, + "spellLevel": 0, + "calculationPath": "magic", + "powerKind": "source-points", + "damagePower": -1000, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105204-spell-496926", + "name": "Poker Shift", + "spellId": 496926, + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1000, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496926, + "componentId": 508647, + "spellLevel": 0, + "calculationPath": "magic", + "powerKind": "source-points", + "damagePower": -2000, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105204-spell-496927", + "name": "Visionary Star", + "spellId": 496927, + "delivery": "projectile", + "target": "primary", + "school": "arcane", + "animation": "cast", + "castTimeMs": 0, + "range": 10, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496927, + "componentId": 508650, + "spellLevel": 0, + "calculationPath": "magic", + "powerKind": "source-points", + "damagePower": -100, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105204-spell-496928", + "name": "Black Hat Burst", + "spellId": 496928, + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 15, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496928, + "componentId": 508651, + "spellLevel": 0, + "calculationPath": "magic", + "powerKind": "source-points", + "damagePower": -40, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105204-spell-496929", + "name": "Conjure Energy", + "spellId": 496929, + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 30, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 496929, + "componentId": 508654, + "spellLevel": 0, + "calculationPath": "magic", + "powerKind": "source-points", + "damagePower": -80, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + }, + { + "id": "rw-grafu-castle-105204-spell-497011", + "name": "Conjure Skill", + "spellId": 497011, + "delivery": "area", + "target": "nearby-party", + "school": "arcane", + "animation": "cast", + "castTimeMs": 1800, + "range": 15, + "cooldownMs": 6500, + "initialCooldownMs": [ + 6500, + 6500 + ], + "repeatCooldownMs": [ + 6500, + 6500 + ], + "damageMultiplier": 0.85, + "radius": 15, + "telegraph": { + "shape": "circle", + "anchor": "source", + "warningMs": 1800, + "radius": 15 + }, + "runewakerFormula": { + "schemaVersion": 1, + "spellId": 497011, + "componentId": 508655, + "spellLevel": 0, + "calculationPath": "magic", + "powerKind": "source-points", + "damagePower": -500, + "fixedValue": 0, + "dotBase": 0, + "runtimeBaselineMultiplier": 0.85, + "provenance": { + "sourceFile": "SourceCodeVS2019/Server/NetGameServer/MainProc/MagicProc/MagicProcess.cpp", + "physicalFunctionLine": 3366, + "magicFunctionLine": 3651, + "magicObjectDatabase": "Resource/data/magicobject.db" + } + } + } + ] + }, + "visual": { + "primaryColor": "hsl(348 22% 38%)", + "accentColor": "hsl(16 38% 64%)", + "scale": 1, + "archetype": "humanoid", + "model": { + "url": "/assets/creatures/grafu-castle/black-hat.glb", + "rotationY": 0, + "groundOffset": 0.01676, + "labelHeight": 3.51313, + "markerRadius": 0.60732, + "animationMode": "native" + } + } + }, + "rw-grafu-castle-105351": { + "id": "rw-grafu-castle-105351", + "name": "Larvanger Barkud", + "kind": "boss", + "title": "Larvanger Barkud", + "hasLoot": false, + "combat": { + "level": 68, + "healthMultiplier": 7.5, + "damageMultiplier": 1.3, + "moveSpeed": 2.25, + "leashRange": 32, + "mechanicIds": [ + "rw-grafu-castle-105351-source-gap-sweep" + ], + "attacks": [ + { + "id": "basic-attack", + "name": "Basic Attack", + "delivery": "melee", + "target": "primary", + "school": "physical", + "animation": "attack", + "range": 4.5, + "cooldownMs": 1900, + "damageMultiplier": 1 + }, + { + "id": "rw-grafu-castle-105351-source-gap-sweep", + "name": "Heavy Sweep", + "delivery": "area", + "target": "nearby-party", + "school": "physical", + "animation": "attack", + "castTimeMs": 1800, + "range": 7, + "cooldownMs": 10000, + "initialCooldownMs": [ + 7000, + 7000 + ], + "repeatCooldownMs": [ + 10000, + 10000 + ], + "damageMultiplier": 1.1, + "telegraph": { + "shape": "cone", + "anchor": "source", + "warningMs": 1800, + "length": 7, + "angleDegrees": 100 + } + } + ] + }, + "visual": { + "primaryColor": "hsl(57 22% 38%)", + "accentColor": "hsl(85 38% 64%)", + "scale": 1, + "archetype": "humanoid", + "model": { + "url": "/assets/creatures/grafu-castle/rafael-bakderr.glb", + "rotationY": 0, + "groundOffset": -0.00039, + "labelHeight": 2.56177, + "markerRadius": 0.4, + "animationMode": "native" + } + } + }, + "rw-grafu-castle-105229": { + "id": "rw-grafu-castle-105229", + "name": "Smirking Castle Clown", + "kind": "mob", + "hasLoot": false, + "combat": { + "level": 63, + "healthMultiplier": 1, + "damageMultiplier": 1, + "moveSpeed": 2.15, + "leashRange": 24, + "attacks": [ + { + "id": "basic-attack", + "name": "Basic Attack", + "delivery": "melee", + "target": "primary", + "school": "physical", + "animation": "attack", + "range": 4.5, + "cooldownMs": 1900, + "damageMultiplier": 1 + } + ] + }, + "visual": { + "primaryColor": "hsl(83 22% 38%)", + "accentColor": "hsl(111 38% 64%)", + "scale": 1, + "archetype": "humanoid", + "model": { + "url": "/assets/creatures/grafu-castle/grinning-clown.glb", + "rotationY": 0, + "groundOffset": -0.03011, + "labelHeight": 1.95005, + "markerRadius": 0.4, + "animationMode": "native" + } + } + }, + "rw-grafu-castle-105230": { + "id": "rw-grafu-castle-105230", + "name": "Odd Castle Clown", + "kind": "mob", + "hasLoot": false, + "combat": { + "level": 63, + "healthMultiplier": 1, + "damageMultiplier": 1, + "moveSpeed": 2.15, + "leashRange": 24, + "attacks": [ + { + "id": "basic-attack", + "name": "Basic Attack", + "delivery": "melee", + "target": "primary", + "school": "physical", + "animation": "attack", + "range": 4.5, + "cooldownMs": 1900, + "damageMultiplier": 1 + } + ] + }, + "visual": { + "primaryColor": "hsl(130 22% 38%)", + "accentColor": "hsl(158 38% 64%)", + "scale": 1, + "archetype": "humanoid", + "model": { + "url": "/assets/creatures/grafu-castle/ghastly-clown-red.glb", + "rotationY": 0, + "groundOffset": 0.00229, + "labelHeight": 1.97034, + "markerRadius": 0.4, + "animationMode": "native" + } + } + }, + "rw-grafu-castle-105599": { + "id": "rw-grafu-castle-105599", + "name": "Cursed Steel Soldier", + "kind": "mob", + "hasLoot": false, + "combat": { + "level": 63, + "healthMultiplier": 1, + "damageMultiplier": 1, + "moveSpeed": 2.15, + "leashRange": 24, + "attacks": [ + { + "id": "basic-attack", + "name": "Basic Attack", + "delivery": "melee", + "target": "primary", + "school": "physical", + "animation": "attack", + "range": 4.5, + "cooldownMs": 1900, + "damageMultiplier": 1 + } + ] + }, + "visual": { + "primaryColor": "hsl(193 22% 38%)", + "accentColor": "hsl(221 38% 64%)", + "scale": 1, + "archetype": "humanoid", + "model": { + "url": "/assets/creatures/grafu-castle/cursed-steel-guardian.glb", + "rotationY": 0, + "groundOffset": 0.00095, + "labelHeight": 2.36454, + "markerRadius": 0.62611, + "animationMode": "native" + } + } + }, + "rw-grafu-castle-105237": { + "id": "rw-grafu-castle-105237", + "name": "Manipulated Bear", + "kind": "mob", + "hasLoot": false, + "combat": { + "level": 63, + "healthMultiplier": 1, + "damageMultiplier": 1, + "moveSpeed": 2.15, + "leashRange": 24, + "attacks": [ + { + "id": "basic-attack", + "name": "Basic Attack", + "delivery": "melee", + "target": "primary", + "school": "physical", + "animation": "attack", + "range": 4.5, + "cooldownMs": 1900, + "damageMultiplier": 1 + } + ] + }, + "visual": { + "primaryColor": "hsl(99 22% 38%)", + "accentColor": "hsl(127 38% 64%)", + "scale": 1, + "archetype": "humanoid", + "model": { + "url": "/assets/creatures/grafu-castle/enraged-big-bear-cream.glb", + "rotationY": 0, + "groundOffset": -0.00083, + "labelHeight": 2.85384, + "markerRadius": 1.06482, + "animationMode": "native" + } + } + }, + "rw-grafu-castle-105236": { + "id": "rw-grafu-castle-105236", + "name": "Giant Enraged Bear", + "kind": "mob", + "hasLoot": false, + "combat": { + "level": 63, + "healthMultiplier": 1, + "damageMultiplier": 1, + "moveSpeed": 2.15, + "leashRange": 24, + "attacks": [ + { + "id": "basic-attack", + "name": "Basic Attack", + "delivery": "melee", + "target": "primary", + "school": "physical", + "animation": "attack", + "range": 4.5, + "cooldownMs": 1900, + "damageMultiplier": 1 + } + ] + }, + "visual": { + "primaryColor": "hsl(52 22% 38%)", + "accentColor": "hsl(80 38% 64%)", + "scale": 1, + "archetype": "humanoid", + "model": { + "url": "/assets/creatures/grafu-castle/enraged-big-bear-purple.glb", + "rotationY": 0, + "groundOffset": -0.00083, + "labelHeight": 2.85384, + "markerRadius": 1.06482, + "animationMode": "native" + } + } + }, + "rw-grafu-castle-105233": { + "id": "rw-grafu-castle-105233", + "name": "Cursed Steel Soldier", + "kind": "mob", + "hasLoot": false, + "combat": { + "level": 63, + "healthMultiplier": 1, + "damageMultiplier": 1, + "moveSpeed": 2.15, + "leashRange": 24, + "attacks": [ + { + "id": "basic-attack", + "name": "Basic Attack", + "delivery": "melee", + "target": "primary", + "school": "physical", + "animation": "attack", + "range": 4.5, + "cooldownMs": 1900, + "damageMultiplier": 1 + } + ] + }, + "visual": { + "primaryColor": "hsl(271 22% 38%)", + "accentColor": "hsl(299 38% 64%)", + "scale": 1, + "archetype": "humanoid", + "model": { + "url": "/assets/creatures/grafu-castle/cursed-steel-guardian.glb", + "rotationY": 0, + "groundOffset": 0.00095, + "labelHeight": 2.36454, + "markerRadius": 0.62611, + "animationMode": "native" + } + } + }, + "rw-grafu-castle-105235": { + "id": "rw-grafu-castle-105235", + "name": "Conjurer's Apprentice", + "kind": "mob", + "hasLoot": false, + "combat": { + "level": 63, + "healthMultiplier": 1, + "damageMultiplier": 1, + "moveSpeed": 2.15, + "leashRange": 24, + "attacks": [ + { + "id": "basic-attack", + "name": "Basic Attack", + "delivery": "melee", + "target": "primary", + "school": "physical", + "animation": "attack", + "range": 4.5, + "cooldownMs": 1900, + "damageMultiplier": 1 + } + ] + }, + "visual": { + "primaryColor": "hsl(5 22% 38%)", + "accentColor": "hsl(33 38% 64%)", + "scale": 1, + "archetype": "humanoid", + "model": { + "url": "/assets/creatures/grafu-castle/black-hat-blue.glb", + "rotationY": 0, + "groundOffset": 0.01676, + "labelHeight": 3.32843, + "markerRadius": 0.60732, + "animationMode": "native" + } + } } } as const satisfies PopulationDefinitionMap; @@ -848,116 +3349,6 @@ export const GRAFU_CASTLE_STATIC_SPAWNS = [ "yaw": 6.195920442, "spawnMask": 1 }, - { - "id": "rw-grafu-castle-spawn-41927381", - "entityId": "rw-grafu-castle-105254", - "position": [ - 53.559575, - -2.777278, - 92.787626 - ], - "yaw": 3.139381431, - "spawnMask": 1 - }, - { - "id": "rw-grafu-castle-spawn-41927382", - "entityId": "rw-grafu-castle-105254", - "position": [ - -21.65209, - -0.397606, - 97.291596 - ], - "yaw": 4.710327694, - "spawnMask": 1 - }, - { - "id": "rw-grafu-castle-spawn-41927384", - "entityId": "rw-grafu-castle-105254", - "position": [ - -93.738071, - -19.097607, - 36.011405 - ], - "yaw": 1.558154035, - "spawnMask": 1 - }, - { - "id": "rw-grafu-castle-spawn-41927385", - "entityId": "rw-grafu-castle-105254", - "position": [ - -87.821567, - -19.097607, - 40.753025 - ], - "yaw": 3.144200155, - "spawnMask": 1 - }, - { - "id": "rw-grafu-castle-spawn-41927386", - "entityId": "rw-grafu-castle-105254", - "position": [ - -171.712961, - -17.89761, - -43.227236 - ], - "yaw": 0.002316122, - "spawnMask": 1 - }, - { - "id": "rw-grafu-castle-spawn-41927387", - "entityId": "rw-grafu-castle-105254", - "position": [ - -171.130459, - -28.975774, - -98.808759 - ], - "yaw": 1.505715461, - "spawnMask": 1 - }, - { - "id": "rw-grafu-castle-spawn-41927391", - "entityId": "rw-grafu-castle-105254", - "position": [ - 116.899832, - -27.977214, - -175.378701 - ], - "yaw": 1.553343034, - "spawnMask": 1 - }, - { - "id": "rw-grafu-castle-spawn-41927392", - "entityId": "rw-grafu-castle-105254", - "position": [ - 74.031253, - -2.779222, - 107.81483 - ], - "yaw": 4.712298433, - "spawnMask": 1 - }, - { - "id": "rw-grafu-castle-spawn-41927395", - "entityId": "rw-grafu-castle-105254", - "position": [ - 129.899832, - -27.997608, - -177.079705 - ], - "yaw": 3.159045946, - "spawnMask": 1 - }, - { - "id": "rw-grafu-castle-spawn-41927396", - "entityId": "rw-grafu-castle-105254", - "position": [ - 98.399832, - -27.997608, - -179.079705 - ], - "yaw": 0.087266205, - "spawnMask": 1 - }, { "id": "rw-grafu-castle-spawn-41927435", "entityId": "rw-grafu-castle-105596", @@ -1343,72 +3734,6 @@ export const GRAFU_CASTLE_STATIC_SPAWNS = [ "yaw": 3.769911184, "spawnMask": 1 }, - { - "id": "rw-grafu-castle-spawn-41927631", - "entityId": "rw-grafu-castle-105258", - "position": [ - 74.099832, - -2.374497, - 122.178456 - ], - "yaw": 4.677482395, - "spawnMask": 1 - }, - { - "id": "rw-grafu-castle-spawn-41927632", - "entityId": "rw-grafu-castle-105258", - "position": [ - -21.926028, - -0.397606, - 120.5779 - ], - "yaw": 2.722713633, - "spawnMask": 1 - }, - { - "id": "rw-grafu-castle-spawn-41927633", - "entityId": "rw-grafu-castle-105258", - "position": [ - -109.296877, - -19.059396, - 40.820295 - ], - "yaw": 3.106686069, - "spawnMask": 1 - }, - { - "id": "rw-grafu-castle-spawn-41927634", - "entityId": "rw-grafu-castle-105258", - "position": [ - -176.200168, - -17.89761, - -38.979705 - ], - "yaw": 4.677482395, - "spawnMask": 1 - }, - { - "id": "rw-grafu-castle-spawn-41927635", - "entityId": "rw-grafu-castle-105258", - "position": [ - -148.506678, - -22.367332, - -89.380206 - ], - "yaw": 3.298672286, - "spawnMask": 1 - }, - { - "id": "rw-grafu-castle-spawn-41928345", - "entityId": "rw-grafu-castle-105210", - "position": [ - -94.484665, - -13.961191, - 111.615987 - ], - "yaw": 5.026548246, - "spawnMask": 1 - }, { "id": "rw-grafu-castle-spawn-41928447", "entityId": "rw-grafu-castle-105211", @@ -1441,10 +3766,540 @@ export const GRAFU_CASTLE_STATIC_SPAWNS = [ ], "yaw": 4.485496178, "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-cayus", + "entityId": "rw-grafu-castle-105616", + "position": [ + 6.029333, + -1.971602, + 92.880381 + ], + "yaw": 6.271411472, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-ardmond", + "entityId": "rw-grafu-castle-105542", + "position": [ + -101.899784, + -14.174433, + 128.257272 + ], + "yaw": 0, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-issac", + "entityId": "rw-grafu-castle-105543", + "position": [ + -101.899874, + -14.164943, + 124.257294 + ], + "yaw": 0, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-boro-boro-brothers", + "entityId": "rw-grafu-castle-105224", + "position": [ + -176.317575, + -17.936858, + -4.856412 + ], + "yaw": 1.574975226, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-conjurer-chapeaunoir", + "entityId": "rw-grafu-castle-105204", + "position": [ + -132.396149, + -35.924047, + -126.162808 + ], + "yaw": 0.791142735, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-larvanger-barkud", + "entityId": "rw-grafu-castle-105351", + "position": [ + 10.319202, + -37.197609, + -101.219726 + ], + "yaw": 3.163869996, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927381-1", + "entityId": "rw-grafu-castle-105598", + "position": [ + 53.559575, + -2.777278, + 92.787626 + ], + "yaw": 3.139381431, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927381-2", + "entityId": "rw-grafu-castle-105229", + "position": [ + 55.554627, + -2.729516, + 90.783828 + ], + "yaw": 3.139381431, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927381-3", + "entityId": "rw-grafu-castle-105229", + "position": [ + 55.564037, + -2.781325, + 94.783146 + ], + "yaw": 3.139381431, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927381-4", + "entityId": "rw-grafu-castle-105230", + "position": [ + 57.565665, + -2.703565, + 92.778583 + ], + "yaw": 3.139381431, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927382-1", + "entityId": "rw-grafu-castle-105598", + "position": [ + -21.65209, + -0.397606, + 97.291596 + ], + "yaw": 4.710327694, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927382-2", + "entityId": "rw-grafu-castle-105229", + "position": [ + -19.647972, + -0.397606, + 99.287469 + ], + "yaw": 4.710327694, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927382-3", + "entityId": "rw-grafu-castle-105229", + "position": [ + -23.647964, + -0.397606, + 99.295715 + ], + "yaw": 4.710327694, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927382-4", + "entityId": "rw-grafu-castle-105230", + "position": [ + -21.643845, + -0.397606, + 101.291588 + ], + "yaw": 4.710327694, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927384-1", + "entityId": "rw-grafu-castle-105596", + "position": [ + -93.738071, + -19.097607, + 36.011405 + ], + "yaw": 1.558154035, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927384-2", + "entityId": "rw-grafu-castle-105597", + "position": [ + -95.763195, + -19.097607, + 34.036849 + ], + "yaw": 1.558154035, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927384-3", + "entityId": "rw-grafu-castle-105597", + "position": [ + -91.763514, + -19.097607, + 33.986281 + ], + "yaw": 1.558154035, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927384-4", + "entityId": "rw-grafu-castle-105601", + "position": [ + -93.788638, + -19.097607, + 32.011725 + ], + "yaw": 1.558154035, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927385-1", + "entityId": "rw-grafu-castle-105599", + "position": [ + -87.821567, + -19.097607, + 40.753025 + ], + "yaw": 3.144200155, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927385-2", + "entityId": "rw-grafu-castle-105598", + "position": [ + -85.816359, + -19.097607, + 38.758247 + ], + "yaw": 3.144200155, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927385-3", + "entityId": "rw-grafu-castle-105598", + "position": [ + -85.826789, + -19.097607, + 42.758233 + ], + "yaw": 3.144200155, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927386-1", + "entityId": "rw-grafu-castle-105599", + "position": [ + -171.712961, + -17.89761, + -43.227236 + ], + "yaw": 0.002316122, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927386-2", + "entityId": "rw-grafu-castle-105598", + "position": [ + -173.717588, + -17.89761, + -41.231874 + ], + "yaw": 0.002316122, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927386-3", + "entityId": "rw-grafu-castle-105598", + "position": [ + -173.708323, + -17.89761, + -45.231863 + ], + "yaw": 0.002316122, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927386-4", + "entityId": "rw-grafu-castle-105601", + "position": [ + -175.71295, + -17.89761, + -43.236501 + ], + "yaw": 0.002316122, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927387-1", + "entityId": "rw-grafu-castle-105599", + "position": [ + -171.130459, + -28.975774, + -98.808759 + ], + "yaw": 1.505715461, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927387-2", + "entityId": "rw-grafu-castle-105601", + "position": [ + -173.268557, + -29.35834, + -100.57685 + ], + "yaw": 1.505715461, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927387-3", + "entityId": "rw-grafu-castle-105601", + "position": [ + -169.273997, + -29.535331, + -100.794094 + ], + "yaw": 1.505715461, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927391-1", + "entityId": "rw-grafu-castle-105237", + "position": [ + 116.899832, + -27.977214, + -175.378701 + ], + "yaw": 1.553343034, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927391-2", + "entityId": "rw-grafu-castle-105236", + "position": [ + 114.865222, + -27.949904, + -177.34312 + ], + "yaw": 1.553343034, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927391-3", + "entityId": "rw-grafu-castle-105236", + "position": [ + 118.864623, + -27.948849, + -177.412906 + ], + "yaw": 1.553343034, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927391-4", + "entityId": "rw-grafu-castle-105237", + "position": [ + 116.83001, + -27.921521, + -179.377327 + ], + "yaw": 1.553343034, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927391-5", + "entityId": "rw-grafu-castle-105236", + "position": [ + 118.829718, + -27.92097, + -179.412213 + ], + "yaw": 1.553343034, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927391-6", + "entityId": "rw-grafu-castle-105236", + "position": [ + 114.830314, + -27.922207, + -179.342432 + ], + "yaw": 1.553343034, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927392-1", + "entityId": "rw-grafu-castle-105598", + "position": [ + 74.031253, + -2.779222, + 107.81483 + ], + "yaw": 4.712298433, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927392-2", + "entityId": "rw-grafu-castle-105229", + "position": [ + 76.031434, + -2.742008, + 109.816672 + ], + "yaw": 4.712298433, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927392-3", + "entityId": "rw-grafu-castle-105229", + "position": [ + 72.031434, + -2.742038, + 109.817032 + ], + "yaw": 4.712298433, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927392-4", + "entityId": "rw-grafu-castle-105230", + "position": [ + 74.031615, + -2.797607, + 111.814399 + ], + "yaw": 4.712298433, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927395-1", + "entityId": "rw-grafu-castle-105233", + "position": [ + 129.899832, + -27.997608, + -177.079705 + ], + "yaw": 3.159045946, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927395-2", + "entityId": "rw-grafu-castle-105235", + "position": [ + 131.934432, + -27.997608, + -179.044496 + ], + "yaw": 3.159045946, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927395-3", + "entityId": "rw-grafu-castle-105235", + "position": [ + 131.864623, + -27.997608, + -175.045105 + ], + "yaw": 3.159045946, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927396-1", + "entityId": "rw-grafu-castle-105233", + "position": [ + 98.399832, + -27.997608, + -179.079705 + ], + "yaw": 0.087266205, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927396-2", + "entityId": "rw-grafu-castle-105235", + "position": [ + 96.233132, + -27.997608, + -177.261627 + ], + "yaw": 0.087266205, + "spawnMask": 1 + }, + { + "id": "rw-grafu-castle-scripted-castle-patrol-41927396-3", + "entityId": "rw-grafu-castle-105235", + "position": [ + 96.581754, + -27.997608, + -181.246405 + ], + "yaw": 0.087266205, + "spawnMask": 1 } ] as const satisfies readonly StaticMobSpawnDefinition[]; -export const GRAFU_CASTLE_BOSSES = [] as const satisfies readonly DungeonBossObjective[]; +export const GRAFU_CASTLE_BOSSES = [ + { + "id": "rw-grafu-castle-scripted-cayus", + "name": "Cayus", + "position": [ + 6.029333, + -1.971602, + 92.880381 + ] + }, + { + "id": "rw-grafu-castle-scripted-ardmond", + "name": "Ardmond and Issac", + "position": [ + -101.899784, + -14.174433, + 128.257272 + ] + }, + { + "id": "rw-grafu-castle-scripted-boro-boro-brothers", + "name": "Boro-Boro Brothers", + "position": [ + -176.317575, + -17.936858, + -4.856412 + ] + }, + { + "id": "rw-grafu-castle-scripted-conjurer-chapeaunoir", + "name": "Conjurer Chapeaunoir", + "position": [ + -132.396149, + -35.924047, + -126.162808 + ] + }, + { + "id": "rw-grafu-castle-scripted-larvanger-barkud", + "name": "Larvanger Barkud", + "position": [ + 10.319202, + -37.197609, + -101.219726 + ] + } +] as const satisfies readonly DungeonBossObjective[]; export const GRAFU_CASTLE_OBJECTIVE_ORDER = GRAFU_CASTLE_BOSSES; @@ -1453,14 +4308,26 @@ export const GRAFU_CASTLE_POPULATION_PROVENANCE = { "dungeonId": "grafu-castle", "zoneId": 137, "sourceRowCount": 249, - "importedSpawnCount": 56, - "excludedRowCount": 193, - "bossSpawnCount": 0, + "importedSpawnCount": 84, + "scriptedSpawnCount": 44, + "excludedRowCount": 209, + "bossSpawnCount": 5, "objectiveOrder": { - "authoritative": false, - "kind": "healerMan-distance-derived", + "authoritative": true, + "kind": "source-authored", "evidence": [ - "scripts/runewaker-pipeline/build-population.mjs" + "Resource/luascript/02345.lua:Dy_Z19_BOSSN_AIC", + "Resource/luascript/02324.lua:kg_105542_01", + "Resource/luascript/02231.lua:ic_105224_01", + "Resource/luascript/02330.lua:cl_136_105205_N", + "Resource/luascript/02297.lua:un_zone19_boss5_allconreal_1_01" + ], + "sourceSpawnIds": [ + 41927224, + 41926939, + 41926924, + 41926979, + 41927071 ] }, "sourceSnapshot": "scripts/runewaker-pipeline/snapshots/grafu-castle-zone-137.json", @@ -1480,7 +4347,7 @@ export const GRAFU_CASTLE_POPULATION_PROVENANCE = { "navmeshProjection": { "maximumAllowedDistance": 4, "maximumObservedDistance": 35.149104, - "maximumCombatDistance": 3.385892, + "maximumCombatDistance": 0.998158, "deferredRowsBeyondCombatLimit": 11 }, "runeWakerIdsAreNamespaced": true, diff --git a/src/game/grafuCastlePopulation.test.ts b/src/game/grafuCastlePopulation.test.ts new file mode 100644 index 00000000..efb3fb51 --- /dev/null +++ b/src/game/grafuCastlePopulation.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from "vitest"; +import { + GRAFU_CASTLE_BOSSES, + GRAFU_CASTLE_ENTITIES, + GRAFU_CASTLE_POPULATION_PROVENANCE, + GRAFU_CASTLE_STATIC_SPAWNS, +} from "./generated/grafuCastlePopulation.generated"; + +const CONTROLLER_ENTITY_IDS = [ + "rw-grafu-castle-105210", + "rw-grafu-castle-105254", + "rw-grafu-castle-105258", +] as const; + +const BOSS_ENTITY_IDS = [ + "rw-grafu-castle-105204", + "rw-grafu-castle-105224", + "rw-grafu-castle-105351", + "rw-grafu-castle-105542", + "rw-grafu-castle-105543", + "rw-grafu-castle-105616", +] as const; + +describe("Grafu Castle population", () => { + it("does not render Lua controllers as mushroom combat mobs", () => { + for (const controllerId of CONTROLLER_ENTITY_IDS) { + expect(GRAFU_CASTLE_ENTITIES).not.toHaveProperty(controllerId); + expect(GRAFU_CASTLE_STATIC_SPAWNS.some((spawn) => ( + String(spawn.entityId) === controllerId + ))).toBe(false); + } + }); + + it("materializes the source-authored patrol formations", () => { + const formationSpawns = GRAFU_CASTLE_STATIC_SPAWNS.filter((spawn) => ( + spawn.id.startsWith("rw-grafu-castle-scripted-castle-patrol-") + )); + expect(formationSpawns).toHaveLength(38); + expect(new Set(formationSpawns.map((spawn) => GRAFU_CASTLE_ENTITIES[spawn.entityId]?.name))) + .toEqual(new Set([ + "Conjurer's Apprentice", + "Cursed Steel Soldier", + "Giant Enraged Bear", + "Manipulated Bear", + "Manipulated Steel Soldier", + "Odd Castle Clown", + "Smirking Castle Clown", + ])); + }); + + it("ships all five normal encounters and both members of the duo", () => { + expect(GRAFU_CASTLE_BOSSES.map((boss) => boss.name)).toEqual([ + "Cayus", + "Ardmond and Issac", + "Boro-Boro Brothers", + "Conjurer Chapeaunoir", + "Larvanger Barkud", + ]); + expect(GRAFU_CASTLE_POPULATION_PROVENANCE.bossSpawnCount).toBe(5); + expect(GRAFU_CASTLE_POPULATION_PROVENANCE.scriptedSpawnCount).toBe(44); + + for (const entityId of BOSS_ENTITY_IDS) { + expect(GRAFU_CASTLE_ENTITIES[entityId].kind).toBe("boss"); + expect(GRAFU_CASTLE_ENTITIES[entityId].visual.model?.url).toMatch( + /^\/assets\/creatures\/grafu-castle\/.+\.glb$/, + ); + expect(GRAFU_CASTLE_STATIC_SPAWNS.some((spawn) => spawn.entityId === entityId)).toBe(true); + } + for (const objective of GRAFU_CASTLE_BOSSES) { + expect(GRAFU_CASTLE_STATIC_SPAWNS.some((spawn) => spawn.id === objective.id)).toBe(true); + } + }); +}); diff --git a/src/game/manastormAdminConfig.ts b/src/game/manastormAdminConfig.ts index 48f9a844..7a368af5 100644 --- a/src/game/manastormAdminConfig.ts +++ b/src/game/manastormAdminConfig.ts @@ -478,23 +478,4 @@ export function isGameMasterSession(session: { readonly kind: string; readonly a return Boolean(session?.kind === "account" && session.accessToken && session.roles?.includes("gm")); } -export function derivePartySpawnSlots(spawn: WorldSpawn, partySize = 5): readonly WorldSpawn[] { - const size = Math.max(1, Math.min(5, Math.trunc(partySize))); - const offsets = [ - [0, 0], - [-1.65, -1.8], - [1.65, -1.8], - [-3.05, -3.5], - [3.05, -3.5], - ] as const; - const sin = Math.sin(spawn.yaw); - const cos = Math.cos(spawn.yaw); - return Object.freeze(offsets.slice(0, size).map(([lateral, forward]) => ({ - footPosition: [ - spawn.footPosition[0] + lateral * cos + forward * sin, - spawn.footPosition[1], - spawn.footPosition[2] - lateral * sin + forward * cos, - ] as const, - yaw: spawn.yaw, - }))); -} +export { derivePartySpawnSlots } from "./partySpawnSlots"; diff --git a/src/game/partySpawnSlots.ts b/src/game/partySpawnSlots.ts new file mode 100644 index 00000000..10dcd8a1 --- /dev/null +++ b/src/game/partySpawnSlots.ts @@ -0,0 +1,22 @@ +import type { WorldSpawn } from "./dungeonTypes"; + +export function derivePartySpawnSlots(spawn: WorldSpawn, partySize = 5): readonly WorldSpawn[] { + const size = Math.max(1, Math.min(5, Math.trunc(partySize))); + const offsets = [ + [0, 0], + [-1.65, -1.8], + [1.65, -1.8], + [-3.05, -3.5], + [3.05, -3.5], + ] as const; + const sin = Math.sin(spawn.yaw); + const cos = Math.cos(spawn.yaw); + return Object.freeze(offsets.slice(0, size).map(([lateral, forward]) => ({ + footPosition: [ + spawn.footPosition[0] + lateral * cos + forward * sin, + spawn.footPosition[1], + spawn.footPosition[2] - lateral * sin + forward * cos, + ] as const, + yaw: spawn.yaw, + }))); +} diff --git a/src/game/runewakerBossMechanics.test.ts b/src/game/runewakerBossMechanics.test.ts index f21e9c38..5f2adec2 100644 --- a/src/game/runewakerBossMechanics.test.ts +++ b/src/game/runewakerBossMechanics.test.ts @@ -24,7 +24,7 @@ describe("modular RuneWaker boss mechanics", () => { )); it("gives every packaged boss a catalog-backed special-mechanic loadout", () => { - expect(bosses).toHaveLength(56); + expect(bosses).toHaveLength(62); for (const { dungeon, entity } of bosses) { const label = `${dungeon.id}:${entity.id}`; const authored = catalog.bosses[entity.id]; @@ -70,6 +70,7 @@ describe("modular RuneWaker boss mechanics", () => { "rw-fa-100561", "rw-fa-100562", "rw-fa-100563", + "rw-grafu-castle-105351", ]); }); }); diff --git a/src/game/runewakerDungeonAnimation.test.ts b/src/game/runewakerDungeonAnimation.test.ts index 9f217016..5015b2f7 100644 --- a/src/game/runewakerDungeonAnimation.test.ts +++ b/src/game/runewakerDungeonAnimation.test.ts @@ -283,8 +283,8 @@ describe("RuneWaker dungeon creature animation coverage", () => { } expect(counts).toEqual({ - configured: 268, - native: 267, + configured: 275, + native: 274, poseOnly: 1, dynamicProxy: 0, }); @@ -301,7 +301,7 @@ describe("RuneWaker dungeon creature animation coverage", () => { } } expect(runtimeCounts, "runtime proxies: " + runtimeProxyIds.join(", ")).toEqual({ - native: 436, + native: 446, poseOnly: 1, proxy: 0, }); diff --git a/src/platform/dualScreenSync.test.ts b/src/platform/dualScreenSync.test.ts index d53b12df..4c0a4095 100644 --- a/src/platform/dualScreenSync.test.ts +++ b/src/platform/dualScreenSync.test.ts @@ -8,6 +8,7 @@ import { clearMobRuntimeRegistry, setMobPosition } from "../game/mobRuntimeRegis import { usePartyStore } from "../game/partyStore"; import { useGameStore } from "../game/store"; import { currentCompanionSnapshot, executeCompanionCommand } from "./dualScreenSync"; +import { SHELVED_MODES_ENABLED } from "../app/releaseFeatures"; describe("dual-screen party commands", () => { beforeEach(() => { @@ -147,7 +148,8 @@ describe("dual-screen party commands", () => { executeCompanionCommand({ name: "returnToMainMenu" }); executeCompanionCommand({ name: "openManastorms" }); - expect(useShellStore.getState().phase).toBe("manastorms"); + expect(useShellStore.getState().phase).toBe(SHELVED_MODES_ENABLED ? "manastorms" : "main-menu"); + if (!SHELVED_MODES_ENABLED) expect(useShellStore.getState().notice).toMatch(/temporarily unavailable/i); executeCompanionCommand({ name: "returnToMainMenu" }); executeCompanionCommand({ name: "openDungeons" }); diff --git a/src/scene/GameScene.tsx b/src/scene/GameScene.tsx index c844644b..04bdcc8c 100644 --- a/src/scene/GameScene.tsx +++ b/src/scene/GameScene.tsx @@ -26,7 +26,6 @@ import { PartyPopulation } from "./PartyPopulation"; import { GroundEffectTelegraphs } from "./GroundEffectTelegraphs"; import { PlayerRig } from "./PlayerRig"; import { useManastormStore } from "../game/manastormStore"; -import { ManastormPortal } from "./ManastormPortal"; import { encounterWorldIsReady, encounterWorldKey, @@ -44,7 +43,6 @@ import { WailingNaralexEvent } from "./WailingNaralexEvent"; import { GameGltfLoaderLifecycle } from "./useGameGLTF"; import { CombatEffects } from "./CombatEffects"; import { PLAYER_GRAVITY } from "../game/playerMovement"; -import { GmPlacementMarkers } from "./GmPlacementMarkers"; import { useOnlineGroupStore } from "../app/onlineGroupStore"; import { useOnlineSessionStore } from "../app/onlineSessionStore"; import { OnlinePlayerPopulation } from "./OnlinePlayerPopulation"; @@ -59,6 +57,7 @@ import { DungeonWorldObjects } from "./DungeonWorldObjects"; import { ActivityAssetWarmup } from "./ActivityAssetWarmup"; import { resolveDungeonAssets, resolveManastormStageAssets } from "../game/dungeonAssets"; import { activeManastormStageAssetPackage } from "../game/manastormStageLoader"; +import { ShelvedGameSceneFeatures } from "@shelvedGameScene"; import { shouldLoadPartyNavigation } from "../game/partyNavigationPolicy"; import { activeDungeonCompletedObjectiveIds, @@ -404,8 +403,7 @@ export function GameScene() { allowedRuntimeIds={allowedRuntimeIds} /> - - + )} diff --git a/src/scene/OriginalCombatEffect.tsx b/src/scene/OriginalCombatEffect.tsx index 0936618b..89b14b72 100644 --- a/src/scene/OriginalCombatEffect.tsx +++ b/src/scene/OriginalCombatEffect.tsx @@ -50,6 +50,7 @@ import { type OriginalCombatMeshGraph, } from "./originalCombatMeshRuntime"; import { updateCombatEffectAnchorPosition } from "./combatEffectAnchorRegistry"; +import { shouldUseGeneratedOriginalFallback } from "./combatEffectRuntime"; import { OriginalWowModel } from "./OriginalWowModel"; import { OriginalParticleGeometry } from "./OriginalParticleGeometry"; import { @@ -99,9 +100,10 @@ const vertexShader = /* glsl */ ` vParticleAlpha = particleAlpha; vParticleFrame = particleFrame; vec4 viewPosition = modelViewMatrix * vec4(position, 1.0); + float inheritedScale = length(modelViewMatrix[0].xyz); gl_Position = projectionMatrix * viewPosition; gl_PointSize = clamp( - particleSize * viewportHeight / max(1.0, -viewPosition.z), + particleSize * inheritedScale * viewportHeight / max(1.0, -viewPosition.z), 1.0, 256.0 ); @@ -894,16 +896,23 @@ export const OriginalCombatEffect = memo(function OriginalCombatEffect({ const selection = loaded === null ? null : selectCombatEffectRender("original", loaded.map((item) => item.asset)); + const hasOriginalInstantKit = event.phase === "release" && event.instantCast === true + && (["cast-start", "impact", "aura-start"] as const).some((phase) => ( + combatOriginalPhaseAssets({ ...event, phase }).length > 0 + )); + const resolvedFallback = shouldUseGeneratedOriginalFallback(event, hasOriginalInstantKit) + ? fallback + : null; useEffect(() => { if ( loaded === null || selection?.kind === "original" - || fallback !== null + || resolvedFallback !== null || completedMissingOriginalRef.current ) return; completedMissingOriginalRef.current = true; onDone(); - }, [fallback, loaded, onDone, selection?.kind]); + }, [loaded, onDone, resolvedFallback, selection?.kind]); if (loaded === null || selection === null) return null; return selection.kind === "original" ? ( @@ -914,7 +923,7 @@ export const OriginalCombatEffect = memo(function OriginalCombatEffect({ loaded={loaded} onDone={onDone} /> - ) : fallback; + ) : resolvedFallback; }, (previous, next) => ( previous.event === next.event && previous.alternateOriginalPhase === next.alternateOriginalPhase diff --git a/src/scene/PartyPopulation.tsx b/src/scene/PartyPopulation.tsx index aa24a360..4d0c7f74 100644 --- a/src/scene/PartyPopulation.tsx +++ b/src/scene/PartyPopulation.tsx @@ -23,7 +23,7 @@ import { resolvedEquipmentItems } from "../game/heirloomEquipment"; import type { CharacterCombatAnimationEvent } from "../game/combatAnimation"; import { resolveDungeonAssets, resolveManastormStageAssets } from "../game/dungeonAssets"; import { activeManastormStageAssetPackage } from "../game/manastormStageLoader"; -import { derivePartySpawnSlots } from "../game/manastormAdminConfig"; +import { derivePartySpawnSlots } from "../game/partySpawnSlots"; import { requireDungeonDefinition } from "../game/dungeonRegistry"; import type { DungeonBossObjective } from "../game/dungeonTypes"; import { getMobPosition } from "../game/mobRuntimeRegistry"; diff --git a/src/scene/ShelvedGameScene.disabled.tsx b/src/scene/ShelvedGameScene.disabled.tsx new file mode 100644 index 00000000..db02a3a3 --- /dev/null +++ b/src/scene/ShelvedGameScene.disabled.tsx @@ -0,0 +1,3 @@ +export function ShelvedGameSceneFeatures() { + return null; +} diff --git a/src/scene/ShelvedGameScene.enabled.tsx b/src/scene/ShelvedGameScene.enabled.tsx new file mode 100644 index 00000000..a3dd8db2 --- /dev/null +++ b/src/scene/ShelvedGameScene.enabled.tsx @@ -0,0 +1,11 @@ +import { GmPlacementMarkers } from "./GmPlacementMarkers"; +import { ManastormPortal } from "./ManastormPortal"; + +export function ShelvedGameSceneFeatures() { + return ( + <> + + + + ); +} diff --git a/src/scene/combatEffectRuntime.test.ts b/src/scene/combatEffectRuntime.test.ts index fc4cf7d7..b411b3d7 100644 --- a/src/scene/combatEffectRuntime.test.ts +++ b/src/scene/combatEffectRuntime.test.ts @@ -7,6 +7,7 @@ import { advanceCombatEffectFrame, originalInstantCastEvent, originalInstantTerminalEvent, + shouldUseGeneratedOriginalFallback, updateCombatEffectQueue, type ActiveCombatEffect, } from "./combatEffectRuntime"; @@ -168,4 +169,22 @@ describe("combat-effect runtime budget", () => { }); expect(originalInstantTerminalEvent(event(22, { phase: "impact" }))).toBeNull(); }); + + it("does not layer a generated release over an instant ability's original phases", () => { + expect(shouldUseGeneratedOriginalFallback(event(30, { + phase: "release", + instantCast: true, + }), true)).toBe(false); + expect(shouldUseGeneratedOriginalFallback(event(31, { + phase: "release", + instantCast: true, + }), false)).toBe(true); + expect(shouldUseGeneratedOriginalFallback(event(31, { + phase: "release", + }), true)).toBe(true); + expect(shouldUseGeneratedOriginalFallback(event(32, { + phase: "impact", + instantCast: true, + }), true)).toBe(true); + }); }); diff --git a/src/scene/combatEffectRuntime.ts b/src/scene/combatEffectRuntime.ts index 9c6c08a5..fd5da6af 100644 --- a/src/scene/combatEffectRuntime.ts +++ b/src/scene/combatEffectRuntime.ts @@ -179,6 +179,15 @@ export function originalInstantTerminalEvent( }; } +/** Instant resolutions immediately emit a terminal phase, so their missing release + * asset must not add a generated layer over the authentic cast and terminal kits. */ +export function shouldUseGeneratedOriginalFallback( + event: CombatPresentationEvent, + hasOriginalInstantKit: boolean, +): boolean { + return event.phase !== "release" || !event.instantCast || !hasOriginalInstantKit; +} + export function addOriginalOnlyEffect( current: readonly ActiveCombatEffect[], event: CombatPresentationEvent, diff --git a/src/scene/originalWowModelRuntime.test.ts b/src/scene/originalWowModelRuntime.test.ts index 25a23ce4..ef0d3574 100644 --- a/src/scene/originalWowModelRuntime.test.ts +++ b/src/scene/originalWowModelRuntime.test.ts @@ -5,6 +5,7 @@ import { Matrix4, Quaternion, Vector3 } from "three"; import { parseOriginalWowModelGraph, updateWowBoneMatrices, wowModelFrame, wowModelTextureUrls, wowParticleChild } from "./originalWowModelRuntime"; const manifest = JSON.parse(readFileSync(path.resolve("public/assets/game/combat-effects/manifest.json"), "utf8")); +const rendererSource = readFileSync(new URL("./OriginalCombatEffect.tsx", import.meta.url), "utf8"); const urls = [...new Set(manifest.entries.filter((e: { source: string }) => e.source === "wow335") .flatMap((e: { phases: Record }) => Object.values(e.phases ?? {}).flat() .map((a) => a.effectGraphUrl).filter(Boolean)))]; @@ -67,4 +68,13 @@ describe("all-class original WoW models", () => { const dispel = parseOriginalWowModelGraph(graphs.find((g) => g.sourceModel === "spells/clense_base.m2")); expect(wowParticleChild(dispel, "Spells\\Dispel_Low_Recursive.mdx")!.particleGraph!.particles).toHaveLength(1); }); + + it("applies recursive parent scale to Purify and Cleanse point sprites", () => { + for (const sourceModel of ["spells/purify_base.m2", "spells/clense_base.m2"]) { + const graph = parseOriginalWowModelGraph(graphs.find((candidate) => candidate.sourceModel === sourceModel)); + expect(graph.particleGraph!.particles.filter((particle) => particle.recursionModel)).toHaveLength(3); + } + expect(rendererSource).toContain("float inheritedScale = length(modelViewMatrix[0].xyz);"); + expect(rendererSource).toContain("particleSize * inheritedScale * viewportHeight"); + }); }); diff --git a/src/ui/CompanionPanel.tsx b/src/ui/CompanionPanel.tsx index 7f1fc63f..6a9dabbb 100644 --- a/src/ui/CompanionPanel.tsx +++ b/src/ui/CompanionPanel.tsx @@ -1,4 +1,4 @@ -import { lazy, Suspense, useState, type CSSProperties } from "react"; +import { useState, type CSSProperties } from "react"; import { classById, FACTIONS, raceById } from "../app/characterCatalog"; import type { CharacterProfile } from "../app/types"; import { useShellStore } from "../app/shellStore"; @@ -28,14 +28,8 @@ import { ActionBar, type ActionBarItem, type ActionBarItems } from "./ActionBar" import { DungeonMapGraphic } from "./DungeonMapGraphic"; import { ExperienceBar } from "./ExperienceBar"; import { BrandMark, FrontSurface } from "./FrontSurface"; -import { - getLoadedCompanionManastormPanel, - loadCompanionManastormPanel, -} from "./gameplayResources"; - -const CompanionManastormPanel = lazy(() => loadCompanionManastormPanel().then((module) => ({ - default: module.CompanionManastormPanel, -}))); +import { ShelvedCompanionPanel } from "@shelvedCompanionPanel"; +import { SHELVED_MODES_ENABLED } from "../app/releaseFeatures"; function safePercent(current: number, maximum: number): number { if (!Number.isFinite(current) || !Number.isFinite(maximum) || maximum <= 0) return 0; @@ -158,7 +152,6 @@ export function CompanionPanel({ snapshot, send, preferControllerBinds = false } }) { const selected = snapshot.characters.find((character) => character.id === snapshot.selectedCharacterId) ?? null; const active = snapshot.activeCharacter ?? selected; - const PreloadedCompanionManastormPanel = getLoadedCompanionManastormPanel(); if (snapshot.phase === "login") { return ( @@ -289,7 +282,7 @@ export function CompanionPanel({ snapshot, send, preferControllerBinds = false }
- + {SHELVED_MODES_ENABLED ? : null}
); @@ -300,13 +293,7 @@ export function CompanionPanel({ snapshot, send, preferControllerBinds = false } } if (snapshot.phase === "manastorms") { - return PreloadedCompanionManastormPanel ? ( - - ) : ( - Loading Manastorm data…}> - - - ); + return ; } const race = active ? raceById(active.raceId) : null; diff --git a/src/ui/ContentManagerScreen.test.ts b/src/ui/ContentManagerScreen.test.ts index 181113ea..7518df70 100644 --- a/src/ui/ContentManagerScreen.test.ts +++ b/src/ui/ContentManagerScreen.test.ts @@ -6,6 +6,14 @@ const content = vi.hoisted(() => ({ installed: true, update: true, bundled: false, + packs: [{ + id: "characters:test", + label: "Test Characters", + bytes: 1024, + fileCount: 1, + bundledOnAndroid: false, + dependencies: [], + }], install: vi.fn(async () => undefined), remove: vi.fn(async () => undefined), })); @@ -29,14 +37,7 @@ vi.mock("../content/useContentManager", () => ({ checking: false, manifest: { version: "content-test", - packs: [{ - id: "characters:test", - label: "Test Characters", - bytes: 1024, - fileCount: 1, - bundledOnAndroid: false, - dependencies: [], - }], + packs: content.packs, }, bundledManifest: null, installingPackId: null, @@ -61,6 +62,10 @@ describe("browser Downloads actions", () => { content.installed = true; content.update = true; content.bundled = false; + content.packs = [{ + id: "characters:test", label: "Test Characters", bytes: 1024, + fileCount: 1, bundledOnAndroid: false, dependencies: [], + }]; }); it("offers an update action when an installed browser pack has changed", () => { @@ -92,4 +97,17 @@ describe("browser Downloads actions", () => { expect(markup).toContain("Included — always available"); expect(markup).not.toContain(">Remove"); }); + + it("hides shelved and internal dependency packs from Downloads", () => { + content.packs = [ + ...content.packs, + { id: "core:arenas", label: "Rated Arenas", bytes: 1, fileCount: 1, bundledOnAndroid: false, dependencies: [] }, + { id: "manastorm:deadmines", label: "Deadmines Manastorm", bytes: 1, fileCount: 1, bundledOnAndroid: false, dependencies: [] }, + ]; + const markup = renderToStaticMarkup(createElement(ContentManagerScreen, { onClose: () => undefined })); + + expect(markup).toContain("Test Characters"); + expect(markup).not.toContain("Rated Arenas"); + expect(markup).not.toContain("Deadmines Manastorm"); + }); }); diff --git a/src/ui/ContentManagerScreen.tsx b/src/ui/ContentManagerScreen.tsx index 97a11605..2ccfcb9a 100644 --- a/src/ui/ContentManagerScreen.tsx +++ b/src/ui/ContentManagerScreen.tsx @@ -12,6 +12,7 @@ import { useContentManagerSnapshot } from "../content/useContentManager"; import { DualDisplayFrame } from "../components/DualDisplayFrame"; import { LazyLocalCompanionPanel } from "./LazyLocalCompanionPanel"; import { BrandMark, FrontSurface } from "./FrontSurface"; +import contentPolicy from "../../shared/content-policy.json"; function formatBytes(bytes: number): string { if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(2)} GB`; @@ -23,7 +24,10 @@ function formatBytes(bytes: number): string { export function ContentManagerScreen({ onClose }: { readonly onClose: () => void }) { const state = useContentManagerSnapshot(); const native = Capacitor.isNativePlatform(); - const packs = useMemo(() => state.manifest?.packs ?? state.bundledManifest?.packs ?? [], [state.bundledManifest, state.manifest]); + const packs = useMemo(() => (state.manifest?.packs ?? state.bundledManifest?.packs ?? []).filter((pack) => ( + !contentPolicy.hiddenContentPackIds.includes(pack.id) + && !contentPolicy.hiddenContentPackPrefixes.some((prefix) => pack.id.startsWith(prefix)) + )), [state.bundledManifest, state.manifest]); const progress = state.totalBytes > 0 ? Math.min(1, state.downloadedBytes / state.totalBytes) : 0; return ( diff --git a/src/ui/Hud.tsx b/src/ui/Hud.tsx index 4c3b2842..3c27b085 100644 --- a/src/ui/Hud.tsx +++ b/src/ui/Hud.tsx @@ -30,7 +30,7 @@ import { EncounterTimeline } from "./EncounterTimeline"; import { PartyFrames } from "./PartyFrames"; import { TimedEffectStrip } from "./TimedEffectStrip"; import { AuraStrip } from "./AuraStrip"; -import { ManastormStatus } from "./ManastormStatus"; +import { ShelvedHud } from "@shelvedHud"; import { ThreatMeterWidget } from "./ThreatMeterWidget"; import { BossLootWindow } from "./BossLootWindow"; import { EditableHudElement } from "./EditableHudElement"; @@ -363,7 +363,7 @@ export function Hud() { return (
- + {dungeonComplete && health > 0 ? ( diff --git a/src/ui/MainMenuScreen.tsx b/src/ui/MainMenuScreen.tsx index 9a022537..3b904da9 100644 --- a/src/ui/MainMenuScreen.tsx +++ b/src/ui/MainMenuScreen.tsx @@ -8,8 +8,8 @@ import { LazyLocalCompanionPanel } from "./LazyLocalCompanionPanel"; import { ControllerButton } from "./ControllerButton"; import { ContentManagerScreen } from "./ContentManagerScreen"; import { BrandMark, ControllerLegend, FrontSurface } from "./FrontSurface"; -import { isGameMasterSession } from "../game/manastormAdminConfig"; import { OnlineGroupPanel } from "./OnlineGroupPanel"; +import { sessionCanUseShelvedGmTools, SHELVED_MODES_ENABLED } from "../app/releaseFeatures"; const GAME_MODES = [ { @@ -19,20 +19,19 @@ const GAME_MODES = [ description: "Choose an expedition and lead your party into the depths.", available: true, }, - { - id: "manastorms", + ...(SHELVED_MODES_ENABLED ? [{ + id: "manastorms" as const, number: "02", name: "Manastorms", description: "Chain random boss encounters as the storm grows stronger and more rewarding.", available: true, - }, - { - id: "arenas", + }, { + id: "arenas" as const, number: "03", name: "Arenas", description: "Rated 2v2 and 3v3 combat against players or rating-matched opponents.", available: true, - }, + }] : []), { id: "trials", number: "04", @@ -77,7 +76,7 @@ export function MainMenuScreen() { const openGmAdmin = useShellStore((state) => state.openGmAdmin); const character = activeCharacter ?? rosterCharacter; const isRom = character?.categoryId === "rom"; - const isGm = isGameMasterSession(session); + const isGm = sessionCanUseShelvedGmTools(session); const actions = useMemo(() => [ { @@ -85,20 +84,19 @@ export function MainMenuScreen() { run: openDungeons, neighbors: { right: "mode-manastorms", down: "back-to-roster" }, }, - { + ...(SHELVED_MODES_ENABLED ? [{ id: "mode-manastorms", run: openManastorms, neighbors: { left: "mode-dungeons", down: "back-to-roster" }, - }, - { + }, { id: "mode-arenas", run: openArenas, neighbors: { left: "mode-manastorms", down: "back-to-roster" }, - }, + }] : []), { id: "mode-downloads", run: () => setContentManagerOpen(true), - neighbors: { left: "mode-manastorms", down: "back-to-roster" }, + neighbors: { left: SHELVED_MODES_ENABLED ? "mode-manastorms" : "mode-dungeons", down: "back-to-roster" }, }, { id: "back-to-roster", diff --git a/src/ui/PauseMenu.tsx b/src/ui/PauseMenu.tsx index 598d818e..b33bfeb2 100644 --- a/src/ui/PauseMenu.tsx +++ b/src/ui/PauseMenu.tsx @@ -3,7 +3,7 @@ import { useShellStore } from "../app/shellStore"; import { useGameStore } from "../game/store"; import { useMenuController, type MenuAction } from "../input/useMenuController"; import { ControllerButton } from "./ControllerButton"; -import { isGameMasterSession } from "../game/manastormAdminConfig"; +import { sessionCanUseShelvedGmTools } from "../app/releaseFeatures"; import { useInputBindingLabels } from "../input/useInputBindingLabels"; import { INPUT_ACTION_BY_ID } from "../input/keybinds"; @@ -33,7 +33,7 @@ function PauseDialog() { abandonManastormRun(); }; const skillMenuLabel = activeCharacter?.categoryId === "rom" ? "Skills" : "Talents"; - const isGm = isGameMasterSession(session); + const isGm = sessionCanUseShelvedGmTools(session); const actions = useMemo(() => [ { id: "pause-resume", run: resume }, diff --git a/src/ui/ShelvedCompanionPanel.disabled.tsx b/src/ui/ShelvedCompanionPanel.disabled.tsx new file mode 100644 index 00000000..a3b5b17d --- /dev/null +++ b/src/ui/ShelvedCompanionPanel.disabled.tsx @@ -0,0 +1,9 @@ +import type { CharacterProfile } from "../app/types"; +import type { CompanionCommand } from "../platform/dualScreenSync"; + +export function ShelvedCompanionPanel(_props: { + readonly selected: CharacterProfile | null; + readonly send: (command: CompanionCommand) => void; +}) { + return null; +} diff --git a/src/ui/ShelvedCompanionPanel.enabled.tsx b/src/ui/ShelvedCompanionPanel.enabled.tsx new file mode 100644 index 00000000..5687457c --- /dev/null +++ b/src/ui/ShelvedCompanionPanel.enabled.tsx @@ -0,0 +1,22 @@ +import { lazy, Suspense } from "react"; +import type { CharacterProfile } from "../app/types"; +import type { CompanionCommand } from "../platform/dualScreenSync"; +import { FrontSurface } from "./FrontSurface"; + +const CompanionManastormPanel = lazy(() => import("./CompanionManastormPanel").then((module) => ({ + default: module.CompanionManastormPanel, +}))); + +export function ShelvedCompanionPanel({ + selected, + send, +}: { + readonly selected: CharacterProfile | null; + readonly send: (command: CompanionCommand) => void; +}) { + return ( + Loading Manastorm data…}> + + + ); +} diff --git a/src/ui/ShelvedHud.disabled.tsx b/src/ui/ShelvedHud.disabled.tsx new file mode 100644 index 00000000..6f8a8480 --- /dev/null +++ b/src/ui/ShelvedHud.disabled.tsx @@ -0,0 +1,3 @@ +export function ShelvedHud() { + return null; +} diff --git a/src/ui/ShelvedHud.enabled.tsx b/src/ui/ShelvedHud.enabled.tsx new file mode 100644 index 00000000..9a05f24a --- /dev/null +++ b/src/ui/ShelvedHud.enabled.tsx @@ -0,0 +1,5 @@ +import { ManastormStatus } from "./ManastormStatus"; + +export function ShelvedHud() { + return ; +} diff --git a/src/ui/gameplayPanelsIntegration.test.ts b/src/ui/gameplayPanelsIntegration.test.ts index 8bf76c9e..b2acbffc 100644 --- a/src/ui/gameplayPanelsIntegration.test.ts +++ b/src/ui/gameplayPanelsIntegration.test.ts @@ -21,6 +21,10 @@ const companionPanelSource = readFileSync( new URL("./CompanionPanel.tsx", import.meta.url), "utf8", ); +const shelvedCompanionSource = readFileSync( + new URL("./ShelvedCompanionPanel.enabled.tsx", import.meta.url), + "utf8", +); describe("gameplay panel loading integration", () => { it("loads talent resources behind the main-menu catalog gate, not during gameplay", () => { @@ -42,9 +46,10 @@ describe("gameplay panel loading integration", () => { expect(gameplayResourcesSource).toContain('import("../GameRuntime")'); expect(gameplayResourcesSource).toContain('import("./PartyManagementPanel")'); expect(gameplayResourcesSource).toContain('import("./BossGuidePanel")'); - expect(gameplayResourcesSource).toContain('import("./CompanionManastormPanel")'); + expect(gameplayResourcesSource).not.toContain('import("./CompanionManastormPanel")'); + expect(shelvedCompanionSource).toContain('import("./CompanionManastormPanel")'); expect(gameplayPanelsSource).toContain(""); expect(gameplayPanelsSource).toContain(""); - expect(companionPanelSource).toContain(" let loadedPartyManagementPanel: typeof import("./PartyManagementPanel")["PartyManagementPanel"] | null = null; let bossGuidePanelModule: Promise | null = null; let loadedBossGuidePanel: typeof import("./BossGuidePanel")["BossGuidePanel"] | null = null; -let companionManastormPanelModule: Promise | null = null; -let loadedCompanionManastormPanel: typeof import("./CompanionManastormPanel")["CompanionManastormPanel"] | null = null; export function loadGameRuntime() { if (!gameRuntimeModule) { @@ -64,31 +62,11 @@ export function getLoadedBossGuidePanel() { return loadedBossGuidePanel; } -export function loadCompanionManastormPanel() { - if (!companionManastormPanelModule) { - companionManastormPanelModule = import("./CompanionManastormPanel") - .then((module) => { - loadedCompanionManastormPanel = module.CompanionManastormPanel; - return module; - }) - .catch((error: unknown) => { - companionManastormPanelModule = null; - throw error; - }); - } - return companionManastormPanelModule; -} - -export function getLoadedCompanionManastormPanel() { - return loadedCompanionManastormPanel; -} - export async function preloadSharedGameplayResources(originalMediaEnabled: boolean): Promise { await Promise.all([ loadGameRuntime(), loadPartyManagementPanel(), loadBossGuidePanel(), - loadCompanionManastormPanel(), ]); if (!originalMediaEnabled) return; const { ensureCombatMediaPack } = await import("../game/combatMediaPack"); diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 69cab0f0..d4e4f3d0 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -5,6 +5,7 @@ interface ImportMetaEnv { readonly VITE_CONTENT_BASE_URL?: string; readonly VITE_CONTENT_MODE: "bundled" | "downloadable"; readonly VITE_APP_VERSION: string; + readonly VITE_SHELVED_MODES_ENABLED: "true" | "false"; } interface ImportMeta { diff --git a/tsconfig.app.json b/tsconfig.app.json index 88734117..363ef2f3 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -14,7 +14,14 @@ "moduleResolution": "Bundler", "baseUrl": ".", "paths": { - "@dungeonAssetModules": ["src/game/dungeonAssetModules.ts"] + "@dungeonAssetModules": ["src/game/dungeonAssetModules.ts"], + "@shelvedGameRuntime": ["src/app/ShelvedGameRuntime.disabled.tsx"], + "@shelvedGameScene": ["src/scene/ShelvedGameScene.disabled.tsx"], + "@shelvedModeRoutes": ["src/app/ShelvedModeRoutes.disabled.tsx"], + "@shelvedOnlineGroupRuntime": ["src/app/ShelvedOnlineGroupRuntime.disabled.ts"], + "@shelvedDungeonSession": ["src/game/ShelvedDungeonSession.disabled.ts"], + "@shelvedCompanionPanel": ["src/ui/ShelvedCompanionPanel.disabled.tsx"], + "@shelvedHud": ["src/ui/ShelvedHud.disabled.tsx"] }, "resolveJsonModule": true, "isolatedModules": true, diff --git a/vite.config.ts b/vite.config.ts index c689726b..f4c0e40e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,7 +9,8 @@ const packageVersion = (JSON.parse(readFileSync(new URL("./package.json", import export default defineConfig(({ mode }) => { const androidBuild = mode === "android"; - const downloadableBuild = androidBuild || mode === "production"; + const shelvedModesEnabled = mode === "shelved"; + const downloadableBuild = androidBuild || mode === "production" || shelvedModesEnabled; return { plugins: [react(), { name: "bundled-content-preview", @@ -19,16 +20,57 @@ export default defineConfig(({ mode }) => { if (!serve(request, response, new URL(request.url ?? "/", "http://localhost").pathname)) next(); }); }, + }, { + name: "bundle-module-report", + generateBundle(_options, bundle) { + const chunks = Object.values(bundle) + .filter((entry): entry is Extract => entry.type === "chunk") + .map((chunk) => ({ + file: chunk.fileName, + entry: chunk.isEntry, + dynamicEntry: chunk.isDynamicEntry, + modules: Object.keys(chunk.modules).map((moduleId) => ( + path.relative(process.cwd(), moduleId).split(path.sep).join("/") + )).sort(), + })); + this.emitFile({ + type: "asset", + fileName: "bundle-module-report.json", + source: `${JSON.stringify({ shelvedModesEnabled, chunks }, null, 2)}\n`, + }); + }, }], publicDir: androidBuild ? ".android-public" : downloadableBuild ? ".web-public" : "public", define: { "import.meta.env.VITE_APP_VERSION": JSON.stringify(packageVersion), "import.meta.env.VITE_CONTENT_MODE": JSON.stringify(downloadableBuild ? "downloadable" : "bundled"), + "import.meta.env.VITE_SHELVED_MODES_ENABLED": JSON.stringify(shelvedModesEnabled ? "true" : "false"), }, resolve: { alias: { "@dungeonAssetModules": path.resolve( - downloadableBuild ? "src/game/dungeonAssetModules.android.ts" : "src/game/dungeonAssetModules.ts", + androidBuild || mode === "production" ? "src/game/dungeonAssetModules.android.ts" : "src/game/dungeonAssetModules.ts", + ), + "@shelvedGameRuntime": path.resolve( + shelvedModesEnabled ? "src/app/ShelvedGameRuntime.enabled.tsx" : "src/app/ShelvedGameRuntime.disabled.tsx", + ), + "@shelvedGameScene": path.resolve( + shelvedModesEnabled ? "src/scene/ShelvedGameScene.enabled.tsx" : "src/scene/ShelvedGameScene.disabled.tsx", + ), + "@shelvedModeRoutes": path.resolve( + shelvedModesEnabled ? "src/app/ShelvedModeRoutes.tsx" : "src/app/ShelvedModeRoutes.disabled.tsx", + ), + "@shelvedOnlineGroupRuntime": path.resolve( + shelvedModesEnabled ? "src/app/ShelvedOnlineGroupRuntime.enabled.ts" : "src/app/ShelvedOnlineGroupRuntime.disabled.ts", + ), + "@shelvedDungeonSession": path.resolve( + shelvedModesEnabled ? "src/game/ShelvedDungeonSession.enabled.ts" : "src/game/ShelvedDungeonSession.disabled.ts", + ), + "@shelvedCompanionPanel": path.resolve( + shelvedModesEnabled ? "src/ui/ShelvedCompanionPanel.enabled.tsx" : "src/ui/ShelvedCompanionPanel.disabled.tsx", + ), + "@shelvedHud": path.resolve( + shelvedModesEnabled ? "src/ui/ShelvedHud.enabled.tsx" : "src/ui/ShelvedHud.disabled.tsx", ), }, },