├── 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 |
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 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
41 |
42 |

43 |
You can access 5 different save slots
44 |
45 | Quicksave -> Press 1, 2,
46 | 3, 4, 5
47 |
48 |
49 | Quickload -> Press Shift + 1,
50 | Shift + 2, Shift + 3,
51 | Shift + 4, Shift + 5
52 |
53 |
54 | Download saves to desktop -> Press Ctrl + Shift + 1
55 |
56 |
57 |
58 |
59 |
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 `
${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 |
--------------------------------------------------------------------------------