├── app ├── battle │ ├── battle-damage-hit.js │ ├── battle-2d.js │ ├── battle-damage-stats.js │ ├── battle-actions-op-control.js │ ├── battle-damage-calc.js │ ├── battle-stack.js │ ├── battle-limits.js │ ├── battle-actions-op-loop.js │ ├── battle-stack-memory.js │ ├── battle-module.js │ ├── battle-controls.js │ ├── battle-formation.js │ ├── battle-menu-limit.js │ └── battle-setup.js ├── menu │ ├── menu-save.js │ ├── menu-party-select.js │ ├── menu-tutorial.js │ ├── menu-game-over.js │ ├── menu-scene.js │ ├── menu-change-disc.js │ └── menu-controls.js ├── data │ ├── cd-fetch-data.js │ ├── exe-fetch-data.js │ ├── world-fetch-data.js │ ├── media-fetch-data.js │ ├── savemap-config.js │ ├── savemap-controls.js │ ├── global-data.js │ ├── field-fetch-data.js │ ├── cache-manager.js │ ├── menu-fetch-data.js │ ├── kernel-fetch-data.js │ └── battle-fetch-data.js ├── helpers │ ├── font-helper.js │ ├── toasts.js │ ├── gametime.js │ ├── media-can-play.js │ ├── display-controls.js │ ├── helpers.js │ ├── custom-log.js │ └── base64-binary.js ├── field │ ├── field-op-codes-assign-helper.js │ ├── field-op-codes-misc.js │ ├── field-metadata.js │ ├── field-ortho-scene.js │ ├── field-ortho-bg-scene.js │ ├── field-battle.js │ ├── field-op-codes-party-helper.js │ ├── field-op-codes-flow-helper.js │ ├── field-controls.js │ └── field-module.js ├── world │ ├── world-3d.js │ ├── world-2d.js │ ├── world-controls.js │ ├── world-module.js │ └── world-scene.js ├── minigame │ ├── minigame-3d.js │ ├── minigame-2d.js │ ├── minigame-controls.js │ ├── minigame-module.js │ └── minigame-scene.js ├── render │ └── renderer.js ├── manager.mjs └── loading │ └── loading-module.js ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── workings-out ├── fade-bg.png ├── keyboard-layout.xcf ├── swirl-shader-test.mp4 ├── swirl-shader-test.png ├── output │ ├── byte-pattern-find-repeated-value.txt │ └── field-model-lighting.json ├── create-op-codes-progress-readme.js ├── field-dialog-examples.js ├── createFieldLayerMetaDataFromFiles.js ├── byte-data-pattern-finding.js ├── gltf-combined.js ├── getModelScaleDownValue.js ├── op-codes-completed.json ├── op-codes-battle-camera-completed.json ├── savemap-wiki-add-offsets.js ├── walkmeshPositionHelper.js ├── fieldModelSelectiveLightingIdentify.js ├── byte-data-pattern-finder.js ├── world-shader.html ├── LINE-op-code-usage.js ├── identify-fields-without-shift-offsets.js └── create-op-codes-battle-camera-progress-readme.js ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── assets ├── fenrir │ └── keyboard-layout.png ├── nanoevents.js ├── main.css ├── threejs-r148 │ └── examples │ │ └── jsm │ │ ├── geometries │ │ └── TextGeometry.js │ │ ├── loaders │ │ └── FontLoader.js │ │ └── libs │ │ └── stats.module.js ├── threejs-r135-dg │ └── examples │ │ └── jsm │ │ ├── geometries │ │ └── TextGeometry.js │ │ ├── libs │ │ └── stats.module.js │ │ └── loaders │ │ └── FontLoader.js └── op-loop-visualiser.css ├── .gitignore ├── site.webmanifest ├── package.json ├── static-server.js └── index.html /app/battle/battle-damage-hit.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dangarfield/ff7-fenrir/HEAD/favicon.ico -------------------------------------------------------------------------------- /favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dangarfield/ff7-fenrir/HEAD/favicon-16x16.png -------------------------------------------------------------------------------- /favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dangarfield/ff7-fenrir/HEAD/favicon-32x32.png -------------------------------------------------------------------------------- /apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dangarfield/ff7-fenrir/HEAD/apple-touch-icon.png -------------------------------------------------------------------------------- /workings-out/fade-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dangarfield/ff7-fenrir/HEAD/workings-out/fade-bg.png -------------------------------------------------------------------------------- /android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dangarfield/ff7-fenrir/HEAD/android-chrome-192x192.png -------------------------------------------------------------------------------- /android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dangarfield/ff7-fenrir/HEAD/android-chrome-512x512.png -------------------------------------------------------------------------------- /workings-out/keyboard-layout.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dangarfield/ff7-fenrir/HEAD/workings-out/keyboard-layout.xcf -------------------------------------------------------------------------------- /assets/fenrir/keyboard-layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dangarfield/ff7-fenrir/HEAD/assets/fenrir/keyboard-layout.png -------------------------------------------------------------------------------- /workings-out/swirl-shader-test.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dangarfield/ff7-fenrir/HEAD/workings-out/swirl-shader-test.mp4 -------------------------------------------------------------------------------- /workings-out/swirl-shader-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dangarfield/ff7-fenrir/HEAD/workings-out/swirl-shader-test.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | field-backgrounds 2 | js 3 | kujata-data 4 | node_modules 5 | package-lock.json 6 | workings-out/unlgp 7 | .DS_Store 8 | *.code-workspace 9 | *.xcf 10 | *.xlsx -------------------------------------------------------------------------------- /app/menu/menu-save.js: -------------------------------------------------------------------------------- 1 | import { loadSaveMenu as loadSaveMainMenu } from './menu-main-save.js' 2 | const loadSaveMenu = async () => { 3 | console.log('loadSaveMenu') 4 | loadSaveMainMenu(true) 5 | } 6 | 7 | export { loadSaveMenu } 8 | -------------------------------------------------------------------------------- /site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /app/data/cd-fetch-data.js: -------------------------------------------------------------------------------- 1 | import { KUJATA_BASE } from './kernel-fetch-data.js' 2 | 3 | const loadCDData = async () => { 4 | const creditsRes = await fetch(`${KUJATA_BASE}/metadata/credits-assets/credits.json`) 5 | const credits = await creditsRes.json() 6 | window.data.cd = { 7 | credits 8 | } 9 | } 10 | 11 | export { loadCDData } 12 | -------------------------------------------------------------------------------- /app/menu/menu-party-select.js: -------------------------------------------------------------------------------- 1 | import { loadPHSMenu } from './menu-main-phs.js' 2 | const loadPartySelectMenu = async (param) => { 3 | console.log('phs loadPartySelectMenu', param) 4 | // showDebugText('Party Select') 5 | // temporaryPHSMenuSetParty() 6 | // setMenuState('party') 7 | await loadPHSMenu(param) 8 | } 9 | 10 | export { loadPartySelectMenu } 11 | -------------------------------------------------------------------------------- /app/menu/menu-tutorial.js: -------------------------------------------------------------------------------- 1 | import { showDebugText } from './menu-scene.js' 2 | import { setMenuState } from './menu-module.js' 3 | const loadMainMenuWithTutorial = tutorialId => { 4 | console.log('loadMainMenuWithTutorial', tutorialId) 5 | showDebugText(`Tutorial: ${tutorialId}`) 6 | setMenuState('tutorial') 7 | } 8 | 9 | export { loadMainMenuWithTutorial } 10 | -------------------------------------------------------------------------------- /app/helpers/font-helper.js: -------------------------------------------------------------------------------- 1 | import { FontLoader } from '../../assets/threejs-r148/examples/jsm/loaders/FontLoader.js' 2 | 3 | const loadFont = async () => { 4 | return new Promise((resolve, reject) => { 5 | new FontLoader().load( 6 | 'assets/threejs-r135-dg/examples/fonts/helvetiker_regular.typeface.json', 7 | font => { 8 | resolve(font) 9 | } 10 | ) 11 | }) 12 | } 13 | export { 14 | loadFont 15 | } 16 | -------------------------------------------------------------------------------- /assets/nanoevents.js: -------------------------------------------------------------------------------- 1 | let createNanoEvents = () => ({ 2 | events: {}, 3 | emit(event, ...args) { 4 | for (let i of this.events[event] || []) { 5 | i(...args) 6 | } 7 | }, 8 | on(event, cb) { 9 | ; (this.events[event] = this.events[event] || []).push(cb) 10 | return () => (this.events[event] = this.events[event].filter(i => i !== cb)) 11 | } 12 | }) 13 | 14 | export { createNanoEvents } 15 | -------------------------------------------------------------------------------- /workings-out/output/byte-pattern-find-repeated-value.txt: -------------------------------------------------------------------------------- 1 | Repeated Values - 0,3,8,11,14 2 | ---------------------- 3 | 0xbd73 16,16,16,16,16 false 4 | 0xbd76 16,16,16,16,16 false 5 | 0xbd7b 16,16,16,16,16 false 6 | 0xbd7e 16,16,16,16,16 false 7 | 0xbd81 16,16,16,16,16 false 8 | 0x2042df 8,8,8,8,8 136,65,139,85 false 9 | 0x20478c 8,8,8,8,8 136,65,139,85 false 10 | 0x204af1 8,8,8,8,8 136,65,139,85 false 11 | 0x51d3f8 2,2,2,2,2 3,1,4,0,5,1,3,4,1 false -------------------------------------------------------------------------------- /app/field/field-op-codes-assign-helper.js: -------------------------------------------------------------------------------- 1 | const bitTest = (num, bit) => { 2 | return (num >> bit) % 2 !== 0 3 | } 4 | window.bitTest = bitTest 5 | const setBitOn = (num, bit) => { 6 | return num | (1 << bit) 7 | } 8 | 9 | const setBitOff = (num, bit) => { 10 | return num & ~(1 << bit) 11 | } 12 | const toggleBit = (num, bit) => { 13 | return bitTest(num, bit) ? setBitOff(num, bit) : setBitOn(num, bit) 14 | } 15 | 16 | export { setBitOn, setBitOff, toggleBit, bitTest } 17 | -------------------------------------------------------------------------------- /app/world/world-3d.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../assets/threejs-r148/build/three.module.js' 2 | import { scene } from './world-scene.js' 3 | 4 | const showDebugObject = () => { 5 | const geometry = new THREE.BoxGeometry() 6 | const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }) 7 | const cube = new THREE.Mesh(geometry, material) 8 | scene.add(cube) 9 | } 10 | 11 | const loadWorldMap3d = () => { 12 | showDebugObject() 13 | } 14 | export { loadWorldMap3d } 15 | -------------------------------------------------------------------------------- /app/minigame/minigame-3d.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../assets/threejs-r148/build/three.module.js' 2 | import { scene } from './minigame-scene.js' 3 | 4 | const showDebugObject = () => { 5 | const geometry = new THREE.BoxGeometry() 6 | const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }) 7 | const cube = new THREE.Mesh(geometry, material) 8 | scene.add(cube) 9 | } 10 | 11 | const loadTempMiniGame3d = () => { 12 | showDebugObject() 13 | } 14 | export { loadTempMiniGame3d } 15 | -------------------------------------------------------------------------------- /app/helpers/toasts.js: -------------------------------------------------------------------------------- 1 | const addToast = msg => { 2 | const toastsHolder = document.querySelector('.toasts') 3 | toastsHolder.innerHTML += ` 4 |
5 |
6 | ${msg} 7 |
8 |
` 9 | setTimeout(() => { 10 | const toast = document.querySelector('.toasts .toast') 11 | console.log('toast', toast) 12 | toast.parentNode.removeChild(toast) 13 | }, 2000) 14 | } 15 | 16 | export { addToast } 17 | -------------------------------------------------------------------------------- /workings-out/create-op-codes-progress-readme.js: -------------------------------------------------------------------------------- 1 | const { 2 | createOpCodesFieldProgressReadme 3 | } = require('./create-op-codes-field-progress-readme.js') 4 | const { 5 | createOpCodesBattleCameraProgressReadme 6 | } = require('./create-op-codes-battle-camera-progress-readme.js') 7 | const { 8 | createActionSequenceOpProgressReadme 9 | } = require('./create-op-codes-action-sequence-progress-readme.js') 10 | 11 | const init = async () => { 12 | await Promise.all([ 13 | createOpCodesBattleCameraProgressReadme(), 14 | createOpCodesFieldProgressReadme(), 15 | createActionSequenceOpProgressReadme() 16 | ]) 17 | } 18 | init() 19 | -------------------------------------------------------------------------------- /app/field/field-op-codes-misc.js: -------------------------------------------------------------------------------- 1 | const SETX = op => { 2 | console.log('SETX', op) 3 | // Not used 4 | return {} 5 | } 6 | const GETX = op => { 7 | console.log('GETX', op) 8 | // Kujata says its used, I can't see it in makou reactor 9 | return {} 10 | } 11 | const SEARCHX = op => { 12 | console.log('SEARCHX', op) 13 | // Not used 14 | return {} 15 | } 16 | const PMJMP = op => { 17 | console.log('PMJMP', op) 18 | // No need to do anything, could consider preloading maybe, but all is taken care of in MAPJUMP 19 | return {} 20 | } 21 | const PMJMP2 = op => { 22 | console.log('PMJMP2', op) 23 | // No need to do anything, could consider preloading maybe, but all is taken care of in MAPJUMP 24 | return {} 25 | } 26 | export { SETX, GETX, SEARCHX, PMJMP, PMJMP2 } 27 | -------------------------------------------------------------------------------- /assets/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | } 4 | 5 | #container { 6 | height: 0; 7 | } 8 | 9 | .lil-gui { 10 | -moz-user-select: none; 11 | -webkit-user-select: none; 12 | -ms-user-select: none; 13 | user-select: none; 14 | z-index: 2 !important; 15 | /* TODO Solve this in HTML */ 16 | } 17 | 18 | .modal.show { 19 | display: block; 20 | } 21 | 22 | .toasts { 23 | position: absolute; 24 | bottom: 0; 25 | left: 0; 26 | } 27 | 28 | .display-controls { 29 | position: fixed; 30 | bottom: 0; 31 | right: 0; 32 | width: 210px; 33 | z-index: 10000000; 34 | } 35 | 36 | .display-controls .btn { 37 | padding: 0px 5px; 38 | background-color: black; 39 | color: #aaaaaa 40 | } 41 | 42 | .lil-gui.hide, 43 | .lil-gui .close-button { 44 | display: none; 45 | } -------------------------------------------------------------------------------- /app/data/exe-fetch-data.js: -------------------------------------------------------------------------------- 1 | import { combineBattleFormationConfig } from '../battle/battle-formation.js' 2 | import { KUJATA_BASE } from './kernel-fetch-data.js' 3 | 4 | const loadExeData = async () => { 5 | const exeDataRes = await fetch(`${KUJATA_BASE}/data/exe/ff7.exe.json`) 6 | const exeData = await exeDataRes.json() 7 | for (let i = 0; i < exeData.limitData.limits.length; i++) { 8 | const limit = exeData.limitData.limits[i] 9 | limit.name = window.data.kernel.magicNames[i + 128].replace( 10 | /^\{COLOR\(\d+\)\}/, 11 | '' 12 | ) 13 | limit.description = window.data.kernel.magicDescriptions[i + 128].replace( 14 | /^\{COLOR\(\d+\)\}/, 15 | '' 16 | ) 17 | } 18 | combineBattleFormationConfig(exeData.battlePlayerFormationData) 19 | window.data.exe = exeData 20 | } 21 | 22 | export { loadExeData } 23 | -------------------------------------------------------------------------------- /app/data/world-fetch-data.js: -------------------------------------------------------------------------------- 1 | import { KUJATA_BASE } from '../data/kernel-fetch-data.js' 2 | 3 | const getFieldToWorldMapTransitionData = async () => { 4 | const dataRes = await fetch( 5 | `${KUJATA_BASE}/metadata/field-id-to-world-map-coords.json` 6 | ) 7 | const data = await dataRes.json() 8 | return data 9 | } 10 | const getWorldToFieldTransitionData = async () => { 11 | const dataRes = await fetch( 12 | `${KUJATA_BASE}/data/world/world_us.lgp/field.tbl.json` 13 | ) 14 | const data = await dataRes.json() 15 | return data 16 | } 17 | const getSceneGraph = async () => { 18 | const dataRes = await fetch(`${KUJATA_BASE}/metadata/scene-graph.json`) 19 | const data = await dataRes.json() 20 | return data 21 | } 22 | 23 | export { 24 | getFieldToWorldMapTransitionData, 25 | getWorldToFieldTransitionData, 26 | getSceneGraph 27 | } 28 | -------------------------------------------------------------------------------- /app/field/field-metadata.js: -------------------------------------------------------------------------------- 1 | import { getFieldMapList } from './field-fetch-data.js' 2 | 3 | let maplist 4 | 5 | const getFieldIdForName = async name => { 6 | if (maplist === undefined) { 7 | maplist = await getFieldMapList() 8 | } 9 | for (let i = 0; i < maplist.length; i++) { 10 | if (maplist[i] === name) { 11 | return i 12 | } 13 | } 14 | return -1 15 | } 16 | const getFieldNameForId = async id => { 17 | if (maplist === undefined) { 18 | maplist = await getFieldMapList() 19 | } 20 | return maplist[id] 21 | } 22 | const getLastFieldId = async () => { 23 | const fieldName = 24 | window.currentField && window.currentField.lastFieldName 25 | ? window.currentField.lastFieldName 26 | : '' 27 | const fieldId = await getFieldIdForName(fieldName) 28 | return fieldId 29 | } 30 | 31 | export { getLastFieldId, getFieldNameForId, getFieldIdForName } 32 | -------------------------------------------------------------------------------- /app/battle/battle-2d.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../assets/threejs-r148/build/three.module.js' 2 | import { TextGeometry } from '../../assets/threejs-r148/examples/jsm/geometries/TextGeometry.js' 3 | 4 | import { orthoScene } from './battle-scene.js' 5 | import { loadFont } from '../helpers/font-helper.js' 6 | 7 | const showDebugText = async text => { 8 | const font = await loadFont() 9 | const textGeo = new TextGeometry(text, { 10 | font, 11 | size: 5, 12 | height: 1, 13 | curveSegments: 10, 14 | bevelEnabled: false 15 | }) 16 | const material = new THREE.MeshBasicMaterial({ 17 | color: 0xffffff, 18 | transparent: true 19 | }) 20 | const mesh = new THREE.Mesh(textGeo, material) 21 | mesh.position.y = 4 22 | mesh.position.x = 4 23 | orthoScene.add(mesh) 24 | } 25 | 26 | const loadTempBattle2d = async battleId => { 27 | showDebugText('Battle ' + battleId) 28 | } 29 | export { loadTempBattle2d } 30 | -------------------------------------------------------------------------------- /app/world/world-2d.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../assets/threejs-r148/build/three.module.js' 2 | import { TextGeometry } from '../../assets/threejs-r148/examples/jsm/geometries/TextGeometry.js' 3 | import { orthoScene } from './world-scene.js' 4 | import { loadFont } from '../helpers/font-helper.js' 5 | 6 | const showDebugText = async text => { 7 | const font = await loadFont() 8 | const textGeo = new TextGeometry(text, { 9 | font, 10 | size: 5, 11 | height: 1, 12 | curveSegments: 10, 13 | bevelEnabled: false 14 | }) 15 | const material = new THREE.MeshBasicMaterial({ 16 | color: 0xffffff, 17 | transparent: true 18 | }) 19 | const mesh = new THREE.Mesh(textGeo, material) 20 | mesh.position.y = 4 21 | mesh.position.x = 4 22 | orthoScene.add(mesh) 23 | } 24 | 25 | const loadWorldMap2d = async description => { 26 | showDebugText(`World map - ${description}`) 27 | } 28 | export { loadWorldMap2d } 29 | -------------------------------------------------------------------------------- /app/minigame/minigame-2d.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../assets/threejs-r148/build/three.module.js' 2 | import { TextGeometry } from '../../assets/threejs-r148/examples/jsm/geometries/TextGeometry.js' 3 | import { orthoScene } from './minigame-scene.js' 4 | import { loadFont } from '../helpers/font-helper.js' 5 | 6 | const showDebugText = async text => { 7 | const font = await loadFont() 8 | const textGeo = new TextGeometry(text, { 9 | font, 10 | size: 5, 11 | height: 1, 12 | curveSegments: 10, 13 | bevelEnabled: false 14 | }) 15 | const material = new THREE.MeshBasicMaterial({ 16 | color: 0xffffff, 17 | transparent: true 18 | }) 19 | const mesh = new THREE.Mesh(textGeo, material) 20 | mesh.position.y = 4 21 | mesh.position.x = 4 22 | orthoScene.add(mesh) 23 | } 24 | 25 | const loadTempMiniGame2d = async gameName => { 26 | showDebugText('Mini Game - ' + gameName) 27 | } 28 | export { loadTempMiniGame2d } 29 | -------------------------------------------------------------------------------- /app/minigame/minigame-controls.js: -------------------------------------------------------------------------------- 1 | import { getKeyPressEmitter } from '../interaction/inputs.js' 2 | import { jumpToMapFromMiniGame } from '../field/field-actions.js' 3 | import { RETURN_DATA } from './minigame-module.js' 4 | 5 | const areMiniGameControlsActive = () => { 6 | return window.anim.activeScene === 'minigame' 7 | } 8 | 9 | const initMiniGameKeypressActions = () => { 10 | getKeyPressEmitter().on('o', firstPress => { 11 | if (areMiniGameControlsActive() && firstPress) { 12 | console.log('press o') 13 | } 14 | }) 15 | 16 | getKeyPressEmitter().on('x', firstPress => { 17 | if (areMiniGameControlsActive() && firstPress) { 18 | console.log('press x') 19 | // Temp 20 | console.log('return', RETURN_DATA) 21 | jumpToMapFromMiniGame( 22 | RETURN_DATA.map, 23 | RETURN_DATA.x, 24 | RETURN_DATA.y, 25 | RETURN_DATA.z 26 | ) 27 | } 28 | }) 29 | } 30 | export { initMiniGameKeypressActions } 31 | -------------------------------------------------------------------------------- /app/field/field-ortho-scene.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../assets/threejs-r148/build/three.module.js' // 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r118/three.module.min.js' 2 | 3 | let scene 4 | let camera 5 | 6 | const setupOrthoCamera = async () => { 7 | scene = new THREE.Scene() 8 | // scene.background = new THREE.Color(0x000000) 9 | // const font = await loadFont() 10 | 11 | camera = new THREE.OrthographicCamera( 12 | 0, 13 | window.config.sizing.width, 14 | window.config.sizing.height, 15 | 0, 16 | 0, 17 | 1001 18 | ) 19 | camera.position.z = 1001 20 | 21 | // const textGeo = new TextGeometry('ORTHO TEST', { 22 | // font: font, 23 | // size: 5, 24 | // height: 1, 25 | // curveSegments: 10, 26 | // bevelEnabled: false 27 | // }) 28 | // const material = new THREE.MeshBasicMaterial({ color: 0xFFFFFF, transparent: true }) 29 | // const text = new THREE.Mesh(textGeo, material) 30 | // text.position.y = 4 31 | // scene.add(text) 32 | 33 | // console.log('setupOrthoCamera: END') 34 | } 35 | 36 | export { setupOrthoCamera, scene, camera } 37 | -------------------------------------------------------------------------------- /app/world/world-controls.js: -------------------------------------------------------------------------------- 1 | import { getKeyPressEmitter } from '../interaction/inputs.js' 2 | 3 | import { 4 | navigateUp, 5 | navigateDown, 6 | navigateLeft, 7 | navigateRight, 8 | navigateSelect 9 | } from './world-destination-selector.js' 10 | 11 | const areWorldControlsActive = () => { 12 | return window.anim.activeScene === 'world' 13 | } 14 | 15 | const initWorldKeypressActions = () => { 16 | getKeyPressEmitter().on('up', () => { 17 | if (areWorldControlsActive()) { 18 | navigateUp() 19 | } 20 | }) 21 | getKeyPressEmitter().on('down', () => { 22 | if (areWorldControlsActive()) { 23 | navigateDown() 24 | } 25 | }) 26 | getKeyPressEmitter().on('left', () => { 27 | if (areWorldControlsActive()) { 28 | navigateLeft() 29 | } 30 | }) 31 | getKeyPressEmitter().on('right', () => { 32 | if (areWorldControlsActive()) { 33 | navigateRight() 34 | } 35 | }) 36 | 37 | getKeyPressEmitter().on('o', firstPress => { 38 | if (areWorldControlsActive() && firstPress) { 39 | navigateSelect() 40 | } 41 | }) 42 | } 43 | export { initWorldKeypressActions } 44 | -------------------------------------------------------------------------------- /app/minigame/minigame-module.js: -------------------------------------------------------------------------------- 1 | import { 2 | setupScenes, 3 | startMiniGameRenderingLoop, 4 | scene, 5 | orthoScene 6 | } from './minigame-scene.js' 7 | import { initMiniGameKeypressActions } from './minigame-controls.js' 8 | import { loadTempMiniGame2d } from './minigame-2d.js' 9 | import { loadTempMiniGame3d } from './minigame-3d.js' 10 | 11 | const GAME_TYPES = [ 12 | 'Bike', 13 | 'ChocoboRace', 14 | 'SnowboardIcicleVersion', 15 | 'FortConder', 16 | 'Submarine', 17 | 'SpeedSquare', 18 | 'SnowboardGoldSaucerVersion' 19 | ] 20 | let RETURN_DATA 21 | 22 | const initMiniGameModule = () => { 23 | setupScenes() 24 | initMiniGameKeypressActions() 25 | } 26 | 27 | const cleanScene = () => { 28 | while (scene.children.length) { 29 | scene.remove(scene.children[0]) 30 | } 31 | while (orthoScene.children.length) { 32 | orthoScene.remove(orthoScene.children[0]) 33 | } 34 | } 35 | 36 | const loadMiniGame = async (gameId, options, returnInstructions) => { 37 | console.log('loadMiniGame', gameId, options, returnInstructions) 38 | RETURN_DATA = returnInstructions 39 | cleanScene() 40 | startMiniGameRenderingLoop() 41 | // Temp 42 | await loadTempMiniGame2d(GAME_TYPES[gameId]) 43 | loadTempMiniGame3d() 44 | } 45 | 46 | export { initMiniGameModule, loadMiniGame, RETURN_DATA } 47 | -------------------------------------------------------------------------------- /workings-out/field-dialog-examples.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const FIELD_FOLDER = path.join( 5 | __dirname, 6 | '..', 7 | '..', 8 | 'kujata-data', 9 | 'data', 10 | 'field', 11 | 'flevel.lgp' 12 | ) 13 | 14 | // field.data.script.dialogStrings.filter(s => s.includes('')) 15 | 16 | const MATCH_STR = 'FLASH' 17 | const init = () => { 18 | const fields = fs.readdirSync(FIELD_FOLDER) //.filter(f => f === 'bugin1c.json') 19 | // console.log('fields', fields) 20 | for (const fieldFile of fields) { 21 | try { 22 | const field = JSON.parse( 23 | fs.readFileSync(path.join(FIELD_FOLDER, fieldFile)) 24 | ) 25 | 26 | if (field && field.script && field.script.dialogStrings) { 27 | const matches = field.script.dialogStrings 28 | .map((s, index) => (s.includes(MATCH_STR) ? index : -1)) 29 | .filter(index => index !== -1) 30 | for (const match of matches) { 31 | console.log( 32 | fieldFile.replace('.json', ''), 33 | '-', 34 | MATCH_STR, 35 | '-', 36 | `Text: ${match}`, 37 | '-', 38 | field.script.dialogStrings[match] 39 | ) 40 | } 41 | } 42 | } catch (error) {} 43 | } 44 | } 45 | 46 | init() 47 | -------------------------------------------------------------------------------- /workings-out/createFieldLayerMetaDataFromFiles.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const path = require('path') 3 | const _ = require('lodash') 4 | 5 | const FIELD_FOLDER = './fenrir-data/field/backgrounds' 6 | const OUT_FILE = path.join(FIELD_FOLDER, 'backgrounds-metadata.json') 7 | 8 | const init = async () => { 9 | console.log('createFieldLayerMetaDataFromFiles - START') 10 | const fieldNames = await fs.readdir(FIELD_FOLDER) 11 | let fieldDatas = {} 12 | for (let i = 0; i < fieldNames.length; i++) { 13 | const fieldName = fieldNames[i] 14 | const fileNames = await fs.readdir(path.join(FIELD_FOLDER, fieldName)) 15 | let layers = [] 16 | for (let j = 0; j < fileNames.length; j++) { 17 | const fileName = fileNames[j] 18 | const depthMatch = fileName.match('_([0-9]*).png') 19 | if (depthMatch && depthMatch.length > 1) { 20 | const depth = parseInt(depthMatch[1]) 21 | // console.log('File', fieldName, fileName, depth) 22 | layers.push({ file: fileName, depth }) 23 | } 24 | } 25 | // console.log('fieldData', fieldData) 26 | fieldDatas[fieldName] = layers 27 | } 28 | console.log(' - fieldDatas.length', fieldDatas.length) 29 | await fs.writeJson(OUT_FILE, fieldDatas, { spaces: '\t' }) 30 | console.log('createFieldLayerMetaDataFromFiles - END') 31 | } 32 | init() 33 | -------------------------------------------------------------------------------- /app/helpers/gametime.js: -------------------------------------------------------------------------------- 1 | import { decrementCountdownClockAndUpdateDisplay } from '../field/field-dialog.js' 2 | import { incrementGameTime } from '../data/savemap-alias.js' 3 | import { updateHomeMenuTime } from '../menu/menu-main-home.js' 4 | import { activateRandomBlinkForFieldCharacters } from '../field/field-model-graphics-operations.js' 5 | let deltaTotal = 0 6 | let deltaSecond = 0 7 | const executeOnceASecond = () => { 8 | decrementCountdownClockAndUpdateDisplay() 9 | incrementGameTime() 10 | updateHomeMenuTime() 11 | if (window.anim.activeScene === 'field') { 12 | activateRandomBlinkForFieldCharacters() 13 | } 14 | } 15 | const updateOnceASecond = () => { 16 | const delta = window.anim.gametimeClock.getDelta() 17 | deltaTotal += delta 18 | const deltaSecondTemp = parseInt(deltaTotal) 19 | // console.log('updateOnceASecond', delta, deltaTotal, deltaSecond, deltaSecondTemp) 20 | if (deltaSecond !== deltaSecondTemp) { 21 | deltaSecond = deltaSecondTemp 22 | // console.log('updateOnceASecond SECOND', deltaSecond) 23 | if (window.data.savemap) { 24 | executeOnceASecond() 25 | } 26 | } 27 | if (deltaSecond > 10000000) { 28 | deltaTotal = 0 29 | deltaSecond = 0 30 | // console.log('updateOnceASecond RESET TO ZERO', delta, deltaTotal, deltaSecond) 31 | } 32 | } 33 | export { updateOnceASecond } 34 | -------------------------------------------------------------------------------- /app/data/media-fetch-data.js: -------------------------------------------------------------------------------- 1 | import { KUJATA_BASE } from '../data/kernel-fetch-data.js' 2 | 3 | const getSoundMetadata = async () => { 4 | const soundMetaRes = await fetch( 5 | `${KUJATA_BASE}/media/sounds/sounds-metadata.json` 6 | ) 7 | const soundMeta = await soundMetaRes.json() 8 | return soundMeta 9 | } 10 | const getMusicMetadata = async () => { 11 | const musicMetaRes = await fetch( 12 | `${KUJATA_BASE}/media/music/music-metadata.json` 13 | ) 14 | const musicMeta = await musicMetaRes.json() 15 | return musicMeta 16 | } 17 | const getMovieMetadata = async () => { 18 | const movieMetaRes = await fetch( 19 | `${KUJATA_BASE}/media/movies/movies-metadata.json` 20 | ) 21 | const movieMeta = await movieMetaRes.json() 22 | return movieMeta 23 | } 24 | const getMoviecamMetadata = async () => { 25 | const moviecamMetaRes = await fetch( 26 | `${KUJATA_BASE}/media/movies/moviecam-metadata.json` 27 | ) 28 | const moviecamMeta = await moviecamMetaRes.json() 29 | return moviecamMeta 30 | } 31 | const getMoviecamData = async name => { 32 | const moviecamRes = await fetch( 33 | `${KUJATA_BASE}/media/movies/${name}.cam.json` 34 | ) 35 | const moviecam = await moviecamRes.json() 36 | return moviecam 37 | } 38 | 39 | export { 40 | getSoundMetadata, 41 | getMusicMetadata, 42 | getMovieMetadata, 43 | getMoviecamMetadata, 44 | getMoviecamData 45 | } 46 | -------------------------------------------------------------------------------- /workings-out/byte-data-pattern-finding.js: -------------------------------------------------------------------------------- 1 | const exampleMethodToBeUsed = () => { 2 | let datas = [] 3 | r.offset = 0 4 | // r.readByte() 5 | while (r.offset < 1206) { 6 | const origOffset = r.offset 7 | 8 | r.offset = origOffset 9 | const line = r.offset 10 | 11 | r.offset = origOffset 12 | const byte = r.readUByte() 13 | r.offset = origOffset 14 | const short = r.readShort() 15 | r.offset = origOffset 16 | const int = r.readInt() 17 | r.offset = origOffset + 1 18 | datas.push({ 19 | line, byte, short, int 20 | }) 21 | // console.log(`'--|${r.readString(120)}|--`) 22 | // console.log(r.offset, '-', r.readInt()) 23 | // r.offset += 120 24 | } 25 | // console.log('datas', datas) 26 | let md = `# ${name}\nLine|Byte|Short1|Short2|Int1|Int2|Int3|Int4\n---|---|---|---|---|---|---|---\n` 27 | for (let i = 0; i < datas.length; i++) { 28 | const data = datas[i] 29 | md += `${data.line}|${data.byte}|${i % 2 ? '' : data.short}|${(i - 1) % 2 ? '' : data.short}|${(i - 0) % 4 ? '' : data.int}|${(i - 1) % 4 ? '' : data.int}|${(i - 2) % 4 ? '' : data.int}|${(i - 3) % 4 ? '' : data.int}\n` 30 | } 31 | // console.log('md', md) 32 | const mdPath = path.join(__dirname, '..', '..', 'ff7-fenrir', 'workings-out', 'output', 'workingout.md') 33 | console.log('mdPath', mdPath) 34 | await fs.writeFile(mdPath, md) 35 | } -------------------------------------------------------------------------------- /workings-out/gltf-combined.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | 3 | const init = () => { 4 | const modelGLTF = fs.readJsonSync( 5 | 'D:/code/ff7/ff7-fenrir/kujata-data/data/field/char.lgp/aaaa.hrc.gltf' 6 | ) 7 | const animGLTF = fs.readJsonSync( 8 | 'D:/code/ff7/ff7-fenrir/kujata-data/data/field/char.lgp/efjd.a.gltf' 9 | ) 10 | var gltf1 = JSON.parse(JSON.stringify(modelGLTF)) // clone 11 | var gltf2 = JSON.parse(JSON.stringify(animGLTF)) // clone 12 | var numModelBuffers = gltf1.buffers.length 13 | var numModelBufferViews = gltf1.bufferViews.length 14 | var numModelAccessors = gltf1.accessors.length 15 | if (!gltf1.animations) { 16 | gltf1.animations = [] 17 | } 18 | for (let buffer of gltf2.buffers) { 19 | gltf1.buffers.push(buffer) 20 | } 21 | for (let bufferView of gltf2.bufferViews) { 22 | bufferView.buffer += numModelBuffers 23 | gltf1.bufferViews.push(bufferView) 24 | } 25 | for (let accessor of gltf2.accessors) { 26 | accessor.bufferView += numModelBufferViews 27 | gltf1.accessors.push(accessor) 28 | } 29 | for (let animation of gltf2.animations) { 30 | for (let sampler of animation.samplers) { 31 | sampler.input += numModelAccessors 32 | sampler.output += numModelAccessors 33 | } 34 | gltf1.animations.push(animation) 35 | } 36 | console.log('combinedGLTF:', gltf1) 37 | fs.writeJsonSync( 38 | 'D:/code/ff7/ff7-fenrir/kujata-data/data/field/char.lgp/test.gltf', 39 | gltf1 40 | ) 41 | } 42 | init() 43 | -------------------------------------------------------------------------------- /app/field/field-ortho-bg-scene.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../assets/threejs-r148/build/three.module.js' 2 | 3 | let scene 4 | let camera 5 | 6 | const createVideoBackground = video => { 7 | const geometry = new THREE.PlaneGeometry( 8 | window.config.sizing.width, 9 | window.config.sizing.height 10 | ) 11 | const texture = new THREE.VideoTexture(video) 12 | texture.minFilter = THREE.LinearFilter 13 | texture.magFilter = THREE.LinearFilter 14 | texture.format = THREE.RGBAFormat 15 | texture.encoding = THREE.sRGBEncoding 16 | const material = new THREE.MeshBasicMaterial({ 17 | map: texture, 18 | transparent: true 19 | }) 20 | // const material = new THREE.MeshBasicMaterial({ color: 0xFFF00F, transparent: true }) 21 | const videoBG = new THREE.Mesh(geometry, material) 22 | videoBG.position.x = window.config.sizing.width / 2 23 | videoBG.position.y = window.config.sizing.height / 2 24 | window.currentField.backgroundVideo.add(videoBG) 25 | } 26 | 27 | const setupOrthoBgCamera = async () => { 28 | scene = new THREE.Scene() 29 | scene.background = new THREE.Color(0x000000) 30 | 31 | camera = new THREE.OrthographicCamera( 32 | 0, 33 | window.config.sizing.width, 34 | window.config.sizing.height, 35 | 0, 36 | 0, 37 | 10000 38 | ) 39 | camera.position.z = 1 40 | window.currentField.backgroundVideo = new THREE.Group() 41 | scene.add(window.currentField.backgroundVideo) 42 | } 43 | 44 | export { setupOrthoBgCamera, scene, camera, createVideoBackground } 45 | -------------------------------------------------------------------------------- /app/data/savemap-config.js: -------------------------------------------------------------------------------- 1 | import { setCurrentGameTime } from './savemap-alias.js' 2 | 3 | const getConfigFieldMessageSpeed = () => { 4 | // 0-255 fast-slow 5 | return window.data.savemap.config.fieldMessageSpeed 6 | } 7 | const setConfigFieldMessageSpeed = speed => { 8 | window.data.savemap.config.fieldMessageSpeed = speed 9 | console.log('setConfigFieldMessageSpeed', getConfigFieldMessageSpeed()) 10 | } 11 | const getConfigWindowColours = () => { 12 | return [ 13 | // 'rgb(0,88,176)' 14 | `rgb(${window.data.savemap.config.windowColorTL})`, 15 | `rgb(${window.data.savemap.config.windowColorTR})`, 16 | `rgb(${window.data.savemap.config.windowColorBL})`, 17 | `rgb(${window.data.savemap.config.windowColorBR})` 18 | ] 19 | } 20 | const debugResetGame = () => { 21 | // This needs testing to confirm. Resets game time to 0, unlocks PHS and Save menu, resets party to Cloud | (empty) | (empty). 22 | setCurrentGameTime(0, 0, 0) 23 | const charNames = Object.keys(window.data.savemap.party.phsVisibility) 24 | for (let i = 0; i < charNames.length; i++) { 25 | const charName = charNames[i] 26 | window.data.savemap.party.phsLocked[charName] = 1 27 | window.data.savemap.party.phsVisibility[charName] = 1 28 | } 29 | window.data.savemap.party.members = ['Cloud', 'None', 'None'] 30 | console.log('debugResetGame - window.data.savemap', window.data.savemap) 31 | } 32 | export { 33 | getConfigFieldMessageSpeed, 34 | setConfigFieldMessageSpeed, 35 | getConfigWindowColours, 36 | debugResetGame 37 | } 38 | -------------------------------------------------------------------------------- /app/data/savemap-controls.js: -------------------------------------------------------------------------------- 1 | import { saveSaveMap, loadGame, downloadSaveMaps } from './savemap.js' 2 | 3 | const initSavemapQuicksaveKeypressActions = () => { 4 | document.addEventListener( 5 | 'keydown', 6 | e => { 7 | console.log('keydown', e.key, e) 8 | if (e.code === 'Digit1' && !e.shiftKey && !e.ctrlKey) { 9 | saveSaveMap(1, 1) 10 | } 11 | if (e.code === 'Digit2' && !e.shiftKey && !e.ctrlKey) { 12 | saveSaveMap(1, 2) 13 | } 14 | if (e.code === 'Digit3' && !e.shiftKey && !e.ctrlKey) { 15 | saveSaveMap(1, 3) 16 | } 17 | if (e.code === 'Digit4' && !e.shiftKey && !e.ctrlKey) { 18 | saveSaveMap(1, 4) 19 | } 20 | if (e.code === 'Digit5' && !e.shiftKey && !e.ctrlKey) { 21 | saveSaveMap(1, 5) 22 | } 23 | 24 | if (e.code === 'Digit1' && e.shiftKey && !e.ctrlKey) { 25 | loadGame(1, 1) 26 | } 27 | if (e.code === 'Digit2' && e.shiftKey && !e.ctrlKey) { 28 | loadGame(1, 2) 29 | } 30 | if (e.code === 'Digit3' && e.shiftKey && !e.ctrlKey) { 31 | loadGame(1, 3) 32 | } 33 | if (e.code === 'Digit4' && e.shiftKey && !e.ctrlKey) { 34 | loadGame(1, 4) 35 | } 36 | if (e.code === 'Digit5' && e.shiftKey && !e.ctrlKey) { 37 | loadGame(1, 5) 38 | } 39 | 40 | if (e.code === 'Digit1' && e.shiftKey && e.ctrlKey) { 41 | downloadSaveMaps() 42 | } 43 | }, 44 | false 45 | ) 46 | } 47 | export { initSavemapQuicksaveKeypressActions } 48 | -------------------------------------------------------------------------------- /app/helpers/media-can-play.js: -------------------------------------------------------------------------------- 1 | import { getTestSoundUrl } from '../media/media-sound.js' 2 | import { showClickScreenForMediaText, hideClickScreenForMediaText } from '../loading/loading-module.js' 3 | 4 | const Howler = window.libraries.howler.Howler 5 | const Howl = window.libraries.howler.Howl 6 | 7 | const waitUntilMediaCanPlay = async () => { 8 | return new Promise(async resolve => { 9 | console.log('waitUntilMediaCanPlay: START') 10 | console.log( 11 | 'waitUntilMediaCanPlay howler', 12 | Howler.usingWebAudio, 13 | Howler.noAudio 14 | ) 15 | const newAudioCtx = new window.AudioContext() 16 | console.log('newAudioCtx', newAudioCtx) 17 | if (newAudioCtx.state !== 'running') { 18 | console.log('waitUntilMediaCanPlay - Please click on the screen to enable audio and video') 19 | showClickScreenForMediaText() 20 | } 21 | const sound = new Howl({ 22 | src: [getTestSoundUrl()], 23 | volume: 0.1, 24 | onplayerror: function () { 25 | console.log('waitUntilMediaCanPlay onplayerror') 26 | sound.once('unlock', function () { 27 | console.log('waitUntilMediaCanPlay onplayerror unlock') 28 | sound.play() 29 | }) 30 | }, 31 | onplay: function () { 32 | hideClickScreenForMediaText() 33 | console.log('waitUntilMediaCanPlay onplay') 34 | console.log('waitUntilMediaCanPlay: END') 35 | resolve() 36 | } 37 | }) 38 | sound.play() 39 | }) 40 | } 41 | 42 | export { waitUntilMediaCanPlay } 43 | -------------------------------------------------------------------------------- /workings-out/getModelScaleDownValue.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const path = require('path') 3 | const _ = require('lodash') 4 | 5 | const FIELD_FOLDER = './kujata-data/data/field/flevel.lgp' 6 | const OUT_FILE = './workings-out/output/getModelScaleDownValue.json' 7 | 8 | const init = async () => { 9 | console.log('getModelScaleDownValue - START') 10 | const fields = await fs.readdir(FIELD_FOLDER) 11 | let scaleDatas = [] 12 | for (let i = 0; i < fields.length; i++) { 13 | const field = fields[i] 14 | const fieldJson = await fs.readJson(path.join(FIELD_FOLDER, field)) 15 | if ( 16 | fieldJson && 17 | fieldJson.model && 18 | fieldJson.model.header && 19 | fieldJson.model.header.modelScale 20 | ) { 21 | console.log( 22 | `${i + 1} of ${fields.length}`, 23 | 'scale', 24 | field, 25 | fieldJson.model.header.modelScale 26 | ) 27 | scaleDatas.push({ 28 | field: field, 29 | scale: fieldJson.model.header.modelScale 30 | }) 31 | } 32 | } 33 | let grouped = _.chain(scaleDatas) 34 | .groupBy('scale') 35 | .map((k, v) => ({ scale: v, count: k.length, fields: k.map(n => n.field) })) 36 | .value() 37 | // console.log('grouped', grouped) 38 | // await fs.writeJson(OUT_FILE, grouped, { spaces: '\t'}) 39 | let formattedOut = JSON.stringify(grouped) 40 | .replace(/\{/g, '\n\t{ ') 41 | .replace(/\,/g, ', ') 42 | .replace(/\:/g, ': ') 43 | await fs.writeFile(OUT_FILE, formattedOut) 44 | console.log('getModelScaleDownValue - END') 45 | } 46 | init() 47 | -------------------------------------------------------------------------------- /assets/threejs-r148/examples/jsm/geometries/TextGeometry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Text = 3D Text 3 | * 4 | * parameters = { 5 | * font: , // font 6 | * 7 | * size: , // size of the text 8 | * height: , // thickness to extrude text 9 | * curveSegments: , // number of points on the curves 10 | * 11 | * bevelEnabled: , // turn on bevel 12 | * bevelThickness: , // how deep into text bevel goes 13 | * bevelSize: , // how far from text outline (including bevelOffset) is bevel 14 | * bevelOffset: // how far from text outline does bevel start 15 | * } 16 | */ 17 | 18 | import { 19 | ExtrudeGeometry 20 | } from 'three'; 21 | 22 | class TextGeometry extends ExtrudeGeometry { 23 | 24 | constructor( text, parameters = {} ) { 25 | 26 | const font = parameters.font; 27 | 28 | if ( font === undefined ) { 29 | 30 | super(); // generate default extrude geometry 31 | 32 | } else { 33 | 34 | const shapes = font.generateShapes( text, parameters.size ); 35 | 36 | // translate parameters to ExtrudeGeometry API 37 | 38 | parameters.depth = parameters.height !== undefined ? parameters.height : 50; 39 | 40 | // defaults 41 | 42 | if ( parameters.bevelThickness === undefined ) parameters.bevelThickness = 10; 43 | if ( parameters.bevelSize === undefined ) parameters.bevelSize = 8; 44 | if ( parameters.bevelEnabled === undefined ) parameters.bevelEnabled = false; 45 | 46 | super( shapes, parameters ); 47 | 48 | } 49 | 50 | this.type = 'TextGeometry'; 51 | 52 | } 53 | 54 | } 55 | 56 | 57 | export { TextGeometry }; 58 | -------------------------------------------------------------------------------- /assets/threejs-r135-dg/examples/jsm/geometries/TextGeometry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Text = 3D Text 3 | * 4 | * parameters = { 5 | * font: , // font 6 | * 7 | * size: , // size of the text 8 | * height: , // thickness to extrude text 9 | * curveSegments: , // number of points on the curves 10 | * 11 | * bevelEnabled: , // turn on bevel 12 | * bevelThickness: , // how deep into text bevel goes 13 | * bevelSize: , // how far from text outline (including bevelOffset) is bevel 14 | * bevelOffset: // how far from text outline does bevel start 15 | * } 16 | */ 17 | 18 | import { 19 | BufferGeometry, 20 | ExtrudeGeometry 21 | } from '../../../build/three.module.js'; 22 | 23 | class TextGeometry extends ExtrudeGeometry { 24 | 25 | constructor( text, parameters = {} ) { 26 | 27 | const font = parameters.font; 28 | 29 | if ( ! ( font && font.isFont ) ) { 30 | 31 | console.error( 'THREE.TextGeometry: font parameter is not an instance of THREE.Font.' ); 32 | return new BufferGeometry(); 33 | 34 | } 35 | 36 | const shapes = font.generateShapes( text, parameters.size ); 37 | 38 | // translate parameters to ExtrudeGeometry API 39 | 40 | parameters.depth = parameters.height !== undefined ? parameters.height : 50; 41 | 42 | // defaults 43 | 44 | if ( parameters.bevelThickness === undefined ) parameters.bevelThickness = 10; 45 | if ( parameters.bevelSize === undefined ) parameters.bevelSize = 8; 46 | if ( parameters.bevelEnabled === undefined ) parameters.bevelEnabled = false; 47 | 48 | super( shapes, parameters ); 49 | 50 | this.type = 'TextGeometry'; 51 | 52 | } 53 | 54 | } 55 | 56 | 57 | export { TextGeometry }; 58 | -------------------------------------------------------------------------------- /app/battle/battle-damage-stats.js: -------------------------------------------------------------------------------- 1 | // TODO - Implement all of these 2 | /* 3 | There are seven Primary Stats and seven Derived Stats that make up your 4 | basic character. The Primary Stats are: 5 | Str: Strength Dex: Dexterity 6 | Vit: Vitality Mag: Magic 7 | Spr: Spirit Lck: Luck 8 | Lvl: Level 9 | 10 | The Derived Stats are: 11 | Att: Attack At%: Attack% 12 | Def: Defense Df%: Defense% 13 | MAt: Magic atk MDf: Magic def 14 | MD%: Magic def% 15 | 16 | 17 | The Primary Stats dictate the overall strengths of your character. Level 18 | dictates exactly how powerful the character is, while the last six stats 19 | round off the character. Each character has a starting value for their 20 | Primary Stats, and every level, there is a chance that these stats will 21 | be raised by a random number of points. In addition, it's possible to 22 | further raise these stats permenantly using Sources. 23 | 24 | 25 | The Derived Stats are based from your Primary Stats and your currently worn 26 | equipment. They are derived as such: 27 | Att = Str + Weapon Attack Bonus 28 | At% = Weapon Attack% Bonus 29 | Def = Vit + Armour Defense Bonus 30 | Df% = [Dex / 4] + Armour Defense% Bonus 31 | MAt = Mag 32 | MDf = Spr + Armour MDefense Bonus 33 | MD% = Armour MDefense% Bonus 34 | */ 35 | 36 | // Base Stats 37 | const str = player => { 38 | return player.data.stats.strength + player.data.stats.strengthBonus 39 | } 40 | 41 | const getPlayerAttackPower = player => { 42 | if (player.data == null) return 0 // Check for stack operations 43 | return 1 44 | } 45 | const getPlayerMagicAttackPower = player => { 46 | if (player.data == null) return 0 // Check for stack operations 47 | return 1 48 | } 49 | export { getPlayerAttackPower, getPlayerMagicAttackPower } 50 | -------------------------------------------------------------------------------- /workings-out/op-codes-completed.json: -------------------------------------------------------------------------------- 1 | ["RET","REQ","REQSW","REQEW","PREQ","PRQSW","PRQEW","RETTO","JMPF","JMPFL","JMPB","JMPBL","IFUB","IFUBL","IFSW","IFSWL","IFUW","IFUWL","WAIT","IFKEY","IFKEYON","IFKEYOFF","NOP","IFPRTYQ","IFMEMBQ","DSKCG","SPECIAL","MINIGAME","BTMD2","BTRLD","BTLTB","MAPJUMP","LSTMP","BATTLE","BTLON","BTLMD","MPJPO","GAMEOVER","PLUS!","PLUS2!","MINUS!","MINUS2!","INC!","INC2!","DEC!","DEC2!","RDMSD","SETBYTE","SETWORD","BITON","BITOFF","BITXOR","MUL","MUL2","DIV","DIV2","MOD","MOD2","AND","AND2","OR","OR2","XOR","XOR2","UNUSED","PLUS","PLUS2","MINUS","MINUS2","INC","INC2","DEC","DEC2","RANDOM","LBYTE","HBYTE","2BYTE","SIN","COS","TUTOR","WCLS","WSIZW","WSPCL","WNUMB","STTIM","MESSAGE","MPARA","MPRA2","MPNAM","ASK","MENU","MENU2","WINDOW","WMOVE","WMODE","WREST","WCLSE","WROW","GWCOL","SWCOL","SPTYE","GTPYE","GOLDU","GOLDD","CHGLD","HMPMAX1","HMPMAX2","MHMMX","HMPMAX3","MPUP","MPDWN","HPUP","HPDWN","STITM","DLITM","CKITM","SMTRA","DMTRA","CMTRA","GETPC","PRTYP","PRTYM","PRTYE","MMBud","MMBLK","MMBUK","JOIN","SPLIT","BLINK","KAWAI","KAWIW","PMOVA","SLIP","UC","PDIRA","PTURA","IDLCK","PGTDR","PXYZI","TLKON","PC","CHAR","DFANM","ANIME1","VISI","XYZI","XYI","XYZ","MOVE","CMOVE","MOVA","TURA","ANIMW","FMOVE","ANIME2","ANIM!1","CANIM1","CANM!1","MSPED","DIR","TURNGEN","TURN","DIRA","GETDIR","GETAXY","GETAI","ANIM!2","CANIM2","CANM!2","ASPED","CC","JUMP","AXYZI","LADER","OFST","OFSTW","TALKR","SLIDR","SOLID","LINE","LINON","SLINE","TLKR2","SLDR2","FCFIX","CCANM","ANIMB","TURNW","BGPDH","BGSCR","BGON","BGOFF","BGROL","BGROL2","BGCLR","STPLS","STPAL","LDPLS","LDPAL","ADPAL","MPPAL2","CPPAL","ADPAL2","MPPAL","NFADE","SHAKE","SCRLO","SCRLC","SCRLA","SCR2D","SCRCC","SCR2DC","SCRLW","SCR2DL","FADE","FADEW","SCRLP","AKAO2","MUSIC","SOUND","AKAO","MUSVT","MUSVM","MULCK","BMUSC","CHMPH","PMVIE","MOVIE","MVIEF","MVCAM","FMUSC","CMUSC","CHMST","BGMOVIE","SETX","GETX","SEARCHX","PMJMP","PMJMP2"] 2 | -------------------------------------------------------------------------------- /workings-out/op-codes-battle-camera-completed.json: -------------------------------------------------------------------------------- 1 | ["RET","REQ","REQSW","REQEW","PREQ","PRQSW","PRQEW","RETTO","JMPF","JMPFL","JMPB","JMPBL","IFUB","IFUBL","IFSW","IFSWL","IFUW","IFUWL","WAIT","IFKEY","IFKEYON","IFKEYOFF","NOP","IFPRTYQ","IFMEMBQ","DSKCG","SPECIAL","MINIGAME","BTMD2","BTRLD","BTLTB","MAPJUMP","LSTMP","BATTLE","BTLON","BTLMD","MPJPO","GAMEOVER","PLUS!","PLUS2!","MINUS!","MINUS2!","INC!","INC2!","DEC!","DEC2!","RDMSD","SETBYTE","SETWORD","BITON","BITOFF","BITXOR","MUL","MUL2","DIV","DIV2","MOD","MOD2","AND","AND2","OR","OR2","XOR","XOR2","UNUSED","PLUS","PLUS2","MINUS","MINUS2","INC","INC2","DEC","DEC2","RANDOM","LBYTE","HBYTE","2BYTE","SIN","COS","TUTOR","WCLS","WSIZW","WSPCL","WNUMB","STTIM","MESSAGE","MPARA","MPRA2","MPNAM","ASK","MENU","MENU2","WINDOW","WMOVE","WMODE","WREST","WCLSE","WROW","GWCOL","SWCOL","SPTYE","GTPYE","GOLDU","GOLDD","CHGLD","HMPMAX1","HMPMAX2","MHMMX","HMPMAX3","MPUP","MPDWN","HPUP","HPDWN","STITM","DLITM","CKITM","SMTRA","DMTRA","CMTRA","GETPC","PRTYP","PRTYM","PRTYE","MMBud","MMBLK","MMBUK","JOIN","SPLIT","BLINK","KAWAI","KAWIW","PMOVA","SLIP","UC","PDIRA","PTURA","IDLCK","PGTDR","PXYZI","TLKON","PC","CHAR","DFANM","ANIME1","VISI","XYZI","XYI","XYZ","MOVE","CMOVE","MOVA","TURA","ANIMW","FMOVE","ANIME2","ANIM!1","CANIM1","CANM!1","MSPED","DIR","TURNGEN","TURN","DIRA","GETDIR","GETAXY","GETAI","ANIM!2","CANIM2","CANM!2","ASPED","CC","JUMP","AXYZI","LADER","OFST","OFSTW","TALKR","SLIDR","SOLID","LINE","LINON","SLINE","TLKR2","SLDR2","FCFIX","CCANM","ANIMB","TURNW","BGPDH","BGSCR","BGON","BGOFF","BGROL","BGROL2","BGCLR","STPLS","STPAL","LDPLS","LDPAL","ADPAL","MPPAL2","CPPAL","ADPAL2","MPPAL","NFADE","SHAKE","SCRLO","SCRLC","SCRLA","SCR2D","SCRCC","SCR2DC","SCRLW","SCR2DL","FADE","FADEW","SCRLP","AKAO2","MUSIC","SOUND","AKAO","MUSVT","MUSVM","MULCK","BMUSC","CHMPH","PMVIE","MOVIE","MVIEF","MVCAM","FMUSC","CMUSC","CHMST","BGMOVIE","SETX","GETX","SEARCHX","PMJMP","PMJMP2"] 2 | -------------------------------------------------------------------------------- /app/render/renderer.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../assets/threejs-r148/build/three.module.js' // 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r118/three.module.min.js'; 2 | import Stats from '../../assets/threejs-r148/examples/jsm/libs/stats.module.js' // 'https://raw.githack.com/mrdoob/three.js/dev/examples/jsm/libs/stats.module.js'; 3 | 4 | // let currentField = window.currentField // Handle this better in the future 5 | // let anim = window.anim 6 | // let config = window.config 7 | const showStats = () => { 8 | window.anim.stats = new Stats() 9 | window.anim.stats.dom.style.cssText = 10 | 'position:fixed;top:0;right:270px;cursor:pointer;opacity:0.9;z-index:10000' 11 | document.querySelector('.stats').appendChild(window.anim.stats.dom) 12 | } 13 | 14 | const initRenderer = () => { 15 | THREE.Cache.enabled = true 16 | window.anim.gametimeClock = new THREE.Clock() 17 | window.anim.clock = new THREE.Clock() 18 | // console.log('cache', THREE.Cache.enabled) 19 | window.anim.renderer = new THREE.WebGLRenderer({ 20 | alpha: true, 21 | antialias: true 22 | }) 23 | window.anim.renderer.setSize( 24 | window.config.sizing.width * window.config.sizing.factor, 25 | window.config.sizing.height * window.config.sizing.factor 26 | ) 27 | window.anim.renderer.autoClear = false 28 | window.anim.renderer.localClippingEnabled = true 29 | THREE.ColorManagement.legacyMode = false 30 | window.anim.renderer.outputEncoding = THREE.sRGBEncoding 31 | // window.anim.renderer.setPixelRatio(config.sizing.width / config.sizing.height) // Set pixel ratio helps with antialias, but messing the background alignment up 32 | // console.log('pixelRatio', window.anim.renderer.getPixelRatio()) 33 | window.anim.container.appendChild(window.anim.renderer.domElement) 34 | window.anim.renderer.domElement.classList.add('fenrir') 35 | } 36 | 37 | export { initRenderer, showStats } 38 | -------------------------------------------------------------------------------- /workings-out/savemap-wiki-add-offsets.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | // Define bank boundaries and bank names 5 | const bankRanges = [ 6 | { offset: 0x0ba4, bankName: '1' }, // 1/2 7 | { offset: 0x0ca4, bankName: '3' }, // 3/4 8 | { offset: 0x0da4, bankName: '11' }, // 11/12 9 | { offset: 0x0ea4, bankName: '13' }, // 13/14 10 | { offset: 0x0fa4, bankName: '7' } // 7/15 11 | ] 12 | 13 | // File paths 14 | const inputFile = path.join(__dirname, 'savemap.orig.md') 15 | const outputFile = path.join(__dirname, 'savemap.md') 16 | 17 | // Helper function to get offset and bank name for a hex value 18 | function getBankInfo (hexValue, bankIndex) { 19 | const bank = bankRanges[bankIndex] 20 | const offset = hexValue - bank.offset 21 | return `''B[${bank.bankName}][${offset}]''` 22 | } 23 | 24 | // Process content 25 | function processContent (lines) { 26 | let bankIndex = -1 // No bank selected initially 27 | return lines.map(line => { 28 | // Update bankIndex based on line content 29 | if (line.includes('== Save Memory Bank')) bankIndex++ 30 | else if (line.includes('== Character Reco')) bankIndex = -1 31 | 32 | // Replace hex values in the current line 33 | return line.replace(/\|\s(0x[0-9A-Fa-f]+)/g, (match, hexString) => { 34 | const hexValue = parseInt(hexString, 16) 35 | return bankIndex >= 0 36 | ? `| ${hexString}
${getBankInfo(hexValue, bankIndex)}` 37 | : `| ${hexString}` 38 | }) 39 | }) 40 | } 41 | 42 | // Read the file, process content, and write output 43 | try { 44 | const fileContent = fs.readFileSync(inputFile, 'utf8').split('\n') 45 | const modifiedContent = processContent(fileContent) 46 | fs.writeFileSync(outputFile, modifiedContent.join('\n')) 47 | console.log('File processed and saved as savemap.md') 48 | } catch (err) { 49 | console.error('Error processing file:', err) 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ff7-fenrir", 3 | "version": "1.0.0", 4 | "description": "> Web based game engine for FF7 - Work-in-progress", 5 | "main": "index.js", 6 | "scripts": { 7 | "unlgp-fields": "cd .\\workings-out\\unlgp\\unlgp-flevel.lgp & C:\\Users\\Carol\\Documents\\code\\ff7\\kujata\\lgp-0.5b\\bin\\unlgp.exe 'C:\\Program Files (x86)\\Steam\\steamapps\\common\\FINAL FANTASY VII\\data\\field\\flevel.lgp'", 8 | "unlgp-char": "cd .\\workings-out\\unlgp\\unlgp-char.lgp & C:\\Users\\Carol\\Documents\\code\\ff7\\kujata\\lgp-0.5b\\bin\\unlgp.exe 'C:\\Program Files (x86)\\Steam\\steamapps\\common\\FINAL FANTASY VII\\data\\field\\char.lgp'", 9 | "unlgp-movies": "cd .\\workings-out\\unlgp\\unlgp-moviecam.lgp & C:\\Users\\Carol\\Documents\\code\\ff7\\kujata\\lgp-0.5b\\bin\\unlgp.exe 'C:\\Program Files (x86)\\Steam\\steamapps\\common\\FINAL FANTASY VII\\data\\movies\\moviecam.lgp'", 10 | "unlgp-menu": "cd .\\workings-out\\unlgp\\unlgp-menu_us.lgp & C:\\Users\\Carol\\Documents\\code\\ff7\\kujata\\lgp-0.5b\\bin\\unlgp.exe 'C:\\Program Files (x86)\\Steam\\steamapps\\common\\FINAL FANTASY VII\\data\\menu\\menu_us.lgp'", 11 | "dev": "node ./static-server.js", 12 | "opcodes-progress": "node workings-out/createOpCodesProgressReadme.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/dangarfield/ff7-fenrir.git" 17 | }, 18 | "author": "", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/dangarfield/ff7-fenrir/issues" 22 | }, 23 | "homepage": "https://github.com/dangarfield/ff7-fenrir#readme", 24 | "nodemonConfig": { 25 | "ignore": [ 26 | "*.json" 27 | ] 28 | }, 29 | "devDependencies": { 30 | "fs-extra": "^9.1.0", 31 | "lodash": "^4.17.21", 32 | "mime": "^4.0.4", 33 | "node-mime-types": "^1.1.2", 34 | "prettier-standard": "^13.0.6", 35 | "standard": "^17.0.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /workings-out/walkmeshPositionHelper.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const path = require('path') 3 | 4 | const FIELDS_FOLDER = './kujata-data/data/field/flevel.lgp' 5 | const MAPLIST_FILE = './kujata-data/data/field/flevel.lgp/maplist.json' 6 | const OUTPUT_FILE = './workings-out/output/walkmesh-position-helper.json' 7 | 8 | let maplist 9 | 10 | const getFieldIdForName = name => { 11 | for (let i = 0; i < maplist.length; i++) { 12 | if (maplist[i] === name) { 13 | return i 14 | } 15 | } 16 | return -1 17 | } 18 | const init = async () => { 19 | console.log('Walkmesh position helper: START') 20 | maplist = await fs.readJSON(MAPLIST_FILE) 21 | const fields = await fs.readdir(FIELDS_FOLDER) 22 | let datas = [] 23 | // for (let i = 0; i < 4; i++) { 24 | for (let i = 0; i < fields.length; i++) { 25 | const field = fields[i] 26 | console.log('field', field, i + 0, 'of', fields.length) 27 | const fieldName = field.replace('.json', '') 28 | const f = await fs.readJson(path.join(FIELDS_FOLDER, field)) 29 | // console.log('f', f) 30 | if (f && f.script && f.script.entities) { 31 | const data = { 32 | field: fieldName, 33 | fieldId: getFieldIdForName(fieldName), 34 | // scriptHeaderUnknown: f.script.header.unknown, // Always 1282 35 | cameraZoom: f.cameraSection.cameras[0].zoom, 36 | cameraUnknown: f.cameraSection.cameras[0].unknown, 37 | cameraRangeLeft: f.triggers.header.cameraRange.left, 38 | cameraRangeBottom: f.triggers.header.cameraRange.bottom, 39 | cameraRangeRight: f.triggers.header.cameraRange.right, 40 | cameraRangeTop: f.triggers.header.cameraRange.top 41 | } 42 | datas.push(data) 43 | } 44 | } 45 | datas.sort((a, b) => a.fieldId - b.fieldId) 46 | await fs.writeJson(OUTPUT_FILE, datas) 47 | 48 | console.log('Walkmesh position helper: END') 49 | } 50 | init() 51 | -------------------------------------------------------------------------------- /app/field/field-battle.js: -------------------------------------------------------------------------------- 1 | import { 2 | incrementBattlesFought, 3 | incrementBattlesEscaped 4 | } from '../data/savemap-alias.js' 5 | 6 | let randomEncountersEnabled = true 7 | let battleLockEnabled = false 8 | let encouterTableIndex = 0 9 | let battleOptions = [] 10 | let lastBattleResult = { escaped: false, defeated: false } 11 | 12 | const setRandomEncountersEnabled = enabled => { 13 | randomEncountersEnabled = enabled 14 | console.log('randomEncountersEnabled', randomEncountersEnabled) 15 | } 16 | const setBattleOptions = options => { 17 | battleOptions = options 18 | console.log('setBattleOptions', battleOptions) 19 | } 20 | 21 | const getLastBattleResult = () => { 22 | return lastBattleResult 23 | } 24 | 25 | const setLastBattleResult = (escaped, defeated) => { 26 | lastBattleResult.escaped = escaped 27 | lastBattleResult.defeated = defeated 28 | incrementBattlesFought() 29 | if (escaped) { 30 | incrementBattlesEscaped() 31 | } 32 | console.log('setLastBattleResult', getLastBattleResult()) 33 | } 34 | const setBattleEncounterTableIndex = index => { 35 | encouterTableIndex = index 36 | console.log('setBattleEncounterTableIndex', encouterTableIndex) 37 | } 38 | const isBattleLockEnabled = () => { 39 | return battleLockEnabled 40 | } 41 | const setBattleLockEnabled = enabled => { 42 | battleLockEnabled = enabled 43 | console.log('setBattleLockEnabled', isBattleLockEnabled()) 44 | } 45 | const initBattleSettings = () => { 46 | console.log('initBattleSettings') 47 | randomEncountersEnabled = true 48 | battleLockEnabled = false 49 | encouterTableIndex = 0 50 | battleOptions = [] 51 | lastBattleResult = { escaped: false, defeated: false } // Reset this every field change ?! 52 | } 53 | export { 54 | initBattleSettings, 55 | setRandomEncountersEnabled, 56 | setBattleOptions, 57 | getLastBattleResult, 58 | setLastBattleResult, 59 | setBattleEncounterTableIndex, 60 | isBattleLockEnabled, 61 | setBattleLockEnabled 62 | } 63 | -------------------------------------------------------------------------------- /app/data/global-data.js: -------------------------------------------------------------------------------- 1 | // Global Objects - Improve in a better way another time 2 | 3 | window.libraries = { 4 | howler: { 5 | Howl: window.Howl, 6 | Howler: window.Howler 7 | }, 8 | async: window.async 9 | } 10 | console.log('window.libraries', window.Howler) 11 | 12 | window.developerMode = window.location.host.includes('localhost') 13 | 14 | window.currentField = {} // Contains field data 15 | 16 | window.anim = { 17 | container: undefined, 18 | stats: undefined, 19 | gui: undefined, 20 | clock: undefined, 21 | renderer: undefined, 22 | axesHelper: undefined, 23 | activeScene: undefined 24 | } 25 | 26 | window.config = { 27 | sizing: { 28 | width: 320, 29 | height: 240, 30 | factor: 2 // 2.67 // Set to 0 to scale to available viewport size 31 | }, 32 | debug: { 33 | active: true, 34 | debugModeNoOpLoops: window.location.search.includes('debug'), 35 | showDebugCamera: false, 36 | showWalkmeshMesh: false, 37 | showWalkmeshLines: false, 38 | showBackgroundLayers: true, 39 | showModelHelpers: false, 40 | showAxes: false, 41 | showMovementHelpers: false, 42 | runByDefault: true 43 | }, 44 | raycast: { 45 | active: false, 46 | raycaster: undefined, 47 | mouse: undefined, 48 | raycasterHelper: undefined 49 | }, 50 | save: { 51 | cardId: 1, 52 | slotId: 1 53 | }, 54 | saveAnywhere: window.developerMode 55 | } 56 | 57 | window.data = { 58 | kernel: undefined, 59 | savemap: undefined 60 | } 61 | 62 | if (window.config.sizing.factor === 0) { 63 | const width = 64 | window.innerWidth || 65 | document.documentElement.clientWidth || 66 | document.body.clientWidth 67 | const height = 68 | window.innerHeight || 69 | document.documentElement.clientHeight || 70 | document.body.clientHeight 71 | window.config.sizing.factor = Math.min( 72 | width / window.config.sizing.width, 73 | height / window.config.sizing.height 74 | ) 75 | // console.log('Set window sizing factor', width, height, window.config.sizing.factor) 76 | } 77 | -------------------------------------------------------------------------------- /app/world/world-module.js: -------------------------------------------------------------------------------- 1 | import { 2 | setupScenes, 3 | startWorldRenderingLoop, 4 | scene, 5 | orthoScene 6 | } from './world-scene.js' 7 | import { initWorldKeypressActions } from './world-controls.js' 8 | import { loadWorldMap2d } from './world-2d.js' 9 | import { loadWorldMap3d } from './world-3d.js' 10 | import { getFieldToWorldMapTransitionData } from '../data/world-fetch-data.js' 11 | import { tempLoadDestinationSelector } from './world-destination-selector.js' 12 | 13 | let FIELD_TO_WORLD_DATA 14 | 15 | const initWorldModule = async () => { 16 | setupScenes() 17 | initWorldKeypressActions() 18 | await loadWorldMapTransitionData() 19 | } 20 | 21 | const cleanScene = () => { 22 | while (scene.children.length) { 23 | scene.remove(scene.children[0]) 24 | } 25 | while (orthoScene.children.length) { 26 | orthoScene.remove(orthoScene.children[0]) 27 | } 28 | } 29 | 30 | const loadWorldMapTransitionData = async () => { 31 | FIELD_TO_WORLD_DATA = await getFieldToWorldMapTransitionData() 32 | } 33 | 34 | const loadWorldMap = async lastWMFieldReference => { 35 | const transitionDataId = `${parseInt(lastWMFieldReference.replace('wm', '')) + 36 | 1}` 37 | let transitionData = FIELD_TO_WORLD_DATA[transitionDataId] 38 | if (transitionData === undefined) { 39 | // TODO: some wmIds in field-id-to-world-map-coords.js do not have a destination 40 | transitionData = { meshX: 'unknown', meshY: 'unknown' } 41 | } 42 | console.log( 43 | 'loadWorldMap', 44 | lastWMFieldReference, 45 | 'transitionData', 46 | transitionDataId, 47 | transitionData 48 | ) 49 | 50 | window.world = { 51 | lastWMFieldReference, 52 | transitionData 53 | } 54 | 55 | cleanScene() 56 | startWorldRenderingLoop() 57 | 58 | // Temp 59 | await loadWorldMap2d( 60 | `${lastWMFieldReference} - meshX: ${transitionData.meshX}, meshY: ${transitionData.meshY} - SELECT DESTINATION` 61 | ) 62 | // loadWorldMap3d() 63 | tempLoadDestinationSelector(lastWMFieldReference) 64 | } 65 | 66 | export { initWorldModule, loadWorldMap } 67 | -------------------------------------------------------------------------------- /app/menu/menu-game-over.js: -------------------------------------------------------------------------------- 1 | import { getMenuBlackOverlay, setMenuState } from './menu-module.js' 2 | import { 3 | createDialogBox, 4 | addGroupToDialog, 5 | addImageToDialog, 6 | fadeOverlayOut, 7 | fadeOverlayIn, 8 | removeGroupChildren 9 | } from './menu-box-helper.js' 10 | import { KEY } from '../interaction/inputs.js' 11 | import { loadMusic, playMusic, stopMusic } from '../media/media-music.js' 12 | import { loadTitleMenu } from './menu-title.js' 13 | 14 | let gameOverDialog, gameOverGroup 15 | 16 | const loadGameOverMenu = async param => { // Note, this will never actually be called... 17 | if (param === null || param === undefined) { 18 | param = 0 19 | } 20 | console.log('gameover loadGameOverMenu', param) 21 | 22 | gameOverDialog = await createDialogBox({ 23 | id: 15, 24 | name: 'gameOverDialog', 25 | w: 320, 26 | h: 240, 27 | x: 0, 28 | y: 0, 29 | expandInstantly: true, 30 | noClipping: true 31 | }) 32 | gameOverDialog.visible = true 33 | removeGroupChildren(gameOverDialog) 34 | gameOverGroup = addGroupToDialog(gameOverDialog, 25) 35 | 36 | drawGameOver() 37 | playGameOverMusic() 38 | await fadeOverlayOut(getMenuBlackOverlay()) 39 | setMenuState('gameover') 40 | } 41 | const playGameOverMusic = async () => { 42 | await loadMusic(101, 'over2') 43 | playMusic(101, false, 1000) 44 | } 45 | const drawGameOver = () => { 46 | removeGroupChildren(gameOverGroup) 47 | 48 | addImageToDialog(gameOverGroup, 'game-over', 'game-over', 'game-over-image', 160, 120, 0.5) 49 | } 50 | const exitMenu = async () => { 51 | console.log('exitMenu') 52 | setMenuState('loading') 53 | stopMusic(1000) 54 | await fadeOverlayIn(getMenuBlackOverlay()) 55 | gameOverDialog.visible = false 56 | 57 | console.log('gameover EXIT') 58 | loadTitleMenu() 59 | } 60 | const keyPress = async (key, firstPress, state) => { 61 | console.log('press MAIN MENU disc', key, firstPress, state) 62 | 63 | if (state === 'gameover') { 64 | if (key) { 65 | exitMenu() 66 | } 67 | } 68 | } 69 | export { loadGameOverMenu, keyPress } 70 | -------------------------------------------------------------------------------- /app/menu/menu-scene.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../assets/threejs-r148/build/three.module.js' 2 | import { TextGeometry } from '../../assets/threejs-r148/examples/jsm/geometries/TextGeometry.js' 3 | import { updateOnceASecond } from '../helpers/gametime.js' 4 | import { loadFont } from '../helpers/font-helper.js' 5 | import TWEEN from '../../assets/tween.esm.js' 6 | const MENU_TWEEN_GROUP = (window.MENU_TWEEN_GROUP = new TWEEN.Group()) 7 | 8 | let scene 9 | let camera 10 | window.menuScene = scene 11 | const showDebugText = async text => { 12 | const font = await loadFont() 13 | const textGeo = new TextGeometry(text, { 14 | font, 15 | size: 5, 16 | height: 1, 17 | curveSegments: 10, 18 | bevelEnabled: false 19 | }) 20 | const material = new THREE.MeshBasicMaterial({ 21 | color: 0xffffff, 22 | transparent: true 23 | }) 24 | const mesh = new THREE.Mesh(textGeo, material) 25 | mesh.position.y = 4 26 | mesh.position.x = 4 27 | scene.add(mesh) 28 | } 29 | 30 | const renderLoop = function () { 31 | if (window.anim.activeScene !== 'menu') { 32 | console.log('Stopping menu renderLoop') 33 | return 34 | } 35 | window.requestAnimationFrame(renderLoop) 36 | updateOnceASecond() 37 | MENU_TWEEN_GROUP.update() 38 | window.anim.renderer.clear() 39 | window.anim.renderer.render(scene, camera) 40 | window.anim.renderer.clearDepth() 41 | 42 | if (window.config.debug.active) { 43 | window.anim.stats.update() 44 | } 45 | } 46 | const initMenuRenderLoop = () => { 47 | if (window.anim.activeScene !== 'menu') { 48 | window.anim.activeScene = 'menu' 49 | renderLoop() 50 | } 51 | } 52 | 53 | const setupMenuCamera = () => { 54 | scene = new THREE.Scene() 55 | scene.background = new THREE.Color(0x000000) 56 | 57 | camera = new THREE.OrthographicCamera( 58 | 0, 59 | window.config.sizing.width, 60 | window.config.sizing.height, 61 | 0, 62 | 0, 63 | 1001 64 | ) 65 | camera.position.z = 1001 66 | } 67 | 68 | export { 69 | setupMenuCamera, 70 | scene, 71 | camera, 72 | showDebugText, 73 | initMenuRenderLoop, 74 | MENU_TWEEN_GROUP 75 | } 76 | -------------------------------------------------------------------------------- /app/helpers/display-controls.js: -------------------------------------------------------------------------------- 1 | const bindDisplayControls = () => { 2 | // keyboard controls 3 | document 4 | .querySelector('.display-controls .controls') 5 | .addEventListener('click', () => { 6 | const div = document.querySelector('.keyboard-instructions .modal') 7 | if (!div.classList.contains('show')) { 8 | div.classList.add('show') 9 | } else { 10 | div.classList.remove('show') 11 | } 12 | }) 13 | document 14 | .querySelector('.keyboard-instructions .close') 15 | .addEventListener('click', () => { 16 | document 17 | .querySelector('.keyboard-instructions .modal') 18 | .classList.remove('show') 19 | }) 20 | // stats 21 | document 22 | .querySelector('.display-controls .stats') 23 | .addEventListener('click', () => { 24 | const div = document.querySelector('.stats') 25 | if (div.style.display === 'none') { 26 | div.style.display = 'block' 27 | } else { 28 | div.style.display = 'none' 29 | } 30 | }) 31 | // datgui 32 | document 33 | .querySelector('.display-controls .debug') 34 | .addEventListener('click', () => { 35 | const datgui = document.querySelector('.lil-gui') 36 | if (!datgui.classList.contains('hide')) { 37 | datgui.classList.add('hide') 38 | } else { 39 | datgui.classList.remove('hide') 40 | } 41 | const loop = document.querySelector('.field-op-loop-visualiser') 42 | if (!loop.classList.contains('hide')) { 43 | loop.classList.add('hide') 44 | } 45 | }) 46 | // loop 47 | document 48 | .querySelector('.display-controls .loop') 49 | .addEventListener('click', () => { 50 | const loop = document.querySelector('.field-op-loop-visualiser') 51 | if (!loop.classList.contains('hide')) { 52 | loop.classList.add('hide') 53 | } else { 54 | loop.classList.remove('hide') 55 | } 56 | const datgui = document.querySelector('.lil-gui') 57 | if (!datgui.classList.contains('hide')) { 58 | datgui.classList.add('hide') 59 | } 60 | }) 61 | } 62 | export { bindDisplayControls } 63 | -------------------------------------------------------------------------------- /workings-out/fieldModelSelectiveLightingIdentify.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const path = require('path') 3 | const _ = require('lodash') 4 | 5 | const FIELD_FOLDER = './kujata-data/data/field/flevel.lgp' 6 | const OUT_FILE = path.join( 7 | 'workings-out', 8 | 'output', 9 | 'field-model-lighting.json' 10 | ) 11 | 12 | const doesLightingMatch = (a, b) => { 13 | return JSON.stringify(a) === JSON.stringify(b) 14 | } 15 | const getlightingForModel = model => { 16 | return { 17 | globalLight: model.globalLight, 18 | light1: model.light1, 19 | light2: model.light2, 20 | light3: model.light3 21 | } 22 | } 23 | const init = async () => { 24 | console.log('fieldModelSelectiveLightingIdentify - START') 25 | let fieldNames = await fs.readJson(path.join(FIELD_FOLDER, 'maplist.json')) 26 | const nonMatchingFields = [] 27 | const errorFields = [] 28 | 29 | fieldLoop: for (let i = 0; i < fieldNames.length; i++) { 30 | const fieldName = fieldNames[i] 31 | 32 | try { 33 | const fieldJson = await fs.readJson( 34 | path.join(FIELD_FOLDER, `${fieldName}.json`) 35 | ) 36 | // console.log('fieldName', fieldName) 37 | const models = fieldJson.model.modelLoaders 38 | const baseLighting = getlightingForModel(models[0]) 39 | for (let i = 0; i < models.length; i++) { 40 | const model = models[i] 41 | const lighting = getlightingForModel(model) 42 | if (!doesLightingMatch(baseLighting, lighting)) { 43 | // console.log(fieldName, 'model', i, '-> ', 'NON MATCH', baseLighting, lighting) 44 | nonMatchingFields.push(fieldName) 45 | continue fieldLoop 46 | } else { 47 | // console.log(fieldName, 'model', i, '-> ', 'match') 48 | } 49 | } 50 | } catch (error) { 51 | errorFields.push(fieldName) 52 | } 53 | } 54 | console.log('nonMatchingFields', nonMatchingFields, nonMatchingFields.length) 55 | console.log('errorFields', errorFields.length) 56 | const data = { 57 | nonMatchingFields, 58 | errorFields 59 | } 60 | await fs.writeJson(OUT_FILE, data, { spaces: '\t' }) 61 | console.log('fieldModelSelectiveLightingIdentify - END') 62 | } 63 | init() 64 | -------------------------------------------------------------------------------- /assets/op-loop-visualiser.css: -------------------------------------------------------------------------------- 1 | .field-op-loop-visualiser { 2 | width: 265px; 3 | height: 100%; 4 | position: absolute; 5 | top: 0; 6 | right: 0; 7 | z-index: 100; 8 | background-color: black; 9 | font-size: 12px; 10 | cursor: pointer; 11 | color: white; 12 | overflow-y: auto; 13 | } 14 | .field-op-loop-visualiser.hide{ 15 | display: none; 16 | } 17 | .field-op-loop-visualiser .entities { 18 | position: relative; 19 | } 20 | .field-op-loop-visualiser .entities-1 { 21 | position: absolute; 22 | top: 0; 23 | left: 0; 24 | width: 50%; 25 | height: 100%; 26 | padding-left: 3px; 27 | padding-right: 3px; 28 | } 29 | .field-op-loop-visualiser .entities-2 { 30 | position: absolute; 31 | top: 0; 32 | right: 0; 33 | width: 50%; 34 | height: 100%; 35 | padding-left: 3px; 36 | padding-right: 3px; 37 | } 38 | 39 | .field-op-loop-visualiser .toggle { 40 | text-align: center; 41 | } 42 | .field-op-loop-visualiser .toggle:hover { 43 | background-color: #111111; 44 | } 45 | .field-op-loop-visualiser .btn-tiny { 46 | padding: 1px; 47 | font-size: 9px; 48 | line-height: 1; 49 | border-radius: 1px; 50 | } 51 | .field-op-loop-visualiser .btn-group { 52 | /* padding: 1px; 53 | font-size: 9px; */ 54 | line-height: 1; 55 | display: block; 56 | /* border-radius: 1px; */ 57 | } 58 | 59 | .field-op-loop-visualiser .btn-priority-0 { 60 | background-color: #0868ac; 61 | border-color: #0868ac; 62 | } 63 | .field-op-loop-visualiser .btn-priority-1 { 64 | background-color: #2b8cbe; 65 | border-color: #2b8cbe; 66 | } 67 | .field-op-loop-visualiser .btn-priority-2 { 68 | background-color: #4eb3d3; 69 | border-color: #4eb3d3; 70 | } 71 | .field-op-loop-visualiser .btn-priority-3 { 72 | background-color: #7bccc4; 73 | border-color: #7bccc4; 74 | } 75 | .field-op-loop-visualiser .btn-priority-4 { 76 | background-color: #a8ddb5; 77 | border-color: #a8ddb5; 78 | color: #111111; 79 | } 80 | .field-op-loop-visualiser .btn-priority-5 { 81 | background-color: #ccebc5; 82 | border-color: #ccebc5; 83 | color: #111111; 84 | } 85 | .field-op-loop-visualiser .btn-priority-6 { 86 | background-color: #e0f3db; 87 | border-color: #e0f3db; 88 | color: #111111; 89 | } 90 | .field-op-loop-visualiser .btn-priority-7 { 91 | background-color: #f7fcf0; 92 | border-color: #f7fcf0; 93 | color: #111111; 94 | } -------------------------------------------------------------------------------- /static-server.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const fs = require('fs') 3 | const path = require('path') 4 | const mime = require('node-mime-types') 5 | const fenrirDirectory = path.join(__dirname) 6 | const kujataDataDirectory = path.join(__dirname, '..', 'kujata-data') 7 | 8 | const addCors = res => { 9 | res.setHeader('Access-Control-Allow-Origin', '*') 10 | res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET') 11 | res.setHeader('Access-Control-Max-Age', 2592000) // 30 days 12 | res.setHeader('Access-Control-Allow-Headers', 'content-type') // Might be helpful 13 | } 14 | const server = http.createServer((req, res) => { 15 | let cacheControlHeader = 'public, max-age=0' 16 | let sourceDirectory = fenrirDirectory 17 | 18 | if (req.url.startsWith('/kujata-data')) { 19 | cacheControlHeader = 'public, max-age=604800' 20 | sourceDirectory = kujataDataDirectory 21 | req.url = decodeURI(req.url.substring(12).split('?')[0]) 22 | if ( 23 | req.url.startsWith('/data/field/') || 24 | req.url.startsWith('/data/battle/') || 25 | req.url.startsWith('/metadata/background-layers/') 26 | ) { 27 | cacheControlHeader = 'public, max-age=0' 28 | } 29 | } else { 30 | req.url = decodeURI(req.url.split('?')[0]) 31 | } 32 | // if ( 33 | // (req.url.startsWith('/metadata') && req.url.endsWith('.png')) || 34 | // req.url.endsWith('.zip') 35 | // ) { 36 | // console.log('file', req.url) 37 | // } 38 | 39 | const filePath = path.join( 40 | sourceDirectory, 41 | req.url === '/' ? 'index.html' : decodeURI(req.url) 42 | ) 43 | 44 | fs.stat(filePath, (err, stats) => { 45 | if (err) { 46 | res.writeHead(404, { 'Content-Type': 'text/plain' }) 47 | res.end('404 Not Found\n') 48 | return 49 | } 50 | 51 | if (stats.isFile()) { 52 | res.setHeader('Cache-Control', cacheControlHeader) 53 | res.setHeader( 54 | 'Content-Type', 55 | mime.getMIMEType(filePath) || 'application/octet-stream' 56 | ) 57 | addCors(res) 58 | fs.createReadStream(filePath).pipe(res) 59 | } else { 60 | res.writeHead(403, { 'Content-Type': 'text/plain' }) 61 | res.end('403 Forbidden\n') 62 | } 63 | }) 64 | }) 65 | 66 | server.listen(3000, () => { 67 | console.log('Fenrir and kujata-data running on http://localhost:3000') 68 | }) 69 | -------------------------------------------------------------------------------- /app/battle/battle-actions-op-control.js: -------------------------------------------------------------------------------- 1 | import { ACTION_DATA, framesToTime } from './battle-actions.js' 2 | import { tweenSleep } from './battle-scene.js' 3 | 4 | const ANIM = async op => { 5 | const attActor = ACTION_DATA.actors.attacker 6 | const attPos = attActor.model.scene.position 7 | const attAnimPos = attActor.model.scene.children[0].position 8 | 9 | console.log('ACTION ANIM:'.op, ACTION_DATA) 10 | // Apply the previous translation to the root... I bit of a mess, but it mostly solves the problems 11 | // This is a mess, need to solve this better, but it appears as though it's related to the animation start frames 12 | if ( 13 | ACTION_DATA.attackerPosition.position !== 0 && 14 | !ACTION_DATA.attackerPosition.applied 15 | ) { 16 | attActor.model.scene.children[0].updateMatrixWorld() 17 | const finalWorldPosition = new THREE.Vector3() 18 | attActor.model.scene.children[0].getWorldPosition(finalWorldPosition) 19 | attPos.x = finalWorldPosition.x 20 | attPos.z = finalWorldPosition.z 21 | attAnimPos.x = 0 22 | attAnimPos.z = 0 23 | } 24 | 25 | const animOptions = {} 26 | if (op.async && ACTION_DATA.attackerPosition.position === 0) { 27 | // Note: Looks like the return to idle anim is always 28 ? 28 | 29 | console.log('ACTION ANIM: async') 30 | ACTION_DATA.actors.attacker.model.userData.playAnimationOnce( 31 | op.animation, 32 | animOptions 33 | ) 34 | } else { 35 | console.log('ACTION ANIM: sync') 36 | await ACTION_DATA.actors.attacker.model.userData.playAnimationOnce( 37 | op.animation, 38 | animOptions 39 | ) 40 | } 41 | console.log('ACTION ANIM: END') 42 | // It visually looks like after you finish, you should hold the first frame of the next animation, maybe... 43 | } 44 | const SETWAIT = op => { 45 | ACTION_DATA.wait = op.frames 46 | } 47 | const WAIT = async () => { 48 | if (ACTION_DATA.wait === 0) ACTION_DATA.wait = 15 // Default is 15 frames if not already set 49 | await tweenSleep(framesToTime(ACTION_DATA.wait)) 50 | ACTION_DATA.wait = 15 51 | } 52 | const NAME = () => { 53 | window.currentBattle.ui.battleText.showBattleMessage(ACTION_DATA.actionName) 54 | } 55 | const MSG = op => { 56 | window.currentBattle.ui.battleText.showBattleMessage(op.message) 57 | } 58 | const RET = () => {} 59 | export { ANIM, SETWAIT, WAIT, NAME, MSG, RET } 60 | -------------------------------------------------------------------------------- /app/data/field-fetch-data.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../assets/threejs-r148/build/three.module.js' 2 | import { setLoadingProgress } from '../loading/loading-module.js' 3 | import { KUJATA_BASE } from './kernel-fetch-data.js' 4 | 5 | const fieldTextures = {} 6 | const getFieldTextures = (window.getFieldTextures = () => { 7 | return fieldTextures 8 | }) 9 | const loadFieldTextures = async () => { 10 | const fieldRes = await fetch( 11 | `${KUJATA_BASE}/metadata/field-assets/flevel.metadata.json` 12 | ) 13 | const field = await fieldRes.json() 14 | return new Promise((resolve, reject) => { 15 | const manager = new THREE.LoadingManager() 16 | manager.onProgress = function (url, itemsLoaded, itemsTotal) { 17 | const progress = itemsLoaded / itemsTotal 18 | setLoadingProgress(progress) 19 | } 20 | manager.onLoad = function () { 21 | console.log('loadFieldTextures Loading complete', fieldTextures) 22 | window.fieldTextures = fieldTextures 23 | resolve() 24 | } 25 | 26 | const textureGroups = [field] 27 | const textureGroupNames = ['field'] 28 | for (let i = 0; i < textureGroups.length; i++) { 29 | const textureGroup = textureGroups[i] 30 | const textureGroupName = textureGroupNames[i] 31 | const assetTypes = Object.keys(textureGroup) 32 | for (let j = 0; j < assetTypes.length; j++) { 33 | const assetType = assetTypes[j] 34 | fieldTextures[assetType] = {} 35 | for (let k = 0; k < textureGroup[assetType].length; k++) { 36 | const asset = textureGroup[assetType][k] 37 | fieldTextures[assetType][asset.description] = asset 38 | fieldTextures[assetType][asset.description].texture = 39 | new THREE.TextureLoader(manager).load( 40 | `${KUJATA_BASE}/metadata/${textureGroupName}-assets/${assetType}/${asset.description}.png` 41 | ) 42 | fieldTextures[assetType][asset.description].texture.magFilter = 43 | THREE.NearestFilter 44 | fieldTextures[assetType][asset.description].texture.encoding = 45 | THREE.sRGBEncoding 46 | fieldTextures[assetType][asset.description].anisotropy = 47 | window.anim.renderer.capabilities.getMaxAnisotropy() 48 | } 49 | } 50 | } 51 | }) 52 | } 53 | 54 | export { loadFieldTextures, getFieldTextures } 55 | -------------------------------------------------------------------------------- /app/helpers/helpers.js: -------------------------------------------------------------------------------- 1 | const sleep = ms => { 2 | return new Promise(resolve => setTimeout(resolve, ms)) 3 | } 4 | const uuid = () => { 5 | // from https://github.com/TylerGarlick/simple-uuid/blob/master/lib/uuid-node.js 6 | const lut = [] 7 | for (let i = 0; i < 256; i++) { 8 | lut[i] = (i < 16 ? '0' : '') + i.toString(16) 9 | } 10 | const d0 = (Math.random() * 0xffffffff) | 0 11 | const d1 = (Math.random() * 0xffffffff) | 0 12 | const d2 = (Math.random() * 0xffffffff) | 0 13 | const d3 = (Math.random() * 0xffffffff) | 0 14 | return ( 15 | lut[d0 & 0xff] + 16 | lut[(d0 >> 8) & 0xff] + 17 | lut[(d0 >> 16) & 0xff] + 18 | lut[(d0 >> 24) & 0xff] + 19 | '-' + 20 | lut[d1 & 0xff] + 21 | lut[(d1 >> 8) & 0xff] + 22 | '-' + 23 | lut[((d1 >> 16) & 0x0f) | 0x40] + 24 | lut[(d1 >> 24) & 0xff] + 25 | '-' + 26 | lut[(d2 & 0x3f) | 0x80] + 27 | lut[(d2 >> 8) & 0xff] + 28 | '-' + 29 | lut[(d2 >> 16) & 0xff] + 30 | lut[(d2 >> 24) & 0xff] + 31 | lut[d3 & 0xff] + 32 | lut[(d3 >> 8) & 0xff] + 33 | lut[(d3 >> 16) & 0xff] + 34 | lut[(d3 >> 24) & 0xff] 35 | ) 36 | } 37 | const dec2bin = dec => { 38 | return (dec >>> 0).toString(2) 39 | } 40 | window.dec2bin = dec2bin 41 | const dec2hex = (dec, padding, rawWithSpaces) => { 42 | const h = parseInt(dec).toString(16) 43 | return `${!rawWithSpaces ? '0x' : ''}${ 44 | padding ? h.padStart(padding, '0') : h 45 | }` 46 | } 47 | window.dec2hex = dec2hex 48 | const dec2hexPairs = dec => { 49 | let s = parseInt(dec).toString(16) 50 | if (s.length % 2) { 51 | s = '0' + s 52 | } 53 | s = s.match(/.{1,2}/g).join(' ') 54 | return s 55 | } 56 | window.dec2hexPairs = dec2hexPairs 57 | 58 | const asyncWrap = fn => { 59 | return new Promise(resolve => { 60 | setTimeout(() => { 61 | fn() 62 | resolve() 63 | }, 0) 64 | }) 65 | } 66 | const disposeAll = obj => { 67 | obj.traverse(child => { 68 | if (child.geometry) child.geometry.dispose() 69 | if (child.material) { 70 | ;(Array.isArray(child.material) 71 | ? child.material 72 | : [child.material] 73 | ).forEach(mat => { 74 | for (const key in mat) if (mat[key]?.isTexture) mat[key].dispose() 75 | mat.dispose() 76 | }) 77 | } 78 | }) 79 | obj.parent?.remove(obj) 80 | } 81 | 82 | export { sleep, uuid, dec2bin, dec2hex, dec2hexPairs, asyncWrap, disposeAll } 83 | -------------------------------------------------------------------------------- /app/menu/menu-change-disc.js: -------------------------------------------------------------------------------- 1 | import { getMenuBlackOverlay, setMenuState, resolveMenuPromise } from './menu-module.js' 2 | import { 3 | LETTER_TYPES, 4 | LETTER_COLORS, 5 | createDialogBox, 6 | addTextToDialog, 7 | addGroupToDialog, 8 | addImageToDialog, 9 | fadeOverlayOut, 10 | fadeOverlayIn, 11 | removeGroupChildren, 12 | ALIGN 13 | } from './menu-box-helper.js' 14 | import { getPlayableCharacterName } from '../field/field-op-codes-party-helper.js' 15 | import { KEY } from '../interaction/inputs.js' 16 | 17 | let discDialog, discGroup 18 | 19 | const loadChangeDiscMenu = async param => { // Note, this will never actually be called... 20 | if (param === null || param === undefined) { 21 | param = 0 22 | } 23 | console.log('disc loadChangeDiscMenu', param) 24 | 25 | discDialog = await createDialogBox({ 26 | id: 15, 27 | name: 'discDialog', 28 | w: 320, 29 | h: 240, 30 | x: 0, 31 | y: 0, 32 | expandInstantly: true, 33 | noClipping: true 34 | }) 35 | discDialog.visible = true 36 | removeGroupChildren(discDialog) 37 | discGroup = addGroupToDialog(discDialog, 25) 38 | 39 | drawDiscImages(param + 1) 40 | 41 | await fadeOverlayOut(getMenuBlackOverlay()) 42 | setMenuState('disc') 43 | } 44 | const drawDiscImages = (discNo) => { 45 | removeGroupChildren(discGroup) 46 | 47 | const randomCharName = getPlayableCharacterName(Math.floor(Math.random() * 8)) 48 | addImageToDialog(discGroup, 'char-bg', randomCharName, 'disc-char-image', 160, 0, 0.5, null, null, ALIGN.TOP) 49 | addImageToDialog(discGroup, 'insert-disc', `disk${discNo}`, 'disc-char-image', 160, 240 - 24, 0.5, null, null, ALIGN.BOTTOM) 50 | 51 | addTextToDialog( 52 | discGroup, 53 | 'Press 〇 to continue', 54 | 'disc-text-label', 55 | LETTER_TYPES.MenuBaseFont, 56 | LETTER_COLORS.White, 57 | 160 - 8, 58 | 230 - 4, 59 | 0.5, 60 | null, 61 | true 62 | ) 63 | } 64 | const exitMenu = async () => { 65 | console.log('exitMenu') 66 | setMenuState('loading') 67 | await fadeOverlayIn(getMenuBlackOverlay()) 68 | discDialog.visible = false 69 | 70 | console.log('disc EXIT') 71 | resolveMenuPromise() 72 | } 73 | const keyPress = async (key, firstPress, state) => { 74 | console.log('press MAIN MENU disc', key, firstPress, state) 75 | 76 | if (state === 'disc') { 77 | if (key === KEY.O) { 78 | exitMenu() 79 | } 80 | } 81 | } 82 | export { loadChangeDiscMenu, keyPress } 83 | -------------------------------------------------------------------------------- /app/data/cache-manager.js: -------------------------------------------------------------------------------- 1 | import { setLoadingProgress } from '../loading/loading-module.js' 2 | import { KUJATA_BASE } from './kernel-fetch-data.js' 3 | 4 | const clearCache = async () => { 5 | if ('serviceWorker' in navigator) { 6 | const registrations = await navigator.serviceWorker.getRegistrations() 7 | for (const registration of registrations) { 8 | await registration.unregister() 9 | } 10 | } 11 | if ('caches' in window) { 12 | const cacheNames = await caches.keys() 13 | for (const cacheName of cacheNames) { 14 | await caches.delete(cacheName) 15 | } 16 | } 17 | 18 | window.location.reload() 19 | } 20 | window.clearCache = clearCache 21 | 22 | // Note: This approach seems to work fine, but when the browser is set to ignore caches, it sends all of the queries to the server :( 23 | const loadZippedAssets = async () => { 24 | const CACHE_NAME = 'zipped-assets-cache' 25 | const cache = await caches.open(CACHE_NAME) 26 | 27 | console.log('loadZippedAssets: START') 28 | const zipResponse = await fetch(`${KUJATA_BASE}/cache.zip`) 29 | const zipBlob = await zipResponse.blob() 30 | const zip = await window.JSZip.loadAsync(zipBlob) 31 | 32 | console.log( 33 | 'loadZippedAssets: preparing cache', 34 | Object.keys(zip.files).length 35 | ) 36 | 37 | const cachedItemOne = await cache.match( 38 | `${KUJATA_BASE}/${Object.keys(zip.files)[0].replace(/\\/g, '/')}` 39 | ) 40 | if (cachedItemOne) { 41 | console.log( 42 | 'loadZippedAssets: END cache already populated', 43 | Object.keys(zip.files).length 44 | ) 45 | return zip 46 | } 47 | const total = Object.keys(zip.files).length 48 | let complete = 0 49 | 50 | const filePromises = Object.keys(zip.files).map(async filePath => { 51 | const file = zip.files[filePath] 52 | if (!file.dir) { 53 | const normalizedPath = filePath.replace(/\\/g, '/') 54 | const requestUrl = `${KUJATA_BASE}/${normalizedPath}` 55 | const fileBlob = await file.async('blob') 56 | await cache.put(requestUrl, new Response(fileBlob)) 57 | 58 | complete++ 59 | // console.log('progress', complete, 'of', total, '->', complete / total) 60 | setLoadingProgress(Math.min(89, complete / total)) // Takes a while to display the progress 61 | } 62 | }) 63 | await Promise.all(filePromises) 64 | console.log('loadZippedAssets: END', Object.keys(zip.files).length) 65 | return zip 66 | } 67 | export { clearCache, loadZippedAssets } 68 | -------------------------------------------------------------------------------- /workings-out/byte-data-pattern-finder.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | const dec2hex = (dec, padding, rawWithSpaces) => { 4 | const h = parseInt(dec).toString(16) 5 | return `${!rawWithSpaces ? '0x' : ''}${ 6 | padding ? h.padStart(padding, '0') : h 7 | }` 8 | } 9 | const dec2bin = dec => { 10 | // For debug only 11 | return (dec >>> 0).toString(2).padStart(8, '0') 12 | } 13 | const f = `C:\\Program Files (x86)\\Steam\\steamapps\\common\\FINAL FANTASY VII\\ff7_en.exe` 14 | // const f = `C:\\Program Files (x86)\\Steam\\steamapps\\common\\FINAL FANTASY VII\\data\\lang_en\\battle\\camdat0.bin` 15 | 16 | const r = Buffer.from(fs.readFileSync(f)) 17 | const lines = [] 18 | const allSame = arr => arr.every(val => val === arr[0]) 19 | const missingNumbers = (arr, min, max) => 20 | Array.from({ length: max - min + 1 }, (_, i) => i + min).filter( 21 | n => !arr.includes(n) 22 | ) 23 | 24 | const findRepeatedValue = values => { 25 | const max = Math.max(...values) 26 | const between = missingNumbers(values, values[0], max) 27 | console.log('values', values) 28 | console.log('between', between) 29 | lines.push(`Repeated Values - ${values}\n----------------------`) 30 | for (let i = 0; i < r.length - max; i++) { 31 | const val = values.map(v => r.at(i + v)) 32 | if (val[0] === 0 || val[0] > 16 || val[0] === 255 || !allSame(val)) continue // Continue if pattern doesn't match 33 | // console.log(dec2hex(i), val) 34 | const betweenVal = missingNumbers(values, val[0], max).map(v => r.at(i + v)) 35 | if (betweenVal.includes(val[0])) continue // Continue if the values between are the same as the target 36 | 37 | lines.push( 38 | `${dec2hex(i)} ${val} ${betweenVal} ${betweenVal.includes(val[0])}` 39 | ) 40 | } 41 | fs.writeFileSync( 42 | './output/byte-pattern-find-repeated-value.txt', 43 | lines.join('\n'), 44 | 'utf-8' 45 | ) 46 | } 47 | const findPatternWithUnknownNumber = arr => { 48 | const arrLength = arr.length 49 | for (let i = 0; i < r.length - arrLength; i++) { 50 | // for (let i = 0; i < 500; i++) { 51 | r.byteOffset = i 52 | r.readInt8() 53 | const a = [] 54 | const root = r.readInt8(i) 55 | for (let ia = 1; ia < arrLength; ia++) { 56 | const next = r.readInt8(i + ia) 57 | a.push(next) 58 | // r.byteOffset++ 59 | if (next !== root + arr[ia]) { 60 | break 61 | } 62 | } 63 | if (a.length >= arr.length - 1) { 64 | console.log(i, a, r.byteOffset) 65 | } 66 | } 67 | } 68 | 69 | const init = async () => { 70 | // findRepeatedValue([0, 3, 8, 11, 14]) 71 | findPatternWithUnknownNumber([0, 0, 1, 2, 2, 2, 2, 0]) 72 | } 73 | 74 | init() 75 | -------------------------------------------------------------------------------- /app/data/menu-fetch-data.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../assets/threejs-r148/build/three.module.js' // 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r118/three.module.min.js'; 2 | import { setLoadingProgress } from '../loading/loading-module.js' 3 | import { KUJATA_BASE } from '../data/kernel-fetch-data.js' 4 | 5 | const menuTextures = {} 6 | const getMenuTextures = (window.getMenuTextures = () => { 7 | return menuTextures 8 | }) 9 | const loadMenuTextures = async () => { 10 | const menuRes = await fetch( 11 | `${KUJATA_BASE}/metadata/menu-assets/menu_us.metadata.json` 12 | ) 13 | const menu = await menuRes.json() 14 | 15 | const creditsRes = await fetch( 16 | `${KUJATA_BASE}/metadata/credits-assets/credits-font.metadata.json` 17 | ) 18 | const credits = await creditsRes.json() 19 | 20 | const discRes = await fetch( 21 | `${KUJATA_BASE}/metadata/disc-assets/disc.metadata.json` 22 | ) 23 | const disc = await discRes.json() 24 | return new Promise((resolve, reject) => { 25 | const manager = new THREE.LoadingManager() 26 | manager.onProgress = function (url, itemsLoaded, itemsTotal) { 27 | const progress = itemsLoaded / itemsTotal 28 | setLoadingProgress(progress) 29 | } 30 | manager.onLoad = function () { 31 | console.log('loadMenuTextures Loading complete', menuTextures) 32 | window.menuTextures = menuTextures 33 | resolve() 34 | } 35 | 36 | const textureGroups = [menu, credits, disc] 37 | const textureGroupNames = ['menu', 'credits', 'disc'] 38 | for (let i = 0; i < textureGroups.length; i++) { 39 | const textureGroup = textureGroups[i] 40 | const textureGroupName = textureGroupNames[i] 41 | const assetTypes = Object.keys(textureGroup) 42 | for (let j = 0; j < assetTypes.length; j++) { 43 | const assetType = assetTypes[j] 44 | menuTextures[assetType] = {} 45 | for (let k = 0; k < textureGroup[assetType].length; k++) { 46 | const asset = textureGroup[assetType][k] 47 | menuTextures[assetType][asset.description] = asset 48 | menuTextures[assetType][asset.description].texture = new THREE.TextureLoader(manager).load( 49 | `${KUJATA_BASE}/metadata/${textureGroupName}-assets/${assetType}/${asset.description}.png` 50 | ) 51 | menuTextures[assetType][asset.description].texture.encoding = THREE.sRGBEncoding 52 | menuTextures[assetType][asset.description].texture.magFilter = THREE.NearestFilter 53 | menuTextures[assetType][asset.description].anisotropy = window.anim.renderer.capabilities.getMaxAnisotropy() 54 | } 55 | } 56 | } 57 | }) 58 | } 59 | 60 | export { loadMenuTextures, getMenuTextures } 61 | -------------------------------------------------------------------------------- /app/world/world-scene.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../assets/threejs-r148/build/three.module.js' 2 | import { updateOnceASecond } from '../helpers/gametime.js' 3 | 4 | let scene 5 | let orthoScene 6 | let fixedCamera 7 | let movingCamera 8 | let debugCamera 9 | let orthoCamera 10 | 11 | const renderLoop = () => { 12 | if (window.anim.activeScene !== 'world') { 13 | console.log('Stopping world renderLoop') 14 | return 15 | } 16 | requestAnimationFrame(renderLoop) 17 | updateOnceASecond() 18 | 19 | if (window.anim.renderer) { 20 | // console.log('render') 21 | const activeCamera = fixedCamera 22 | 23 | window.anim.renderer.clear() 24 | window.anim.renderer.render(scene, fixedCamera) 25 | 26 | window.anim.renderer.clearDepth() 27 | window.anim.renderer.render(orthoScene, orthoCamera) 28 | } 29 | if (window.config.debug.active) { 30 | window.anim.stats.update() 31 | } 32 | } 33 | const startWorldRenderingLoop = () => { 34 | if (window.anim.activeScene !== 'world') { 35 | window.anim.activeScene = 'world' 36 | renderLoop() 37 | } 38 | } 39 | 40 | const setupScenes = () => { 41 | scene = new THREE.Scene() 42 | orthoScene = new THREE.Scene() 43 | 44 | const light = new THREE.DirectionalLight(0xffffff) 45 | light.position.set(0, 0, 50).normalize() 46 | scene.add(light) 47 | const ambientLight = new THREE.AmbientLight(0x404040) 48 | scene.add(ambientLight) 49 | 50 | fixedCamera = new THREE.PerspectiveCamera( 51 | 100, 52 | window.config.sizing.width / window.config.sizing.height, 53 | 0.001, 54 | 1000 55 | ) 56 | fixedCamera.position.x = 5 57 | fixedCamera.position.y = 5 58 | fixedCamera.position.z = 5 59 | 60 | movingCamera = new THREE.PerspectiveCamera( 61 | 100, 62 | window.config.sizing.width / window.config.sizing.height, 63 | 0.001, 64 | 1000 65 | ) 66 | movingCamera.position.x = 10 67 | movingCamera.position.y = 20 68 | movingCamera.position.z = 30 69 | 70 | debugCamera = new THREE.PerspectiveCamera( 71 | 100, 72 | window.config.sizing.width / window.config.sizing.height, 73 | 0.001, 74 | 1000 75 | ) 76 | debugCamera.position.x = 10 77 | debugCamera.position.y = 20 78 | debugCamera.position.z = 30 79 | 80 | orthoCamera = new THREE.OrthographicCamera( 81 | 0, 82 | window.config.sizing.width, 83 | window.config.sizing.height, 84 | 0, 85 | 0, 86 | 1001 87 | ) 88 | orthoCamera.position.z = 1001 89 | } 90 | 91 | export { 92 | scene, 93 | orthoScene, 94 | fixedCamera, 95 | movingCamera, 96 | orthoCamera, 97 | setupScenes, 98 | startWorldRenderingLoop 99 | } 100 | -------------------------------------------------------------------------------- /app/battle/battle-damage-calc.js: -------------------------------------------------------------------------------- 1 | const DMG_TYPE = { 2 | HIT: 'HIT', 3 | MISS: 'MISS', 4 | DEATH: 'DEATH', 5 | RECOVERY: 'RECOVERY' 6 | } 7 | // +0e [][] damage flags (0x0001 - heal, 0x0002 - critical damage, 0x0004 - damage to MP). 8 | 9 | const getDefault = () => { 10 | return { 11 | amount: 0, // Numerical 12 | type: DMG_TYPE.HIT, // hit, miss, death, recovery - How does this affects sounds and impact? 13 | isCritical: false, 14 | isRestorative: false, 15 | isMp: false, 16 | // isBarrier? isFrog? anything else that may affects the impact effect, sound or animation? 17 | status: { 18 | add: [], 19 | removed: [] 20 | } 21 | // TODO - What happens about calculating absorb values for HP Absorb / MP Absorb after actions etc? Not sure yet 22 | } 23 | } 24 | // https://github.com/Akari1982/q-gears_reverse/blob/master/ffvii/documents/final_fantasy_vii_battle_mech.txt 25 | // https://wiki.ffrtt.ru/index.php/FF7/Battle/Battle_Mechanics 26 | // https://wiki.ffrtt.ru/index.php/FF7/Battle/Damage_Calculation 27 | const calcDamage = (actor, command, attack, targets) => { 28 | const isCritical = Math.random() >= 0.5 29 | const damages = targets.map(t => { 30 | const d = getDefault() 31 | d.amount = 0 32 | 33 | if (actor.index === 0) { 34 | d.type = DMG_TYPE.HIT 35 | d.amount = isCritical ? 2468 : 1234 36 | d.isCritical = isCritical 37 | } else if (actor.index === 1) { 38 | // d.type = DMG_TYPE.HIT 39 | d.isRestorative = true 40 | d.amount = 1234 41 | d.isMp = true 42 | } else if (actor.index === 2) { 43 | d.amount = 123 44 | d.isMp = true 45 | } else if (actor.index === 4) { 46 | // d.type = DMG_TYPE.MISS 47 | d.amount = 1234 48 | d.isMp = true 49 | } 50 | return d 51 | }) 52 | return damages 53 | } 54 | const hasStatus = (char, status) => { 55 | return char?.status?.includes(status) 56 | } 57 | const hasOneOfStatuses = (char, statuses) => { 58 | return char?.status?.some(status => statuses.includes(status)) 59 | } 60 | const addStatus = (char, status) => { 61 | !char.status.includes(status) && char.status.push(status) 62 | } 63 | const addStatuses = (char, statuses) => { 64 | statuses.forEach( 65 | status => !char.status.includes(status) && char.status.push(status) 66 | ) 67 | } 68 | const removeStatus = (char, status) => { 69 | char.status = char.status.filter(s => s !== status) 70 | } 71 | const removeStatuses = (char, statuses) => { 72 | char.status = char.status.filter(s => !statuses.includes(s)) 73 | } 74 | export { 75 | calcDamage, 76 | DMG_TYPE, 77 | hasStatus, 78 | hasOneOfStatuses, 79 | addStatus, 80 | addStatuses, 81 | removeStatus, 82 | removeStatuses 83 | } 84 | -------------------------------------------------------------------------------- /app/minigame/minigame-scene.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../assets/threejs-r148/build/three.module.js' 2 | import { updateOnceASecond } from '../helpers/gametime.js' 3 | 4 | let scene 5 | let orthoScene 6 | let fixedCamera 7 | let movingCamera 8 | let debugCamera 9 | let orthoCamera 10 | 11 | const renderLoop = () => { 12 | if (window.anim.activeScene !== 'minigame') { 13 | console.log('Stopping minigame renderLoop') 14 | return 15 | } 16 | requestAnimationFrame(renderLoop) 17 | updateOnceASecond() 18 | 19 | if (window.anim.renderer) { 20 | // console.log('render') 21 | const activeCamera = fixedCamera 22 | 23 | window.anim.renderer.clear() 24 | window.anim.renderer.render(scene, fixedCamera) 25 | 26 | window.anim.renderer.clearDepth() 27 | window.anim.renderer.render(orthoScene, orthoCamera) 28 | } 29 | if (window.config.debug.active) { 30 | window.anim.stats.update() 31 | } 32 | } 33 | const startMiniGameRenderingLoop = () => { 34 | if (window.anim.activeScene !== 'minigame') { 35 | window.anim.activeScene = 'minigame' 36 | renderLoop() 37 | } 38 | } 39 | 40 | const setupScenes = () => { 41 | scene = new THREE.Scene() 42 | orthoScene = new THREE.Scene() 43 | 44 | const light = new THREE.DirectionalLight(0xffffff) 45 | light.position.set(0, 0, 50).normalize() 46 | scene.add(light) 47 | const ambientLight = new THREE.AmbientLight(0x404040) 48 | scene.add(ambientLight) 49 | 50 | fixedCamera = new THREE.PerspectiveCamera( 51 | 100, 52 | window.config.sizing.width / window.config.sizing.height, 53 | 0.001, 54 | 1000 55 | ) 56 | fixedCamera.position.x = 5 57 | fixedCamera.position.y = 5 58 | fixedCamera.position.z = 5 59 | 60 | movingCamera = new THREE.PerspectiveCamera( 61 | 100, 62 | window.config.sizing.width / window.config.sizing.height, 63 | 0.001, 64 | 1000 65 | ) 66 | movingCamera.position.x = 10 67 | movingCamera.position.y = 20 68 | movingCamera.position.z = 30 69 | 70 | debugCamera = new THREE.PerspectiveCamera( 71 | 100, 72 | window.config.sizing.width / window.config.sizing.height, 73 | 0.001, 74 | 1000 75 | ) 76 | debugCamera.position.x = 10 77 | debugCamera.position.y = 20 78 | debugCamera.position.z = 30 79 | 80 | orthoCamera = new THREE.OrthographicCamera( 81 | 0, 82 | window.config.sizing.width, 83 | window.config.sizing.height, 84 | 0, 85 | 0, 86 | 1001 87 | ) 88 | orthoCamera.position.z = 1001 89 | } 90 | 91 | export { 92 | scene, 93 | orthoScene, 94 | fixedCamera, 95 | movingCamera, 96 | orthoCamera, 97 | setupScenes, 98 | startMiniGameRenderingLoop 99 | } 100 | -------------------------------------------------------------------------------- /app/data/kernel-fetch-data.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../assets/threejs-r148/build/three.module.js' // 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r118/three.module.min.js'; 2 | import { setLoadingProgress } from '../loading/loading-module.js' 3 | const KUJATA_BASE = window.developerMode 4 | ? '/kujata-data' 5 | : 'https://kujata-data-dg.netlify.app' 6 | 7 | const windowTextures = {} 8 | const getWindowTextures = (window.getWindowTextures = () => { 9 | return windowTextures 10 | }) 11 | 12 | const loadWindowTextures = async zip => { 13 | console.log('loadWindowTextures: START') 14 | const windowBinRes = await fetch( 15 | `${KUJATA_BASE}/metadata/window-assets/window.bin.metadata.json` 16 | ) 17 | const windowBin = await windowBinRes.json() 18 | const assetTypes = Object.keys(windowBin) 19 | 20 | return new Promise((resolve, reject) => { 21 | const start = new Date() 22 | const manager = new THREE.LoadingManager() 23 | manager.onProgress = function (url, itemsLoaded, itemsTotal) { 24 | const progress = itemsLoaded / itemsTotal 25 | setLoadingProgress(Math.min(0.89, progress)) 26 | } 27 | manager.onLoad = function () { 28 | console.log('loadWindowTextures: END', windowTextures, new Date() - start) 29 | resolve() 30 | } 31 | const loader = new THREE.TextureLoader(manager) 32 | 33 | for (let i = 0; i < assetTypes.length; i++) { 34 | const assetType = assetTypes[i] 35 | windowTextures[assetType] = {} 36 | for (let j = 0; j < windowBin[assetType].length; j++) { 37 | const asset = windowBin[assetType][j] 38 | windowTextures[assetType][asset.description] = asset 39 | 40 | windowTextures[assetType][asset.description].texture = loader.load( 41 | `${KUJATA_BASE}/metadata/window-assets/${assetType}/${asset.description}.png` 42 | ) 43 | windowTextures[assetType][asset.description].texture.encoding = 44 | THREE.sRGBEncoding 45 | windowTextures[assetType][asset.description].anisotropy = 46 | window.anim.renderer.capabilities.getMaxAnisotropy() 47 | } 48 | } 49 | }) 50 | } 51 | const loadKernelData = async () => { 52 | const kernelBinRes = await fetch(`${KUJATA_BASE}/data/kernel/kernel.bin.json`) 53 | const kernelBin = await kernelBinRes.json() 54 | const allItemData = [] 55 | allItemData.push.apply(allItemData, kernelBin.itemData) 56 | allItemData.push.apply(allItemData, kernelBin.weaponData) 57 | allItemData.push.apply(allItemData, kernelBin.armorData) 58 | allItemData.push.apply(allItemData, kernelBin.accessoryData) 59 | kernelBin.allItemData = allItemData 60 | window.data.kernel = kernelBin 61 | } 62 | 63 | export { KUJATA_BASE, loadWindowTextures, getWindowTextures, loadKernelData } 64 | -------------------------------------------------------------------------------- /app/battle/battle-stack.js: -------------------------------------------------------------------------------- 1 | import { sleep } from '../helpers/helpers.js' 2 | import * as stackOps from './battle-stack-ops.js' 3 | 4 | const executeScript = async (actorIndex, script) => { 5 | const stack = [] // Each invocation gets it's own stack instance 6 | let exit = false 7 | let currentScriptPosition = 0 8 | while (!exit) { 9 | const op = script[currentScriptPosition] 10 | console.log('battleStack op', op, actorIndex, script) 11 | const result = await stackOps[op.op](stack, op, actorIndex) 12 | stackOps.logStack(stack) 13 | stackOps.logMemory() 14 | console.log('battleStack result', result) 15 | if (result.exit) exit = true 16 | if (result.next) { 17 | currentScriptPosition = script.findIndex(o => o.index === result.next) 18 | } else { 19 | currentScriptPosition++ 20 | } 21 | await sleep(50) // Just add this for walking through 22 | } 23 | for (const op of script) { 24 | console.log('battleStack queue', 'op', op) 25 | } 26 | } 27 | 28 | const executeAllInitScripts = async currentBattle => { 29 | for (const actor of currentBattle.actors.filter(a => a.type === 'player')) { 30 | if (actor.script && actor.script.init && actor.script.init.count > 0) { 31 | console.log('battleStack init: START', actor) 32 | await executeScript(actor.index, actor.script.init.script) 33 | console.log('battleStack init: END', actor) 34 | } 35 | } 36 | for (const actor of currentBattle.actors.filter(a => a.type === 'enemy')) { 37 | if (actor.script && actor.script.init && actor.script.init.count > 0) { 38 | console.log('battleStack init: START', actor) 39 | await executeScript(actor.index, actor.script.init.script) 40 | console.log('battleStack init: END', actor) 41 | } 42 | } 43 | for (const actor of currentBattle.actors.filter( 44 | a => a.type === 'formation' 45 | )) { 46 | // ?!?! Not sure yet 47 | if (actor.script && actor.script.init && actor.script.init.count > 0) { 48 | console.log('battleStack init: START', actor) 49 | await executeScript(actor.index, actor.script.init.script) 50 | console.log('battleStack init: END', actor) 51 | } 52 | } 53 | } 54 | 55 | const executeAllPreActionSetupScripts = async () => { 56 | for (const actor of window.currentBattle.actors) { 57 | // Any order?! 58 | if ( 59 | actor.script && 60 | actor.script.preActionSetup && 61 | actor.script.preActionSetup.count > 0 62 | ) { 63 | console.log('battleStack preActionSetup: START', actor) 64 | await executeScript(actor.index, actor.script.preActionSetup.script) 65 | console.log('battleStack preActionSetup: END', actor) 66 | } 67 | } 68 | } 69 | 70 | export { executeScript, executeAllInitScripts, executeAllPreActionSetupScripts } 71 | -------------------------------------------------------------------------------- /app/field/field-op-codes-party-helper.js: -------------------------------------------------------------------------------- 1 | const getPlayableCharacterId = c => { 2 | if (c === 'Cloud') return 0 3 | if (c === 'Barret') return 1 4 | if (c === 'Tifa') return 2 5 | if (c === 'Aeris') return 3 6 | if (c === 'RedXIII') return 4 7 | if (c === 'Yuffie') return 5 8 | if (c === 'CaitSith') return 6 9 | if (c === 'Vincent') return 7 10 | if (c === 'Cid') return 8 11 | if (c === 'YoungCloud') return 9 12 | if (c === 'Sephiroth') return 10 13 | if (c === 'Choco') return 11 14 | return 255 15 | } 16 | 17 | const getPlayableCharacterName = c => { 18 | if (c === 0) return 'Cloud' 19 | if (c === 1) return 'Barret' 20 | if (c === 2) return 'Tifa' 21 | if (c === 3) return 'Aeris' 22 | if (c === 4) return 'RedXIII' 23 | if (c === 5) return 'Yuffie' 24 | if (c === 6) return 'CaitSith' 25 | if (c === 7) return 'Vincent' 26 | if (c === 8) return 'Cid' 27 | if (c === 9) return 'YoungCloud' 28 | if (c === 10) return 'Sephiroth' 29 | if (c === 11 || c === 100) return 'Choco' 30 | return 'None' 31 | } 32 | const getSpecialTextName = textId => { 33 | return `Name ${textId}` // TODO - Doesn't look like currentField dialogStrings 34 | } 35 | const setCharacterNameFromSpecialText = (c, textId) => { 36 | // This is not really used in the game 37 | const characterName = getPlayableCharacterName(c) 38 | window.data.savemap.characters[characterName].name = getSpecialTextName( 39 | textId 40 | ) 41 | console.log( 42 | 'setCharacterNameFromSpecialText', 43 | characterName, 44 | window.data.savemap.characters[characterName] 45 | ) 46 | } 47 | const getCharacterSaveMap = characterName => { 48 | if (characterName === 'Sephiroth') { 49 | return window.data.savemap.characters.Vincent 50 | } else if (characterName === 'YoungCloud') { 51 | return window.data.savemap.characters.CaitSith 52 | } else { 53 | return window.data.savemap.characters[characterName] 54 | } 55 | } 56 | 57 | const temporaryPHSMenuSetParty = () => { 58 | const newParty = [] 59 | const characterNames = Object.keys(window.data.savemap.party.phsLocked) 60 | for (let i = 0; i < characterNames.length; i++) { 61 | const name = characterNames[i] 62 | if (window.data.savemap.party.phsLocked[name] === 1) { 63 | newParty.push(name) 64 | } 65 | } 66 | for (let i = 0; i < characterNames.length; i++) { 67 | const name = characterNames[i] 68 | if ( 69 | window.data.savemap.party.phsVisibility[name] === 1 && 70 | !newParty.includes(name) 71 | ) { 72 | newParty.push(name) 73 | } 74 | } 75 | window.data.savemap.party.members = newParty.slice(0, 3) 76 | console.log( 77 | 'temporaryPHSMenuSetParty', 78 | newParty, 79 | window.data.savemap.party.members 80 | ) 81 | } 82 | export { 83 | getPlayableCharacterName, 84 | getPlayableCharacterId, 85 | setCharacterNameFromSpecialText, 86 | getCharacterSaveMap, 87 | temporaryPHSMenuSetParty 88 | } 89 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Fenrir.js 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 |
22 | 23 |
24 |
25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 | 60 |
61 | 62 |
63 | 64 | 65 | 66 | 67 | 68 | 69 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /app/battle/battle-limits.js: -------------------------------------------------------------------------------- 1 | import { getPlayableCharacterName } from '../field/field-op-codes-party-helper.js' 2 | 3 | const LIMIT_MENU_TYPES = { 4 | STANDARD: 'standard', 5 | SLOTS_TIFA: 'slots-tifa', 6 | SLOTS_CAITSITH: 'slots-caitsith' 7 | } 8 | 9 | const standardLimit = index => { 10 | return { 11 | levelsTotal: 4, 12 | limitsPerLevel: 2, 13 | limitAttackIndex: index, 14 | menuType: LIMIT_MENU_TYPES.STANDARD 15 | } 16 | } 17 | 18 | const CONFIG = { 19 | Cloud: standardLimit(0), // There is a duplicate Finishing Touch at 70 ? 20 | Barret: standardLimit(7), 21 | Tifa: { 22 | levelsTotal: 4, 23 | limitsPerLevel: 2, 24 | limitAttackIndex: 21, 25 | menuType: LIMIT_MENU_TYPES.SLOTS_TIFA 26 | }, //standardLimit(0),// 21 - 27 27 | Aeris: standardLimit(14), 28 | RedXIII: standardLimit(35), 29 | Yuffie: standardLimit(49), 30 | CaitSith: { 31 | levelsTotal: 1, 32 | limitsPerLevel: 1, 33 | limitAttackIndex: 42, 34 | limitAttackLevel2Skip: 1, 35 | menuType: [LIMIT_MENU_TYPES.STANDARD, LIMIT_MENU_TYPES.SLOTS_CAITSITH] 36 | }, // standardLimit(0), // 42-44 but attacks are: 56-60 37 | Vincent: { 38 | levelsTotal: 1, 39 | limitsPerLevel: 1, 40 | limitAttackIndex: 45, 41 | menuType: LIMIT_MENU_TYPES.STANDARD 42 | }, //standardLimit(0), // 45-48 but attacks are: 61-69 43 | Cid: standardLimit(28), 44 | YoungCloud: standardLimit(0) // Is this needed? 45 | // Sephiroth: standardLimit(0), // Required? 46 | // Chocobo: standardLimit(0) 47 | } 48 | 49 | const getLimitAttack = (playerName, limitEnum) => { 50 | const limitSplit = limitEnum.split('_') 51 | const level = parseInt(limitSplit[1]) 52 | const levelIndex = limitSplit.length > 2 ? parseInt(limitSplit[2]) : 1 53 | const limitConfig = CONFIG[playerName] 54 | let limitAttackId = 55 | limitConfig.limitAttackIndex + 56 | (level - 1) * limitConfig.limitsPerLevel + 57 | (levelIndex - 1) 58 | if (limitConfig.limitAttackLevel2Skip && level === 2) 59 | limitAttackId = limitAttackId + limitConfig.limitAttackLevel2Skip 60 | console.log( 61 | 'battleUI LIMIT: attack', 62 | playerName, 63 | limitSplit, 64 | level, 65 | levelIndex, 66 | limitConfig, 67 | limitAttackId 68 | ) 69 | return window.data.exe.limitData.limits[limitAttackId] 70 | } 71 | 72 | const getLimitMenuData = char => { 73 | const playerName = getPlayableCharacterName(char.id) 74 | const level = char.limit.level 75 | const limits = char.limit.learnedLimitSkills 76 | .filter(l => l.startsWith(`Limit_${level}`)) 77 | .map(l => getLimitAttack(playerName, l)) 78 | // console.log('battleUI LIMIT: ', char, level, limits) 79 | 80 | const menuType = Array.isArray(CONFIG[playerName].menuType) 81 | ? CONFIG[playerName].menuType[level - 1] 82 | : CONFIG[playerName].menuType 83 | return { limits, menuType } 84 | } 85 | const getActionSequenceIndexForSelectedLimit = (actor, pos) => { 86 | // TODO - Is this right? Not sure about Tifa, CaitSith, Vincent yet 87 | return 60 + (actor.data.limit.level - 1) * 2 + pos 88 | } 89 | export { 90 | getLimitMenuData, 91 | LIMIT_MENU_TYPES, 92 | getActionSequenceIndexForSelectedLimit 93 | } 94 | -------------------------------------------------------------------------------- /app/manager.mjs: -------------------------------------------------------------------------------- 1 | import { setupInputs } from './interaction/inputs.js' 2 | import { initRenderer, showStats } from './render/renderer.js' 3 | import { loadWindowTextures, loadKernelData } from './data/kernel-fetch-data.js' 4 | import { loadExeData } from './data/exe-fetch-data.js' 5 | import { loadBattleData } from './data/battle-fetch-data.js' 6 | import { loadCDData } from './data/cd-fetch-data.js' 7 | import { loadMenuTextures } from './data/menu-fetch-data.js' 8 | import { loadFieldTextures } from './data/field-fetch-data.js' 9 | import { 10 | initLoadingModule, 11 | setLoadingText, 12 | showLoadingScreen 13 | } from './loading/loading-module.js' 14 | import { loadGame, initNewSaveMap } from './data/savemap.js' 15 | import { setDefaultMediaConfig } from './media/media-module.js' 16 | import { 17 | initMenuModule, 18 | loadMenuWithWait, 19 | MENU_TYPE 20 | } from './menu/menu-module.js' 21 | import { initBattleModule } from './battle/battle-module.js' 22 | import { initBattleSwirlModule } from './battle-swirl/battle-swirl-module.js' 23 | import { initMiniGameModule } from './minigame/minigame-module.js' 24 | import { initWorldModule } from './world/world-module.js' 25 | import { bindDisplayControls } from './helpers/display-controls.js' 26 | import { waitUntilMediaCanPlay } from './helpers/media-can-play.js' 27 | import { loadZippedAssets } from './data/cache-manager.js' 28 | 29 | const initManager = async () => { 30 | // Generic Game loading 31 | window.anim.container = document.getElementById('container') 32 | if (window.config.debug.active) { 33 | showStats() 34 | } 35 | initRenderer() 36 | await initLoadingModule() 37 | console.log('loading ALL START') 38 | showLoadingScreen() 39 | setupInputs() 40 | 41 | setLoadingText('Loading Core - Step 1 of 7') 42 | await initWorldModule() // 3 json 43 | await loadKernelData() // 1 json 44 | await loadExeData() // 1 json 45 | await loadCDData() // 1 json 46 | 47 | setLoadingText('Loading Battle Data - Step 2 of 7') 48 | await loadBattleData() // 1 json 49 | 50 | setLoadingText('Loading Core Assets - Step 3 of 7 - First load only') 51 | const zip = await loadZippedAssets() 52 | setLoadingText('Loading Window Textures - Step 4 of 7') 53 | await loadWindowTextures(zip) // 1 json then 2k images, 650 kb 54 | setLoadingText('Loading Menu Textures - Step 5 of 7') 55 | await loadMenuTextures(zip) // 3 json then: menu: 5k images, 3 mb. credits: 650 images, 14 images, 1 mb 56 | setLoadingText('Loading Field Textures - Step 6 of 7') 57 | await loadFieldTextures(zip) // 1 json then 23 files, 8 kb 58 | setLoadingText('Initialising Game - Step 7 of 7') 59 | // zip = null // Clear a little memory 60 | 61 | console.log('loading ALL END') 62 | initMenuModule() 63 | initBattleSwirlModule() 64 | initBattleModule() 65 | initMiniGameModule() 66 | setDefaultMediaConfig() 67 | bindDisplayControls() 68 | await waitUntilMediaCanPlay() 69 | 70 | if (window.developerMode) { 71 | // Quick start 72 | loadGame(window.config.save.cardId, window.config.save.slotId) 73 | } else { 74 | // Correct behaviour 75 | initNewSaveMap() 76 | loadMenuWithWait(MENU_TYPE.Title) 77 | } 78 | } 79 | initManager() 80 | -------------------------------------------------------------------------------- /app/helpers/custom-log.js: -------------------------------------------------------------------------------- 1 | window.console = (function (origConsole) { 2 | if (!window.console || !origConsole) { 3 | origConsole = {} 4 | } 5 | const limit = true 6 | return { 7 | terms: [ 8 | // 'press', 9 | // 'executeOpDEBUG', 10 | // 'executeOp', 11 | // 'joinLeader' 12 | // 'loadBattle', 13 | // 'battle', 14 | // 'battleOP', 15 | // 'battleMemory', 16 | // 'battleTimer', 17 | // 'battleQueue', 18 | // 'battleStack', 19 | // 'battleMenu', 20 | // 'battleStack', 21 | // 'battleUI', 22 | // 'battleUI SLOTS', 23 | // 'CAMERA pos', 24 | // 'CAMERA focus', 25 | // 'CAMERA calcPosition', 26 | // 'sceneData' 27 | // 'getOrientedOpZ' 28 | // 'BONE' 29 | // 'LOGG' 30 | // 'battleUI LIMIT' 31 | // 'battlePointer', 32 | // 'battleQueue', 33 | // 'executeEnemyAction', 34 | // 'executePlayerAction', 35 | // 'getActionSequenceForCommand', 36 | // 'CAMERA runScriptPair', 37 | // 'battleUI', 38 | // 'battleQueue addPlayerActionToQueue', 39 | // 'battleQueue player action', 40 | // 'cannotExecuteAction', 41 | 'ACTION runActionSequence', 42 | 'battleStats' 43 | // 'ACTION' 44 | // 'HURT', 45 | // 'DAMAGE', 46 | // 'LOAD BATTLE SOUNDS', 47 | // 'ACTION triggerSound', 48 | // 'battleOp COPY', 49 | // 'battleOp GLOB', 50 | // 'battleMemory', 51 | // 'getGlobalValueFromAlias', 52 | // 'executeEnemyAction' 53 | // 'battleOP DISPLAY' 54 | // 'executeEnemyAction', 55 | // 'battleStack', 56 | // 'battleOp' 57 | // 'LOAD BATTLE SOUNDS', 58 | // 'EFFECT' 59 | // 'updateOrthoPosition' 60 | // 'renderToTexture', 61 | // 'doSwirl', 62 | // 'loadField', 63 | // 'transitionIn', 64 | // 'initialiseOpLoops', 65 | // 'initEntityInit', 66 | // 'initLoop', 67 | // 'executeScriptLoop', 68 | // 'executeScriptLoopDEBUG', 69 | // 'SCR2D', 70 | // 'textureLetter', 71 | // 'getImageTexture', 72 | // 'SET MENU TEXTURES', 73 | // 'sceneData' 74 | ], 75 | log: function () { 76 | if (limit) { 77 | for (let i = 0; i < arguments.length; i++) { 78 | const argument = arguments[i] 79 | if (typeof argument === 'string') { 80 | for (let j = 0; j < this.terms.length; j++) { 81 | const term = this.terms[j] 82 | if (argument.includes(term)) { 83 | origConsole.log.apply(origConsole, arguments) 84 | break 85 | } 86 | } 87 | } 88 | } 89 | } else { 90 | origConsole.log.apply(origConsole, arguments) 91 | } 92 | }, 93 | warn: function () { 94 | // if ( 95 | // arguments[0] !== 96 | // 'THREE.GLTFLoader: Missing min/max properties for accessor POSITION.' 97 | // ) { 98 | origConsole.warn.apply(origConsole, arguments) 99 | // } 100 | }, 101 | error: function () { 102 | origConsole.error.apply(origConsole, arguments) 103 | }, 104 | info: function (v) { 105 | origConsole.info.apply(origConsole, arguments) 106 | } 107 | } 108 | })(window.console) 109 | -------------------------------------------------------------------------------- /app/battle/battle-actions-op-loop.js: -------------------------------------------------------------------------------- 1 | import { ACTION_DATA } from './battle-actions.js' 2 | import * as actions from './battle-actions-op-actions.js' 3 | import * as movement from './battle-actions-op-movement.js' 4 | import * as control from './battle-actions-op-control.js' 5 | import { loadSound } from '../media/media-sound.js' 6 | 7 | // https://wiki.ffrtt.ru/index.php?title=FF7/Battle/Battle_Animation/Animation_Script 8 | 9 | const executeOp = async op => { 10 | console.log('ACTION execute op: START', op) 11 | switch (op.op) { 12 | case 'ROTF': // FC 13 | movement.ROTF() 14 | break 15 | case 'ROTI': 16 | movement.ROTI() 17 | break 18 | case 'ANIM': 19 | await control.ANIM(op) 20 | break 21 | case 'SOUND': 22 | actions.SOUND(op) 23 | break 24 | case 'MOVJ': 25 | movement.MOVJ(op) 26 | break 27 | case 'MOVE': 28 | movement.MOVE(op) 29 | break 30 | case 'MOVI': 31 | await movement.MOVI() 32 | break 33 | case 'HURT': 34 | actions.HURT(op) 35 | break 36 | case 'ATT': 37 | actions.ATT(op) 38 | break 39 | case 'DAMAGE': 40 | actions.DAMAGE(op) 41 | break 42 | case 'ED': 43 | movement.ED() 44 | break 45 | case 'EB': 46 | movement.EB() 47 | break 48 | case 'MOVIZ': 49 | movement.MOVIZ() 50 | break 51 | case 'SETWAIT': 52 | control.SETWAIT(op) 53 | break 54 | case 'WAIT': 55 | await control.WAIT() 56 | break 57 | case 'NAME': 58 | control.NAME() 59 | break 60 | case 'MSG': 61 | control.MSG(op) 62 | break 63 | case 'RET': 64 | control.RET() 65 | break 66 | case 'DUST': 67 | actions.DUST() 68 | break 69 | default: 70 | // window.alert( 71 | // `--------- CAMERA POSITION OP: ${op.op} - NOT YET IMPLEMENTED ---------` 72 | // ) 73 | break 74 | } 75 | console.log('ACTION execute op: END') 76 | } 77 | 78 | const runActionSequence = async sequence => { 79 | console.log('ACTION runActionSequence: START', sequence, ACTION_DATA) 80 | // TODO - Preload anything that needs to be loaded, sounds, assets etc 81 | loadSound(26) 82 | 83 | // Add next anim so we can 'hold' it - NOPE, NOT IT! 84 | // for (let i = sequence.length - 1; i >= 0; i--) { 85 | // if (sequence[i].op === 'ANIM') { 86 | // for (let j = i - 1; j >= 0; j--) { 87 | // if (sequence[j].op === 'ANIM') { 88 | // sequence[j].hold = sequence[i].animation 89 | // break 90 | // } 91 | // } 92 | // } 93 | // } 94 | 95 | // Anims are sync apart from if MOVI is after it?! Looks ok ?! 96 | for (let i = sequence.length - 1; i >= 0; i--) { 97 | if (sequence[i].op === 'MOVI') { 98 | for (let j = i - 1; j >= 0; j--) { 99 | if (sequence[j].op === 'ANIM') { 100 | sequence[j].async = true 101 | break 102 | } 103 | } 104 | } 105 | } 106 | 107 | for (const op of sequence) { 108 | await executeOp(op) 109 | if (op.op === 'RET') { 110 | break 111 | } 112 | } 113 | 114 | // TODO - Make this better - Play default 'idle' animation, eg 0 or whatever is appropriate for injured, dead, status afflicted etc 115 | ACTION_DATA.actors.attacker.model.userData.playAnimation(0) 116 | console.log('ACTION runActionSequence: END') 117 | } 118 | export { runActionSequence } 119 | -------------------------------------------------------------------------------- /app/battle/battle-stack-memory.js: -------------------------------------------------------------------------------- 1 | import { 2 | getGlobalValueFromAlias, 3 | setGlobalValueFromAlias 4 | } from './battle-stack-memory-global-alias.js' 5 | import { getPlayerValueFromAlias } from './battle-stack-memory-player.js' 6 | 7 | const variables = { 8 | local: Array.from({ length: 10 }, Object), 9 | global: {}, 10 | actor: Array.from({ length: 10 }, Object) 11 | } 12 | 13 | /* 14 | Each address references a specific bit of memory rather than a byte which means 15 | individual bits can be manipulated without using BIT-wise operations in script. 16 | Every 8 address values is the beginning of a byte (ie. 0x0000, 0x0008, etc.). 17 | The code used to access an address determines whether a bit, byte, word, or dword is being read/written. 18 | 19 | https://stackoverflow.com/questions/6972717/how-do-i-create-bit-array-in-javascript 20 | 21 | Just do something very simple for now 22 | */ 23 | 24 | const logMemory = () => { 25 | console.log('battleMemory logMemory', JSON.stringify(variables, null, 2)) 26 | } 27 | const initAllVariables = () => { 28 | variables.local = Array.from({ length: 10 }, Object) 29 | variables.global = {} 30 | variables.actor = Array.from({ length: 10 }, Object) 31 | return variables 32 | } 33 | 34 | const getLocalValue = (actorIndex, addressHex, returnType) => { 35 | const value = variables.local[actorIndex][addressHex] 36 | if (value === undefined) return 0b0 37 | // TODO - Do something with returnType? 38 | console.log( 39 | 'battleMemory getLocalValue', 40 | actorIndex, 41 | addressHex, 42 | returnType, 43 | value 44 | ) 45 | return value 46 | } 47 | const setLocalValue = (actorIndex, addressHex, value) => { 48 | variables.local[actorIndex][addressHex] = value 49 | console.log('battleMemory setLocalValue', actorIndex, addressHex, value) 50 | } 51 | const getGlobalValue = (actorIndex, addressHex, returnType) => { 52 | const value = getGlobalValueFromAlias( 53 | variables.global, 54 | actorIndex, 55 | addressHex 56 | ) 57 | console.log('battleMemory getGlobalValue', addressHex, returnType, value) 58 | return value 59 | } 60 | const setGlobalValue = (addressHex, value) => { 61 | console.log('battleMemory setGlobalValue', addressHex, value) 62 | setGlobalValueFromAlias(variables.global, addressHex, value) 63 | } 64 | const getActorValueAll = (actorIndex, addressHex, returnType) => { 65 | // TODO 66 | const value = Array.from({ length: 10 }, () => 0) 67 | getPlayerValueFromAlias(actorIndex, variables.actor, addressHex) 68 | console.log('battleMemory getActorValueAll', addressHex, returnType, value) 69 | return value 70 | } 71 | // const getActorValue = (actorIndex, address, returnType) => { // Is this every specifically used?! 72 | 73 | // } 74 | const setActorValue = (actorIndex, address, value) => {} 75 | 76 | const getBitMaskFromCriteria = (list, criteria) => 77 | list.reduce((mask, item, i) => mask | (criteria(item) << i), 0) 78 | const getBitMaskFromEnums = (enumList, items) => 79 | items.reduce((mask, item) => mask | enumList[item], 0) 80 | const getObjectByBitmask = (array, bitmask) => 81 | array.find((_, i) => (bitmask & (1 << i)) !== 0) 82 | const getObjectsByBitmask = (array, bitmask) => 83 | array.filter((_, i) => (bitmask & (1 << i)) !== 0) 84 | 85 | export { 86 | initAllVariables, 87 | logMemory, 88 | getLocalValue, 89 | setLocalValue, 90 | getGlobalValue, 91 | setGlobalValue, 92 | getActorValueAll, 93 | setActorValue, 94 | getBitMaskFromCriteria, 95 | getBitMaskFromEnums, 96 | getObjectByBitmask, 97 | getObjectsByBitmask 98 | } 99 | -------------------------------------------------------------------------------- /app/helpers/base64-binary.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2011, Daniel Guerrero 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL DANIEL GUERRERO BE LIABLE FOR ANY 17 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | */ 24 | 25 | /** 26 | * Uses the new array typed in javascript to binary base64 encode/decode 27 | * at the moment just decodes a binary base64 encoded 28 | * into either an ArrayBuffer (decodeArrayBuffer) 29 | * or into an Uint8Array (decode) 30 | * 31 | * References: 32 | * https://developer.mozilla.org/en/JavaScript_typed_arrays/ArrayBuffer 33 | * https://developer.mozilla.org/en/JavaScript_typed_arrays/Uint8Array 34 | */ 35 | 36 | const Base64Binary = { 37 | _keyStr: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', 38 | 39 | /* will return a Uint8Array type */ 40 | decodeArrayBuffer: function (input) { 41 | const bytes = (input.length / 4) * 3 42 | const ab = new ArrayBuffer(bytes) 43 | this.decode(input, ab) 44 | 45 | return ab 46 | }, 47 | 48 | removePaddingChars: function (input) { 49 | const lkey = this._keyStr.indexOf(input.charAt(input.length - 1)) 50 | if (lkey == 64) { 51 | return input.substring(0, input.length - 1) 52 | } 53 | return input 54 | }, 55 | 56 | decode: function (input, arrayBuffer) { 57 | // get last chars to see if are valid 58 | input = this.removePaddingChars(input) 59 | input = this.removePaddingChars(input) 60 | 61 | const bytes = parseInt((input.length / 4) * 3, 10) 62 | 63 | let uarray 64 | let chr1, chr2, chr3 65 | let enc1, enc2, enc3, enc4 66 | let i = 0 67 | let j = 0 68 | 69 | if (arrayBuffer) uarray = new Uint8Array(arrayBuffer) 70 | else uarray = new Uint8Array(bytes) 71 | 72 | input = input.replace(/[^A-Za-z0-9\+\/\=]/g, '') 73 | 74 | for (i = 0; i < bytes; i += 3) { 75 | // get the 3 octects in 4 ascii chars 76 | enc1 = this._keyStr.indexOf(input.charAt(j++)) 77 | enc2 = this._keyStr.indexOf(input.charAt(j++)) 78 | enc3 = this._keyStr.indexOf(input.charAt(j++)) 79 | enc4 = this._keyStr.indexOf(input.charAt(j++)) 80 | 81 | chr1 = (enc1 << 2) | (enc2 >> 4) 82 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2) 83 | chr3 = ((enc3 & 3) << 6) | enc4 84 | 85 | uarray[i] = chr1 86 | if (enc3 != 64) uarray[i + 1] = chr2 87 | if (enc4 != 64) uarray[i + 2] = chr3 88 | } 89 | 90 | return uarray 91 | } 92 | } 93 | export { Base64Binary } 94 | -------------------------------------------------------------------------------- /app/battle/battle-module.js: -------------------------------------------------------------------------------- 1 | import { 2 | setupScenes, 3 | startBattleRenderingLoop, 4 | sceneGroup, 5 | BATTLE_TWEEN_GROUP, 6 | setBattleTickActive, 7 | BATTLE_TWEEN_UI_GROUP 8 | } from './battle-scene.js' 9 | import { initBattleKeypressActions } from './battle-controls.js' 10 | import { importModels } from './battle-3d.js' 11 | import { setupBattle } from './battle-setup.js' 12 | import { initAllVariables } from './battle-stack-memory.js' 13 | import { initBattleQueue } from './battle-queue.js' 14 | import { executeAllInitScripts } from './battle-stack.js' 15 | import { initBattleMenu } from './battle-menu.js' 16 | import { setLoadingText, showLoadingScreen } from '../loading/loading-module.js' 17 | import { showBattleMessageForFormation } from './battle-formation.js' 18 | import { executeInitialCameraScript } from './battle-camera.js' 19 | import { preLoadBattleSounds } from './battle-actions.js' 20 | let BATTLE_PROMISE 21 | 22 | /* 23 | https://gamefaqs.gamespot.com/ps/197341-final-fantasy-vii/faqs/77403 24 | https://finalfantasy.fandom.com/wiki/Final_Fantasy_VII_battle_system 25 | */ 26 | 27 | const initBattleModule = () => { 28 | setupScenes() 29 | initBattleKeypressActions() 30 | } 31 | 32 | const cleanSceneGroup = () => { 33 | while (sceneGroup.children.length) { 34 | sceneGroup.remove(sceneGroup.children[0]) 35 | } 36 | 37 | BATTLE_TWEEN_UI_GROUP.removeAll() 38 | BATTLE_TWEEN_GROUP.removeAll() 39 | // while (orthoScene.children.length) { 40 | // orthoScene.remove(orthoScene.children[0]) 41 | // } 42 | } 43 | const preLoadBattle = async (battleId, options) => { 44 | setLoadingText('Loading battle...') 45 | showLoadingScreen(false) 46 | 47 | console.log('battle preload: START') 48 | cleanSceneGroup() 49 | 50 | const currentBattle = setupBattle(battleId) // TODO, add from random / world map etc 51 | // console.log('loadBattle', battleId, options) 52 | 53 | await importModels(currentBattle) 54 | // await loadTempBattle2d(`${currentBattle.sceneId} - ${currentBattle.formationId}`) 55 | 56 | currentBattle.memory = initAllVariables() 57 | initBattleQueue(currentBattle) 58 | // await executeAllInitScripts(currentBattle) // For some reason this takes a huge amount of time, need to look into it 59 | await initBattleMenu(currentBattle) 60 | 61 | // Debugging 62 | window.a0 = window?.currentBattle?.actors[0] 63 | window.a1 = window?.currentBattle?.actors[1] 64 | window.a2 = window?.currentBattle?.actors[2] 65 | window.a4 = window?.currentBattle?.actors[4] 66 | window.a0p = window?.currentBattle?.actors[0]?.model?.scene?.position 67 | window.a1p = window?.currentBattle?.actors[1]?.model?.scene?.position 68 | window.a2p = window?.currentBattle?.actors[2]?.model?.scene?.position 69 | window.a4p = window?.currentBattle?.actors[4]?.model?.scene?.position 70 | 71 | await preLoadBattleSounds() 72 | console.log('battle preload: END') 73 | return currentBattle 74 | } 75 | const loadBattle = async (battleId, options) => { 76 | const currentBattle = await preLoadBattle(battleId, options) 77 | window.anim.clock.start() 78 | startBattleRenderingLoop() 79 | window.currentBattle.ui.battleStartPlane.userData.triggerUncovering() 80 | await executeInitialCameraScript(currentBattle) 81 | console.log('battle loadBattle: START') 82 | showBattleMessageForFormation() 83 | if (!window.location.host.includes('localhost')) { 84 | window.alert('Placeholder battles - Press Y to skip') // TEMP - Need to remove 85 | } 86 | setBattleTickActive(true) 87 | return new Promise(resolve => { 88 | BATTLE_PROMISE = resolve 89 | }) 90 | } 91 | 92 | const resolveBattlePromise = () => { 93 | if (BATTLE_PROMISE) { 94 | BATTLE_PROMISE() 95 | } 96 | } 97 | export { initBattleModule, loadBattle, resolveBattlePromise, preLoadBattle } 98 | -------------------------------------------------------------------------------- /workings-out/output/field-model-lighting.json: -------------------------------------------------------------------------------- 1 | { 2 | "nonMatchingFields": [ 3 | "qa", 4 | "qc", 5 | "qd", 6 | "blackbg1", 7 | "blackbg2", 8 | "blackbgb", 9 | "blackbgk", 10 | "mds7st2", 11 | "mds7_w2", 12 | "mds7pb_1", 13 | "mds7pb_2", 14 | "mds7plr1", 15 | "sbwy4_1", 16 | "sbwy4_3", 17 | "sbwy4_6", 18 | "chrin_2", 19 | "colne_1", 20 | "onna_4", 21 | "onna_52", 22 | "wcrimb_1", 23 | "md0", 24 | "sinbil_1", 25 | "sinbil_2", 26 | "blinele", 27 | "blin66_5", 28 | "blin70_1", 29 | "blin70_2", 30 | "trackin", 31 | "trackin2", 32 | "sinin2_1", 33 | "nvmkin1", 34 | "elminn_2", 35 | "elmin1_1", 36 | "elmin2_2", 37 | "elmin3_2", 38 | "elmtow", 39 | "elmin4_1", 40 | "elmin4_2", 41 | "farm", 42 | "psdun_2", 43 | "psdun_3", 44 | "psdun_4", 45 | "junonr4", 46 | "junmin2", 47 | "junmin3", 48 | "juninn", 49 | "junpb_2", 50 | "junmin4", 51 | "junin1", 52 | "junin6", 53 | "junbin5", 54 | "subin_2b", 55 | "subin_3", 56 | "ujunon1", 57 | "ujun_w", 58 | "shpin_22", 59 | "shpin_3", 60 | "del2", 61 | "delmin12", 62 | "mtcrl_3", 63 | "mtcrl_4", 64 | "mtcrl_5", 65 | "mtcrl_7", 66 | "jetin1", 67 | "ghotin_2", 68 | "gldinfo", 69 | "gonjun1", 70 | "gnmk", 71 | "gninn", 72 | "goson", 73 | "cos_btm", 74 | "cosin2", 75 | "cosin3", 76 | "cosmin7", 77 | "cos_top", 78 | "bugin1a", 79 | "bugin1b", 80 | "gidun_1", 81 | "gidun_2", 82 | "gidun_4", 83 | "seto1", 84 | "rkt_w", 85 | "rkt_i", 86 | "rktsid", 87 | "rktmin2", 88 | "rcktin2", 89 | "utmin1", 90 | "yufy2", 91 | "uttmpin1", 92 | "uttmpin2", 93 | "uttmpin4", 94 | "jtempl", 95 | "jtemplb", 96 | "kuro_1", 97 | "kuro_2", 98 | "kuro_3", 99 | "kuro_4", 100 | "kuro_5", 101 | "kuro_6", 102 | "kuro_7", 103 | "kuro_8", 104 | "kuro_82", 105 | "kuro_9", 106 | "losin1", 107 | "losin2", 108 | "losin3", 109 | "losinn", 110 | "whitein", 111 | "snw_w", 112 | "snmin1", 113 | "snmin2", 114 | "gaia_1", 115 | "gaiin_2", 116 | "gaia_2", 117 | "gaia_31", 118 | "gaiin_5", 119 | "trnad_1", 120 | "trnad_2", 121 | "itown1b", 122 | "itown2", 123 | "ithill", 124 | "itown_w", 125 | "itown_i", 126 | "itown_m", 127 | "md8_5", 128 | "tunnel_4", 129 | "tunnel_5", 130 | "las0_4", 131 | "las0_5", 132 | "las1_1", 133 | "las1_2", 134 | "las1_3", 135 | "las1_4", 136 | "hill2", 137 | "jtemplc", 138 | "tunnel_6", 139 | "md8_52" 140 | ], 141 | "errorFields": [ 142 | "dummy", 143 | "wm0", 144 | "wm1", 145 | "wm2", 146 | "wm3", 147 | "wm4", 148 | "wm5", 149 | "wm6", 150 | "wm7", 151 | "wm8", 152 | "wm9", 153 | "wm10", 154 | "wm11", 155 | "wm12", 156 | "wm13", 157 | "wm14", 158 | "wm15", 159 | "wm16", 160 | "wm17", 161 | "wm18", 162 | "wm19", 163 | "wm20", 164 | "wm21", 165 | "wm22", 166 | "wm23", 167 | "wm24", 168 | "wm25", 169 | "wm26", 170 | "wm27", 171 | "wm28", 172 | "wm29", 173 | "wm30", 174 | "wm31", 175 | "wm32", 176 | "wm33", 177 | "wm34", 178 | "wm35", 179 | "wm36", 180 | "wm37", 181 | "wm38", 182 | "wm39", 183 | "wm40", 184 | "wm41", 185 | "wm42", 186 | "wm43", 187 | "wm44", 188 | "wm45", 189 | "wm46", 190 | "wm47", 191 | "wm48", 192 | "wm49", 193 | "wm50", 194 | "wm51", 195 | "wm52", 196 | "wm53", 197 | "wm54", 198 | "wm55", 199 | "wm56", 200 | "wm57", 201 | "wm58", 202 | "wm59", 203 | "wm60", 204 | "wm61", 205 | "wm62", 206 | "wm63", 207 | "qe", 208 | "blackbga", 209 | "blackbgf", 210 | "blackbgg", 211 | "whitebg1", 212 | "whitebg2", 213 | "onna_1", 214 | "onna_3", 215 | "onna_6", 216 | "blin69_2", 217 | "trap", 218 | "convil_3", 219 | "junmon", 220 | "subin_4", 221 | "pass", 222 | "hyou14", 223 | "xmvtes", 224 | "fallp", 225 | "m_endo", 226 | "fship_26", 227 | "" 228 | ] 229 | } 230 | -------------------------------------------------------------------------------- /app/field/field-op-codes-flow-helper.js: -------------------------------------------------------------------------------- 1 | import { sleep } from '../helpers/helpers.js' 2 | import { getBankData } from '../data/savemap.js' 3 | 4 | const executeCompare = (a, operator, b) => { 5 | if (operator === 0) return a === b 6 | if (operator === 1) return a !== b 7 | if (operator === 2) return a > b 8 | if (operator === 3) return a < b 9 | if (operator === 4) return a >= b 10 | if (operator === 5) return a <= b 11 | if (operator === 6) return a & b 12 | if (operator === 7) return a ^ b 13 | if (operator === 8) return a | b 14 | if (operator === 9) return a & (1 << b) 15 | if (operator === 10) return !(a & (1 << b)) 16 | window.alert('unknown operator', operator) 17 | return false 18 | } 19 | const printCompare = (a, operator, b) => { 20 | if (operator === 0) return `${a} === ${b}` 21 | if (operator === 1) return `${a} !== ${b}` 22 | if (operator === 2) return `${a} > ${b}` 23 | if (operator === 3) return `${a} < ${b}` 24 | if (operator === 4) return `${a} >= ${b}` 25 | if (operator === 5) return `${a} <= ${b}` 26 | if (operator === 6) return `${a} & ${b}` 27 | if (operator === 7) return `${a} ^ ${b}` 28 | if (operator === 8) return `${a} | ${b}` 29 | if (operator === 9) return `${a} & (1 << ${b})` 30 | if (operator === 10) return `!((${a} & (1 << ${b})))` 31 | // window.alert('unknown operator', operator) 32 | return false 33 | } 34 | const getOpIndexForByteIndex = (ops, goto) => { 35 | let minusOne 36 | for (let i = 0; i < ops.length; i++) { 37 | if (ops[i].byteIndex === goto) { 38 | return { goto: i, gotoByteIndex: goto } 39 | } 40 | if (ops[i].byteIndex === goto - 1) { 41 | minusOne = { goto: i, gotoByteIndex: goto } 42 | } 43 | } 44 | 45 | // This should not really happen, bugs found: 46 | // window.alert(`No matching byteIndex for goto - ${goto} - ${JSON.stringify(closest)}`) 47 | 48 | if (minusOne) { 49 | console.log( 50 | `FLOW ERROR - No matching byteIndex for goto - ${goto} - using minusOne - ${JSON.stringify( 51 | minusOne 52 | )}` 53 | ) 54 | return minusOne 55 | } 56 | console.log( 57 | `FLOW ERROR - No matching byteIndex for goto - ${goto} - no minusOne found` 58 | ) 59 | // return closest 60 | return { exit: true } 61 | } 62 | 63 | const compareFromBankData = (ops, op) => { 64 | const leftCompare = op.b1 === 0 ? op.a : getBankData(op.b1, op.a) 65 | const rightCompare = op.b2 === 0 ? op.v : getBankData(op.b2, op.v) 66 | const result = executeCompare(leftCompare, op.c, rightCompare) 67 | const printedCompare = printCompare(leftCompare, op.c, rightCompare) 68 | if (op.b1 === 3 && (op.a === 224 || op.a === 9)) { 69 | console.log( 70 | ' printedCompare', 71 | `Var[${op.b1}][${op.a}]`, 72 | `Var[${op.b2}][${op.v}]`, 73 | '->', 74 | printedCompare, 75 | '->', 76 | result, 77 | '->', 78 | getOpIndexForByteIndex(ops, op.goto) 79 | ) 80 | } 81 | 82 | // await sleep(2000) 83 | if (result) { 84 | // Continue inside if statement 85 | return {} 86 | } else { 87 | // Bypass if statement 88 | return getOpIndexForByteIndex(ops, op.goto) 89 | } 90 | } 91 | const KEYS = { 92 | l2: 1, // Camera 93 | r2: 2, // Target 94 | l1: 4, // PageUp 95 | r1: 8, // PageDown 96 | triangle: 16, // Menu 97 | o: 32, // OK 98 | x: 64, // Cancel 99 | square: 128, // Switch 100 | select: 256, // Assist 101 | unknown1: 512, 102 | unknown2: 1024, 103 | start: 2048, // Start 104 | up: 4096, // Up 105 | right: 8192, // Right 106 | down: 16384, // Down 107 | left: 32768 // Left 108 | } 109 | const getKeysFromBytes = val => { 110 | const enums = [] 111 | for (const prop in KEYS) { 112 | if ((val & KEYS[prop]) === KEYS[prop]) { 113 | // Bitwise matching 114 | enums.push(prop) 115 | } 116 | } 117 | return enums 118 | } 119 | export { getOpIndexForByteIndex, compareFromBankData, getKeysFromBytes } 120 | -------------------------------------------------------------------------------- /app/field/field-controls.js: -------------------------------------------------------------------------------- 1 | import { getKeyPressEmitter } from '../interaction/inputs.js' 2 | import { togglePositionHelperVisility } from './field-position-helpers.js' 3 | import { 4 | isActionInProgress, 5 | transitionOutAndLoadMenu, 6 | processTalkContactTrigger, 7 | togglePauseField 8 | } from './field-actions.js' 9 | import { 10 | nextPageOrCloseActiveDialogs, 11 | navigateChoice, 12 | isChoiceActive 13 | } from './field-dialog-helper.js' 14 | import { isMenuEnabled } from './field-module.js' 15 | import { MENU_TYPE } from '../menu/menu-module.js' 16 | import { stopCurrentMovie } from '../media/media-movies.js' 17 | 18 | let INIT_COMPLETE = false 19 | 20 | const areFieldControlsActive = () => { 21 | return window.anim.activeScene === 'field' 22 | } 23 | const initFieldKeypressActions = () => { 24 | if (INIT_COMPLETE) { 25 | return 26 | } 27 | getKeyPressEmitter().on('o', firstPress => { 28 | if (areFieldControlsActive() && firstPress) { 29 | nextPageOrCloseActiveDialogs() 30 | } 31 | 32 | if ( 33 | areFieldControlsActive() && 34 | firstPress && 35 | window.currentField.playableCharacter 36 | ) { 37 | // Check talk request - Initiate talk 38 | console.log('o', isActionInProgress()) 39 | // Probably need to look at a more intelligent way to define which actions are performed 40 | // Should really be done in the rendering loop for collision 41 | processTalkContactTrigger() 42 | } 43 | }) 44 | 45 | getKeyPressEmitter().on('r1', firstPress => { 46 | if ( 47 | areFieldControlsActive() && 48 | firstPress && 49 | isActionInProgress() === 'talk' 50 | ) { 51 | // console.log('r1', isActionInProgress()) 52 | // clearActionInProgress() 53 | // setPlayableCharacterIsInteracting(false) 54 | } 55 | }) 56 | 57 | getKeyPressEmitter().on('triangle', async firstPress => { 58 | if (areFieldControlsActive() && firstPress && isMenuEnabled()) { 59 | // Also need to check is menu is disabled 60 | // Toggle position helper visibility 61 | console.log('triangle', isActionInProgress()) 62 | transitionOutAndLoadMenu(MENU_TYPE.MainMenu, 1) 63 | } 64 | }) 65 | getKeyPressEmitter().on('r2', async firstPress => { 66 | if ( 67 | areFieldControlsActive() && 68 | firstPress && 69 | isActionInProgress() === 'menu' 70 | ) { 71 | // // Toggle position helper visibility 72 | // console.log('r2', isActionInProgress()) 73 | // unfreezeField() 74 | } 75 | }) 76 | getKeyPressEmitter().on('start', firstPress => { 77 | if (areFieldControlsActive() && firstPress) { 78 | // For testing, can remove later 79 | togglePauseField() 80 | } 81 | }) 82 | getKeyPressEmitter().on('select', firstPress => { 83 | if (areFieldControlsActive() && firstPress) { 84 | // Toggle position helper visibility 85 | togglePositionHelperVisility() 86 | } 87 | }) 88 | 89 | getKeyPressEmitter().on('l1', async firstPress => { 90 | if (areFieldControlsActive() && firstPress) { 91 | console.log('controls l1') 92 | // transitionOutAndLoadMenu(MENU_TYPE.Shop, 6) 93 | // transitionOutAndLoadMenu(MENU_TYPE.CharacterNameEntry, 0x64) 94 | // transitionOutAndLoadMenu(MENU_TYPE.GameOver) 95 | } 96 | }) 97 | getKeyPressEmitter().on('l2', async firstPress => { 98 | if (areFieldControlsActive() && firstPress) { 99 | await nextPageOrCloseActiveDialogs() 100 | stopCurrentMovie() 101 | } 102 | }) 103 | 104 | getKeyPressEmitter().on('up', firstPress => { 105 | if (areFieldControlsActive() && isChoiceActive) { 106 | console.log('navigate choice UP') 107 | navigateChoice(false) 108 | } 109 | }) 110 | getKeyPressEmitter().on('down', firstPress => { 111 | if (areFieldControlsActive() && isChoiceActive) { 112 | console.log('navigate choice DOWN') 113 | navigateChoice(true) 114 | } 115 | }) 116 | 117 | INIT_COMPLETE = true 118 | } 119 | 120 | export { initFieldKeypressActions } 121 | -------------------------------------------------------------------------------- /app/battle/battle-controls.js: -------------------------------------------------------------------------------- 1 | import { getKeyPressEmitter, KEY } from '../interaction/inputs.js' 2 | import { setLastBattleResult } from '../field/field-battle.js' 3 | import { resolveBattlePromise } from './battle-module.js' 4 | import { togglePauseBattle, BATTLE_PAUSED } from './battle-scene.js' 5 | import { 6 | sendKeyPressToBattleMenu, 7 | toggleHelperText, 8 | toggleTargetLabel 9 | } from './battle-menu.js' 10 | import { cycleActiveSelectionPlayer } from './battle-queue.js' 11 | import { DATA, temporarilyConcealCommands } from './battle-menu-command.js' 12 | 13 | const areBattleControlsActive = () => { 14 | return window.anim.activeScene === 'battle' 15 | } 16 | 17 | const initBattleKeypressActions = () => { 18 | getKeyPressEmitter().on(KEY.L2, firstPress => { 19 | if (areBattleControlsActive() && firstPress && !BATTLE_PAUSED) { 20 | console.log('press x') 21 | // Temp 22 | setLastBattleResult(true, false) 23 | resolveBattlePromise() 24 | } 25 | }) 26 | getKeyPressEmitter().on(KEY.START, firstPress => { 27 | if (areBattleControlsActive() && firstPress) { 28 | console.log('press start') 29 | togglePauseBattle() 30 | } 31 | }) 32 | getKeyPressEmitter().on(KEY.SELECT, firstPress => { 33 | if (areBattleControlsActive() && firstPress && !BATTLE_PAUSED) { 34 | console.log('press select') 35 | toggleHelperText() 36 | } 37 | }) 38 | 39 | getKeyPressEmitter().on(KEY.TRIANGLE, firstPress => { 40 | if (areBattleControlsActive() && firstPress && !BATTLE_PAUSED) { 41 | console.log('press triangle') 42 | if (DATA.state === 'conceal') return 43 | if (DATA.state.startsWith('slots')) return 44 | window.currentBattle.ui.battlePointer.closeIfOpen() 45 | cycleActiveSelectionPlayer() 46 | } 47 | }) 48 | getKeyPressEmitter().on(KEY.SQUARE, firstPress => { 49 | if (areBattleControlsActive()) { 50 | // console.log('press square value:', firstPress) 51 | if (firstPress === -1) { 52 | console.log('press square: ended') 53 | temporarilyConcealCommands(true) 54 | } else if (firstPress) { 55 | console.log('press square: started') 56 | temporarilyConcealCommands(false) 57 | } 58 | } 59 | }) 60 | 61 | getKeyPressEmitter().on(KEY.O, firstPress => { 62 | if (areBattleControlsActive() && !BATTLE_PAUSED) { 63 | sendKeyPressToBattleMenu(KEY.O) 64 | } 65 | }) 66 | getKeyPressEmitter().on(KEY.X, firstPress => { 67 | if (areBattleControlsActive() && !BATTLE_PAUSED) { 68 | sendKeyPressToBattleMenu(KEY.X) 69 | } 70 | }) 71 | getKeyPressEmitter().on(KEY.UP, firstPress => { 72 | if (areBattleControlsActive() && !BATTLE_PAUSED) { 73 | sendKeyPressToBattleMenu(KEY.UP) 74 | } 75 | }) 76 | getKeyPressEmitter().on(KEY.DOWN, firstPress => { 77 | if (areBattleControlsActive() && !BATTLE_PAUSED) { 78 | sendKeyPressToBattleMenu(KEY.DOWN) 79 | } 80 | }) 81 | getKeyPressEmitter().on(KEY.LEFT, firstPress => { 82 | if (areBattleControlsActive() && !BATTLE_PAUSED) { 83 | sendKeyPressToBattleMenu(KEY.LEFT) 84 | } 85 | }) 86 | getKeyPressEmitter().on(KEY.RIGHT, firstPress => { 87 | if (areBattleControlsActive() && !BATTLE_PAUSED) { 88 | sendKeyPressToBattleMenu(KEY.RIGHT) 89 | } 90 | }) 91 | getKeyPressEmitter().on(KEY.L1, firstPress => { 92 | if (areBattleControlsActive() && !BATTLE_PAUSED) { 93 | sendKeyPressToBattleMenu(KEY.L1) 94 | } 95 | }) 96 | getKeyPressEmitter().on(KEY.R1, firstPress => { 97 | if (areBattleControlsActive() && !BATTLE_PAUSED) { 98 | sendKeyPressToBattleMenu(KEY.R1) 99 | } 100 | }) 101 | getKeyPressEmitter().on(KEY.L2, firstPress => { 102 | if (areBattleControlsActive() && !BATTLE_PAUSED) { 103 | sendKeyPressToBattleMenu(KEY.L2) 104 | } 105 | }) 106 | getKeyPressEmitter().on(KEY.R2, firstPress => { 107 | if (areBattleControlsActive() && !BATTLE_PAUSED) { 108 | toggleTargetLabel() 109 | } 110 | }) 111 | } 112 | export { initBattleKeypressActions } 113 | -------------------------------------------------------------------------------- /workings-out/world-shader.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Curved Plane with Texture 7 | 11 | 12 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 32 | 33 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /assets/threejs-r148/examples/jsm/loaders/FontLoader.js: -------------------------------------------------------------------------------- 1 | import { 2 | FileLoader, 3 | Loader, 4 | ShapePath 5 | } from 'three'; 6 | 7 | class FontLoader extends Loader { 8 | 9 | constructor( manager ) { 10 | 11 | super( manager ); 12 | 13 | } 14 | 15 | load( url, onLoad, onProgress, onError ) { 16 | 17 | const scope = this; 18 | 19 | const loader = new FileLoader( this.manager ); 20 | loader.setPath( this.path ); 21 | loader.setRequestHeader( this.requestHeader ); 22 | loader.setWithCredentials( this.withCredentials ); 23 | loader.load( url, function ( text ) { 24 | 25 | const font = scope.parse( JSON.parse( text ) ); 26 | 27 | if ( onLoad ) onLoad( font ); 28 | 29 | }, onProgress, onError ); 30 | 31 | } 32 | 33 | parse( json ) { 34 | 35 | return new Font( json ); 36 | 37 | } 38 | 39 | } 40 | 41 | // 42 | 43 | class Font { 44 | 45 | constructor( data ) { 46 | 47 | this.isFont = true; 48 | 49 | this.type = 'Font'; 50 | 51 | this.data = data; 52 | 53 | } 54 | 55 | generateShapes( text, size = 100 ) { 56 | 57 | const shapes = []; 58 | const paths = createPaths( text, size, this.data ); 59 | 60 | for ( let p = 0, pl = paths.length; p < pl; p ++ ) { 61 | 62 | shapes.push( ...paths[ p ].toShapes() ); 63 | 64 | } 65 | 66 | return shapes; 67 | 68 | } 69 | 70 | } 71 | 72 | function createPaths( text, size, data ) { 73 | 74 | const chars = Array.from( text ); 75 | const scale = size / data.resolution; 76 | const line_height = ( data.boundingBox.yMax - data.boundingBox.yMin + data.underlineThickness ) * scale; 77 | 78 | const paths = []; 79 | 80 | let offsetX = 0, offsetY = 0; 81 | 82 | for ( let i = 0; i < chars.length; i ++ ) { 83 | 84 | const char = chars[ i ]; 85 | 86 | if ( char === '\n' ) { 87 | 88 | offsetX = 0; 89 | offsetY -= line_height; 90 | 91 | } else { 92 | 93 | const ret = createPath( char, scale, offsetX, offsetY, data ); 94 | offsetX += ret.offsetX; 95 | paths.push( ret.path ); 96 | 97 | } 98 | 99 | } 100 | 101 | return paths; 102 | 103 | } 104 | 105 | function createPath( char, scale, offsetX, offsetY, data ) { 106 | 107 | const glyph = data.glyphs[ char ] || data.glyphs[ '?' ]; 108 | 109 | if ( ! glyph ) { 110 | 111 | console.error( 'THREE.Font: character "' + char + '" does not exists in font family ' + data.familyName + '.' ); 112 | 113 | return; 114 | 115 | } 116 | 117 | const path = new ShapePath(); 118 | 119 | let x, y, cpx, cpy, cpx1, cpy1, cpx2, cpy2; 120 | 121 | if ( glyph.o ) { 122 | 123 | const outline = glyph._cachedOutline || ( glyph._cachedOutline = glyph.o.split( ' ' ) ); 124 | 125 | for ( let i = 0, l = outline.length; i < l; ) { 126 | 127 | const action = outline[ i ++ ]; 128 | 129 | switch ( action ) { 130 | 131 | case 'm': // moveTo 132 | 133 | x = outline[ i ++ ] * scale + offsetX; 134 | y = outline[ i ++ ] * scale + offsetY; 135 | 136 | path.moveTo( x, y ); 137 | 138 | break; 139 | 140 | case 'l': // lineTo 141 | 142 | x = outline[ i ++ ] * scale + offsetX; 143 | y = outline[ i ++ ] * scale + offsetY; 144 | 145 | path.lineTo( x, y ); 146 | 147 | break; 148 | 149 | case 'q': // quadraticCurveTo 150 | 151 | cpx = outline[ i ++ ] * scale + offsetX; 152 | cpy = outline[ i ++ ] * scale + offsetY; 153 | cpx1 = outline[ i ++ ] * scale + offsetX; 154 | cpy1 = outline[ i ++ ] * scale + offsetY; 155 | 156 | path.quadraticCurveTo( cpx1, cpy1, cpx, cpy ); 157 | 158 | break; 159 | 160 | case 'b': // bezierCurveTo 161 | 162 | cpx = outline[ i ++ ] * scale + offsetX; 163 | cpy = outline[ i ++ ] * scale + offsetY; 164 | cpx1 = outline[ i ++ ] * scale + offsetX; 165 | cpy1 = outline[ i ++ ] * scale + offsetY; 166 | cpx2 = outline[ i ++ ] * scale + offsetX; 167 | cpy2 = outline[ i ++ ] * scale + offsetY; 168 | 169 | path.bezierCurveTo( cpx1, cpy1, cpx2, cpy2, cpx, cpy ); 170 | 171 | break; 172 | 173 | } 174 | 175 | } 176 | 177 | } 178 | 179 | return { offsetX: glyph.ha * scale, path: path }; 180 | 181 | } 182 | 183 | export { FontLoader, Font }; 184 | -------------------------------------------------------------------------------- /assets/threejs-r148/examples/jsm/libs/stats.module.js: -------------------------------------------------------------------------------- 1 | var Stats = function () { 2 | 3 | var mode = 0; 4 | 5 | var container = document.createElement( 'div' ); 6 | container.style.cssText = 'position:fixed;top:0;left:0;cursor:pointer;opacity:0.9;z-index:10000'; 7 | container.addEventListener( 'click', function ( event ) { 8 | 9 | event.preventDefault(); 10 | showPanel( ++ mode % container.children.length ); 11 | 12 | }, false ); 13 | 14 | // 15 | 16 | function addPanel( panel ) { 17 | 18 | container.appendChild( panel.dom ); 19 | return panel; 20 | 21 | } 22 | 23 | function showPanel( id ) { 24 | 25 | for ( var i = 0; i < container.children.length; i ++ ) { 26 | 27 | container.children[ i ].style.display = i === id ? 'block' : 'none'; 28 | 29 | } 30 | 31 | mode = id; 32 | 33 | } 34 | 35 | // 36 | 37 | var beginTime = ( performance || Date ).now(), prevTime = beginTime, frames = 0; 38 | 39 | var fpsPanel = addPanel( new Stats.Panel( 'FPS', '#0ff', '#002' ) ); 40 | var msPanel = addPanel( new Stats.Panel( 'MS', '#0f0', '#020' ) ); 41 | 42 | if ( self.performance && self.performance.memory ) { 43 | 44 | var memPanel = addPanel( new Stats.Panel( 'MB', '#f08', '#201' ) ); 45 | 46 | } 47 | 48 | showPanel( 0 ); 49 | 50 | return { 51 | 52 | REVISION: 16, 53 | 54 | dom: container, 55 | 56 | addPanel: addPanel, 57 | showPanel: showPanel, 58 | 59 | begin: function () { 60 | 61 | beginTime = ( performance || Date ).now(); 62 | 63 | }, 64 | 65 | end: function () { 66 | 67 | frames ++; 68 | 69 | var time = ( performance || Date ).now(); 70 | 71 | msPanel.update( time - beginTime, 200 ); 72 | 73 | if ( time >= prevTime + 1000 ) { 74 | 75 | fpsPanel.update( ( frames * 1000 ) / ( time - prevTime ), 100 ); 76 | 77 | prevTime = time; 78 | frames = 0; 79 | 80 | if ( memPanel ) { 81 | 82 | var memory = performance.memory; 83 | memPanel.update( memory.usedJSHeapSize / 1048576, memory.jsHeapSizeLimit / 1048576 ); 84 | 85 | } 86 | 87 | } 88 | 89 | return time; 90 | 91 | }, 92 | 93 | update: function () { 94 | 95 | beginTime = this.end(); 96 | 97 | }, 98 | 99 | // Backwards Compatibility 100 | 101 | domElement: container, 102 | setMode: showPanel 103 | 104 | }; 105 | 106 | }; 107 | 108 | Stats.Panel = function ( name, fg, bg ) { 109 | 110 | var min = Infinity, max = 0, round = Math.round; 111 | var PR = round( window.devicePixelRatio || 1 ); 112 | 113 | var WIDTH = 80 * PR, HEIGHT = 48 * PR, 114 | TEXT_X = 3 * PR, TEXT_Y = 2 * PR, 115 | GRAPH_X = 3 * PR, GRAPH_Y = 15 * PR, 116 | GRAPH_WIDTH = 74 * PR, GRAPH_HEIGHT = 30 * PR; 117 | 118 | var canvas = document.createElement( 'canvas' ); 119 | canvas.width = WIDTH; 120 | canvas.height = HEIGHT; 121 | canvas.style.cssText = 'width:80px;height:48px'; 122 | 123 | var context = canvas.getContext( '2d' ); 124 | context.font = 'bold ' + ( 9 * PR ) + 'px Helvetica,Arial,sans-serif'; 125 | context.textBaseline = 'top'; 126 | 127 | context.fillStyle = bg; 128 | context.fillRect( 0, 0, WIDTH, HEIGHT ); 129 | 130 | context.fillStyle = fg; 131 | context.fillText( name, TEXT_X, TEXT_Y ); 132 | context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT ); 133 | 134 | context.fillStyle = bg; 135 | context.globalAlpha = 0.9; 136 | context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT ); 137 | 138 | return { 139 | 140 | dom: canvas, 141 | 142 | update: function ( value, maxValue ) { 143 | 144 | min = Math.min( min, value ); 145 | max = Math.max( max, value ); 146 | 147 | context.fillStyle = bg; 148 | context.globalAlpha = 1; 149 | context.fillRect( 0, 0, WIDTH, GRAPH_Y ); 150 | context.fillStyle = fg; 151 | context.fillText( round( value ) + ' ' + name + ' (' + round( min ) + '-' + round( max ) + ')', TEXT_X, TEXT_Y ); 152 | 153 | context.drawImage( canvas, GRAPH_X + PR, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT, GRAPH_X, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT ); 154 | 155 | context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, GRAPH_HEIGHT ); 156 | 157 | context.fillStyle = bg; 158 | context.globalAlpha = 0.9; 159 | context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, round( ( 1 - ( value / maxValue ) ) * GRAPH_HEIGHT ) ); 160 | 161 | } 162 | 163 | }; 164 | 165 | }; 166 | 167 | export default Stats; 168 | -------------------------------------------------------------------------------- /workings-out/LINE-op-code-usage.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const path = require('path') 3 | 4 | const FIELDS_FOLDER = './kujata-data/data/field/flevel.lgp' 5 | const OUTPUT_FILE = './workings-out/output/line-occurences.json' 6 | 7 | const init = async () => { 8 | console.log('LINE op code usage: START') 9 | const fields = await fs.readdir(FIELDS_FOLDER) 10 | let datas = [] 11 | for (let i = 0; i < fields.length; i++) { 12 | const field = fields[i] 13 | console.log('field', field) 14 | const f = await fs.readJson(path.join(FIELDS_FOLDER, field)) 15 | // console.log('f', f) 16 | if (f && f.script && f.script.entities) { 17 | for (let j = 0; j < f.script.entities.length; j++) { 18 | const entity = f.script.entities[j] 19 | const entityName = entity.entityName 20 | let data = { 21 | line: 0, 22 | ok: 0, 23 | okOps: 0, 24 | move1: 0, 25 | move1Ops: 0, 26 | move2: 0, 27 | move2Ops: 0, 28 | moveBothPresent: false, 29 | moveBothActive: false, 30 | go: 0, 31 | goOps: 0, 32 | go1x: 0, 33 | go1xOps: 0, 34 | goAway: 0, 35 | goAwayOps: 0, 36 | goGo1xActive: false, 37 | goGoAwayActive: false, 38 | go1xGoAwayActive: false, 39 | goAllActive: false, 40 | field, 41 | entity: entityName 42 | } 43 | for (let k = 0; k < entity.scripts.length; k++) { 44 | const script = entity.scripts[k] 45 | if (script.scriptType === '[OK]') { 46 | data.ok++ 47 | data.okOps = script.ops.length 48 | } 49 | if (script.scriptType === 'Move' && script.index === 2) { 50 | data.move1++ 51 | data.move1Ops = script.ops.length 52 | } 53 | if (script.scriptType === 'Move' && script.index === 3) { 54 | data.move2++ 55 | data.move2Ops = script.ops.length 56 | } 57 | if (script.scriptType === 'Go') { 58 | data.go++ 59 | data.goOps = script.ops.length 60 | } 61 | if (script.scriptType === 'Go 1x') { 62 | data.go1x++ 63 | data.go1xOps = script.ops.length 64 | } 65 | if (script.scriptType === 'Go away') { 66 | data.goAway++ 67 | data.goAwayOps = script.ops.length 68 | } 69 | 70 | for (let l = 0; l < script.ops.length; l++) { 71 | const op = script.ops[l] 72 | if (op.op === 'LINE') { 73 | data.line++ 74 | } 75 | } 76 | } 77 | if (data.move1Ops > 0 && data.move2Ops > 0) { 78 | data.moveBothPresent = true 79 | } 80 | if (data.move1Ops > 1 && data.move2Ops > 1) { 81 | data.moveBothActive = true 82 | } 83 | 84 | if (data.goOps > 1 && data.go1xOps > 1) { 85 | data.goGo1xActive = true 86 | } 87 | if (data.goOps > 1 && data.goAwayOps > 1) { 88 | data.goGoAwayActive = true 89 | } 90 | if (data.go1xOps > 1 && data.goAwayOps > 1) { 91 | data.go1xGoAwayActive = true 92 | } 93 | if (data.goOps > 1 && data.go1xOps > 1 && data.goAwayOps > 1) { 94 | data.goAllActive = true 95 | } 96 | 97 | if (data.line > 0) { 98 | datas.push(data) 99 | } 100 | 101 | // console.log('data', field, entityName, data) 102 | } 103 | } 104 | } 105 | let dataString = '[\n' 106 | for (let i = 0; i < datas.length; i++) { 107 | const data = datas[i] 108 | dataString += `{ "line": ${data.line}, "okOps": ${data.okOps}, "move1Ops": ${data.move1Ops}, "move2Ops": ${data.move2Ops}, "moveBothPresent": ${data.moveBothPresent}, "moveBothActive": ${data.moveBothActive}, "goOps": ${data.goOps}, "go1xOps": ${data.go1xOps}, "goAwayOps": ${data.goAwayOps}, "goGo1xActive": ${data.goGo1xActive}, "goGoAwayActive": ${data.goGoAwayActive}, "go1xGoAwayActive": ${data.go1xGoAwayActive}, "goAllActive": ${data.goAllActive}, "field": "${data.field}", "entity": "${data.entity}" },\n` 109 | } 110 | dataString += '\n]' 111 | await fs.writeFile(OUTPUT_FILE, dataString) 112 | 113 | console.log('LINE op code usage: END') 114 | } 115 | init() 116 | -------------------------------------------------------------------------------- /assets/threejs-r135-dg/examples/jsm/libs/stats.module.js: -------------------------------------------------------------------------------- 1 | var Stats = function () { 2 | 3 | var mode = 0; 4 | 5 | var container = document.createElement( 'div' ); 6 | container.style.cssText = 'position:fixed;top:0;left:0;cursor:pointer;opacity:0.9;z-index:10000'; 7 | container.addEventListener( 'click', function ( event ) { 8 | 9 | event.preventDefault(); 10 | showPanel( ++ mode % container.children.length ); 11 | 12 | }, false ); 13 | 14 | // 15 | 16 | function addPanel( panel ) { 17 | 18 | container.appendChild( panel.dom ); 19 | return panel; 20 | 21 | } 22 | 23 | function showPanel( id ) { 24 | 25 | for ( var i = 0; i < container.children.length; i ++ ) { 26 | 27 | container.children[ i ].style.display = i === id ? 'block' : 'none'; 28 | 29 | } 30 | 31 | mode = id; 32 | 33 | } 34 | 35 | // 36 | 37 | var beginTime = ( performance || Date ).now(), prevTime = beginTime, frames = 0; 38 | 39 | var fpsPanel = addPanel( new Stats.Panel( 'FPS', '#0ff', '#002' ) ); 40 | var msPanel = addPanel( new Stats.Panel( 'MS', '#0f0', '#020' ) ); 41 | 42 | if ( self.performance && self.performance.memory ) { 43 | 44 | var memPanel = addPanel( new Stats.Panel( 'MB', '#f08', '#201' ) ); 45 | 46 | } 47 | 48 | showPanel( 0 ); 49 | 50 | return { 51 | 52 | REVISION: 16, 53 | 54 | dom: container, 55 | 56 | addPanel: addPanel, 57 | showPanel: showPanel, 58 | 59 | begin: function () { 60 | 61 | beginTime = ( performance || Date ).now(); 62 | 63 | }, 64 | 65 | end: function () { 66 | 67 | frames ++; 68 | 69 | var time = ( performance || Date ).now(); 70 | 71 | msPanel.update( time - beginTime, 200 ); 72 | 73 | if ( time >= prevTime + 1000 ) { 74 | 75 | fpsPanel.update( ( frames * 1000 ) / ( time - prevTime ), 100 ); 76 | 77 | prevTime = time; 78 | frames = 0; 79 | 80 | if ( memPanel ) { 81 | 82 | var memory = performance.memory; 83 | memPanel.update( memory.usedJSHeapSize / 1048576, memory.jsHeapSizeLimit / 1048576 ); 84 | 85 | } 86 | 87 | } 88 | 89 | return time; 90 | 91 | }, 92 | 93 | update: function () { 94 | 95 | beginTime = this.end(); 96 | 97 | }, 98 | 99 | // Backwards Compatibility 100 | 101 | domElement: container, 102 | setMode: showPanel 103 | 104 | }; 105 | 106 | }; 107 | 108 | Stats.Panel = function ( name, fg, bg ) { 109 | 110 | var min = Infinity, max = 0, round = Math.round; 111 | var PR = round( window.devicePixelRatio || 1 ); 112 | 113 | var WIDTH = 80 * PR, HEIGHT = 48 * PR, 114 | TEXT_X = 3 * PR, TEXT_Y = 2 * PR, 115 | GRAPH_X = 3 * PR, GRAPH_Y = 15 * PR, 116 | GRAPH_WIDTH = 74 * PR, GRAPH_HEIGHT = 30 * PR; 117 | 118 | var canvas = document.createElement( 'canvas' ); 119 | canvas.width = WIDTH; 120 | canvas.height = HEIGHT; 121 | canvas.style.cssText = 'width:80px;height:48px'; 122 | 123 | var context = canvas.getContext( '2d' ); 124 | context.font = 'bold ' + ( 9 * PR ) + 'px Helvetica,Arial,sans-serif'; 125 | context.textBaseline = 'top'; 126 | 127 | context.fillStyle = bg; 128 | context.fillRect( 0, 0, WIDTH, HEIGHT ); 129 | 130 | context.fillStyle = fg; 131 | context.fillText( name, TEXT_X, TEXT_Y ); 132 | context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT ); 133 | 134 | context.fillStyle = bg; 135 | context.globalAlpha = 0.9; 136 | context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT ); 137 | 138 | return { 139 | 140 | dom: canvas, 141 | 142 | update: function ( value, maxValue ) { 143 | 144 | min = Math.min( min, value ); 145 | max = Math.max( max, value ); 146 | 147 | context.fillStyle = bg; 148 | context.globalAlpha = 1; 149 | context.fillRect( 0, 0, WIDTH, GRAPH_Y ); 150 | context.fillStyle = fg; 151 | context.fillText( round( value ) + ' ' + name + ' (' + round( min ) + '-' + round( max ) + ')', TEXT_X, TEXT_Y ); 152 | 153 | context.drawImage( canvas, GRAPH_X + PR, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT, GRAPH_X, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT ); 154 | 155 | context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, GRAPH_HEIGHT ); 156 | 157 | context.fillStyle = bg; 158 | context.globalAlpha = 0.9; 159 | context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, round( ( 1 - ( value / maxValue ) ) * GRAPH_HEIGHT ) ); 160 | 161 | } 162 | 163 | }; 164 | 165 | }; 166 | 167 | export default Stats; 168 | -------------------------------------------------------------------------------- /assets/threejs-r135-dg/examples/jsm/loaders/FontLoader.js: -------------------------------------------------------------------------------- 1 | import { 2 | FileLoader, 3 | Loader, 4 | ShapePath 5 | } from '../../../build/three.module.js'; 6 | 7 | class FontLoader extends Loader { 8 | 9 | constructor( manager ) { 10 | 11 | super( manager ); 12 | 13 | } 14 | 15 | load( url, onLoad, onProgress, onError ) { 16 | 17 | const scope = this; 18 | 19 | const loader = new FileLoader( this.manager ); 20 | loader.setPath( this.path ); 21 | loader.setRequestHeader( this.requestHeader ); 22 | loader.setWithCredentials( scope.withCredentials ); 23 | loader.load( url, function ( text ) { 24 | 25 | let json; 26 | 27 | try { 28 | 29 | json = JSON.parse( text ); 30 | 31 | } catch ( e ) { 32 | 33 | console.warn( 'THREE.FontLoader: typeface.js support is being deprecated. Use typeface.json instead.' ); 34 | json = JSON.parse( text.substring( 65, text.length - 2 ) ); 35 | 36 | } 37 | 38 | const font = scope.parse( json ); 39 | 40 | if ( onLoad ) onLoad( font ); 41 | 42 | }, onProgress, onError ); 43 | 44 | } 45 | 46 | parse( json ) { 47 | 48 | return new Font( json ); 49 | 50 | } 51 | 52 | } 53 | 54 | // 55 | 56 | class Font { 57 | 58 | constructor( data ) { 59 | 60 | this.type = 'Font'; 61 | 62 | this.data = data; 63 | 64 | } 65 | 66 | generateShapes( text, size = 100 ) { 67 | 68 | const shapes = []; 69 | const paths = createPaths( text, size, this.data ); 70 | 71 | for ( let p = 0, pl = paths.length; p < pl; p ++ ) { 72 | 73 | Array.prototype.push.apply( shapes, paths[ p ].toShapes() ); 74 | 75 | } 76 | 77 | return shapes; 78 | 79 | } 80 | 81 | } 82 | 83 | function createPaths( text, size, data ) { 84 | 85 | const chars = Array.from( text ); 86 | const scale = size / data.resolution; 87 | const line_height = ( data.boundingBox.yMax - data.boundingBox.yMin + data.underlineThickness ) * scale; 88 | 89 | const paths = []; 90 | 91 | let offsetX = 0, offsetY = 0; 92 | 93 | for ( let i = 0; i < chars.length; i ++ ) { 94 | 95 | const char = chars[ i ]; 96 | 97 | if ( char === '\n' ) { 98 | 99 | offsetX = 0; 100 | offsetY -= line_height; 101 | 102 | } else { 103 | 104 | const ret = createPath( char, scale, offsetX, offsetY, data ); 105 | offsetX += ret.offsetX; 106 | paths.push( ret.path ); 107 | 108 | } 109 | 110 | } 111 | 112 | return paths; 113 | 114 | } 115 | 116 | function createPath( char, scale, offsetX, offsetY, data ) { 117 | 118 | const glyph = data.glyphs[ char ] || data.glyphs[ '?' ]; 119 | 120 | if ( ! glyph ) { 121 | 122 | console.error( 'THREE.Font: character "' + char + '" does not exists in font family ' + data.familyName + '.' ); 123 | 124 | return; 125 | 126 | } 127 | 128 | const path = new ShapePath(); 129 | 130 | let x, y, cpx, cpy, cpx1, cpy1, cpx2, cpy2; 131 | 132 | if ( glyph.o ) { 133 | 134 | const outline = glyph._cachedOutline || ( glyph._cachedOutline = glyph.o.split( ' ' ) ); 135 | 136 | for ( let i = 0, l = outline.length; i < l; ) { 137 | 138 | const action = outline[ i ++ ]; 139 | 140 | switch ( action ) { 141 | 142 | case 'm': // moveTo 143 | 144 | x = outline[ i ++ ] * scale + offsetX; 145 | y = outline[ i ++ ] * scale + offsetY; 146 | 147 | path.moveTo( x, y ); 148 | 149 | break; 150 | 151 | case 'l': // lineTo 152 | 153 | x = outline[ i ++ ] * scale + offsetX; 154 | y = outline[ i ++ ] * scale + offsetY; 155 | 156 | path.lineTo( x, y ); 157 | 158 | break; 159 | 160 | case 'q': // quadraticCurveTo 161 | 162 | cpx = outline[ i ++ ] * scale + offsetX; 163 | cpy = outline[ i ++ ] * scale + offsetY; 164 | cpx1 = outline[ i ++ ] * scale + offsetX; 165 | cpy1 = outline[ i ++ ] * scale + offsetY; 166 | 167 | path.quadraticCurveTo( cpx1, cpy1, cpx, cpy ); 168 | 169 | break; 170 | 171 | case 'b': // bezierCurveTo 172 | 173 | cpx = outline[ i ++ ] * scale + offsetX; 174 | cpy = outline[ i ++ ] * scale + offsetY; 175 | cpx1 = outline[ i ++ ] * scale + offsetX; 176 | cpy1 = outline[ i ++ ] * scale + offsetY; 177 | cpx2 = outline[ i ++ ] * scale + offsetX; 178 | cpy2 = outline[ i ++ ] * scale + offsetY; 179 | 180 | path.bezierCurveTo( cpx1, cpy1, cpx2, cpy2, cpx, cpy ); 181 | 182 | break; 183 | 184 | } 185 | 186 | } 187 | 188 | } 189 | 190 | return { offsetX: glyph.ha * scale, path: path }; 191 | 192 | } 193 | 194 | Font.prototype.isFont = true; 195 | 196 | export { FontLoader, Font }; 197 | -------------------------------------------------------------------------------- /app/battle/battle-formation.js: -------------------------------------------------------------------------------- 1 | const FACING = { IN: 'in', OUT: 'out' } 2 | const battleFormationConfig = { 3 | // Updated with exe data on load 4 | row: 516, // Seems to be 1700<->2216 in game 5 | formations: { 6 | Normal: { 7 | // DONE - 99 8 | targetGroups: ['enemy', 'player'], 9 | playerTargetGroups: [1, 1, 1], 10 | directions: { 11 | player: { initial: FACING.IN, default: FACING.IN }, 12 | enemy: { initial: FACING.IN, default: FACING.IN } 13 | } 14 | }, 15 | Preemptive: { 16 | // DONE 17 | message: 50, 18 | targetGroups: ['enemy', 'player'], 19 | playerTargetGroups: [1, 1, 1], 20 | directions: { 21 | player: { initial: FACING.IN, default: FACING.IN }, 22 | enemy: { initial: FACING.OUT, default: FACING.IN } 23 | } 24 | }, 25 | BackAttack: { 26 | // DONE 101 27 | message: 51, 28 | targetGroups: ['enemy', 'player'], 29 | playerTargetGroups: [1, 1, 1], 30 | playerRowSwap: true, 31 | directions: { 32 | player: { initial: FACING.OUT, default: FACING.IN }, 33 | enemy: { initial: FACING.IN, default: FACING.IN } 34 | } 35 | }, 36 | SideAttack1: { 37 | // DONE - 511 38 | message: 52, 39 | targetGroups: ['player', 'enemy', 'player'], 40 | playerTargetGroups: [0, 2, 0], 41 | enemyTargetGroup: 1, 42 | playerRowLocked: true, 43 | directions: { 44 | player: { initial: FACING.IN, default: FACING.IN }, 45 | enemy: { initial: FACING.OUT, default: FACING.OUT } 46 | } 47 | }, 48 | PincerAttack: { 49 | // DONE 50 | message: 53, 51 | targetGroups: ['enemy', 'player', 'enemy'], 52 | playerTargetGroups: [1, 1, 1], 53 | playerRowLocked: true, 54 | directions: { 55 | player: { initial: FACING.OUT, default: FACING.OUT }, 56 | enemy: { initial: FACING.IN, default: FACING.IN } 57 | } 58 | }, 59 | SideAttack2: { 60 | message: 52, 61 | targetGroups: ['enemy', 'player'], 62 | playerTargetGroups: [1, 1, 1], 63 | playerRowLocked: true, // Appears to be back row?! 64 | directions: { 65 | player: { initial: FACING.IN, default: FACING.IN }, 66 | enemy: { initial: FACING.OUT, default: FACING.OUT } 67 | } 68 | }, 69 | SideAttack3: { 70 | message: 52, 71 | targetGroups: ['enemy', 'player'], 72 | playerTargetGroups: [1, 1, 1], 73 | playerRowLocked: true, 74 | directions: { 75 | player: { initial: FACING.IN, default: FACING.IN }, 76 | enemy: { initial: FACING.OUT, default: FACING.OUT } 77 | } 78 | }, 79 | SideAttack4: { 80 | message: 52, 81 | targetGroups: ['enemy', 'player'], 82 | playerTargetGroups: [1, 1, 1], 83 | playerRowLocked: true, 84 | directions: { 85 | player: { initial: FACING.IN, default: FACING.IN }, 86 | enemy: { initial: FACING.IN, default: FACING.IN } 87 | } 88 | }, 89 | NormalLockFrontRow: { 90 | targetGroups: ['enemy', 'player'], 91 | playerTargetGroups: [1, 1, 1], 92 | playerRowLocked: true, 93 | directions: { 94 | player: { initial: FACING.IN, default: FACING.IN }, 95 | enemy: { initial: FACING.IN, default: FACING.IN } 96 | } 97 | } 98 | } 99 | } 100 | const combineBattleFormationConfig = exeFormationData => { 101 | // Updated from exe-extractor.js in kujata 102 | 103 | for (const battleType of Object.keys(battleFormationConfig.formations)) { 104 | battleFormationConfig.formations[battleType].positions = 105 | exeFormationData[battleType].positions 106 | battleFormationConfig.formations[battleType].positions['1'] = [ 107 | exeFormationData[battleType].positions['3'][1] 108 | ] 109 | battleFormationConfig.formations[battleType].rotations = 110 | exeFormationData[battleType].rotations 111 | } 112 | // console.log('FORMATION merged', battleFormationConfig) 113 | } 114 | 115 | const showBattleMessageForFormation = () => { 116 | const message = 117 | window.data.kernel.battleText[ 118 | battleFormationConfig.formations[ 119 | window.currentBattle.setup.battleLayoutType 120 | ].message 121 | ] 122 | if (message) { 123 | window.currentBattle.ui.battleText.showBattleMessage(message) 124 | } 125 | } 126 | export { 127 | combineBattleFormationConfig, 128 | battleFormationConfig, 129 | FACING, 130 | showBattleMessageForFormation 131 | } 132 | -------------------------------------------------------------------------------- /app/loading/loading-module.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../assets/threejs-r148/build/three.module.js' // 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r118/three.module.min.js' 2 | import TWEEN from '../../assets/tween.esm.js' 3 | import { TextGeometry } from '../../assets/threejs-r148/examples/jsm/geometries/TextGeometry.js' 4 | 5 | import { updateOnceASecond } from '../helpers/gametime.js' 6 | import { loadFont } from '../helpers/font-helper.js' 7 | 8 | let scene 9 | let camera 10 | let bar 11 | let text 12 | let progress = 0 13 | let font 14 | let mediaText 15 | 16 | const LOADING_TWEEN_GROUP = new TWEEN.Group() 17 | 18 | const createTextGeometry = text => { 19 | return new TextGeometry(text, { 20 | font, 21 | size: 5, 22 | height: 1, 23 | curveSegments: 10, 24 | bevelEnabled: false 25 | }) 26 | } 27 | const initLoadingModule = async () => { 28 | scene = new THREE.Scene() 29 | scene.background = new THREE.Color(0x000000) 30 | font = await loadFont() 31 | 32 | camera = new THREE.OrthographicCamera( 33 | 0, 34 | window.config.sizing.width, 35 | window.config.sizing.height, 36 | 0, 37 | 0, 38 | 10 39 | ) 40 | 41 | const geometry = new THREE.PlaneGeometry(1, 1) 42 | const material = new THREE.MeshBasicMaterial({ 43 | color: 0xffffff, 44 | transparent: true 45 | }) 46 | material.opacity = 0 47 | bar = new THREE.Mesh(geometry, material) 48 | bar.scale.set(1, 1, 0) 49 | bar.position.x = 0 50 | bar.position.y = 2 51 | 52 | scene.add(bar) 53 | 54 | camera.position.z = 1 55 | 56 | const textGeo = createTextGeometry('Starting game...') 57 | text = new THREE.Mesh(textGeo, material) 58 | text.position.x = 2 59 | text.position.y = 6 60 | scene.add(text) 61 | } 62 | const renderLoop = function () { 63 | if (window.anim.activeScene !== 'loading') { 64 | console.log('Stopping loading renderLoop') 65 | return 66 | } 67 | requestAnimationFrame(renderLoop) 68 | updateOnceASecond() 69 | LOADING_TWEEN_GROUP.update() 70 | const opacity = text.material.opacity // Fade in and out quickly 71 | if (progress < 0.9) { 72 | text.material.opacity = opacity > 1 ? 1 : opacity + 0.05 73 | bar.material.opacity = opacity > 1 ? 1 : opacity + 0.05 74 | } else { 75 | text.material.opacity = opacity < 0 ? 0 : opacity - 0.05 76 | bar.material.opacity = opacity < 0 ? 0 : opacity - 0.05 77 | } 78 | 79 | window.anim.renderer.clear() 80 | window.anim.renderer.render(scene, camera) 81 | window.anim.renderer.clearDepth() 82 | 83 | if (window.config.debug.active) { 84 | window.anim.stats.update() 85 | } 86 | } 87 | const showLoadingScreen = whiteTransition => { 88 | if (window.anim.activeScene !== 'loading') { 89 | window.anim.activeScene = 'loading' 90 | if (whiteTransition) { 91 | scene.background = new THREE.Color(0xffffff) 92 | bar.material.color = new THREE.Color(0x000000) 93 | text.material.color = new THREE.Color(0x000000) 94 | } else { 95 | scene.background = new THREE.Color(0x000000) 96 | bar.material.color = new THREE.Color(0xffffff) 97 | text.material.color = new THREE.Color(0xffffff) 98 | } 99 | setLoadingProgress(0) 100 | text.material.opacity = 0 101 | bar.material.opacity = 0 102 | renderLoop() 103 | } 104 | } 105 | const setLoadingProgress = val => { 106 | progress = val 107 | bar.scale.x = window.config.sizing.width * 2 * progress 108 | } 109 | const setLoadingText = textToSet => { 110 | text.geometry = createTextGeometry(textToSet) 111 | } 112 | 113 | const showClickScreenForMediaText = () => { 114 | const material = new THREE.MeshBasicMaterial({ 115 | color: 0xffffff, 116 | transparent: true 117 | }) 118 | const textGeo = createTextGeometry( 119 | 'Please click on the screen to enable audio and video' 120 | ) 121 | mediaText = new THREE.Mesh(textGeo, material) 122 | mediaText.position.x = 2 123 | mediaText.position.y = 6 124 | mediaText.userData.mediaText = 'Click the screen' 125 | scene.add(mediaText) 126 | console.log( 127 | 'waitUntilMediaCanPlay showClickScreenForMediaText', 128 | scene, 129 | mediaText 130 | ) 131 | } 132 | 133 | const hideClickScreenForMediaText = () => { 134 | if (mediaText) { 135 | scene.remove(mediaText) 136 | } 137 | } 138 | 139 | export { 140 | initLoadingModule, 141 | showLoadingScreen, 142 | setLoadingProgress, 143 | setLoadingText, 144 | showClickScreenForMediaText, 145 | hideClickScreenForMediaText, 146 | LOADING_TWEEN_GROUP 147 | } 148 | -------------------------------------------------------------------------------- /workings-out/identify-fields-without-shift-offsets.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | // Path to the kujata-data metadata directory 5 | const metadataPath = path.join(__dirname, '../../kujata-data/metadata/background-layers') 6 | 7 | function checkLayerShifts () { 8 | try { 9 | // Read all directories in the backgrnd-layers folder 10 | const directories = fs.readdirSync(metadataPath, { withFileTypes: true }) 11 | .filter(dirent => dirent.isDirectory()) 12 | .map(dirent => dirent.name) 13 | 14 | console.log('Fields with layer shifts and offsets:') 15 | console.log('====================================') 16 | 17 | let fieldsWithShifts = 0 18 | let fieldsWithOffsets = 0 19 | let fieldsWithBoth = 0 20 | let fieldsWithJustOffsets = 0 21 | let fieldsWithJustShifts = 0 22 | let fieldsWithNeither = 0 23 | let totalFields = 0 24 | 25 | for (const dirName of directories) { 26 | const jsonFilePath = path.join(metadataPath, dirName, `${dirName}.json`) 27 | 28 | // Check if the JSON file exists 29 | if (!fs.existsSync(jsonFilePath)) { 30 | console.log(`Warning: ${dirName}.json not found in ${dirName} directory`) 31 | continue 32 | } 33 | 34 | totalFields++ 35 | 36 | try { 37 | // Read and parse the JSON file 38 | const jsonData = JSON.parse(fs.readFileSync(jsonFilePath, 'utf8')) 39 | 40 | // Check if layerShifts exists and has non-zero values 41 | if (jsonData.shiftData.layerShifts && Array.isArray(jsonData.shiftData.layerShifts)) { 42 | const nonZeroShifts = jsonData.shiftData.layerShifts.filter(shift => 43 | shift.x !== 0 || shift.y !== 0 44 | ) 45 | 46 | const hasShifts = nonZeroShifts.length > 0 47 | const hasOffsets = jsonData.shiftData.offsetX !== 0 || jsonData.shiftData.offsetY !== 0 48 | 49 | // Count categories 50 | if (hasShifts) fieldsWithShifts++ 51 | if (hasOffsets) fieldsWithOffsets++ 52 | 53 | if (hasShifts && hasOffsets) { 54 | fieldsWithBoth++ 55 | } else if (hasShifts && !hasOffsets) { 56 | fieldsWithJustShifts++ 57 | } else if (!hasShifts && hasOffsets) { 58 | fieldsWithJustOffsets++ 59 | } else { 60 | fieldsWithNeither++ 61 | } 62 | 63 | // Display fields that have either shifts or offsets 64 | if (hasShifts || hasOffsets) { 65 | const shiftStrings = jsonData.shiftData.layerShifts.map(shift => 66 | `${shift.x ? (shift.x + '').padStart(2, ' ') : ' '},${shift.y ? (shift.y + '').padStart(2, ' ') : ' '}` 67 | ) 68 | const offsetInfo = `Offsets: ${jsonData.shiftData.offsetX}, ${jsonData.shiftData.offsetY}` 69 | const hasInfo = `[${hasShifts ? 'S' : ' '}${hasOffsets ? 'O' : ' '}]` 70 | console.log(`${dirName.padEnd(12, ' ')} ${hasInfo} -> Shifts: ${shiftStrings.join(' - ')} | ${offsetInfo}`) 71 | } 72 | } else { 73 | // No shift data structure found 74 | fieldsWithNeither++ 75 | } 76 | } catch (parseError) { 77 | console.log(`Error parsing ${dirName}.json: ${parseError.message}`) 78 | } 79 | } 80 | 81 | console.log('====================================') 82 | console.log('SUMMARY:') 83 | console.log('====================================') 84 | console.log(`Total fields processed: ${totalFields}`) 85 | console.log(`Fields with both offsets and shifts: ${fieldsWithBoth}`) 86 | console.log(`Fields with just offsets: ${fieldsWithJustOffsets}`) 87 | console.log(`Fields with just shifts: ${fieldsWithJustShifts}`) 88 | console.log(`Fields with neither: ${fieldsWithNeither}`) 89 | console.log('------------------------------------') 90 | console.log(`Total fields with offsets: ${fieldsWithOffsets}`) 91 | console.log(`Total fields with shifts: ${fieldsWithShifts}`) 92 | console.log('====================================') 93 | 94 | // Verification 95 | const totalCounted = fieldsWithBoth + fieldsWithJustOffsets + fieldsWithJustShifts + fieldsWithNeither 96 | if (totalCounted !== totalFields) { 97 | console.log(`⚠️ Warning: Count mismatch! Total: ${totalFields}, Counted: ${totalCounted}`) 98 | } 99 | } catch (error) { 100 | console.error('Error reading metadata directory:', error.message) 101 | console.error('Make sure the kujata-data/metadata/backgrnd-layers directory exists') 102 | } 103 | } 104 | 105 | // Run the check 106 | checkLayerShifts() 107 | -------------------------------------------------------------------------------- /app/data/battle-fetch-data.js: -------------------------------------------------------------------------------- 1 | import { KUJATA_BASE } from './kernel-fetch-data.js' 2 | import { GLTFLoader } from '../../assets/threejs-r148/examples/jsm/loaders/GLTFLoader.js' 3 | import { addBlendingToMaterials } from '../field/field-fetch-data.js' 4 | 5 | const battleTextures = {} 6 | window.battleTextures = battleTextures 7 | const getBattleTextures = (window.getBattleTextures = () => { 8 | return battleTextures 9 | }) 10 | 11 | const loadBattleData = async () => { 12 | const sceneDataRes = await fetch( 13 | `${KUJATA_BASE}/data/battle/scene.bin/scene.bin.json` 14 | ) 15 | const sceneData = await sceneDataRes.json() 16 | window.data.sceneData = sceneData 17 | 18 | window.data.battle = {} 19 | 20 | const camDataRes = await fetch(`${KUJATA_BASE}/data/battle/camdat.bin.json`) 21 | const camData = await camDataRes.json() 22 | window.data.battle.camData = camData 23 | 24 | const markDataRes = await fetch(`${KUJATA_BASE}/data/battle/mark.dat.json`) 25 | const markData = await markDataRes.json() 26 | window.data.battle.mark = markData 27 | 28 | const actionSequencesRes = await fetch( 29 | `${KUJATA_BASE}/data/battle/action-sequences.json` 30 | ) 31 | const actionSequences = await actionSequencesRes.json() 32 | window.data.battle.actionSequences = actionSequences 33 | 34 | const actionSequenceMetadataPlayerRes = await fetch( 35 | `${KUJATA_BASE}/metadata/action-sequence-metadata-player.json` 36 | ) 37 | const actionSequenceMetadataPlayer = 38 | await actionSequenceMetadataPlayerRes.json() 39 | window.data.battle.actionSequenceMetadataPlayer = actionSequenceMetadataPlayer 40 | 41 | window.data.battle.assets = {} 42 | const effects32Res = await fetch( 43 | `${KUJATA_BASE}/metadata/battle-assets/effects-32.json` 44 | ) 45 | const effects32 = await effects32Res.json() 46 | const columns = effects32[Object.keys(effects32)[0]].count 47 | // console.log('EFFECTS', effects32, columns) 48 | const rows = Object.keys(effects32).length 49 | battleTextures.effects32 = { 50 | assets: effects32, 51 | texture: new THREE.TextureLoader().load( 52 | `${KUJATA_BASE}/metadata/battle-assets/effects-32.png` 53 | ), 54 | metadata: { 55 | columns, 56 | rows, 57 | frameWidth: 1 / columns, 58 | frameHeight: 1 / rows 59 | } 60 | } 61 | 62 | // const allRows = [] 63 | for (const scene of sceneData) { 64 | // for (let i = 0; i < scene.battleFormations.length; i++) { 65 | // const formation = scene.battleFormations[i] 66 | // const setup = scene.battleSetup[i] 67 | // if (!allRows.includes(setup.initialCameraPosition)) { 68 | // allRows.push(setup.initialCameraPosition) 69 | // } 70 | // if ([58, 59].includes(setup.initialCameraPosition)) { 71 | // console.log( 72 | // 'sceneData', 73 | // scene, 74 | // formation, 75 | // setup, 76 | // setup.initialCameraPosition, 77 | // scene.sceneId * 4 + i 78 | // ) 79 | // } 80 | // } 81 | for (const attackData of scene.attackData) { 82 | const l = [26, 28] 83 | if ( 84 | l.includes(attackData.cameraMovementIdSingleTargets) || 85 | l.includes(attackData.cameraMovementIdMultipleTargets) 86 | ) { 87 | console.log( 88 | 'sceneData', 89 | scene, 90 | scene.sceneId * 4, 91 | attackData.cameraMovementIdSingleTargets, 92 | attackData.cameraMovementIdMultipleTargets, 93 | attackData.name, 94 | attackData 95 | ) 96 | } 97 | } 98 | } 99 | // const sorted = allRows.sort((a, b) => a - b) 100 | // console.log('sceneData all rows ', sorted) 101 | } 102 | const loadSceneModel = async (modelCode, manager) => { 103 | // These models aren't cached, we really should cache them 104 | 105 | console.log('loading sceneModel', modelCode, 'START') 106 | const modelGLTFRes = await fetch( 107 | `${KUJATA_BASE}/data/battle/battle.lgp/${modelCode.toLowerCase()}.hrc.gltf`, 108 | { cache: 'force-cache' } 109 | ) 110 | const modelGLTF = await modelGLTFRes.json() 111 | return new Promise((resolve, reject) => { 112 | const loader = new GLTFLoader(manager) 113 | console.log('battle loader', loader.textureLoader) 114 | 115 | loader.parse( 116 | JSON.stringify(modelGLTF), 117 | `${KUJATA_BASE}/data/battle/battle.lgp/`, 118 | async function (gltf) { 119 | console.log('battle parsed gltf:', gltf) 120 | addBlendingToMaterials(gltf) 121 | // console.log("combined gltf:", gltf) 122 | 123 | // Quick hack for smooth animations until we remove the duplicate frames in the gltfs 124 | for (const anim of gltf.animations) { 125 | for (const track of anim.tracks) { 126 | track.optimize() 127 | } 128 | } 129 | console.log('loading sceneModel', modelCode, 'END') 130 | resolve(gltf) 131 | } 132 | ) 133 | }) 134 | } 135 | 136 | export { loadBattleData, loadSceneModel, getBattleTextures } 137 | -------------------------------------------------------------------------------- /app/field/field-module.js: -------------------------------------------------------------------------------- 1 | import { 2 | startFieldRenderLoop, 3 | setupFieldCamera, 4 | setupDebugControls, 5 | initFieldDebug, 6 | setupViewClipping 7 | } from './field-scene.js' 8 | import { 9 | loadFieldData, 10 | loadFieldBackground, 11 | loadModels 12 | } from './field-fetch-data.js' 13 | import { initFieldKeypressActions } from './field-controls.js' 14 | import { transitionIn, drawFaders } from './field-fader.js' 15 | import { showLoadingScreen } from '../loading/loading-module.js' 16 | import { setupOrthoCamera } from './field-ortho-scene.js' 17 | import { setupOrthoBgCamera } from './field-ortho-bg-scene.js' 18 | import { 19 | initialiseOpLoops, 20 | debugLogOpCodeCompletionForField 21 | } from './field-op-loop.js' 22 | import { resetTempBank } from '../data/savemap.js' 23 | import { updateSavemapLocationField } from '../data/savemap-alias.js' 24 | import { preLoadFieldMediaData } from '../media/media-module.js' 25 | import { clearAllDialogs } from './field-dialog.js' 26 | import { initBattleSettings } from './field-battle.js' 27 | import { placeModelsDebug, setupModelSceneGroup } from './field-models.js' 28 | import { drawWalkmesh, placeBG } from './field-backgrounds.js' 29 | import { loadWorldMap } from '../world/world-module.js' 30 | 31 | // Uses global states: 32 | // let currentField = window.currentField // Handle this better in the future 33 | // let anim = window.anim 34 | // let config = window.config 35 | 36 | const setMenuEnabled = enabled => { 37 | window.currentField.menuEnabled = enabled 38 | } 39 | const isMenuEnabled = () => { 40 | return window.currentField.menuEnabled 41 | } 42 | 43 | const loadField = async (fieldName, playableCharacterInitData) => { 44 | console.log('loadField', fieldName, playableCharacterInitData) 45 | // Reset field values 46 | if (fieldName.startsWith('wm')) { 47 | loadWorldMap(fieldName) 48 | return 49 | } 50 | 51 | const lastFieldName = 52 | window.currentField && window.currentField.name 53 | ? window.currentField.name 54 | : '' 55 | 56 | window.currentField = { 57 | name: fieldName, 58 | lastFieldName, 59 | data: undefined, 60 | backgroundData: undefined, 61 | metaData: undefined, 62 | models: undefined, 63 | playableCharacter: undefined, 64 | playableCharacterCanMove: true, 65 | playableCharacterIsInteracting: false, 66 | fieldScene: undefined, 67 | fieldCamera: undefined, 68 | fieldCameraHelper: undefined, 69 | fieldCameraFollowPlayer: true, 70 | fieldCameraPosition: { 71 | current: { x: 0, y: 0 }, 72 | next: { x: 0, y: 0 }, 73 | shake: { 74 | current: { x: 0, y: 0 }, 75 | next: { x: 0, y: 0 } 76 | } 77 | }, 78 | isScrolling: false, 79 | videoCamera: undefined, 80 | showVideoCamera: false, 81 | allowVideoCamera: true, 82 | debugCamera: undefined, 83 | walkmeshMesh: undefined, 84 | walkmeshLines: undefined, 85 | gatewayLines: undefined, 86 | triggerLines: undefined, 87 | backgroundLayers: undefined, 88 | backgroundVideo: undefined, 89 | positionHelpers: undefined, 90 | cameraTarget: undefined, 91 | playableCharacterInitData, 92 | media: undefined, 93 | menuEnabled: true, 94 | gatewayTriggersEnabled: true, 95 | movementHelpers: undefined, 96 | playerAnimations: { 97 | stand: 0, 98 | walk: 1, 99 | run: 2 100 | } 101 | } 102 | updateSavemapLocationField(fieldName, fieldName) 103 | showLoadingScreen( 104 | playableCharacterInitData 105 | ? playableCharacterInitData.whiteTransition 106 | : false 107 | ) 108 | resetTempBank() 109 | window.currentField.data = await loadFieldData(fieldName) 110 | window.currentField.media = await preLoadFieldMediaData() 111 | // console.log('field-module -> window.currentField.data', window.currentField.data) 112 | // console.log('field-module -> window.anim', window.anim) 113 | window.currentField.cameraTarget = setupFieldCamera() 114 | await setupOrthoBgCamera() 115 | await setupOrthoCamera() 116 | drawFaders( 117 | playableCharacterInitData 118 | ? playableCharacterInitData.whiteTransition 119 | : false 120 | ) 121 | clearAllDialogs() 122 | window.currentField.backgroundData = await loadFieldBackground(fieldName) 123 | window.currentField.models = await loadModels( 124 | window.currentField.data.model.modelLoaders 125 | ) 126 | setupModelSceneGroup() 127 | console.log('window.currentField', window.currentField) 128 | initBattleSettings() 129 | await placeBG(fieldName) 130 | setupDebugControls() 131 | startFieldRenderLoop() 132 | await setupViewClipping() 133 | drawWalkmesh() 134 | if (window.config.debug.debugModeNoOpLoops) { 135 | placeModelsDebug() 136 | } 137 | 138 | if (window.config.debug.active) { 139 | await initFieldDebug(loadField) 140 | debugLogOpCodeCompletionForField() 141 | } 142 | initFieldKeypressActions() 143 | if (!window.config.debug.debugModeNoOpLoops) { 144 | await initialiseOpLoops() 145 | } 146 | window.anim.clock.start() 147 | await transitionIn() 148 | } 149 | 150 | export { loadField, setMenuEnabled, isMenuEnabled } 151 | -------------------------------------------------------------------------------- /app/menu/menu-controls.js: -------------------------------------------------------------------------------- 1 | import { KEY, getKeyPressEmitter } from '../interaction/inputs.js' 2 | import { getMenuState, resolveMenuPromise } from './menu-module.js' 3 | import { keyPress as keyPressMain } from './menu-main-home.js' 4 | import { keyPress as keyPressItems } from './menu-main-items.js' 5 | import { keyPress as keyPressMagic } from './menu-main-magic.js' 6 | import { keyPress as keyPressMateria } from './menu-main-materia.js' 7 | import { keyPress as keyPressEquip } from './menu-main-equip.js' 8 | import { keyPress as keyPressStatus } from './menu-main-status.js' 9 | import { keyPress as keyPressLimit } from './menu-main-limit.js' 10 | import { keyPress as keyPressConfig } from './menu-main-config.js' 11 | import { keyPress as keyPressPHS } from './menu-main-phs.js' 12 | import { keyPress as keyPressSave } from './menu-main-save.js' 13 | import { keyPress as keyPressChar } from './menu-char-name.js' 14 | import { keyPress as keyPressShop } from './menu-shop.js' 15 | import { keyPress as keyPressCredits } from './menu-credits.js' 16 | import { keyPress as keyPressTitle } from './menu-title.js' 17 | import { keyPress as keyPressChangeDisc } from './menu-change-disc.js' 18 | import { keyPress as keyPressGameOver } from './menu-game-over.js' 19 | 20 | const areMenuControlsActive = () => { 21 | return window.anim.activeScene === 'menu' 22 | } 23 | 24 | const sendKeyPressToMenu = (key, firstPress, state) => { 25 | if (state.startsWith('home')) { 26 | keyPressMain(key, firstPress, state) 27 | } else if (state.startsWith('items')) { 28 | keyPressItems(key, firstPress, state) 29 | } else if (state.startsWith('magic')) { 30 | keyPressMagic(key, firstPress, state) 31 | } else if (state.startsWith('materia')) { 32 | keyPressMateria(key, firstPress, state) 33 | } else if (state.startsWith('equip')) { 34 | keyPressEquip(key, firstPress, state) 35 | } else if (state.startsWith('status')) { 36 | keyPressStatus(key, firstPress, state) 37 | } else if (state.startsWith('limit')) { 38 | keyPressLimit(key, firstPress, state) 39 | } else if (state.startsWith('config')) { 40 | keyPressConfig(key, firstPress, state) 41 | } else if (state.startsWith('phs')) { 42 | keyPressPHS(key, firstPress, state) 43 | } else if (state.startsWith('save')) { 44 | keyPressSave(key, firstPress, state) 45 | } else if (state.startsWith('char')) { 46 | keyPressChar(key, firstPress, state) 47 | } else if (state.startsWith('shop')) { 48 | keyPressShop(key, firstPress, state) 49 | } else if (state.startsWith('credits')) { 50 | keyPressCredits(key, firstPress, state) 51 | } else if (state.startsWith('title')) { 52 | keyPressTitle(key, firstPress, state) 53 | } else if (state.startsWith('disc')) { 54 | keyPressChangeDisc(key, firstPress, state) 55 | } else if (state.startsWith('gameover')) { 56 | keyPressGameOver(key, firstPress, state) 57 | } else if (state.startsWith('quit')) { 58 | // Nothing... 59 | } else if (state === 'loading') { 60 | // Do nothing 61 | } else { 62 | resolveMenuPromise() 63 | } 64 | } 65 | const initMenuKeypressActions = () => { 66 | getKeyPressEmitter().on(KEY.O, firstPress => { 67 | if (areMenuControlsActive()) { 68 | sendKeyPressToMenu(KEY.O, firstPress, getMenuState()) 69 | } 70 | }) 71 | getKeyPressEmitter().on(KEY.X, async firstPress => { 72 | if (areMenuControlsActive()) { 73 | sendKeyPressToMenu(KEY.X, firstPress, getMenuState()) 74 | } 75 | }) 76 | getKeyPressEmitter().on(KEY.SQUARE, firstPress => { 77 | if (areMenuControlsActive()) { 78 | sendKeyPressToMenu(KEY.SQUARE, firstPress, getMenuState()) 79 | } 80 | }) 81 | getKeyPressEmitter().on(KEY.TRIANGLE, async firstPress => { 82 | if (areMenuControlsActive()) { 83 | sendKeyPressToMenu(KEY.TRIANGLE, firstPress, getMenuState()) 84 | } 85 | }) 86 | getKeyPressEmitter().on(KEY.UP, firstPress => { 87 | if (areMenuControlsActive()) { 88 | sendKeyPressToMenu(KEY.UP, firstPress, getMenuState()) 89 | } 90 | }) 91 | getKeyPressEmitter().on(KEY.DOWN, async firstPress => { 92 | if (areMenuControlsActive()) { 93 | sendKeyPressToMenu(KEY.DOWN, firstPress, getMenuState()) 94 | } 95 | }) 96 | getKeyPressEmitter().on(KEY.LEFT, firstPress => { 97 | if (areMenuControlsActive()) { 98 | sendKeyPressToMenu(KEY.LEFT, firstPress, getMenuState()) 99 | } 100 | }) 101 | getKeyPressEmitter().on(KEY.RIGHT, async firstPress => { 102 | if (areMenuControlsActive()) { 103 | sendKeyPressToMenu(KEY.RIGHT, firstPress, getMenuState()) 104 | } 105 | }) 106 | getKeyPressEmitter().on(KEY.L1, firstPress => { 107 | if (areMenuControlsActive()) { 108 | sendKeyPressToMenu(KEY.L1, firstPress, getMenuState()) 109 | } 110 | }) 111 | getKeyPressEmitter().on(KEY.R1, async firstPress => { 112 | if (areMenuControlsActive()) { 113 | sendKeyPressToMenu(KEY.R1, firstPress, getMenuState()) 114 | } 115 | }) 116 | getKeyPressEmitter().on(KEY.START, async firstPress => { 117 | if (areMenuControlsActive()) { 118 | sendKeyPressToMenu(KEY.START, firstPress, getMenuState()) 119 | } 120 | }) 121 | } 122 | export { initMenuKeypressActions } 123 | -------------------------------------------------------------------------------- /app/battle/battle-menu-limit.js: -------------------------------------------------------------------------------- 1 | import { KEY } from '../interaction/inputs.js' 2 | import { 3 | addImageToDialog, 4 | addTextToDialog, 5 | ALIGN, 6 | closeDialog, 7 | createDialogBox, 8 | LETTER_COLORS, 9 | LETTER_TYPES, 10 | movePointer, 11 | POINTERS, 12 | removeGroupChildren, 13 | showDialog, 14 | WINDOW_COLORS_SUMMARY 15 | } from '../menu/menu-box-helper.js' 16 | import { getActionSequenceIndexForSelectedLimit } from './battle-limits.js' 17 | import { DATA } from './battle-menu-command.js' 18 | 19 | const offsets = { 20 | dialog: { x: 160 / 2, y: 348 / 2, w: 272 / 2, h: 53 / 2, hAdj: 32 / 2 }, 21 | header: { 22 | xLimit: 10 / 2, 23 | xLevel: 68 / 2, 24 | xLevelValue: 126 / 2, 25 | y: 13 / 2 26 | }, 27 | pointer: { x: (160 + 12 - 16 - 4) / 2, y: (348 + 24 - 8 + 22) / 2 }, 28 | line: { 29 | x: (16 - 16) / 2, 30 | y: (40 - 8) / 2, 31 | yAdj: 30 / 2 32 | } 33 | } 34 | 35 | let limitDialog 36 | 37 | const openLimitDialog = async commandContainerGroup => { 38 | DATA.limit.pos = 0 39 | 40 | limitDialog = createDialogBox({ 41 | id: 20, 42 | name: 'limit', 43 | w: offsets.dialog.w, 44 | h: 45 | offsets.dialog.h + 46 | (DATA.actor.battleStats.menu.limit.limits.length - 1) * 47 | offsets.dialog.hAdj, 48 | x: offsets.dialog.x, 49 | y: offsets.dialog.y, 50 | scene: commandContainerGroup, 51 | colors: WINDOW_COLORS_SUMMARY.DIALOG_SPECIAL 52 | }) 53 | 54 | const labelLimit = addImageToDialog( 55 | limitDialog, 56 | 'labels', 57 | 'limit', 58 | 'limit-title-limit', 59 | offsets.dialog.x + offsets.header.xLimit, 60 | offsets.dialog.y + offsets.header.y, 61 | 0.5, 62 | null, 63 | ALIGN.LEFT 64 | ) 65 | labelLimit.userData.isText = true 66 | window.labelLimit = labelLimit 67 | 68 | const labelLevel = addImageToDialog( 69 | limitDialog, 70 | 'labels', 71 | 'level', 72 | 'limit-title-level', 73 | offsets.dialog.x + offsets.header.xLevel, 74 | offsets.dialog.y + offsets.header.y, 75 | 0.5, 76 | null, 77 | ALIGN.LEFT 78 | ) 79 | labelLevel.userData.isText = true 80 | 81 | const labelLevelValue = addImageToDialog( 82 | limitDialog, 83 | 'limit-level', 84 | `limit-level-${DATA.actor.data.limit.level}`, 85 | 'limit-title-level-value', 86 | offsets.dialog.x + offsets.header.xLevelValue, 87 | offsets.dialog.y + offsets.header.y - 0.5, 88 | 0.5, 89 | null, 90 | ALIGN.LEFT 91 | ) 92 | labelLevelValue.userData.isText = true 93 | 94 | for (let i = 0; i < DATA.actor.battleStats.menu.limit.limits.length; i++) { 95 | addTextToDialog( 96 | limitDialog, 97 | DATA.actor.battleStats.menu.limit.limits[i].name, 98 | `limit-level-${i}`, 99 | LETTER_TYPES.BattleBaseFont, 100 | LETTER_COLORS.White, 101 | offsets.dialog.x + offsets.line.x, 102 | offsets.dialog.y + offsets.line.y + i * offsets.line.yAdj, 103 | 0.5 104 | ) 105 | } 106 | 107 | await showDialog(limitDialog) 108 | console.log('battleUI LIMIT: openLimitDialog', limitDialog) 109 | } 110 | const updateInfoForSelectedLimit = () => { 111 | // TODO - Change color 112 | const limit = DATA.actor.battleStats.menu.limit.limits[DATA.limit.pos] 113 | window.currentBattle.ui.battleDescriptions.setText(limit.description, true) 114 | } 115 | const drawPointer = () => { 116 | movePointer( 117 | POINTERS.pointer1, 118 | offsets.pointer.x, 119 | offsets.pointer.y + DATA.limit.pos * offsets.line.yAdj 120 | ) 121 | } 122 | let promiseToResolve 123 | const selectLimit = async () => { 124 | return new Promise(resolve => { 125 | console.log('battleUI LIMIT: selectLimit') 126 | promiseToResolve = resolve 127 | updateInfoForSelectedLimit() 128 | drawPointer() 129 | }) 130 | } 131 | const closeLimitDialog = async () => { 132 | POINTERS.pointer1.visible = false 133 | await closeDialog(limitDialog) 134 | removeGroupChildren(limitDialog) 135 | limitDialog.parent.remove(limitDialog) 136 | limitDialog = undefined 137 | } 138 | 139 | const wrapAround = (start, min, max, delta) => { 140 | const range = max - min + 1 141 | return ((((start - min + delta) % range) + range) % range) + min 142 | } 143 | 144 | const changeLimit = async delta => { 145 | DATA.limit.pos = wrapAround( 146 | DATA.limit.pos, 147 | 0, 148 | DATA.actor.battleStats.menu.limit.limits.length - 1, 149 | delta 150 | ) 151 | 152 | updateInfoForSelectedLimit() 153 | drawPointer() 154 | } 155 | const handleKeyPressLimit = async key => { 156 | switch (key) { 157 | case KEY.UP: 158 | changeLimit(-1) 159 | break 160 | case KEY.DOWN: 161 | changeLimit(1) 162 | break 163 | case KEY.O: 164 | const data = DATA.actor.battleStats.menu.limit.limits[DATA.limit.pos] 165 | const attack = { 166 | actionSequenceIndex: getActionSequenceIndexForSelectedLimit( 167 | DATA.actor, 168 | DATA.limit.pos 169 | ), 170 | name: data.name, 171 | data 172 | } 173 | promiseToResolve(attack) 174 | break 175 | case KEY.X: 176 | DATA.state = 'returning' 177 | promiseToResolve(null) 178 | break 179 | default: 180 | break 181 | } 182 | } 183 | export { openLimitDialog, selectLimit, closeLimitDialog, handleKeyPressLimit } 184 | -------------------------------------------------------------------------------- /app/battle/battle-setup.js: -------------------------------------------------------------------------------- 1 | import { battleFormationConfig } from './battle-formation.js' 2 | import { 3 | getBattleStatsForChar, 4 | getBattleStatsForEnemy 5 | } from './battle-stats.js' 6 | import { initTimers } from './battle-timers.js' 7 | 8 | let currentBattle = {} 9 | 10 | const locationIdToLocationCode = i => { 11 | const id = i + 370 12 | const id2 = Math.floor(id / 26) 13 | const id3 = id - id2 * 26 14 | return String.fromCharCode(id2 + 97) + String.fromCharCode(id3 + 97) + 'aa' 15 | } 16 | const enemyIdToEnemyCode = i => { 17 | const id = i + 0 18 | const id2 = Math.floor(id / 26) 19 | const id3 = id - id2 * 26 20 | return String.fromCharCode(id2 + 97) + String.fromCharCode(id3 + 97) + 'aa' 21 | } 22 | const enemyCodeToEnemyId = code => { 23 | const id2 = code.charCodeAt(0) - 97 24 | const id3 = code.charCodeAt(1) - 97 25 | return id2 * 26 + id3 26 | } 27 | const characterNameToModelCode = name => { 28 | let modelName = 'CLOUD' 29 | if (name === 'Cloud') modelName = 'CLOUD' 30 | if (name === 'Barret') modelName = 'BARRETT' 31 | if (name === 'Tifa') modelName = 'TIFA' 32 | if (name === 'Aeris') modelName = 'EARITH' 33 | if (name === 'RedXIII') modelName = 'RED13' 34 | if (name === 'Yuffie') modelName = 'YUFI' 35 | if (name === 'Cid') modelName = 'CID1' 36 | if (name === 'CaitSith') modelName = 'KETCY' 37 | if (name === 'Vincent') modelName = 'VINSENT' 38 | // TODO - Othe chars, special frog, vincent limits, sephiroth, weapons, multiple barret? 39 | return window.data.exe.battleCharacterModels.find(m => m.name === modelName) 40 | .hrc 41 | } 42 | 43 | const setupBattle = battleId => { 44 | const sceneId = battleId >> 2 // eg, Math.trunc(battleId / 4) 45 | const formationId = battleId & 3 // eg, battleId % 4 46 | const scene = { ...window.data.sceneData.find(s => s.sceneId === sceneId) } 47 | currentBattle = { 48 | sceneId, 49 | formationId, 50 | scene, // temp - remove after 51 | setup: { ...scene.battleSetup[formationId] }, 52 | camera: { ...scene.cameraPlacement[formationId] }, 53 | actors: [], 54 | attackData: [...scene.attackData.filter(a => a.id !== 0xffff)] 55 | } 56 | currentBattle.formationConfig = 57 | battleFormationConfig.formations[currentBattle.setup.battleLayoutType] 58 | 59 | for (const [i, partyMember] of window.data.savemap.party.members.entries()) { 60 | if (partyMember === 'None') { 61 | currentBattle.actors.push({ active: false, index: i }) 62 | } else { 63 | const data = structuredClone(window.data.savemap.characters[partyMember]) 64 | if (currentBattle.formationConfig.playerRowSwap) { 65 | data.status.battleOrder = 66 | data.status.battleOrder === 'BackRow' ? 'Normal' : 'BackRow' 67 | } 68 | if (currentBattle.formationConfig.playerRowLocked) { 69 | data.status.battleOrder = 'Normal' 70 | } 71 | const battleStats = getBattleStatsForChar(data) 72 | 73 | currentBattle.actors.push({ 74 | active: true, 75 | index: i, 76 | data, 77 | battleStats, 78 | modelCode: characterNameToModelCode(partyMember), 79 | type: 'player', 80 | targetGroup: currentBattle.formationConfig.playerTargetGroups?.[i] ?? 1 81 | }) 82 | } 83 | } 84 | // TODO ?!?! Not sure yet - This is the formation type, I believe 85 | currentBattle.actors.push({ active: false, index: 3, type: 'formation' }) // Battle Actor that hod formation AI 86 | 87 | for (const [i, enemy] of scene.battleFormations[formationId].entries()) { 88 | if (enemy.enemyId === 0xffff) { 89 | currentBattle.actors.push({ active: false, index: i + 4 }) 90 | } else { 91 | let enemyData 92 | let script 93 | if (scene.enemyId1 === enemy.enemyId) { 94 | enemyData = { ...scene.enemyData1 } 95 | script = { ...scene.enemyScript1 } 96 | } 97 | if (scene.enemyId2 === enemy.enemyId) { 98 | enemyData = { ...scene.enemyData2 } 99 | script = { ...scene.enemyScript1 } 100 | } 101 | if (scene.enemyId3 === enemy.enemyId) { 102 | enemyData = { ...scene.enemyData3 } 103 | script = { ...scene.enemyScript1 } 104 | } 105 | 106 | const data = JSON.parse(JSON.stringify(enemyData)) 107 | const battleStats = getBattleStatsForEnemy({ data }) 108 | currentBattle.actors.push({ 109 | active: true, 110 | index: i + 4, 111 | initialData: enemy, 112 | data, 113 | modelCode: enemyIdToEnemyCode(enemy.enemyId), 114 | script, 115 | battleStats, 116 | type: 'enemy', 117 | targetGroup: 118 | currentBattle.formationConfig.enemyTargetGroup ?? 119 | (enemy.initialConditionFlags.includes('SideAttackInitialDirection') 120 | ? 0 121 | : 2) 122 | }) 123 | } 124 | } 125 | currentBattle.setup.targetGroups = ['enemy', 'player'] // TODO - Update based on battle type, pincer, back attack etc 126 | currentBattle.setup.locationCode = locationIdToLocationCode( 127 | currentBattle.setup.locationId 128 | ) 129 | 130 | initTimers(currentBattle) 131 | 132 | for (const actor of currentBattle.actors) { 133 | if (!actor.active) continue 134 | actor.actionSequences = 135 | window.data.battle.actionSequences[actor.modelCode.substring(0, 2) + 'ab'] 136 | } 137 | window.currentBattle = currentBattle 138 | console.log('battle currentBattle', currentBattle) 139 | return currentBattle 140 | } 141 | 142 | export { setupBattle, currentBattle } 143 | -------------------------------------------------------------------------------- /workings-out/create-op-codes-battle-camera-progress-readme.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const path = require('path') 3 | const _ = require('lodash') 4 | const OPS_FOLDER = path.join(__dirname, '..', '..', 'kujata-data', 'metadata') 5 | const OPS_README = path.join( 6 | __dirname, 7 | '..', 8 | 'OPS_CODES_BATTLE_CAMERA_README.md' 9 | ) 10 | const POSITION_LOOP = path.join( 11 | __dirname, 12 | '..', 13 | 'app', 14 | 'battle', 15 | 'battle-camera-op-position.js' 16 | ) 17 | const FOCUS_LOOP = path.join( 18 | __dirname, 19 | '..', 20 | 'app', 21 | 'battle', 22 | 'battle-camera-op-focus.js' 23 | ) 24 | 25 | const getCompletedOpCodes = async filePath => { 26 | const completedCodes = [] 27 | let c = fs.readFileSync(filePath).toString() 28 | if (c.includes('export {')) { 29 | c = c.split('export {') 30 | c = c[1].replace('}', '').split(',') 31 | for (let i = 0; i < c.length; i++) { 32 | let name = c[i].trim() 33 | name = name.replace('TWO_', '2') 34 | name = name.replace('_', '!') 35 | completedCodes.push(name) 36 | } 37 | } 38 | // console.log('completedCodes', completedCodes) 39 | return completedCodes 40 | } 41 | const generateProgress = async () => { 42 | const metadata = fs.readJsonSync( 43 | path.join(OPS_FOLDER, 'battle-camera-op-metadata.json') 44 | ) 45 | 46 | // Add usage 47 | const usage = fs.readJsonSync( 48 | path.join(OPS_FOLDER, 'battle-camera-op-usage.json') 49 | ) 50 | for (type of Object.keys(usage)) { 51 | for (const opHex of Object.keys(usage[type])) { 52 | const u = usage[type][opHex] 53 | const op = metadata[type].opCodes.find(op => op.shortName === opHex) 54 | // console.log('u', type, opHex, u, op) 55 | op.usage = u 56 | } 57 | } 58 | // Add completion 59 | const positionComplete = { 60 | type: 'position', 61 | complete: await getCompletedOpCodes(POSITION_LOOP) 62 | } 63 | // console.log('positionComplete', positionComplete) 64 | const focusComplete = { 65 | type: 'focus', 66 | complete: await getCompletedOpCodes(FOCUS_LOOP) 67 | } 68 | for (items of [positionComplete, focusComplete]) { 69 | for (const opCode of items.complete) { 70 | // console.log('opCode', items.type, opCode) 71 | const op = metadata[items.type].opCodes.find( 72 | op => op.shortName === opCode 73 | ) 74 | if (op) { 75 | op.complete = true 76 | } 77 | } 78 | } 79 | 80 | // console.log('metadata', JSON.stringify(metadata, null, 2)) 81 | 82 | let total = metadata.position.opCodes.length + metadata.focus.opCodes.length 83 | let totalComplete = 84 | positionComplete.complete.length + focusComplete.complete.length 85 | // console.log('total', total, totalComplete) 86 | return { metadata, total, totalComplete } 87 | } 88 | const createReadmeCell = op => { 89 | if (!op) { 90 | return '' 91 | } else { 92 | if (!op.usage) { 93 | op.usage = { initial: 0, main: 0, victory: 0 } 94 | } 95 | let color = 'green' 96 | let status = 'COMPLETE' 97 | if (!op.complete) { 98 | color = 'red' 99 | status = 'INCOMPLETE' 100 | } 101 | return `![Generic badge](https://img.shields.io/badge/${op.shortName}-${status}-${color}.svg)
${op.hex}
${op.description}

Usage:
Initial: ${op.usage.initial}
Main: ${op.usage.main}
Victory: ${op.usage.victory}` 102 | } 103 | } 104 | const renderReadme = async data => { 105 | const totalProgress = Object.values(data.metadata).reduce( 106 | (acc, { opCodes }) => ({ 107 | completed: acc.completed + opCodes.filter(o => o.complete).length, 108 | total: acc.total + opCodes.length 109 | }), 110 | { completed: 0, total: 0 } 111 | ) 112 | 113 | let r = `# FF7 - Fenrir - Battle Camera Op Code Implementation Progress - ${Math.round( 114 | (100 * totalProgress.completed) / totalProgress.total 115 | )}%\n` 116 | 117 | r = r + `\nNote: This page is autogenerated\n` 118 | r = 119 | r + 120 | `\nTotal progress: ${totalProgress.completed} of ${totalProgress.total}\n` 121 | 122 | for (const categoryName of Object.keys(data.metadata)) { 123 | const category = data.metadata[categoryName] 124 | // // console.log('category', category.name) 125 | r = 126 | r + 127 | `\n\n## ${category.name}\n${category.description} - Progress: ${ 128 | category.opCodes.filter(o => o.complete).length 129 | } of ${category.opCodes.length}\n` 130 | const opChunks = _.chunk(category.opCodes, 6) 131 | // console.log('opChunks', opChunks.length) 132 | r = r + `| | | | | | |\n` 133 | r = r + `|:---:|:---:|:---:|:---:|:---:|:---:|\n` 134 | for (let j = 0; j < opChunks.length; j++) { 135 | const opChunk = opChunks[j] 136 | // console.log('opChunk', opChunk) 137 | r = 138 | r + 139 | `| ${createReadmeCell(opChunk[0])} | ${createReadmeCell( 140 | opChunk[1] 141 | )} | ${createReadmeCell(opChunk[2])} | ${createReadmeCell( 142 | opChunk[3] 143 | )} | ${createReadmeCell(opChunk[4])} | ${createReadmeCell( 144 | opChunk[5] 145 | )} |\n` 146 | } 147 | } 148 | await fs.writeFile(OPS_README, r) 149 | } 150 | const createOpCodesBattleCameraProgressReadme = async () => { 151 | console.log('create-op-codes-battle-camera-progress-readme: START') 152 | const data = await generateProgress() 153 | await renderReadme(data) 154 | console.log('create-op-codes-battle-camera-progress-readme: END') 155 | } 156 | 157 | module.exports = { 158 | createOpCodesBattleCameraProgressReadme 159 | } 160 | --------------------------------------------------------------------------------