-
-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathdefault.js
165 lines (155 loc) · 7.33 KB
/
default.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// An experiment in rendering Rescue on Fractalus mountains on a web page
// (c) 2021 Bertrand Le Roy
// Self-imposed constraints:
// * The rendering algorithm must use only tools available to a 6502 processor, meaning avoiding
// multiplications or floating point operations, preferring 16-bit addition, logical operations
// and bitwise operations (The 6502 is 8-bit but 16-bit operations composition from 8-bit is easy enough).
// * Multiplication and division are acceptable if we strictly limit how many we do per frame.
// See for example https://llx.com/Neil/a2/mult.html for 6502 multiplication and division implementations.
// * Valkyrie movement is also done using simple 16-bit arithmetic and lookup tables.
// * Math can be used to set-up the map and lookup tables.
'use strict';
import {
mapSize, bitsBetweenTops, coordinateBits, overheadMapScalePowerOfTwo, viewportScalePowerOfTwo, viewportPowerOfTwoAngleUnitPerScreenPixel,
viewportWidth, viewportHeight, viewportVerticalOffset, maxHeight, mazeHoleiness, viewDistance, defaultThrust, minThrust, maxThrust,
displacementAttenuationPower, fractalFlipBit, tick
} from './lib/settings.js';
import {
sin, cos, tan, mod, distance, angleFromCoordinates, north, quarterCircle, halfCircle, fullCircle, findIndexOf
} from './lib/trigo.js';
import { clamp, vary } from './lib/random.js';
import { Map, OverheadMap } from './lib/map.js';
import { fractalusColorScale, fogShader } from './lib/color.js';
import { linearInterpolation, fractalInterpolation } from './lib/interpolation.js';
import { Viewport } from './lib/viewport.js';
import { Compass } from './lib/compass.js';
import { Valkyrie } from './lib/ship.js';
import { interpolationTester, graphTester, rangeTester, fieldTester } from './lib/test.js';
import { Debugger } from './lib/debug.js';
/** The number of ticks since the game started running. */
let frame = 0;
document.addEventListener('DOMContentLoaded', e => {
const interpolation = fractalInterpolation(bitsBetweenTops, maxHeight, displacementAttenuationPower, fractalFlipBit);
const map = new Map(mapSize, maxHeight, interpolation);
const ship = new Valkyrie(
bitsBetweenTops,
coordinateBits,
(1 << coordinateBits) >> 1,
(1 << coordinateBits) >> 1,
maxHeight + 10,
defaultThrust,
north, 0, 0, 1);
const mapEl = document.getElementsByClassName('map')[0];
const shipImg = document.getElementsByClassName('ship')[0];
const coordContainer = document.getElementsByClassName('coord-container')[0];
const overheadMap = new OverheadMap(
mapEl,
shipImg,
overheadMapScalePowerOfTwo,
map,
ship,
coordContainer,
fractalusColorScale(maxHeight),
bitsBetweenTops,
interpolation);
map.generateMaze(mazeHoleiness);
const compassEl = document.getElementsByClassName('compass')[0];
new Compass(compassEl, ship);
const viewportEl = document.getElementById('viewport');
const viewport = new Viewport(
viewportEl,
viewportWidth,
viewportHeight,
viewportVerticalOffset,
viewDistance,
viewportScalePowerOfTwo,
viewportPowerOfTwoAngleUnitPerScreenPixel,
bitsBetweenTops,
maxHeight,
displacementAttenuationPower,
map,
ship,
interpolation,
fogShader(viewDistance));
// Controls
let paused = false;
const playBtn = document.getElementsByClassName('play-btn')[0];
playBtn.addEventListener('click', () => {
paused = false;
ship.reverse = false;
});
const pauseBtn = document.getElementsByClassName('pause-btn')[0];
pauseBtn.addEventListener('click', () => {
paused = true;
});
const backwardsBtn = document.getElementsByClassName('backwards-btn')[0];
backwardsBtn.addEventListener('click', () => {
paused = false;
ship.reverse = true;
});
const stepBackBtn = document.getElementsByClassName('step-back-btn')[0];
stepBackBtn.addEventListener('click', () => {
ship.reverse = true;
paused = false;
gameLoop();
paused = true;
});
const stepForwardBtn = document.getElementsByClassName('step-forward-btn')[0];
stepForwardBtn.addEventListener('click', () => {
ship.reverse = false;
paused = false;
gameLoop();
paused = true;
});
// Setup the visual debugger component
const visualDebugger = new Debugger(viewport, overheadMap);
// To make the game smoother, we prefer dropped frames to uneven timing -> setInterval, not setTimeout
setInterval(gameLoop, tick);
var rendering = false;
function gameLoop() {
if (!paused) {
frame++;
if (rendering) return; // Drop the frame if not done rendering but JS being single-threaded... this is not that useful. Then again, we could offset processing to a worker.
rendering = true;
ship.move();
rendering = false;
}
}
// Tests
const testSection = document.getElementById("testSection");
const testGraph = graphTester(testSection);
const testRange = rangeTester(testSection);
const testField = fieldTester(testSection);
const testInterpolation = interpolationTester(testSection);
const runTestsButton = document.getElementById("runTests");
runTestsButton.addEventListener("click", () => {
testSection.innerHTML = "";
testGraph(sin, [-1440, 1440], [-1 << bitsBetweenTops, 1 << bitsBetweenTops], 1/4, 50 / (1 << bitsBetweenTops));
testGraph(cos, [-1440, 1440], [-1 << bitsBetweenTops, 1 << bitsBetweenTops], 1/4, 50 / (1 << bitsBetweenTops));
testGraph(tan, [-2880, 2880], [-1500, 1500], 1/8, 1/10);
testGraph(x => clamp(x, 10), [-1, 15], [-1, 11], 5, 5, "clamp");
testGraph(x => vary(x, 5, 40), [-10, 60], [-1, 56], 1, 1, "vary");
testGraph(x => mod(x, 10), [-25, 25], [-1, 11], 5, 5, "mod");
testRange(
x => findIndexOf(x, [3, 7, 15, 15, 16, 18]),
[...Array(20).keys()],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 4, 4, 5, 5, 5, 5],
"findIndexOf");
testField(
(x, y) => ({angle: angleFromCoordinates(x, y)}),
[-10, 10], [-10, 10], 10, 1,
"angleFromCoordinates");
testField(
(x, y) => ({amplitude: distance(0, 0, x, y) >> 2}),
[-10, 10], [-10, 10], 10, 1,
"distanceFromCenter");
const lowScreenY = 64;
const highScreenY = 255;
const interpolationDisplacement = (highScreenY - lowScreenY) >> displacementAttenuationPower;
const fractalAlgorithm = fractalInterpolation(bitsBetweenTops, maxHeight, displacementAttenuationPower, fractalFlipBit);
testInterpolation(linearInterpolation, 512, 256, 0, maxHeight, lowScreenY, highScreenY, interpolationDisplacement, 'linear interpolation');
testInterpolation(fractalAlgorithm, 512, 256, 0, maxHeight, lowScreenY, highScreenY, interpolationDisplacement, 'fractal interpolation 0-max');
testInterpolation(fractalAlgorithm, 512, 256, 0, 0, lowScreenY, lowScreenY, interpolationDisplacement, 'fractal interpolation 0-0');
testInterpolation(fractalAlgorithm, 512, 256, maxHeight, maxHeight, highScreenY, highScreenY, interpolationDisplacement, 'fractal interpolation max-max');
});
});