├── castaway.png ├── src ├── index.mjs ├── favicon.ico ├── assets │ └── jonny.png ├── dgds │ ├── compression │ │ ├── rle2.mjs │ │ ├── rle.mjs │ │ └── lzw.mjs │ ├── utils │ │ ├── string.mjs │ │ └── dump.mjs │ ├── compression.mjs │ ├── resources │ │ ├── pal.mjs │ │ ├── scr.mjs │ │ ├── bmp.mjs │ │ ├── ttm.mjs │ │ └── ads.mjs │ ├── graphics.mjs │ ├── data │ │ └── scripting.mjs │ ├── resource.mjs │ ├── audio.mjs │ └── scripting │ │ └── process.mjs ├── manifest.json ├── scrantic │ ├── palette.mjs │ ├── metadata │ │ ├── types.mjs │ │ └── scenes.mjs │ ├── main.mjs │ └── story.mjs ├── index.html └── dump.mjs ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── docs └── resindex.md └── README.md /castaway.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xesf/castaway/HEAD/castaway.png -------------------------------------------------------------------------------- /src/index.mjs: -------------------------------------------------------------------------------- 1 | import { run } from './scrantic/main.mjs'; 2 | 3 | run(); 4 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xesf/castaway/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /src/data 2 | dist 3 | node_modules 4 | build 5 | server-log.txt 6 | /data 7 | -------------------------------------------------------------------------------- /src/assets/jonny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xesf/castaway/HEAD/src/assets/jonny.png -------------------------------------------------------------------------------- /src/dgds/compression/rle2.mjs: -------------------------------------------------------------------------------- 1 | export const decompressRLE2 = () => { 2 | throw 'Decompress Type RLE not implemented'; 3 | }; 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 4, 3 | semi: true, 4 | trailingComma: 'all', 5 | singleQuote: true, 6 | printWidth: 120 7 | }; 8 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "castaway-viewer", 3 | "name": "Johnny Castaway Viewer", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/dgds/utils/string.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts byte sequence numbers into String 3 | * @param {*} buffer DataView buffer 4 | * @param {*} offset Offset of the current DataView buffer 5 | * @param {*} length Max size of the string 6 | */ 7 | export const getString = (buffer, offset, length = 100) => { 8 | let str = ''; 9 | for (let i = 0; i < length; i += 1) { 10 | const char = buffer.getUint8(offset + i); 11 | if (char === 0) { 12 | break; 13 | } 14 | str += String.fromCharCode(char); 15 | } 16 | return str; 17 | }; 18 | -------------------------------------------------------------------------------- /src/dgds/compression.mjs: -------------------------------------------------------------------------------- 1 | import { decompressRLE } from './compression/rle.mjs'; 2 | import { decompressLZW } from './compression/lzw.mjs'; 3 | import { decompressRLE2 } from './compression/rle2.mjs'; 4 | 5 | export const CompressionType = [ 6 | { index: 0, type: 'None', callback: null }, 7 | { index: 1, type: 'RLE', callback: decompressRLE }, 8 | { index: 2, type: 'LZW', callback: decompressLZW }, 9 | { index: 3, type: 'RLE2', callback: decompressRLE2 }, 10 | ]; 11 | 12 | export const decompress = (type, data, offset, length) => { 13 | if (!type) { 14 | return data; 15 | } 16 | return CompressionType[type].callback(data, offset, length); 17 | }; 18 | -------------------------------------------------------------------------------- /src/scrantic/palette.mjs: -------------------------------------------------------------------------------- 1 | export const PALETTE = [ 2 | { a: 0, r: 168, g: 0, b: 168 }, 3 | { a: 255, r: 0, g: 0, b: 168 }, 4 | { a: 255, r: 0, g: 168, b: 0 }, 5 | { a: 255, r: 0, g: 168, b: 168 }, 6 | { a: 255, r: 168, g: 0, b: 0 }, 7 | { a: 255, r: 0, g: 0, b: 0 }, 8 | { a: 255, r: 168, g: 168, b: 0 }, 9 | { a: 255, r: 212, g: 212, b: 212 }, 10 | { a: 255, r: 128, g: 128, b: 128 }, 11 | { a: 255, r: 0, g: 0, b: 255 }, 12 | { a: 255, r: 0, g: 255, b: 0 }, 13 | { a: 255, r: 0, g: 255, b: 255 }, 14 | { a: 255, r: 255, g: 0, b: 0 }, 15 | { a: 255, r: 255, g: 0, b: 255 }, 16 | { a: 255, r: 255, g: 255, b: 0 }, 17 | { a: 255, r: 255, g: 255, b: 255 }, 18 | ]; 19 | -------------------------------------------------------------------------------- /src/dgds/compression/rle.mjs: -------------------------------------------------------------------------------- 1 | export const decompressRLE = (data, offset, length) => { 2 | const pdata = []; 3 | while (offset < length) { 4 | const control = data.getUint8(offset, true); 5 | offset += 1; 6 | if ((control & 0x80) === 0x80) { 7 | const len = (control & 0x7F); 8 | const value = data.getUint8(offset, true); 9 | offset += 1; 10 | for (let i = 0; i < len; i += 1) { 11 | pdata.push(value); 12 | } 13 | } else { 14 | for (let i = 0; i < control; i += 1) { 15 | pdata.push(data.getUint8(offset, true)); 16 | offset += 1; 17 | } 18 | } 19 | } 20 | return pdata; 21 | }; 22 | -------------------------------------------------------------------------------- /src/scrantic/metadata/types.mjs: -------------------------------------------------------------------------------- 1 | /** Scene Execution Flag Types */ 2 | export const FlagType = { 3 | NONE: 0x00, 4 | STORY_SCENE: 0x01, 5 | SPECIAL_SCENE: 0x02, 6 | LOW_TIDE: 0x04, 7 | LEFT_ISLAND: 0x08, 8 | NO_SEQUENCE: 0x10, 9 | NO_ISLAND: 0x20, 10 | NO_RAFT: 0x40 11 | }; 12 | 13 | /** Island Location Points */ 14 | export const PointType = { 15 | A: 0, 16 | B: 1, 17 | C: 2, 18 | D: 3, 19 | E: 4, 20 | F: 5, 21 | }; 22 | 23 | /** Island Location Heading Towards */ 24 | export const HeadingType = { 25 | S: 0, // South 26 | SW: 1, // South-West 27 | W: 2, // West 28 | NW: 3, // North-West 29 | N: 4, // North 30 | NE: 5, // North-East 31 | E: 6, // East 32 | SE: 7, // South-East 33 | }; 34 | -------------------------------------------------------------------------------- /src/scrantic/main.mjs: -------------------------------------------------------------------------------- 1 | import { drawScreen } from '../dgds/graphics.mjs'; 2 | import { loadResources, loadResourceEntry } from '../dgds/resource.mjs'; 3 | import { startProcess, stopProcess } from '../dgds/scripting/process.mjs'; 4 | import Story from './story.mjs'; 5 | 6 | export const run = async () => { 7 | const mainContext = document.getElementById('mainCanvas').getContext('2d'); 8 | mainContext.clearRect(0, 0, 640, 480); 9 | 10 | const [resIndex, resFile] = await Promise.all([ 11 | fetch('data/RESOURCE.MAP'), 12 | fetch('data/RESOURCE.001'), 13 | ]); 14 | 15 | const res = loadResources(await resIndex.arrayBuffer(), await resFile.arrayBuffer()); 16 | const resource = res.getResource('RESOURCE.001'); 17 | 18 | const introRes = resource.loadEntry('INTRO.SCR'); 19 | drawScreen(introRes, mainContext); 20 | 21 | await new Promise(r => setTimeout(r, window.location.hostname === 'localhost' ? 1000: 3000)); 22 | 23 | const story = new Story(resource); 24 | await story.play(); 25 | }; 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alexandre Fontoura 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/dgds/resources/pal.mjs: -------------------------------------------------------------------------------- 1 | import { getString } from '../utils/string.mjs'; 2 | 3 | export const loadPALResourceEntry = (entry) => { 4 | let offset = 0; 5 | const type = getString(entry.data, offset, 3); 6 | if (type !== 'PAL') { 7 | throw `Invalid Type ${type}: expecting header type PAL`; 8 | } 9 | offset += 4; 10 | // skip unknown bytes 11 | offset += 4; 12 | 13 | const block = getString(entry.data, offset, 3); 14 | if (block !== 'VGA') { 15 | throw `Invalid Type ${block}: expecting block type VGA`; 16 | } 17 | offset += 4; 18 | 19 | const palette = []; 20 | // read all 256 palette colors 21 | for (let c = 0; c < 256; c += 1) { 22 | const r = entry.data.getUint8(offset + 0, true); 23 | const g = entry.data.getUint8(offset + 1, true); 24 | const b = entry.data.getUint8(offset + 2, true); 25 | offset += 3; 26 | 27 | palette.push({ 28 | index: c, 29 | a: 255, 30 | r: r * 4, 31 | g: g * 4, 32 | b: b * 4, 33 | }); 34 | } 35 | 36 | return { 37 | name: entry.name, 38 | type, 39 | palette, 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Johnny Castaway 10 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 |
34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/dump.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | 6 | import { 7 | dumpResourceIndex, 8 | dumpResourceEntriesCompressed, 9 | dumpAvailableTypes, 10 | dumpImages, 11 | dumpMovieScripts, 12 | dumpADSScripts, 13 | dumpSamples, 14 | } from './dgds/utils/dump.mjs'; 15 | 16 | import { loadResources } from './dgds/resource.mjs' 17 | 18 | const __dirname = path.resolve(); 19 | 20 | const filepath = path.join(__dirname, 'data'); 21 | const fc = fs.readFileSync(path.join(filepath, 'RESOURCE.MAP')); 22 | const buffer = fc.buffer.slice(fc.byteOffset, fc.byteOffset + fc.byteLength); 23 | 24 | // read resource file to get extra content like name for easy identification of the asset 25 | const resfn = path.join(filepath, 'RESOURCE.001'); 26 | const resfc = fs.readFileSync(resfn); 27 | const resbuffer = resfc.buffer.slice(resfc.byteOffset, resfc.byteOffset + resfc.byteLength); 28 | 29 | const scrfn = path.join(filepath, 'SCRANTIC.SCR'); 30 | const scrfc = fs.readFileSync(scrfn); 31 | const scrbuffer = scrfc.buffer.slice(scrfc.byteOffset, scrfc.byteOffset + scrfc.byteLength); 32 | 33 | const resindex = loadResources(buffer, resbuffer); 34 | 35 | // Export Wave files 36 | dumpSamples(filepath, scrbuffer); 37 | 38 | // Export Resource Index in JSON file 39 | dumpResourceIndex(filepath, resindex); 40 | dumpResourceEntriesCompressed(filepath, resindex); 41 | dumpAvailableTypes(filepath, resindex); 42 | dumpImages(filepath, resindex); 43 | dumpMovieScripts(filepath, resindex); 44 | dumpADSScripts(filepath, resindex); 45 | 46 | console.log('Dump Complete!!'); 47 | -------------------------------------------------------------------------------- /src/scrantic/story.mjs: -------------------------------------------------------------------------------- 1 | import { loadResourceEntry } from '../dgds/resource.mjs'; 2 | import { startProcess } from '../dgds/scripting/process.mjs'; 3 | import { StoryScenes } from './metadata/scenes.mjs'; 4 | 5 | export default class Story { 6 | constructor(resource) { 7 | this.currentDay = localStorage.getItem('currentDay') || 1; 8 | this.startDate = localStorage.getItem('startDate') || (new Date()).toLocaleDateString(); 9 | this.resource = resource; 10 | } 11 | 12 | getRandomScene() { 13 | const numScenes = StoryScenes.length; 14 | const randomSceneIndex = Math.floor(Math.random() * Math.floor(numScenes)); 15 | return StoryScenes[randomSceneIndex]; 16 | } 17 | 18 | async play() { 19 | if (this.startDate !== (new Date()).toLocaleDateString()) { 20 | this.currentDay += 1; 21 | } 22 | localStorage.setItem('currentDay', this.currentDay); 23 | localStorage.setItem('startDate', this.startDate); 24 | 25 | const context = document.getElementById('canvas').getContext('2d'); 26 | context.clearRect(0, 0, 640, 480); 27 | 28 | const mainContext = document.getElementById('mainCanvas').getContext('2d'); 29 | mainContext.clearRect(0, 0, 640, 480); 30 | 31 | const scene = this.getRandomScene(); 32 | const data = this.resource.loadEntry(scene.name); 33 | 34 | startProcess({ 35 | type: 'ADS', 36 | context, 37 | mainContext, 38 | data, 39 | entries: this.resource.entries, 40 | }); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /docs/resindex.md: -------------------------------------------------------------------------------- 1 | # Resource Index File Format [rev 0.0.1] 2 | 3 | Resource Map is a file containing index information about resources. 4 | 5 | It allow us to identify which resource files need to be imported and the details of each entry of that resource file. 6 | 7 | # Engine 8 | Dynamix Game Development System (DGDS) - an engine originally created by Dynamix based on Sierra pre-existing engine. 9 | 10 | ## Games 11 | - Johny Castaway Screen Saver 12 | - Quarky & Quaysoo's Turbo Science, 13 | - Heart of China, 14 | - The Adventures of Willy Beamish, 15 | - Rise of the Dragon 16 | 17 | # Format 18 | 19 | It is composed by: 20 | - Header 21 | - Resource List 22 | - Resource Entries 23 | 24 | ## Header 25 | 26 | The header is static with length of 6 bytes 27 | 28 | - u8 unknow0 29 | - u8 unknow1 30 | - u8 unknow2 31 | - u8 unknow3 32 | - u8 numResources 33 | - number of resources files available in this index 34 | - u8 unknow5 35 | 36 | 37 | ## Resource List 38 | 39 | For each numResources entries from Header section do the following: 40 | 41 | - *u8: name 42 | - List of characters containing name of the resource file 43 | - Static size of 13 characters 44 | - Last byte is always zero to allow string termination 45 | - This will allow to parse and decompress all the resource files used 46 | 47 | - u16: numEntries 48 | - Number of entries the resource file has to be imported 49 | 50 | 51 | ## Resource Entry Header 52 | 53 | For each numEntries from Resource List do the following: 54 | 55 | - u32: length 56 | - Decompressed entry size 57 | - This will help understanding which entries need to be decompressed. More details on the Resource file format documentation 58 | 59 | - u32: offest 60 | - Offset of the entry inside the Resource file 61 | 62 | Note, to get the size of each of the compressed entries, you just need to calculate the different between the offsets. 63 | - current_offset_size = next_offset - current_offset 64 | 65 | 66 | At the end of this implementation, you should be ready to parse the Resource files, which will be describe in a separate document file. 67 | 68 | # Document History 69 | - v0.0.1 2018-11-09: First document draft 70 | 71 | # Author 72 | Alexandre Fontoura aka xesf 73 | - xesfnet@gmail.com 74 | - https://github.com/xesf 75 | 76 | ## References 77 | - Hans Milling aka nivs1978 (github) 78 | - https://github.com/nivs1978/Johnny-Castaway-Open-Source/blob/master/JCOS/Map.cs 79 | 80 | - Vasco Costa aka vcosta (github) 81 | - https://github.com/vcosta/scummvm/wiki -------------------------------------------------------------------------------- /src/dgds/graphics.mjs: -------------------------------------------------------------------------------- 1 | export const drawImage = (image, context, posX, posY) => { 2 | const img = context.createImageData(image.width, image.height); 3 | for (let p = 0; p < image.pixels.length; p += 1) { 4 | img.data[(p * 4) + 0] = image.pixels[p].r; 5 | img.data[(p * 4) + 1] = image.pixels[p].g; 6 | img.data[(p * 4) + 2] = image.pixels[p].b; 7 | img.data[(p * 4) + 3] = image.pixels[p].a; 8 | } 9 | 10 | context.putImageData(img, posX, posY); 11 | }; 12 | 13 | export const drawImageDirty = (image, context, posX, posY, dX, dY, dW, dH) => { 14 | const img = context.createImageData(image.width, image.height); 15 | for (let p = 0; p < image.pixels.length; p += 1) { 16 | img.data[(p * 4) + 0] = image.pixels[p].r; 17 | img.data[(p * 4) + 1] = image.pixels[p].g; 18 | img.data[(p * 4) + 2] = image.pixels[p].b; 19 | img.data[(p * 4) + 3] = image.pixels[p].a; 20 | } 21 | 22 | context.putImageData(img, posX, posY, dX, dY, dW, dH); 23 | }; 24 | 25 | export const drawAllImages = (data, context) => { 26 | let posX = 0; 27 | let totalWidth = 0; 28 | let maxHeight = 0; 29 | 30 | for (let i = 0; i < data.images.length; i += 1) { 31 | const image = data.images[i]; 32 | totalWidth += image.width; 33 | if (image.height > maxHeight) { 34 | maxHeight = image.height; 35 | } 36 | } 37 | 38 | context.fillStyle = 'black'; 39 | context.fillRect(0, 0, totalWidth, maxHeight); 40 | 41 | context.canvas.width = totalWidth; 42 | context.canvas.height = maxHeight; 43 | 44 | for (let i = 0; i < data.images.length; i += 1) { 45 | const image = data.images[i]; 46 | drawImage(image, context, posX, 0); 47 | posX += image.width; 48 | } 49 | }; 50 | 51 | export const drawPalette = (data, context) => { 52 | context.canvas.width = 640; 53 | context.canvas.height = 480; 54 | 55 | context.fillStyle = 'black'; 56 | context.fillRect(0, 0, 640, 480); 57 | 58 | for (let p = 0; p < data.palette.length; p += 1) { 59 | const c = data.palette[p]; 60 | context.fillStyle = getPaletteColor(c); 61 | context.fillRect(p * 2, 0, 2, 480); 62 | } 63 | }; 64 | 65 | export const drawScreen = (data, context) => { 66 | context.fillStyle = 'black'; 67 | context.fillRect(0, 0, 640, 480); 68 | 69 | context.canvas.width = 640; 70 | context.canvas.height = 480; 71 | 72 | drawImage(data.images[0], context, 0, 0); 73 | }; 74 | 75 | export const getPaletteColor = (c) => `rgb(${c.r},${c.g},${c.b})`; 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # castaway 2 | 3 | The aim of this project is to provide a complete re-implemtation of the Johnny Castaway Screen Saver created by Dynamix (Sierra On-Line Subsidiary) using Javascript modules. 4 | 5 | ![alt text](castaway.png "Dynamix Johnny Castaway Screen Saver") 6 | 7 | ## Live Demo 8 | 9 | [Check here the current development state](https://castaway.xesf.net) 10 | 11 | ## Purpose 12 | 13 | * Re-implementation of the Johnny Castaway Screen Saver; 14 | 15 | * Learn the Dynamix Game Development System (DGDS); 16 | 17 | * Document the files format used; 18 | 19 | * Dump tools using NodeJS Shell Script; 20 | 21 | * Focus on taking advantage of the modern web development languages and the usage of ES modules; 22 | 23 | * Have fun implementing it!! 24 | 25 | ## Enhancements Roadmap 26 | 27 | List of new features to add to Johnny Castaway experience: 28 | * Day/Night loop in 24h instead of 8h 29 | * Day/Night based on user location sunrise and sunset 30 | * Moving cloulds 31 | * Add waves like the static screen 32 | * Accelarate time 33 | * Tides based on user locations with real time coutry low tide info 34 | * Play Full Story Sequence 35 | * Choose single activities to play 36 | * Number of full complete stories played worldwide 37 | * Total hours worldwide played 38 | * Statistics per Activity 39 | * Total Jogging 40 | * Fishing 41 | * etc. 42 | * Extend festive days from the original - could be based on user location 43 | 44 | ## Documents 45 | 46 | [Resource Index File Format](docs/resindex.md) 47 | 48 | ## Usage 49 | 50 | Create a "data" folder in the "src" directory and place the original files. 51 | * SCRANTIC.SCR 52 | * RESOURCE.MAP 53 | * RESOURCE.001 54 | 55 | Install: 56 | * http-server (you can use "brew install http-server") 57 | 58 | ### Run Johnny Castaway 59 | 60 | Run this commands in the root folder: 61 | 62 | > cd src 63 | 64 | > http-server -c-1 65 | 66 | > open localhost:8080 67 | 68 | ### Dump Resources 69 | 70 | This application allows you to extract the resources of Johnny Castaway. A data/dump folder will be created when application is executed. 71 | 72 | > cd src 73 | 74 | > chmod +x ./dump.mjs 75 | 76 | > ./dump.mjs 77 | 78 | ## Spetial Thanks 79 | 80 | * Jérémie Guillaume (jno6809) for sharing his findings while developing Johnn Reborn (https://github.com/jno6809/jc_reborn) 81 | 82 | * Hans Milling (nivs1978) for publishing his C# attempt to remake Johnny Castaway (https://github.com/nivs1978/Johnny-Castaway-Open-Source) 83 | 84 | * Vasco Costa (vcosta) for his efforst in the DGDS ScummVM engine (https://github.com/vcosta/scummvm/tree/master/engines/dgds) 85 | 86 | ## DGDS Viewer 87 | 88 | I've create a DGDS Resource Viewer while I was building the initial version of castaway. I've then split it into its own project and it can be found here: https://github.com/xesf/dgds-viewer 89 | -------------------------------------------------------------------------------- /src/dgds/resources/scr.mjs: -------------------------------------------------------------------------------- 1 | import { getString } from '../utils/string.mjs'; 2 | import { decompress } from '../compression.mjs'; 3 | 4 | import { PALETTE } from '../../scrantic/palette.mjs'; 5 | 6 | export const loadSCRResourceEntry = (entry) => { 7 | let offset = 0; 8 | const type = getString(entry.data, offset, 3); 9 | if (type !== 'SCR') { 10 | throw `Invalid Type ${type}: expecting header type SCR`; 11 | } 12 | // info does not seem to be used - also has weird values 13 | const totalSize = entry.data.getUint16(offset + 4, true); 14 | const flags = entry.data.getUint16(offset + 6, true); 15 | offset += 8; 16 | 17 | let block = getString(entry.data, offset, 3); 18 | if (block !== 'DIM') { 19 | throw `Invalid Type ${block}: expecting block type DIM`; 20 | } 21 | let blockSize = entry.data.getUint32(offset + 4, true); 22 | const width = entry.data.getUint16(offset + 8, true); 23 | const height = entry.data.getUint16(offset + 10, true); 24 | offset += 12; 25 | 26 | block = getString(entry.data, offset, 3); 27 | if (block !== 'BIN') { 28 | throw `Invalid Type ${block}: expecting block type BIN`; 29 | } 30 | blockSize = entry.data.getUint32(offset + 4, true); 31 | const compressionType = entry.data.getUint8(offset + 8, true); 32 | /* const uncompressedSize = */ entry.data.getUint32(offset + 9, true); 33 | offset += 13; 34 | blockSize -= 5; // take type and size out of the block 35 | 36 | const compressedData = new DataView(entry.buffer.slice(offset, offset + blockSize)); 37 | const data = decompress(compressionType, compressedData, 0, compressedData.byteLength); 38 | 39 | const numImages = 1; 40 | const images = [{ 41 | width, 42 | height, 43 | buffer: [], 44 | pixels: [] 45 | }]; 46 | const image = images[0]; 47 | let dataIndex = 0; 48 | let pixelIndex = 0; 49 | for (let h = 0; h < height; h += 1) { 50 | for (let w = 0; w < width; w += 1) { 51 | let c = data[dataIndex]; 52 | if (pixelIndex % 2 === 0) { 53 | c >>= 4; 54 | } else { 55 | c &= 0x0f; 56 | dataIndex += 1; 57 | } 58 | const pal = PALETTE[c]; 59 | image.buffer[w + image.width * h] = c; 60 | image.pixels[w + image.width * h] = { 61 | index: c, 62 | a: pal.a, 63 | r: pal.r, 64 | g: pal.g, 65 | b: pal.b, 66 | }; 67 | pixelIndex += 1; 68 | } 69 | } 70 | 71 | return { 72 | name: entry.name, 73 | type, 74 | totalSize, 75 | flags, 76 | width, 77 | height, 78 | numImages, 79 | images, 80 | }; 81 | }; 82 | -------------------------------------------------------------------------------- /src/scrantic/metadata/scenes.mjs: -------------------------------------------------------------------------------- 1 | import { FlagType, PointType, HeadingType } from './types.mjs'; 2 | 3 | const defaultScene = { 4 | scene: '', 5 | storyDay: 0, 6 | tag: 0, 7 | description: '', 8 | startPoint: PointType.A, 9 | startHeading: HeadingType.S, 10 | endPoint: PointType.A, 11 | endHeading: HeadingType.S, 12 | flags: FlagType.NONE 13 | }; 14 | 15 | // order as per ADS scene tags 16 | export const StoryScenes = [ 17 | // ACTIVITY 18 | { 19 | ...defaultScene, 20 | name: 'ACTIVITY.ADS', 21 | tag: 1, 22 | description: 'GAG DIVES', 23 | startPoint: PointType.E, 24 | startHeading: HeadingType.SE, 25 | flags: FlagType.SPECIAL_SCENE 26 | }, 27 | { 28 | ...defaultScene, 29 | name: 'ACTIVITY.ADS', 30 | tag: 12, 31 | description: 'GULL 3 STILL READING', 32 | startPoint: PointType.D, 33 | startHeading: HeadingType.SW, 34 | flags: FlagType.SPECIAL_SCENE | FlagType.LOW_TIDE 35 | }, 36 | { 37 | ...defaultScene, 38 | name: 'ACTIVITY.ADS', 39 | tag: 11, 40 | description: 'GULL 2 BATHING', 41 | flags: FlagType.SPECIAL_SCENE | FlagType.NO_SEQUENCE 42 | }, 43 | { 44 | ...defaultScene, 45 | name: 'ACTIVITY.ADS', 46 | tag: 10, 47 | description: 'GULL 1 READING', 48 | startPoint: PointType.D, 49 | startHeading: HeadingType.SW, 50 | flags: FlagType.SPECIAL_SCENE | FlagType.LOW_TIDE 51 | }, 52 | { 53 | ...defaultScene, 54 | name: 'ACTIVITY.ADS', 55 | tag: 4, 56 | description: 'MUNDANE DIVE', 57 | startPoint: PointType.E, 58 | startHeading: HeadingType.SE, 59 | endPoint: PointType.E, 60 | endHeading: HeadingType.SE, 61 | flags: FlagType.LOW_TIDE 62 | }, 63 | { 64 | ...defaultScene, 65 | name: 'ACTIVITY.ADS', 66 | tag: 5, 67 | description: 'NATIVE 1', 68 | startPoint: PointType.E, 69 | startHeading: HeadingType.SW, 70 | flags: FlagType.SPECIAL_SCENE | FlagType.LOW_TIDE 71 | }, 72 | { 73 | ...defaultScene, 74 | name: 'ACTIVITY.ADS', 75 | tag: 6, 76 | description: 'GAG JOHN READ', 77 | startPoint: PointType.D, 78 | startHeading: HeadingType.SW, 79 | flags: FlagType.SPECIAL_SCENE 80 | }, 81 | { 82 | ...defaultScene, 83 | name: 'ACTIVITY.ADS', 84 | tag: 7, 85 | description: 'MUNDANE JOHN READ', 86 | startPoint: PointType.D, 87 | startHeading: HeadingType.SW, 88 | endPoint: PointType.F, 89 | endHeading: HeadingType.SW, 90 | flags: FlagType.LOW_TIDE 91 | }, 92 | { 93 | ...defaultScene, 94 | name: 'ACTIVITY.ADS', 95 | tag: 8, 96 | description: 'JOHN BATH', 97 | endPoint: PointType.D, 98 | endHeading: HeadingType.SE, 99 | flags: FlagType.NO_SEQUENCE 100 | }, 101 | { 102 | ...defaultScene, 103 | name: 'ACTIVITY.ADS', 104 | tag: 9, 105 | description: 'NATIVE 3', 106 | startPoint: PointType.E, 107 | startHeading: HeadingType.E, 108 | flags: FlagType.SPECIAL_SCENE | FlagType.LOW_TIDE 109 | } 110 | ]; 111 | -------------------------------------------------------------------------------- /src/dgds/resources/bmp.mjs: -------------------------------------------------------------------------------- 1 | import { getString } from '../utils/string.mjs'; 2 | import { decompress } from '../compression.mjs'; 3 | 4 | import { PALETTE } from '../../scrantic/palette.mjs'; 5 | 6 | export const loadBMPResourceEntry = (entry) => { 7 | let offset = 0; 8 | const type = getString(entry.data, offset, 3); 9 | if (type !== 'BMP') { 10 | throw `Invalid Type ${type}: expecting header type BMP`; 11 | } 12 | // info does not seem to be used - also has weird values 13 | const width = entry.data.getUint16(offset + 4, true); 14 | const height = entry.data.getUint16(offset + 6, true); 15 | offset += 8; 16 | 17 | let block = getString(entry.data, offset, 3); 18 | if (block !== 'INF') { 19 | throw `Invalid Type ${block}: expecting block type INF`; 20 | } 21 | // const blockSize = entry.data.getUint32(offset + 4, true); 22 | const numImages = entry.data.getUint16(offset + 8, true); 23 | offset += 10; 24 | 25 | const images = []; 26 | // get width value for all images 27 | for (let i = 0; i < numImages; i += 1) { 28 | const w = entry.data.getUint16(offset, true); 29 | offset += 2; 30 | 31 | images.push({ width: w, height: 0, pixels: [], buffer: [] }); 32 | } 33 | // get height value for all images 34 | for (let i = 0; i < numImages; i += 1) { 35 | const h = entry.data.getUint16(offset, true); 36 | offset += 2; 37 | 38 | images[i].height = h; 39 | } 40 | block = getString(entry.data, offset, 3); 41 | if (block !== 'BIN') { 42 | throw `Invalid Type ${block}: expecting block type BIN`; 43 | } 44 | let blockSize = entry.data.getUint32(offset + 4, true); 45 | const compressionType = entry.data.getUint8(offset + 8, true); 46 | /* const uncompressedSize = */ entry.data.getUint32(offset + 9, true); 47 | offset += 13; 48 | blockSize -= 5; // take type and size out of the block 49 | const compressedData = new DataView(entry.buffer.slice(offset, offset + blockSize)); 50 | const data = decompress(compressionType, compressedData, 0, compressedData.byteLength); 51 | let dataIndex = 0; 52 | let pixelIndex = 0; 53 | for (let i = 0; i < numImages; i += 1) { 54 | const image = images[i]; 55 | for (let h = 0; h < image.height; h += 1) { 56 | for (let w = 0; w < image.width; w += 1) { 57 | let c = data[dataIndex]; 58 | if (pixelIndex % 2 === 0) { 59 | c >>= 4; 60 | } else { 61 | c &= 0x0f; 62 | dataIndex += 1; 63 | } 64 | image.buffer[w + image.width * h] = c; 65 | image.pixels[w + image.width * h] = { 66 | index: c, 67 | a: PALETTE[c].a, 68 | r: PALETTE[c].r, 69 | g: PALETTE[c].g, 70 | b: PALETTE[c].b, 71 | }; 72 | pixelIndex += 1; 73 | } 74 | } 75 | } 76 | 77 | return { 78 | name: entry.name, 79 | type, 80 | width, 81 | height, 82 | numImages, 83 | images, 84 | }; 85 | }; 86 | -------------------------------------------------------------------------------- /src/dgds/data/scripting.mjs: -------------------------------------------------------------------------------- 1 | export const TTMCommandType = [ 2 | { opcode: 0x0020, command: 'SAVE_BACKGROUND' }, // not used 3 | { opcode: 0x0080, command: 'DRAW_BACKGROUND' }, 4 | { opcode: 0x0110, command: 'PURGE' }, 5 | { opcode: 0x0FF0, command: 'UPDATE' }, 6 | { opcode: 0x1020, command: 'SET_DELAY' }, 7 | { opcode: 0x1050, command: 'SLOT_IMAGE' }, 8 | { opcode: 0x1060, command: 'SLOT_PALETTE' }, 9 | { opcode: 0x1100, command: 'UNKNOWN_0' }, // Scene related? 10 | { opcode: 0x1110, command: 'SET_SCENE' }, 11 | { opcode: 0x1120, command: 'SET_BACKGROUND' }, 12 | { opcode: 0x1200, command: 'GOTO' }, 13 | { opcode: 0x2000, command: 'SET_COLORS' }, 14 | { opcode: 0x2010, command: 'SET_FRAME1' }, 15 | { opcode: 0x2020, command: 'UNKNOWN_3' }, // SET_FRAME2 ??? 16 | { opcode: 0x4000, command: 'SET_CLIP_REGION' }, 17 | { opcode: 0x4110, command: 'FADE_OUT' }, 18 | { opcode: 0x4120, command: 'FADE_IN' }, 19 | { opcode: 0x4200, command: 'SAVE_IMAGE0' }, 20 | { opcode: 0x4210, command: 'SAVE_IMAGE1' }, 21 | { opcode: 0xA000, command: 'UNKNOWN_4' }, // Draw Line related? 22 | { opcode: 0xA050, command: 'UNKNOWN_5' }, // Draw Line related? 23 | { opcode: 0xA060, command: 'UNKNOWN_6' }, // Draw Line related? 24 | { opcode: 0xA0A0, command: 'DRAW_LINE' }, 25 | { opcode: 0xA100, command: 'DRAW_RECT' }, 26 | { opcode: 0xA400, command: 'DRAW_BUBBLE' }, 27 | { opcode: 0xA500, command: 'DRAW_SPRITE' }, 28 | { opcode: 0xA510, command: 'DRAW_SPRITE1' }, // not used 29 | { opcode: 0xA520, command: 'DRAW_SPRITE_FLIP' }, 30 | { opcode: 0xA530, command: 'DRAW_SPRITE3' }, // not used 31 | { opcode: 0xA600, command: 'CLEAR_SCREEN' }, 32 | { opcode: 0xB600, command: 'DRAW_SCREEN' }, 33 | { opcode: 0xC020, command: 'LOAD_SAMPLE' }, 34 | { opcode: 0xC030, command: 'SELECT_SAMPLE' }, 35 | { opcode: 0xC040, command: 'DESELECT_SAMPLE' }, 36 | { opcode: 0xC050, command: 'PLAY_SAMPLE' }, 37 | { opcode: 0xC060, command: 'STOP_SAMPLE' }, 38 | { opcode: 0xF010, command: 'LOAD_SCREEN' }, 39 | { opcode: 0xF020, command: 'LOAD_IMAGE' }, 40 | { opcode: 0xF050, command: 'LOAD_PALETTE' }, 41 | ]; 42 | 43 | export const ADSCommandType = [ 44 | { opcode: 0x1070, paramSize: 2, command: 'UNKNOWN_0', indent: null }, 45 | { opcode: 0x1330, paramSize: 2, command: 'IF_NOT_PLAYED', indent: 1 }, 46 | { opcode: 0x1350, paramSize: 2, command: 'IF_PLAYED', indent: 1 }, // SKIP_NEXT_IF 47 | { opcode: 0x1360, paramSize: 2, command: 'IF_NOT_RUNNING', indent: 1 }, 48 | { opcode: 0x1370, paramSize: 2, command: 'IF_RUNNING', indent: 1 }, 49 | { opcode: 0x1420, paramSize: 0, command: 'AND', indent: null }, 50 | { opcode: 0x1430, paramSize: 0, command: 'OR', indent: null }, 51 | { opcode: 0x1510, paramSize: 0, command: 'PLAY_SCENE', indent: 0 }, 52 | { opcode: 0x1520, paramSize: 5, command: 'PLAY_SCENE_2', indent: 0 }, 53 | { opcode: 0x2005, paramSize: 4, command: 'ADD_SCENE', indent: null }, 54 | { opcode: 0x2010, paramSize: 3, command: 'STOP_SCENE', indent: null }, 55 | { opcode: 0x3010, paramSize: 0, command: 'RANDOM_START', indent: 1 }, 56 | { opcode: 0x3020, paramSize: 1, command: 'RANDOM_UNKNOWN_0', indent: null }, 57 | { opcode: 0x30ff, paramSize: 0, command: 'RANDOM_END', indent: -1 }, 58 | { opcode: 0x4000, paramSize: 3, command: 'UNKNOWN_6', indent: null }, 59 | { opcode: 0xf010, paramSize: 0, command: 'FADE_OUT', indent: 0 }, 60 | { opcode: 0xf200, paramSize: 1, command: 'RUN_SCRIPT', indent: 0 }, 61 | { opcode: 0xffff, paramSize: 0, command: 'END' }, 62 | // Add for text script 63 | { opcode: 0xfff0, paramSize: 0, command: 'END_IF' }, 64 | ]; 65 | -------------------------------------------------------------------------------- /src/dgds/compression/lzw.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const getBits = (data, offset, numBits, current, nextBit) => { 3 | let value = 0, innerOffset = 0; 4 | if (numBits === 0) { 5 | return { value: 0, innerOffset: 0, c: current, nb: nextBit }; 6 | } 7 | for (let b = 0; b < numBits; b++) { 8 | if (((current & (1 << nextBit))) !== 0) { 9 | value += (1 << b); 10 | } 11 | nextBit++; 12 | if (nextBit > 7) { 13 | if (offset + innerOffset >= data.byteLength) { 14 | current = 0; 15 | } else { 16 | current = data.getUint8(offset + innerOffset, true); 17 | innerOffset++; 18 | } 19 | nextBit = 0; 20 | } 21 | } 22 | return { value, innerOffset, c: current, nb: nextBit }; 23 | }; 24 | 25 | export const decompressLZW = (data, offset, length) => { 26 | const pdata = []; 27 | const decodeStack = []; 28 | const codeTable = []; 29 | let numBits = 9; 30 | let freeEntry = 257; 31 | let nextBit = 0, stackIndex = 0, bitPos = 0; 32 | 33 | let current = data.getUint8(offset++, true); 34 | 35 | const { value, innerOffset, c, nb } = getBits(data, offset, numBits, current, nextBit); 36 | let oldCode = value; 37 | nextBit = nb; 38 | current = c; 39 | offset += innerOffset; 40 | let lastByte = oldCode; 41 | 42 | pdata.push(oldCode); 43 | 44 | try { 45 | while (offset < length) { 46 | const { value, innerOffset, c, nb } = getBits(data, offset, numBits, current, nextBit); 47 | const newCode = value; 48 | nextBit = nb; 49 | current = c; 50 | offset += innerOffset; 51 | bitPos += numBits; 52 | if (newCode === 256) { 53 | const numBits3 = numBits << 3; 54 | const numSkip = (numBits3 - ((bitPos - 1) % numBits3)) - 1; 55 | const { value, innerOffset, c, nb } = getBits(data, offset, numSkip, current, nextBit); 56 | nextBit = nb; 57 | current = c; 58 | offset += innerOffset; 59 | numBits = 9; 60 | freeEntry = 256; 61 | bitPos = 0; 62 | } else { 63 | let code = newCode; 64 | if (code >= freeEntry) { 65 | if (stackIndex >= 4096) { 66 | break; 67 | } 68 | decodeStack[stackIndex] = lastByte; 69 | stackIndex++; 70 | code = oldCode; 71 | } 72 | while (code >= 256) { 73 | if (code > 4095) { 74 | break; 75 | } 76 | decodeStack[stackIndex] = codeTable[code].append; 77 | stackIndex++; 78 | code = codeTable[code].prefix; 79 | } 80 | decodeStack[stackIndex] = code; 81 | stackIndex++; 82 | lastByte = code; 83 | while (stackIndex > 0) { 84 | stackIndex--; 85 | pdata.push(decodeStack[stackIndex]); 86 | } 87 | if (freeEntry < 4096) { 88 | codeTable[freeEntry] = { 89 | prefix: oldCode, 90 | append: lastByte, 91 | }; 92 | freeEntry++; 93 | if (freeEntry >= (1 << numBits) && numBits < 12) { 94 | numBits++; 95 | bitPos = 0; 96 | } 97 | } 98 | oldCode = newCode; 99 | } 100 | } 101 | } catch (error) { 102 | console.error(error); 103 | } 104 | return pdata; 105 | }; 106 | /* eslint-enable */ 107 | -------------------------------------------------------------------------------- /src/dgds/resource.mjs: -------------------------------------------------------------------------------- 1 | import { getString } from './utils/string.mjs'; 2 | 3 | import { loadADSResourceEntry } from './resources/ads.mjs'; 4 | import { loadBMPResourceEntry } from './resources/bmp.mjs'; 5 | import { loadPALResourceEntry } from './resources/pal.mjs'; 6 | import { loadSCRResourceEntry } from './resources/scr.mjs'; 7 | import { loadTTMResourceEntry } from './resources/ttm.mjs'; 8 | 9 | const INDEX_STRING_SIZE = 12; 10 | const INDEX_HEADER_SIZE = 6; 11 | 12 | export const ResourceType = [ 13 | { type: 'ADS', callback: loadADSResourceEntry }, // Animation sequences 14 | { type: 'BMP', callback: loadBMPResourceEntry }, // Various raw images 15 | { type: 'PAL', callback: loadPALResourceEntry }, // VGA palette 16 | { type: 'SCR', callback: loadSCRResourceEntry }, // Background raw images 17 | { type: 'TTM', callback: loadTTMResourceEntry }, // Scripting macros 18 | ]; 19 | 20 | /** 21 | * Load all Resource details based on index resource file 22 | * @param {*} filepath Full path of the file 23 | * @param {*} filename File name 24 | */ 25 | export const loadResources = (buffer, resbuffer) => { 26 | let offset = 0; // current resource offest 27 | const resources = []; // list of resource files 28 | const data = new DataView(buffer, offset, buffer.byteLength); 29 | 30 | const header = { 31 | unk0: data.getUint8(offset, true), 32 | unk1: data.getUint8(offset + 1, true), // number of entries? 33 | unk2: data.getUint8(offset + 2, true), 34 | unk3: data.getUint8(offset + 3, true), 35 | numResources: data.getUint8(offset + 4, true), 36 | unk5: data.getUint8(offset + 5, true), 37 | }; 38 | offset += INDEX_HEADER_SIZE; 39 | 40 | // Read resource files and entries 41 | // Read number of resource files (castaway only uses a single one) 42 | for (let r = 0; r < header.numResources; r += 1) { 43 | let innerOffset = offset; 44 | const res = { 45 | name: getString(data, innerOffset, INDEX_STRING_SIZE), 46 | numEntries: data.getUint16(innerOffset + 13, true), 47 | size: 0, 48 | entries: [], 49 | }; 50 | res.getEntry = (name) => res.entries.find(e => e.name === name); 51 | res.loadEntry = (name) => { 52 | const entry = res.getEntry(name); 53 | if (entry !== undefined) { 54 | return loadResourceEntry(entry); 55 | } 56 | return null; 57 | }; 58 | resources.push(res); 59 | innerOffset += 15; 60 | 61 | res.size = resbuffer.byteLength; 62 | const resData = new DataView(resbuffer, 0, res.size); 63 | 64 | for (let e = 0; e < res.numEntries; e += 1) { 65 | // from index 66 | const entrySize = data.getUint16(innerOffset, true); // uncompressed size 67 | const entryOffset = data.getUint32(innerOffset + 4, true); 68 | // from resource 69 | const name = getString(resData, entryOffset, INDEX_STRING_SIZE); 70 | const entryCompressedSize = resData.getUint32(entryOffset + 13, true); 71 | const startOffset = entryOffset + 17; 72 | const endOffset = startOffset + entryCompressedSize; 73 | 74 | const entry = { 75 | name, 76 | type: name.split('.')[1], 77 | size: entrySize, // uncompressed size 78 | offset: entryOffset, 79 | compressedSize: entryCompressedSize, 80 | buffer: resbuffer.slice(startOffset, endOffset), 81 | data: new DataView(resbuffer, startOffset, entryCompressedSize), 82 | }; 83 | innerOffset += 8; 84 | 85 | res.entries.push(entry); 86 | } 87 | } 88 | 89 | return { 90 | header, 91 | resources, 92 | getResource: (name) => resources.find((r) => r.name === name), 93 | }; 94 | }; 95 | 96 | export const loadResourceEntry = (entry) => { 97 | const resType = ResourceType.find((r) => r.type === entry.type); 98 | return resType.callback(entry); 99 | }; 100 | -------------------------------------------------------------------------------- /src/dgds/audio.mjs: -------------------------------------------------------------------------------- 1 | const samplesSourceCache = []; 2 | 3 | export const sampleOffsets = [ 4 | -1, 5 | 0x1DC00, 0x20800, 0x20E00, 6 | 0x22C00, 0x24000, 0x24C00, 7 | 0x28A00, 0x2C600, 0x2D000, 8 | 0x2DE00, 9 | -1, 0x34400, 0x32E00, 10 | 0x39C00, 0x43400, 0x37200, 11 | 0x37E00, 0x45A00, 0x3AE00, 12 | 0x3E600, 0x3F400, 0x41200, 13 | 0x42600, 0x42C00, 0x43400 14 | ]; 15 | 16 | const createAudioContext = () => { 17 | window.AudioContext = window.AudioContext || window.webkitAudioContext; 18 | return new AudioContext(); 19 | }; 20 | 21 | const getSoundFxSource = (config, context, data) => { 22 | const source = { 23 | volume: config.soundFxVolume, 24 | isPlaying: false, 25 | loop: false, 26 | currentIndex: -1, 27 | bufferSource: null, 28 | gainNode: context.createGain(), 29 | lowPassFilter: context.createBiquadFilter(), 30 | pause: () => {}, 31 | data 32 | }; 33 | source.lowPassFilter.type = 'allpass'; 34 | 35 | source.play = () => { 36 | source.isPlaying = true; 37 | source.bufferSource.start(); 38 | }; 39 | source.stop = () => { 40 | try { 41 | if (source.bufferSource) { 42 | source.bufferSource.stop(); 43 | } 44 | } catch (error) { 45 | // eslint-disable-next-line no-console 46 | console.debug(error); 47 | } 48 | source.isPlaying = false; 49 | }; 50 | source.suspend = () => { 51 | context.suspend(); 52 | }; 53 | source.resume = () => { 54 | context.resume(); 55 | }; 56 | source.load = (index, callback) => { 57 | if (index <= -1 || 58 | (source.currentIndex === index && source.isPlaying) || 59 | sampleOffsets[index] === -1) { 60 | return; 61 | } 62 | if (source.isPlaying) { 63 | source.stop(); 64 | } 65 | source.currentIndex = index; 66 | source.bufferSource = context.createBufferSource(); 67 | source.bufferSource.onended = () => { 68 | source.isPlaying = false; 69 | }; 70 | 71 | if (samplesSourceCache[index]) { 72 | source.bufferSource.buffer = samplesSourceCache[index]; 73 | source.connect(); 74 | callback.call(); 75 | } else { 76 | fetch('data/SCRANTIC.SCR').then((response) => response.arrayBuffer()).then((fileBuffer) => { 77 | const data = new DataView(fileBuffer); 78 | const size = data.getInt32(sampleOffsets[index] + 4, true) + 8; 79 | const buffer = data.buffer.slice(sampleOffsets[index], sampleOffsets[index] + size); 80 | 81 | context.decodeAudioData( 82 | buffer, 83 | (decodeBuffer) => { 84 | if (!samplesSourceCache[index]) { 85 | if (!source.bufferSource.buffer) { 86 | source.bufferSource.buffer = decodeBuffer; 87 | samplesSourceCache[index] = decodeBuffer; 88 | source.connect(); 89 | callback.call(); 90 | } 91 | } 92 | }, (err) => { 93 | console.error(err); 94 | } 95 | ); 96 | }); 97 | } 98 | }; 99 | 100 | source.connect = () => { 101 | // source->gain->context 102 | source.bufferSource.connect(source.gainNode); 103 | source.gainNode.gain.setValueAtTime(source.volume, context.currentTime + 1); 104 | source.gainNode.connect(source.lowPassFilter); 105 | source.lowPassFilter.connect(context.destination); 106 | }; 107 | 108 | return source; 109 | }; 110 | 111 | export const createAudioManager = (config) => { 112 | const context = createAudioContext(); 113 | const sfxSource = getSoundFxSource(config, context); 114 | return { 115 | context, 116 | getSoundFxSource: () => sfxSource, 117 | }; 118 | }; 119 | -------------------------------------------------------------------------------- /src/dgds/resources/ttm.mjs: -------------------------------------------------------------------------------- 1 | import { getString } from '../utils/string.mjs'; 2 | import { decompress } from '../compression.mjs'; 3 | 4 | import { TTMCommandType } from '../data/scripting.mjs'; 5 | 6 | export const loadTTMResourceEntry = (entry) => { 7 | let offset = 0; 8 | let type = getString(entry.data, offset, 3); 9 | if (type !== 'VER') { 10 | throw `Invalid Type ${type}: expecting header type VER`; 11 | } 12 | const versionSize = entry.data.getUint32(offset + 4, true); 13 | /* const version = */ getString(entry.data, offset + 8, versionSize); // 4.09 14 | offset += 8; 15 | offset += versionSize; 16 | 17 | let block = getString(entry.data, offset, 3); 18 | if (block !== 'PAG') { 19 | throw `Invalid Type ${block}: expecting block type PAG`; 20 | } 21 | offset += 4; 22 | const numPages = entry.data.getUint32(offset, true); 23 | const pagUnknown02 = entry.data.getUint16(offset + 4, true); 24 | // Skip unknown fields 25 | offset += 6; 26 | 27 | block = getString(entry.data, offset, 3); 28 | if (block !== 'TT3') { 29 | throw `Invalid Type ${block}: expecting block type TT3`; 30 | } 31 | let blockSize = entry.data.getUint32(offset + 4, true); 32 | const compressionType = entry.data.getUint8(offset + 8, true); 33 | const uncompressedSize = entry.data.getUint32(offset + 9, true); 34 | offset += 13; 35 | blockSize -= 5; // take type and size out of the block 36 | const compressedData = new DataView(entry.buffer.slice(offset, offset + blockSize)); 37 | offset += blockSize; 38 | let data = decompress(compressionType, compressedData, 0, compressedData.byteLength); 39 | data = new DataView(new Int8Array(data).buffer); 40 | 41 | block = getString(entry.data, offset, 3); 42 | if (block !== 'TTI') { 43 | throw `Invalid Type ${block}: expecting block type TTI`; 44 | } 45 | offset += 4; 46 | const ttiUnknown01 = entry.data.getUint16(offset, true); 47 | const ttiUnknown02 = entry.data.getUint16(offset + 2, true); 48 | // Skip unknown fields 49 | offset += 4; 50 | 51 | block = getString(entry.data, offset, 3); 52 | if (block !== 'TAG') { 53 | throw `Invalid Type ${block}: expecting block type TAG`; 54 | } 55 | offset += 4; 56 | /* const tagSize = */ entry.data.getUint32(offset, true); 57 | const numTags = entry.data.getUint16(offset + 4, true); 58 | offset += 6; 59 | 60 | const tags = []; 61 | for (let t = 0; t < numTags; t += 1) { 62 | const id = entry.data.getUint16(offset, true); 63 | const description = getString(entry.data, offset + 2); 64 | tags.push({ 65 | id, 66 | description 67 | }); 68 | offset += 2; 69 | offset += description.length + 1; 70 | } 71 | 72 | let lineNumber = 1; 73 | let innerOffset = 0; 74 | const scripts = []; 75 | let prevTagId = 0; 76 | const scenes = []; 77 | let sceneScripts = []; 78 | while (innerOffset < uncompressedSize) { 79 | let opcode = data.getUint16(innerOffset, true); 80 | innerOffset += 2; 81 | const size = opcode & 0x000f; 82 | opcode &= 0xfff0; 83 | const command = { 84 | opcode, 85 | lineNumber, 86 | line: null, 87 | name: null, 88 | tag: null, 89 | params: [] 90 | }; 91 | if (opcode === 0x1110 && size === 1) { 92 | const tagId = data.getUint16(innerOffset, true); 93 | innerOffset += 2; 94 | command.tag = tags.find((t) => t.id === tagId); 95 | if (command.tag !== undefined) { 96 | command.name = `${tagId}:${command.tag.description}`; 97 | } else { 98 | command.name = ` tag[${tagId}]`; 99 | } 100 | scenes.push({ 101 | tagId: prevTagId, 102 | script: sceneScripts 103 | }); 104 | sceneScripts = []; // reset scene script 105 | prevTagId = tagId; 106 | } else if (size === 15) { 107 | command.name = getString(data, innerOffset); 108 | innerOffset += command.name.length; 109 | if (data.getUint8(innerOffset, true) === 0) { 110 | innerOffset += 1; 111 | } 112 | if (data.getUint8(innerOffset, true) === 0) { 113 | innerOffset += 1; 114 | } 115 | command.params.push(command.name); 116 | } else { 117 | for (let b = 0; b < size; b += 1) { 118 | command.params.push(data.getInt16(innerOffset, true)); 119 | innerOffset += 2; 120 | } 121 | } 122 | 123 | type = TTMCommandType.find((ct) => ct.opcode === command.opcode); 124 | command.line = ''; // [0x${c.opcode.toString(16)}] 125 | if (type !== undefined) { 126 | command.line += `${type.command} `; 127 | if (command.opcode === 0x1110 && command.name) { 128 | command.line += command.name.toUpperCase(); 129 | } 130 | } else { 131 | command.line = 'UNKNOWN '; 132 | } 133 | for (let p = 0; p < command.params.length; p += 1) { 134 | command.line += `${command.params[p]} `; 135 | } 136 | 137 | lineNumber += 1; 138 | scripts.push(command); 139 | sceneScripts.push(command); 140 | } 141 | 142 | scenes.push({ 143 | tagId: prevTagId, 144 | script: sceneScripts 145 | }); 146 | 147 | return { 148 | name: entry.name, 149 | type: entry.type, 150 | numPages, 151 | pagUnknown02, 152 | ttiUnknown01, 153 | ttiUnknown02, 154 | tags, 155 | buffer: data, 156 | scripts, 157 | scenes, 158 | }; 159 | }; 160 | -------------------------------------------------------------------------------- /src/dgds/resources/ads.mjs: -------------------------------------------------------------------------------- 1 | import { getString } from '../utils/string.mjs'; 2 | import { decompress } from '../compression.mjs'; 3 | 4 | import { ADSCommandType } from '../data/scripting.mjs'; 5 | 6 | export const loadADSResourceEntry = (entry) => { 7 | let offset = 0; 8 | const type = getString(entry.data, offset, 3); 9 | if (type !== 'VER') { 10 | throw `Invalid Type ${type}: expecting header type VER`; 11 | } 12 | const versionSize = entry.data.getUint32(offset + 4, true); 13 | /* const version = */ getString(entry.data, offset + 8, versionSize); // 4.09 14 | offset += 8; 15 | offset += versionSize; 16 | 17 | let block = getString(entry.data, offset, 3); 18 | if (block !== 'ADS') { 19 | throw `Invalid Type ${block}: expecting block type ADS`; 20 | } 21 | offset += 4; 22 | const adsUnknown01 = entry.data.getUint16(offset, true); 23 | const adsUnknown02 = entry.data.getUint16(offset + 2, true); 24 | // Skip unknown fields 25 | offset += 4; 26 | 27 | block = getString(entry.data, offset, 3); 28 | if (block !== 'RES') { 29 | throw `Invalid Type ${block}: expecting block type RES`; 30 | } 31 | offset += 4; 32 | /* const resSize = */ entry.data.getUint32(offset, true); 33 | const numResources = entry.data.getUint16(offset + 4, true); 34 | // Skip unknown fields 35 | offset += 6; 36 | 37 | const resources = []; 38 | for (let r = 0; r < numResources; r += 1) { 39 | const id = entry.data.getUint16(offset, true); 40 | const name = getString(entry.data, offset + 2); 41 | resources.push({ 42 | id, 43 | name 44 | }); 45 | offset += 2; 46 | offset += name.length + 1; 47 | } 48 | 49 | block = getString(entry.data, offset, 3); 50 | if (block !== 'SCR') { 51 | throw `Invalid Type ${block}: expecting block type SCR`; 52 | } 53 | let blockSize = entry.data.getUint32(offset + 4, true); 54 | const compressionType = entry.data.getUint8(offset + 8, true); 55 | const uncompressedSize = entry.data.getUint32(offset + 9, true); 56 | offset += 13; 57 | blockSize -= 5; // take type and size out of the block 58 | const compressedData = new DataView(entry.buffer.slice(offset, offset + blockSize)); 59 | offset += blockSize; 60 | let data = decompress(compressionType, compressedData, 0, compressedData.byteLength); 61 | data = new DataView(new Int8Array(data).buffer); 62 | 63 | block = getString(entry.data, offset, 3); 64 | if (block !== 'TAG') { 65 | throw `Invalid Type ${block}: expecting block type TAG`; 66 | } 67 | offset += 4; 68 | /* const tagSize = */ entry.data.getUint32(offset, true); 69 | const numTags = entry.data.getUint16(offset + 4, true); 70 | offset += 6; 71 | 72 | const tags = []; 73 | for (let t = 0; t < numTags; t += 1) { 74 | const id = entry.data.getUint16(offset, true); 75 | const description = getString(entry.data, offset + 2); 76 | tags.push({ 77 | id, 78 | description 79 | }); 80 | offset += 2; 81 | offset += description.length + 1; 82 | } 83 | 84 | let lineNumber = 1; 85 | let indent = 0; 86 | let innerOffset = 0; 87 | let prevTagId = 0; 88 | const scripts = []; 89 | const scenes = []; 90 | let sceneScripts = []; 91 | while (innerOffset < uncompressedSize) { 92 | const opcode = data.getUint16(innerOffset, true); 93 | innerOffset += 2; 94 | const command = { 95 | opcode, 96 | lineNumber, 97 | line: '', 98 | indent: 0, 99 | tag: null, 100 | params: [] 101 | }; 102 | const c = ADSCommandType.find((ct) => ct.opcode === opcode); 103 | if (c !== undefined && opcode > 0x100) { 104 | const size = c.paramSize; 105 | command.line += `${c.command} `; 106 | 107 | for (let b = 0; b < size; b += 1) { 108 | const param = data.getInt16(innerOffset, true); 109 | command.params.push(param); 110 | innerOffset += 2; 111 | 112 | command.line += `${param} `; 113 | } 114 | 115 | command.indent = indent; 116 | if (c.indent !== null) { 117 | indent += c.indent; 118 | } 119 | if (c.indent < 0) { 120 | command.indent = indent; 121 | } 122 | if (c.indent === 0) { 123 | command.indent = 0; 124 | while (indent) { 125 | indent -= 1; 126 | scripts.push({ 127 | opcode: 0xfff0, 128 | lineNumber, 129 | line: 'END_IF', 130 | indent, 131 | tag: null, 132 | params: [] 133 | }); 134 | lineNumber += 1; 135 | } 136 | indent = 0; 137 | command.lineNumber = lineNumber; 138 | } 139 | sceneScripts.push(command); 140 | } else { 141 | command.tag = tags.find((t) => t.id === command.opcode); 142 | command.line += `${command.tag.description}`; 143 | command.indent = 0; 144 | indent = 0; 145 | if (prevTagId) { 146 | scenes.push({ 147 | tagId: prevTagId, 148 | script: sceneScripts 149 | }); 150 | } 151 | sceneScripts = []; // reset scene script 152 | prevTagId = command.tag; 153 | } 154 | 155 | lineNumber += 1; 156 | scripts.push(command); 157 | } 158 | 159 | return { 160 | name: entry.name, 161 | type: entry.type, 162 | adsUnknown01, 163 | adsUnknown02, 164 | resources, 165 | tags, 166 | buffer: data, 167 | scripts, 168 | scenes, 169 | }; 170 | }; 171 | -------------------------------------------------------------------------------- /src/dgds/utils/dump.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import os from 'os'; 4 | 5 | import { loadResources, loadResourceEntry } from '../resource.mjs'; 6 | import { TTMCommandType, ADSCommandType } from '../data/scripting.mjs'; 7 | import { sampleOffsets } from '../audio.mjs'; 8 | 9 | export const dumpSamples = (filepath, scrbuffer) => { 10 | let dumppath = path.join(filepath, 'dump'); 11 | if (!fs.existsSync(dumppath)) { 12 | fs.mkdirSync(dumppath); 13 | } 14 | dumppath = path.join(dumppath, 'riffs'); 15 | if (!fs.existsSync(dumppath)) { 16 | fs.mkdirSync(dumppath); 17 | } 18 | const data = new DataView(scrbuffer); 19 | for (let index = 0; index < sampleOffsets.length; index += 1) { 20 | if (sampleOffsets[index] === -1) { 21 | continue; 22 | } 23 | const size = data.getInt32(sampleOffsets[index] + 4, true) + 8; 24 | const buffer = data.buffer.slice(sampleOffsets[index], sampleOffsets[index] + size); 25 | fs.writeFileSync(path.join(dumppath, `sample${index}.wav`), Buffer.from(buffer), 'utf-8'); 26 | } 27 | }; 28 | 29 | export const dumpResourceIndex = (filepath, resindex) => { 30 | const dumppath = path.join(filepath, 'dump'); 31 | if (!fs.existsSync(dumppath)) { 32 | fs.mkdirSync(dumppath); 33 | } 34 | fs.writeFileSync(path.join(dumppath, 'resindex.json'), JSON.stringify(resindex, null, 2), 'utf-8'); 35 | }; 36 | 37 | export const dumpResourceEntriesCompressed = (filepath, resindex) => { 38 | const dumppath = path.join(filepath, 'dump/compressed'); 39 | if (!fs.existsSync(dumppath)) { 40 | fs.mkdirSync(dumppath); 41 | } 42 | const res = resindex.resources[0]; 43 | for (let e = 0; e < res.numEntries; e += 1) { 44 | const entry = res.entries[e]; 45 | fs.writeFileSync(path.join(dumppath, entry.name), Buffer.from(entry.buffer), 'utf-8'); 46 | } 47 | }; 48 | 49 | export const dumpAvailableTypes = (filepath, resindex) => { 50 | const types = []; 51 | const res = resindex.resources[0]; 52 | for (let e = 0; e < res.numEntries; e += 1) { 53 | const entry = res.entries[e]; 54 | if (!types.find((f) => f === entry.type)) { 55 | types.push(entry.type); 56 | } 57 | } 58 | const dumppath = path.join(filepath, 'dump'); 59 | fs.writeFileSync(path.join(dumppath, 'restypes.json'), JSON.stringify({ types }, null, 2), 'utf-8'); 60 | }; 61 | 62 | export const dumpImages = (filepath, resindex) => { 63 | const dumppath = path.join(filepath, 'dump', 'images'); 64 | if (!fs.existsSync(dumppath)) { 65 | fs.mkdirSync(dumppath); 66 | } 67 | const res = resindex.resources[0]; 68 | for (let j = 0; j < res.numEntries; j += 1) { 69 | const entry = res.entries[j]; 70 | if (entry.type === 'BMP') { 71 | const e = loadResourceEntry(entry); 72 | for (let i = 0; i < e.numImages; i += 1) { 73 | fs.writeFileSync(path.join(dumppath, `${e.name}_img_${i}.json`), JSON.stringify(e.images[i], null, 2), 'utf-8'); 74 | fs.writeFileSync(path.join(dumppath, `${e.name}_img_${i}.raw`), Buffer.from(e.images[i].buffer)); 75 | } 76 | } 77 | } 78 | }; 79 | 80 | export const dumpMovieScripts = (filepath, resindex) => { 81 | const dumppath = path.join(filepath, 'dump', 'scripts'); 82 | if (!fs.existsSync(dumppath)) { 83 | fs.mkdirSync(dumppath); 84 | } 85 | const res = resindex.resources[0]; 86 | for (let j = 0; j < res.numEntries; j += 1) { 87 | const entry = res.entries[j]; 88 | if (entry.type === 'TTM') { 89 | const e = loadResourceEntry(entry); 90 | fs.writeFileSync(path.join(dumppath, `${e.name}_script.txt`), ''); 91 | fs.writeFileSync(path.join(dumppath, `${e.name}_script.raw`), Buffer.from(entry.buffer)); 92 | 93 | fs.appendFileSync(path.join(dumppath, `${e.name}_script.txt`), `[TAGS]:${os.EOL}`); 94 | for (let i = 0; i < e.tags.length; i += 1) { 95 | const t = e.tags[i]; 96 | fs.appendFileSync( 97 | path.join(dumppath, 98 | `${e.name}_script.txt`), 99 | `${t.id} ${t.description} ${os.EOL}`); 100 | } 101 | 102 | fs.appendFileSync(path.join(dumppath, `${e.name}_script.txt`), `${os.EOL}[SCRIPTS]:${os.EOL}`); 103 | for (let i = 0; i < e.scripts.length; i += 1) { 104 | const c = e.scripts[i]; 105 | const type = TTMCommandType.find((ct) => ct.opcode === c.opcode); 106 | let command = ''; // [0x${c.opcode.toString(16)}] 107 | if (type !== undefined) { 108 | if (type.command === 'SET_SCENE') { 109 | command += os.EOL; 110 | } 111 | command += `${type.command} `; 112 | if (type.command === 'SET_SCENE' && c.name) { 113 | command += c.name; 114 | } 115 | } else { 116 | command = 'UNKNOWN '; 117 | } 118 | for (let p = 0; p < c.params.length; p += 1) { 119 | command += `${c.params[p]} `; 120 | } 121 | command += os.EOL; 122 | fs.appendFileSync(path.join(dumppath, `${e.name}_script.txt`), command); 123 | } 124 | } 125 | } 126 | }; 127 | 128 | export const dumpADSScripts = (filepath, resindex) => { 129 | const dumppath = path.join(filepath, 'dump', 'scripts'); 130 | if (!fs.existsSync(dumppath)) { 131 | fs.mkdirSync(dumppath); 132 | } 133 | 134 | const res = resindex.resources[0]; 135 | for (let e = 0; e < res.numEntries; e += 1) { 136 | const entry = res.entries[e]; 137 | if (entry.type === 'ADS') { 138 | const e = loadResourceEntry(entry); 139 | fs.writeFileSync(path.join(dumppath, `${e.name}_script.txt`), ''); 140 | fs.writeFileSync(path.join(dumppath, `${e.name}_script.raw`), Buffer.from(entry.buffer)); 141 | 142 | 143 | fs.appendFileSync(path.join(dumppath, `${e.name}_script.txt`), `[RESOURCES]: ${os.EOL}`); 144 | for (let i = 0; i < e.resources.length; i += 1) { 145 | const r = e.resources[i]; 146 | fs.appendFileSync( 147 | path.join(dumppath, 148 | `${e.name}_script.txt`), 149 | `${r.id} ${r.name} ${os.EOL}`); 150 | } 151 | fs.appendFileSync(path.join(dumppath, `${e.name}_script.txt`), `${os.EOL}[TAGS]:${os.EOL}`); 152 | for (let i = 0; i < e.tags.length; i += 1) { 153 | const t = e.tags[i]; 154 | fs.appendFileSync( 155 | path.join(dumppath, 156 | `${e.name}_script.txt`), 157 | `${t.id} ${t.description} ${os.EOL}`); 158 | } 159 | 160 | fs.appendFileSync(path.join(dumppath, `${e.name}_script.txt`), `${os.EOL}[SCRIPTS]:${os.EOL}`); 161 | 162 | for (let i = 0; i < e.scripts.length; i += 1) { 163 | const c = e.scripts[i]; 164 | let command = ''; // [0x${c.opcode.toString(16)}] 165 | if (c.opcode > 0x100) { 166 | const type = ADSCommandType.find((ct) => ct.opcode === c.opcode); 167 | command = command.padStart(c.indent * 2, ' '); 168 | if (type !== undefined) { 169 | command += `${type.command} `; 170 | } 171 | for (let p = 0; p < c.params.length; p += 1) { 172 | command += `${c.params[p]} `; 173 | } 174 | command += os.EOL; 175 | if (type.command === 'END') { 176 | command += os.EOL; 177 | } 178 | } else { 179 | command += `${c.opcode}:${c.tag.description}`; 180 | command += os.EOL; 181 | } 182 | fs.appendFileSync(path.join(dumppath, `${e.name}_script.txt`), command); 183 | } 184 | } 185 | } 186 | }; 187 | -------------------------------------------------------------------------------- /src/dgds/scripting/process.mjs: -------------------------------------------------------------------------------- 1 | import { createAudioManager } from '../audio.mjs'; 2 | import { loadResourceEntry } from '../resource.mjs'; 3 | import { drawImage, drawScreen, getPaletteColor } from '../graphics.mjs'; 4 | 5 | import { PALETTE } from '../../scrantic/palette.mjs'; 6 | 7 | let tick = null; 8 | let prevTick = Date.now(); 9 | let elapsed = null; 10 | const fps = 1000 / 60; 11 | 12 | let state = null; 13 | let currentScene = 0; 14 | 15 | let scenesRes = []; 16 | let scenes = []; 17 | let scenesRandom = []; 18 | let addScenes = []; 19 | let removeScenes = []; 20 | 21 | let bkgScreen = null; 22 | let bkgRes = null; 23 | let bkgOcean = []; 24 | let bkgRaft = null; 25 | let cloudIdx = Math.floor((Math.random() * 3) + 15); 26 | let cloudX = Math.floor((Math.random() * 640)); 27 | let cloudY = Math.floor((Math.random() * 80)); 28 | let cloudElapsed = 0; 29 | 30 | const clearContext = (context) => { 31 | context.clearRect(0, 0, 640, 480); 32 | }; 33 | 34 | const drawContext = (state, index) => { 35 | const save = state.save[state.saveIndex]; 36 | if (save.canDraw) { 37 | save.canDraw = false; 38 | state.context.drawImage(save.context.canvas, 0, 0); 39 | } 40 | } 41 | 42 | // FIXME Improve this code repetition 43 | const drawBackground = (state, context) => { 44 | // Draw background / ocean / night 45 | if (bkgScreen) { 46 | drawScreen(bkgScreen, context); 47 | } 48 | 49 | if (state.island) { 50 | const posX = (state.island === 1) ? 288 : 16; 51 | 52 | if (!cloudElapsed) { 53 | cloudElapsed = Math.floor((Math.random() * 640)) + Date.now(); 54 | } 55 | if (Date.now() > cloudElapsed) { 56 | cloudElapsed = 0; 57 | cloudX--; 58 | } 59 | 60 | // Draw island 61 | if (bkgRes) { 62 | // Draw clouds (random and animated) 63 | let image = bkgRes.images[cloudIdx]; 64 | drawImage(image, state.tmpContext, 0, 0); 65 | context.drawImage(state.tmpContext.canvas, 0, 0, image.width, image.height, cloudX, cloudY, image.width, image.height); 66 | 67 | // Draw raft based on state 68 | image = bkgRaft.images[3]; 69 | drawImage(image, state.tmpContext, 0, 0); 70 | context.drawImage(state.tmpContext.canvas, 0, 0, image.width, image.height, posX + 222, 268, image.width, image.height); 71 | 72 | // isle 73 | image = bkgRes.images[0]; 74 | drawImage(image, state.tmpContext, 0, 0); 75 | context.drawImage(state.tmpContext.canvas, 0, 0, image.width, image.height, posX, 280, image.width, image.height); 76 | 77 | // palm tree 78 | image = bkgRes.images[14]; 79 | drawImage(image, state.tmpContext, 0, 0); 80 | context.drawImage(state.tmpContext.canvas, 0, 0, image.width, image.height, posX + 108, 280, image.width, image.height); 81 | image = bkgRes.images[13]; 82 | drawImage(image, state.tmpContext, 0, 0); 83 | context.drawImage(state.tmpContext.canvas, 0, 0, image.width, image.height, posX + 154, 148, image.width, image.height); 84 | image = bkgRes.images[12]; 85 | drawImage(image, state.tmpContext, 0, 0); 86 | context.drawImage(state.tmpContext.canvas, 0, 0, image.width, image.height, posX + 77, 122, image.width, image.height); 87 | 88 | // Draw shore with animations 89 | image = bkgRes.images[3]; 90 | drawImage(image, state.tmpContext, 0, 0); 91 | context.drawImage(state.tmpContext.canvas, 0, 0, image.width, image.height, posX - 13, 305, image.width, image.height); 92 | 93 | image = bkgRes.images[6]; 94 | drawImage(image, state.tmpContext, 0, 0); 95 | context.drawImage(state.tmpContext.canvas, 0, 0, image.width, image.height, posX + 76, 320, image.width, image.height); 96 | 97 | image = bkgRes.images[10]; 98 | drawImage(image, state.tmpContext, 0, 0); 99 | context.drawImage(state.tmpContext.canvas, 0, 0, image.width, image.height, posX + 230, 303, image.width, image.height); 100 | 101 | // Draw low tide 102 | } 103 | } 104 | } 105 | 106 | // TTM COMMANDS 107 | const SAVE_BACKGROUND = (state) => { }; 108 | 109 | const DRAW_BACKGROUND = (state) => { 110 | // RESTORE_REGION(state, 0, 0, 0, 0); 111 | drawBackground(state, state.mainContext); 112 | }; 113 | 114 | const PURGE = (state) => { 115 | // state.purge = true; 116 | }; 117 | 118 | const UPDATE = (state) => { 119 | if (state.continue) { 120 | if (!state.delay) { 121 | return; 122 | } 123 | state.continue = false; 124 | state.elapsed = state.delay + Date.now(); 125 | } 126 | if (Date.now() > state.elapsed) { 127 | state.elapsed = 0; 128 | state.continue = true; 129 | // TODO not reaching here for some reason 130 | if (state.lastCommand) { 131 | state.lastCommand = false; 132 | state.played = true; // time is over since last update 133 | } 134 | } 135 | }; 136 | 137 | const SET_DELAY = (state, delay) => { 138 | state.delay = ((delay === 0 ? 1 : delay) * 20); 139 | }; 140 | 141 | const SLOT_IMAGE = (state, slot) => { 142 | state.slot = slot; 143 | }; 144 | 145 | const SLOT_PALETTE = (state) => { }; 146 | const TTM_UNKNOWN_0 = (state) => { }; 147 | 148 | const SET_SCENE = (state) => {}; 149 | 150 | const SET_BACKGROUND = (state, index) => { 151 | state.saveIndex = index; 152 | }; 153 | 154 | const GOTO = (state, tagId) => { 155 | state.reentry = 0; // TODO check for other scenes 156 | }; 157 | 158 | const SET_COLORS = (state, fc, bc) => { 159 | if (fc < 16) { 160 | state.foregroundColor = PALETTE[fc]; 161 | } 162 | if (bc < 16) { 163 | state.backgroundColor = PALETTE[bc]; 164 | } 165 | }; 166 | 167 | const SET_FRAME1 = (state) => { }; 168 | 169 | const SET_TIMER = (state, delay, timer) => { 170 | // state.delay = ((delay === 0 ? 1 : delay) * 20); 171 | state.timer = timer * 20 + ((delay === 0 ? 1 : delay) * 20); 172 | }; 173 | 174 | const SET_CLIP_REGION = (state, x1, y1, x2, y2) => { 175 | state.clip = { 176 | x: x1, 177 | y: y1, 178 | width: x2 - x1, 179 | height: y2 - y1, 180 | }; 181 | // console.log('SET_CLIP_REGION', state.clip); 182 | // state.context.strokeStyle = getPaletteColor(PALETTE[12]); 183 | // state.context.lineWidth = '3'; 184 | // state.context.rect(state.clip.x, state.clip.y, state.clip.width, state.clip.height); 185 | // state.context.stroke(); 186 | }; 187 | 188 | const FADE_OUT = (state) => { }; 189 | const FADE_IN = (state) => { }; 190 | 191 | const DRAW_BACKGROUND_REGION = (state, x, y, width, height) => { 192 | const save = state.saveBkg[0]; 193 | save.canDraw = true; 194 | save.x = x; 195 | save.y = y; 196 | save.width = width; 197 | save.height = height; 198 | 199 | save.context.drawImage( 200 | state.context.canvas, 201 | x, y, width, height, 202 | x, y, width, height, 203 | ); 204 | }; 205 | 206 | const SAVE_IMAGE_REGION = (state, x, y, width, height) => { 207 | // const save = state.save[state.saveIndex]; 208 | // save.canDraw = true; 209 | // save.x = x; 210 | // save.y = y; 211 | // save.width = width; 212 | // save.height = height; 213 | 214 | // save.context.drawImage( 215 | // state.context.canvas, 216 | // x, y, width, height, 217 | // x, y, width, height, 218 | // ); 219 | }; 220 | 221 | const TTM_UNKNOWN_4 = (state, x, y, width, height) => { 222 | // console.log('TTM_UNKNOWN_4', state.clip); 223 | // state.context.strokeStyle = getPaletteColor(PALETTE[12]); 224 | // state.context.lineWidth = '3'; 225 | // state.context.rect(x, y, width, height); 226 | // state.context.stroke(); 227 | }; 228 | 229 | const SAVE_REGION = (state, x, y, width, height) => { 230 | // state.clip = { 231 | // x, 232 | // y, 233 | // width, 234 | // height, 235 | // }; 236 | }; 237 | 238 | const RESTORE_REGION = (state, x, y, width, height) => { 239 | const save = state.saveBkg[0]; 240 | save.canDraw = false; 241 | save.x = 0; 242 | save.y = 0; 243 | save.width = 0; 244 | save.height = 0; 245 | clearContext(save.context); 246 | }; 247 | 248 | const DRAW_LINE = (state, x1, y1, x2, y2) => { 249 | state.context.beginPath(); 250 | state.context.moveTo(x1, y1); 251 | state.context.lineTo(x2, y2); 252 | state.context.closePath(); 253 | state.context.strokeStyle = 'white'; 254 | state.context.stroke(); 255 | }; 256 | 257 | const DRAW_RECT = (state, x, y, width, height) => { 258 | state.context.fillStyle = getPaletteColor(state.foregroundColor); 259 | state.context.fillRect(x, y, width, height); 260 | }; 261 | 262 | const DRAW_BUBBLE = (state, x, y, width, height) => { 263 | const centerX = width / 2; 264 | const centerY = height / 2; 265 | const radius = width / 2; 266 | state.context.beginPath(); 267 | state.context.arc(x + centerX, y + centerY, radius, 0, 2 * Math.PI, false); 268 | state.context.closePath(); 269 | state.context.fillStyle = 'white'; 270 | state.context.fill(); 271 | state.context.strokeStyle = 'white'; 272 | state.context.stroke(); 273 | }; 274 | 275 | const DRAW_SPRITE = (state, offsetX, offsetY, index, slot) => { 276 | if (state.res[slot] === undefined) { 277 | return; 278 | } 279 | const image = state.res[slot].images[index]; 280 | if (image !== undefined) { 281 | state.context.save(); 282 | state.context.beginPath(); 283 | state.context.rect(state.clip.x, state.clip.y, state.clip.width, state.clip.height); 284 | state.context.clip(); 285 | 286 | drawImage(image, state.tmpContext, 0, 0); 287 | state.context.drawImage(state.tmpContext.canvas, 0, 0, image.width, image.height, offsetX, offsetY, image.width, image.height); 288 | state.context.restore(); 289 | } 290 | }; 291 | 292 | const DRAW_SPRITE_FLIP = (state, offsetX, offsetY, index, slot) => { 293 | if (state.res[slot] === undefined) { 294 | return; 295 | } 296 | const image = state.res[slot].images[index]; 297 | if (image !== undefined) { 298 | state.context.save(); 299 | state.context.beginPath(); 300 | state.context.rect(state.clip.x, state.clip.y, state.clip.width, state.clip.height); 301 | state.context.clip(); 302 | 303 | drawImage(image, state.tmpContext, 0, 0); 304 | state.context.save(); 305 | state.context.translate(image.width, 0); 306 | state.context.scale(-1, 1); 307 | state.context.drawImage(state.tmpContext.canvas, 0, 0, image.width, image.height, -offsetX, offsetY, image.width, image.height); 308 | state.context.restore(); 309 | state.context.restore(); 310 | } 311 | }; 312 | 313 | const DRAW_SPRITE1 = (state) => { }; 314 | const DRAW_SPRITE3 = (state) => { }; 315 | 316 | const clearScreen = (state, index) => { 317 | clearContext(state.context); 318 | clearContext(state.tmpContext); 319 | drawContext(state); 320 | }; 321 | 322 | const CLEAR_SCREEN = (state, index) => { 323 | clearScreen(state, index); 324 | }; 325 | 326 | const DRAW_SCREEN = (state) => { }; 327 | 328 | const LOAD_SAMPLE = (state) => { }; 329 | const SELECT_SAMPLE = (state) => { }; 330 | const DESELECT_SAMPLE = (state) => { }; 331 | 332 | const PLAY_SAMPLE = (state, index) => { 333 | const sampleSource = state.audioManager.getSoundFxSource(); 334 | sampleSource.load(index, () => { 335 | sampleSource.play(); 336 | }); 337 | }; 338 | 339 | const STOP_SAMPLE = (state) => { }; 340 | 341 | const SCREEN_TYPE = { 342 | 'ISLETEMP.SCR': 1, 343 | 'ISLAND2.SCR': 2, 344 | 'SUZBEACH.SCR': 0, 345 | 'JOFFICE.SCR': 0, 346 | 'THEEND.SCR': 0, 347 | 'INTRO.SCR': 0, 348 | } 349 | 350 | const loadBackground = (state) => { 351 | // Load background assets if not loaded yet 352 | if (!bkgRes) { 353 | const entry = state.entries.find(e => e.name === 'BACKGRND.BMP'); 354 | if (entry !== undefined) { 355 | bkgRes = loadResourceEntry(entry); 356 | } 357 | } 358 | } 359 | 360 | const loadRaft = (state) => { 361 | if (!bkgRaft) { 362 | const entry = state.entries.find(e => e.name === 'MRAFT.BMP'); 363 | if (entry !== undefined) { 364 | bkgRaft = loadResourceEntry(entry); 365 | } 366 | } 367 | } 368 | 369 | const loadOcean = (state) => { 370 | if (bkgOcean.length === 0) { 371 | // FIXME shorten this code later 372 | let entry = state.entries.find(e => e.name === 'OCEAN00.SCR'); 373 | if (entry !== undefined) { 374 | bkgOcean.push(loadResourceEntry(entry)); 375 | } 376 | entry = state.entries.find(e => e.name === 'OCEAN01.SCR'); 377 | if (entry !== undefined) { 378 | bkgOcean.push(loadResourceEntry(entry)); 379 | } 380 | entry = state.entries.find(e => e.name === 'OCEAN02.SCR'); 381 | if (entry !== undefined) { 382 | bkgOcean.push(loadResourceEntry(entry)); 383 | } 384 | entry = state.entries.find(e => e.name === 'NIGHT.SCR'); 385 | if (entry !== undefined) { 386 | bkgOcean.push(loadResourceEntry(entry)); 387 | } 388 | const isNight = false; // calculate night shift here 389 | let oceanIdx = Math.floor((Math.random() * 4)); // 0 to 3 (adding night for now) 390 | if (isNight) { 391 | oceanIdx = 4; 392 | } 393 | bkgScreen = bkgOcean[oceanIdx]; 394 | } 395 | } 396 | 397 | const LOAD_SCREEN = (state, name) => { 398 | state.island = SCREEN_TYPE[name]; 399 | 400 | if (!bkgScreen) { 401 | const entry = state.entries.find(e => e.name === name); 402 | if (entry !== undefined) { 403 | bkgScreen = loadResourceEntry(entry); 404 | } 405 | } 406 | 407 | if (state.island) { 408 | loadBackground(state); 409 | loadRaft(state); 410 | loadOcean(state); 411 | } 412 | }; 413 | 414 | const LOAD_IMAGE = (state, name) => { 415 | if (name === 'FLAME.BMP' || name === 'FLURRY.BMP') { 416 | name = 'FIRE1.BMP'; 417 | } 418 | const entry = state.entries.find(e => e.name === name); 419 | if (entry !== undefined) { 420 | state.res[state.slot] = loadResourceEntry(entry); 421 | } 422 | }; 423 | 424 | const LOAD_PALETTE = (state) => { }; 425 | 426 | // ADS COMMANDS 427 | const ADS_UNKNOWN_0 = (state) => { }; 428 | 429 | const IF_NOT_PLAYED = (state, sceneIdx, tagId) => { 430 | // if (state.continue) { 431 | // state.continue = false; 432 | // } 433 | // if (state.played.length > 0) { 434 | // const scene = state.played.find(s => s.sceneIdx === sceneIdx && s.tagId === tagId); 435 | // if (scene !== undefined) { 436 | // state.continue = true; 437 | // // TODO OR Skip 438 | // } 439 | // } else { 440 | // state.continue = true; 441 | // } 442 | }; 443 | 444 | const IF_PLAYED = (state, sceneIdx, tagId) => { 445 | if (state.continue) { 446 | state.continue = false; 447 | } 448 | let scene = scenes.find(s => 449 | s.sceneIdx === sceneIdx && s.tagId === tagId 450 | && s.state.played); 451 | if (scene !== undefined) { 452 | if (scene.state.timer === 0) { 453 | removeScenes.push({ 454 | sceneIdx, 455 | tagId, 456 | }); 457 | } 458 | state.continue = true; 459 | return; 460 | } 461 | 462 | scene = scenes.find(s => 463 | s.sceneIdx === sceneIdx && s.tagId === tagId); 464 | if (scene === undefined) { 465 | state.continue = true; 466 | } 467 | }; 468 | 469 | const IF_NOT_RUNNING = (state) => { }; 470 | const IF_RUNNING = (state) => { }; 471 | const AND = (state) => { }; 472 | const OR = (state) => { }; 473 | 474 | const PLAY_SCENE = (state) => { 475 | if (state.continue) { 476 | state.continue = false; 477 | 478 | console.log("Scenes", scenes.slice(0)); 479 | console.log("Remove Scenes", removeScenes.slice(0)); 480 | 481 | if (removeScenes.length > 0) { 482 | removeScenes.forEach(s => { 483 | const index = scenes.indexOf(s => s.sceneIdx === sceneIdx && s.tagId === tagId); 484 | scenes.splice(index, 1); 485 | }); 486 | removeScenes = []; 487 | } 488 | if (addScenes.length > 0) { 489 | addScenes.forEach(s => { 490 | scenes.push(getSceneState( 491 | state, 492 | s.sceneIdx, 493 | s.tagId, 494 | s.retriesDelay, 495 | s.unk, 496 | )); 497 | }); 498 | addScenes = []; 499 | } 500 | } 501 | 502 | let canContinue = false; 503 | scenes.forEach(s => { 504 | canContinue = canContinue | (s.state.runs > 0) ? true : false; 505 | }); 506 | 507 | if (scenes.length === 0) { 508 | canContinue = true; 509 | } 510 | 511 | console.log("Remove Scenes", removeScenes.slice(0)); 512 | console.log("Scenes", scenes.slice(0)); 513 | 514 | state.continue = canContinue; 515 | }; // runScripts has the continue logic 516 | 517 | const PLAY_SCENE_2 = (state) => { }; 518 | 519 | const initialState = { 520 | reentry: 0, 521 | lastCommand: false, 522 | runs: 0, 523 | played: false, 524 | continue: true, 525 | skip: false, 526 | island: 1, 527 | elapsedTimer: 0, 528 | timer: 0, 529 | delay: 0, 530 | }; 531 | 532 | const ADD_SCENE = (state, sceneIdx, tagId, retriesDelay, unk) => { 533 | if (state.randomize) { 534 | scenesRandom.push({ 535 | sceneIdx, 536 | tagId, 537 | retriesDelay, 538 | unk, 539 | }); 540 | return; 541 | } 542 | 543 | addScenes.push({ 544 | sceneIdx, 545 | tagId, 546 | retriesDelay, 547 | unk, 548 | }); 549 | } 550 | 551 | const getSceneState = (state, sceneIdx, tagId, retriesDelay, unk) => { 552 | const ttm = scenesRes[sceneIdx - 1]; 553 | if (ttm === undefined || ttm.scenes === undefined) { 554 | console.log('add failed ttm', sceneIdx, tagId); 555 | return; 556 | } 557 | const scene = ttm.scenes.find(s => s.tagId === tagId); 558 | const retries = retriesDelay >= 0 ? retriesDelay : 0; 559 | const delay = retriesDelay < 0 ? retriesDelay : state.delay; 560 | 561 | const canvas = document.createElement("canvas"); 562 | canvas.width = 640; 563 | canvas.height = 480; 564 | 565 | const stateInit = { ...initialState, context: canvas.getContext('2d') }; 566 | 567 | const s = Object.assign({ sceneIdx, delay, retries }, scene); 568 | if (s.script === undefined) { 569 | console.log('add failed script', sceneIdx, tagId, scene, ttm); 570 | return; 571 | } 572 | if (!scenes.length) { 573 | s.script.unshift(...ttm.scenes[0].script); 574 | s.state = Object.assign({}, state, stateInit); 575 | } else { 576 | s.state = Object.assign({}, scenes[0].state, stateInit); 577 | } 578 | return s; 579 | }; 580 | 581 | const STOP_SCENE = (state, sceneIdx, tagId, retries) => { 582 | removeScenes.push({ 583 | sceneIdx, 584 | tagId, 585 | retries, 586 | }); 587 | // console.log(scenes); 588 | // remove(scenes, s => s.sceneIdx === sceneIdx && s.tagId === tagId); 589 | // const index = scenes.indexOf(s => s.sceneIdx === sceneIdx && s.tagId === tagId); 590 | // scenes.splice(index, 1); 591 | // delete scenes[index]; 592 | // scenes = scenes.filter(s => s.sceneIdx !== sceneIdx && s.tagId !== tagId); 593 | // console.log(scenes); 594 | // const index = scenes.indexOf(s => s.sceneIdx === sceneIdx && s.tagId === tagId); 595 | // scenes.splice(index, 1); 596 | 597 | // const s = scenes.filter(s => s.sceneIdx !== sceneIdx && s.tagId !== tagId); 598 | // if (s !== undefined) { 599 | // scenes = s; 600 | // } 601 | /* else { 602 | scenes = []; 603 | }*/ 604 | }; 605 | 606 | const RANDOM_START = (state) => { 607 | state.randomize = true; 608 | scenesRandom = []; 609 | }; 610 | 611 | const RANDOM_UNKNOWN_0 = (state) => { }; 612 | 613 | const RANDOM_END = (state) => { 614 | state.randomize = false; 615 | const index = Math.floor((Math.random() * scenesRandom.length)); 616 | const scene = scenesRandom[index]; 617 | if (scene !== undefined) { 618 | ADD_SCENE(state, scene.sceneIdx, scene.tagId, scene.retriesDelay, scene.unk); 619 | } 620 | }; 621 | 622 | const ADS_UNKNOWN_6 = (state) => { }; 623 | const ADS_FADE_OUT = (state) => { }; 624 | const RUN_SCRIPT = (state) => { }; 625 | 626 | const END = (state) => { 627 | if (!state.continue) { 628 | state.continue = true; 629 | } else if (state.continue) { 630 | state.continue = false; 631 | } 632 | const scene = scenes.find(s => s.state.played); 633 | if (state.lastCommand && scene !== undefined) { 634 | scenes = []; 635 | state.continue = true; 636 | } 637 | }; 638 | 639 | // CUSTOM COMMAND 640 | const END_IF = (state) => { }; 641 | 642 | const CommandType = [ 643 | // TTM COMMANDS 644 | { opcode: 0x0020, callback: SAVE_BACKGROUND }, 645 | { opcode: 0x0080, callback: DRAW_BACKGROUND }, 646 | { opcode: 0x0110, callback: PURGE }, 647 | { opcode: 0x0FF0, callback: UPDATE }, 648 | { opcode: 0x1020, callback: SET_DELAY }, 649 | { opcode: 0x1050, callback: SLOT_IMAGE }, 650 | { opcode: 0x1060, callback: SLOT_PALETTE }, 651 | { opcode: 0x1100, callback: TTM_UNKNOWN_0 }, 652 | { opcode: 0x1110, callback: SET_SCENE }, 653 | { opcode: 0x1120, callback: SET_BACKGROUND }, 654 | { opcode: 0x1200, callback: GOTO }, 655 | { opcode: 0x2000, callback: SET_COLORS }, 656 | { opcode: 0x2010, callback: SET_FRAME1 }, 657 | { opcode: 0x2020, callback: SET_TIMER }, 658 | { opcode: 0x4000, callback: SET_CLIP_REGION }, 659 | { opcode: 0x4110, callback: FADE_OUT }, 660 | { opcode: 0x4120, callback: FADE_IN }, 661 | { opcode: 0x4200, callback: DRAW_BACKGROUND_REGION }, 662 | { opcode: 0x4210, callback: SAVE_IMAGE_REGION }, 663 | { opcode: 0xA000, callback: TTM_UNKNOWN_4 }, 664 | { opcode: 0xA050, callback: SAVE_REGION }, 665 | { opcode: 0xA060, callback: RESTORE_REGION }, 666 | { opcode: 0xA0A0, callback: DRAW_LINE }, 667 | { opcode: 0xA100, callback: DRAW_RECT }, 668 | { opcode: 0xA400, callback: DRAW_BUBBLE }, 669 | { opcode: 0xA500, callback: DRAW_SPRITE }, 670 | { opcode: 0xA510, callback: DRAW_SPRITE1 }, 671 | { opcode: 0xA520, callback: DRAW_SPRITE_FLIP }, 672 | { opcode: 0xA530, callback: DRAW_SPRITE3 }, 673 | { opcode: 0xA600, callback: CLEAR_SCREEN }, 674 | { opcode: 0xB600, callback: DRAW_SCREEN }, 675 | { opcode: 0xC020, callback: LOAD_SAMPLE }, 676 | { opcode: 0xC030, callback: SELECT_SAMPLE }, 677 | { opcode: 0xC040, callback: DESELECT_SAMPLE }, 678 | { opcode: 0xC050, callback: PLAY_SAMPLE }, 679 | { opcode: 0xC060, callback: STOP_SAMPLE }, 680 | { opcode: 0xF010, callback: LOAD_SCREEN }, 681 | { opcode: 0xF020, callback: LOAD_IMAGE }, 682 | { opcode: 0xF050, callback: LOAD_PALETTE }, 683 | // ADS COMMANDS 684 | { opcode: 0x1070, callback: ADS_UNKNOWN_0 }, 685 | { opcode: 0x1330, callback: IF_NOT_PLAYED }, 686 | { opcode: 0x1350, callback: IF_PLAYED }, 687 | { opcode: 0x1360, callback: IF_NOT_RUNNING }, 688 | { opcode: 0x1370, callback: IF_RUNNING }, 689 | { opcode: 0x1420, callback: AND }, 690 | { opcode: 0x1430, callback: OR }, 691 | { opcode: 0x1510, callback: PLAY_SCENE }, 692 | { opcode: 0x1520, callback: PLAY_SCENE_2 }, 693 | { opcode: 0x2005, callback: ADD_SCENE }, 694 | { opcode: 0x2010, callback: STOP_SCENE }, 695 | { opcode: 0x3010, callback: RANDOM_START }, 696 | { opcode: 0x3020, callback: RANDOM_UNKNOWN_0 }, 697 | { opcode: 0x30ff, callback: RANDOM_END }, 698 | { opcode: 0x4000, callback: ADS_UNKNOWN_6 }, 699 | { opcode: 0xf010, callback: ADS_FADE_OUT }, 700 | { opcode: 0xf200, callback: RUN_SCRIPT }, 701 | { opcode: 0xffff, callback: END }, 702 | // CUSTOM: Added for text script 703 | { opcode: 0xfff0, callback: END_IF }, 704 | ]; 705 | 706 | const runScript = (state, script, main = false) => { 707 | if (script === undefined || state.reentry === -1) { 708 | return true; 709 | } 710 | for (let i = state.reentry; i < script.length; i++) { 711 | const c = script[i]; 712 | const type = CommandType.find(ct => ct.opcode === c.opcode); 713 | if (!type) { 714 | continue; 715 | } 716 | if (main) { 717 | console.log(c.line); 718 | } 719 | if (i === (script.length - 1)) { 720 | state.lastCommand = true; 721 | } 722 | type.callback(state, ...c.params); 723 | state.reentry = i; 724 | if (!state.continue) { 725 | break; 726 | } 727 | } 728 | if (state.reentry === (script.length - 1)) { 729 | state.lastCommand = true; 730 | state.reentry = 0; 731 | state.runs++; 732 | if (!state.continue) { 733 | state.continue = true; 734 | } 735 | state.played = true; 736 | if (main) { 737 | currentScene++; 738 | } 739 | if (state.type === 'TTM') { 740 | return true; 741 | } 742 | } 743 | return false; 744 | }; 745 | 746 | const runScripts = () => { 747 | if (state.type === 'ADS') { 748 | let exitFrame = false; 749 | 750 | clearContext(state.context); 751 | 752 | if (state.island) { 753 | drawBackground(state, state.mainContext); 754 | } 755 | const saveBkg = state.saveBkg[0]; 756 | if (saveBkg.canDraw) { 757 | state.context.drawImage(saveBkg.context.canvas, 0, 0); 758 | } 759 | 760 | const scene = state.data.scenes[currentScene]; 761 | if (scene !== undefined) { 762 | exitFrame = runScript(state, scene.script, true); 763 | } 764 | 765 | if (!state.continue) { 766 | scenes.forEach(s => { 767 | runScript(s.state, s.script); 768 | }); 769 | scenes.forEach(s => { 770 | state.context.drawImage(s.state.context.canvas, 0, 0); 771 | }); 772 | } 773 | return exitFrame; 774 | } else { 775 | if (state.island) { 776 | drawBackground(state, state.mainContext); 777 | } 778 | return runScript(state, state.data.scripts); 779 | } 780 | }; 781 | 782 | export const startProcess = (initialState) => { 783 | // FIXME this state needs a deep clean up 784 | state = { 785 | data: null, 786 | context: null, 787 | tmpContext: null, 788 | mainContext: null, 789 | save: [], 790 | saveIndex: 0, 791 | saveBkg: [], 792 | audioManager: null, 793 | slot: 0, 794 | res: [], 795 | // this should be for multiple running scripts 796 | reentry: 0, 797 | elapsed: 0, 798 | elapsedTimer: 0, 799 | delay: 0, 800 | timer: 0, 801 | continue: true, 802 | frameId: null, 803 | island: 1, 804 | foregroundColor: PALETTE[0], 805 | backgroundColor: PALETTE[0], 806 | clip: { x: 0, y: 0, width: 640, height: 480 }, 807 | type: null, 808 | skip: false, 809 | randomize: false, 810 | purge: false, 811 | played: false, 812 | runs: 0, 813 | lastCommand: false, 814 | ...initialState, 815 | }; 816 | bkgScreen = null; 817 | bkgRes = null; 818 | bkgOcean = []; 819 | bkgRaft = null; 820 | currentScene = 0; 821 | 822 | // temp canvas 823 | const tmpCanvas = document.createElement("canvas"); 824 | tmpCanvas.width = 640; 825 | tmpCanvas.height = 480; 826 | state.tmpContext = tmpCanvas.getContext('2d'); 827 | 828 | for (let s = 0; s < 3; s += 1) { 829 | const c = document.createElement("canvas"); 830 | c.width = 640; 831 | c.height = 480; 832 | state.save.push({ 833 | context: c.getContext('2d'), 834 | x: 0, 835 | y: 0, 836 | width: 0, 837 | height: 0, 838 | canDraw: false, 839 | }); 840 | } 841 | 842 | const c = document.createElement("canvas"); 843 | c.width = 640; 844 | c.height = 480; 845 | state.saveBkg.push({ 846 | context: c.getContext('2d'), 847 | x: 0, 848 | y: 0, 849 | width: 0, 850 | height: 0, 851 | canDraw: false, 852 | }); 853 | 854 | state.audioManager = createAudioManager({ soundFxVolume: 0.50 }); 855 | 856 | if (state.type === 'ADS') { 857 | state.data.resources.forEach(r => { 858 | const entry = state.entries.find(e => e.name === r.name); 859 | if (entry !== undefined) { 860 | scenesRes.push(loadResourceEntry(entry)); 861 | } 862 | }); 863 | } 864 | console.log(state.data.scenes); 865 | mainloop(); 866 | 867 | return state; 868 | }; 869 | 870 | export const stopProcess = () => { 871 | if (state && state.frameId) { 872 | cancelAnimationFrame(state.frameId); 873 | } 874 | 875 | tick = null; 876 | prevTick = Date.now(); 877 | elapsed = null; 878 | 879 | state = null; 880 | currentScene = 0; 881 | 882 | scenesRes = []; 883 | scenes = []; 884 | 885 | bkgScreen = null; 886 | bkgRes = null; 887 | bkgOcean = []; 888 | bkgRaft = null; 889 | cloudIdx = Math.floor((Math.random() * 3) + 15); 890 | cloudX = Math.floor((Math.random() * 640)); 891 | cloudY = Math.floor((Math.random() * 80)); 892 | cloudElapsed = 0; 893 | }; 894 | 895 | window.requestAnimationFrame = window.requestAnimationFrame 896 | || window.mozRequestAnimationFrame 897 | || window.webkitRequestAnimationFrame 898 | || window.msRequestAnimationFrame 899 | || ((f) => setTimeout(f, fps)); 900 | 901 | const mainloop = () => { 902 | state.frameId = requestAnimationFrame(mainloop); 903 | 904 | tick = Date.now(); 905 | elapsed = tick - prevTick; 906 | 907 | if (elapsed > fps) { 908 | prevTick = tick - (elapsed % fps); 909 | } 910 | 911 | if (runScripts()) { 912 | cancelAnimationFrame(state.frameId); 913 | } 914 | } 915 | /* eslint-enable */ 916 | --------------------------------------------------------------------------------