├── 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 | 
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 |
--------------------------------------------------------------------------------