-
Notifications
You must be signed in to change notification settings - Fork 20
/
Copy pathexample-spaceship.html
241 lines (214 loc) · 7.76 KB
/
example-spaceship.html
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
<!DOCTYPE html>
<html>
<head>
<title>Example - Spaceship</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<!-- Replace with a separate style.css file if you want -->
<style type="text/css">
body {
background: black;
color: white;
}
</style>
<!-- These 2 script tags are required for the 3D effect.
You can remove these if you disabled THREE_SETTINGS. -->
<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "./three-0.156.1/three.module.min.js",
"three/addons/": "./three-0.156.1/addons/"
}
}
</script>
<script type="module">
import * as qx from "./qx82/qx.js";
import * as qxa from "./qx82/qxa.js";
import * as qut from "./qx82/qut.js";
const CH_SPACESHIP = 0xf0;
const CH_ENEMY_LEFT = 0xf1;
const CH_ENEMY_RIGHT = 0xf3;
const CH_ENEMY_DOWN = 0xf5;
const CH_LASER = 0x84;
const MIN_Y = 24, MAX_Y = 168;
const MIN_X = 16, MAX_X = 248;
const ACCEL = 3;
const FRICTION = 2;
const MAX_SPEED = 4;
// Ship's collision rectangle
const PLAYER_COLL_RECT = { x: 1, y: 1, w: 6, h: 6 };
// Enemy's collision rectangle
const ENEMY_COLL_RECT = { x: 1, y: 1, w: 6, h: 6 };
// Laser's collision rectangle.
const LASER_COLL_RECT = { x: 2, y: 0, w: 4, h: 16 };
// Difficulty parameters are specified per score slice of 500, so the first value
// is used when the player has 0-499 points, the second value is for 500-999, etc.
const MIN_ENEMIES = [ 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 ];
const MAX_ENEMIES = [ 5, 5, 6, 6, 7, 7, 8, 8, 9, 9 ];
const SPAWN_INTERVAL = [ 100, 90, 80, 70, 60, 50, 40, 30, 30, 30 ];
const ENEMY_SPEED = [ 1, 2, 2, 3, 3, 4, 4, 5, 5, 6 ];
const ENEMY_COLOR = [ 12, 14, 13, 15, 11, 10, 12, 14, 13, 15 ];
// Player's coordinates (Y is fixed).
let playerX = 124, playerY = MAX_Y;
// Player's speed
let playerSpeed = 0;
// If a laser exists, laser.x, laser.y are its coordinates. Otherwise this is null.
let laser = null;
// Array of enemies.
const enemies = [];
// Countdown to spawn next enemy.
let spawnCountdown = 0;
// If partFx != null, then we're doing a particle effect at partFx.x, partFx.y
// partFx.ticks indicates for how many ticks we have been playing it.
let partFx = null;
let score = 0;
let gameOver = false;
let backgroundImg = null;
// Sound effects.
let sfxLaser = null;
let sfxHit = null;
let sfxBlast = null;
async function main() {
qx.cls();
qx.print("Loading...");
backgroundImg = await qxa.loadImage("assets/city.png");
sfxLaser = await qxa.loadSound("assets/laser.wav")
sfxHit = await qxa.loadSound("assets/hit.wav")
sfxBlast = await qxa.loadSound("assets/blast.wav")
qx.drawImage(0, 0, backgroundImg);
qx.locate(4, 6);
qx.color(14, -1);
qx.print("\u00d7 SPACESHIP EXAMPLE \u00d8 \u00d9");
qx.locate(3, 8);
qx.color(7, -1);
qx.print("Press any button to start.\n\n");
qx.print("Controls:\n SPACE or 'A' button\n to shoot.");
await qxa.key();
qx.cls();
qx.frame(doFrame);
}
async function doFrame() {
qx.cls();
qx.drawImage(0, 0, backgroundImg);
// Update and draw everything.
updateAndDrawPlayer();
if (laser) updateAndDrawLaser();
enemies.forEach(updateAndDrawEnemy);
updateAndDrawPartFx();
// Spawn enemy, if it's time. Or if we are below the minimum number of enemies.
if (--spawnCountdown < 0 || enemies.length < calcParam(MIN_ENEMIES)) spawnEnemy();
// Remove dead enemies.
for (let i = enemies.length - 1; i >= 0; i--) {
if (enemies[i].dead) enemies.splice(i, 1);
}
// Draw score
qx.color(7);
qx.locate(25, 1);
qx.print((score < 10 ? "000" : score < 100 ? "00" : score < 1000 ? "0" : "") + score);
// Game over?
if (gameOver) {
qx.frame(null);
showGameOver();
}
}
function updateAndDrawPlayer() {
// Apply acceleration based on up/down keys.
playerSpeed += (qx.key("ArrowRight") ? 1 : qx.key("ArrowLeft") ? -1 : 0) * ACCEL;
// Apply friction (reduce speed).
playerSpeed = playerSpeed > 0 ?
Math.max(0, playerSpeed - FRICTION) : Math.min(0, playerSpeed + FRICTION);
// Cap speed to maximum.
playerSpeed = qut.clamp(playerSpeed, -MAX_SPEED, MAX_SPEED);
// Apply speed to position.
playerX = qut.clamp(playerX + playerSpeed, MIN_X, MAX_X);
// Draw the spaceship.
qx.color(15);
qx.spr(CH_SPACESHIP, playerX, playerY);
// If there is no laser and the player presses Space (or the A button on mobile), shoot.
if (!laser && (qx.key(" ") || qx.key("ButtonA"))) shootLaser();
}
function updateAndDrawLaser() {
laser.y -= 16;
// Draw the laser. It's a double sprite (16 pixels tall).
qx.color(15);
qx.spr(CH_LASER, laser.x, laser.y);
qx.spr(CH_LASER, laser.x, laser.y + 8);
// Remove laser if it went off-screen.
if (laser.y < -16) laser = null;
}
function shootLaser() {
laser = {x: playerX, y: playerY };
qx.playSound(sfxLaser);
}
function spawnEnemy() {
const maxEnemies = calcParam(MAX_ENEMIES);
if (enemies.length >= maxEnemies) return;
const e = {
y: MIN_Y,
x: Math.round(MIN_X + Math.random() * (MAX_X - MIN_X)), ticks: 0,
speed: calcParam(ENEMY_SPEED),
color: calcParam(ENEMY_COLOR),
// If 1, we're moving right, if -1 we're moving left.
dir: Math.random() > 0.5 ? 1 : -1,
// If > 0, we are sliding vertically against a wall.
sliding: 0,
};
enemies.push(e);
spawnCountdown = calcParam(SPAWN_INTERVAL);
}
function updateAndDrawEnemy(e) {
++e.ticks;
if (e.sliding > 0) {
// Sliding vertically against wall.
e.y += e.speed;
--e.sliding;
} else {
// Moving horizontally.
e.x += e.dir * e.speed;
// Hit a wall? If so, start a slide.
if (e.x <= MIN_X) { e.x = MIN_X; e.dir = 1; e.sliding = 8; }
if (e.x >= MAX_X) { e.x = MAX_X; e.dir = -1; e.sliding = 8; }
}
// Don't go past player
e.y = Math.min(e.y, playerY);
// Hit by laser?
if (laser && qut.intersectRects(LASER_COLL_RECT, ENEMY_COLL_RECT, laser.x, laser.y, e.x, e.y)) {
e.dead = true;
laser = null;
partFx = {x: e.x, y: e.y, ticks: 0, color: e.color};
score += 50;
qx.playSound(sfxHit);
}
// Hit player?
if (qut.intersectRects(ENEMY_COLL_RECT, PLAYER_COLL_RECT, e.x, e.y, playerX, playerY)) {
if (!gameOver) qx.playSound(sfxBlast);
gameOver = true;
}
// Draw.
const animFrame = Math.sign(e.ticks & 8);
const base = e.sliding ? CH_ENEMY_DOWN : e.dir > 0 ? CH_ENEMY_RIGHT : CH_ENEMY_LEFT;
qx.color(e.color);
qx.spr(base + animFrame, e.x, e.y);
}
function updateAndDrawPartFx() {
if (!partFx) return;
if (++partFx.ticks > 6) { partFx = null; return; }
qx.color(partFx.color);
qx.spr(0xe2 + Math.floor(partFx.ticks / 2), partFx.x, partFx.y);
}
function showGameOver() {
qx.color(15, 2);
qx.locate(0, 10);
qx.printBox(32, 3);
qx.locate(12, 11);
qx.print("GAME OVER");
}
function calcParam(paramSpec) {
return paramSpec[Math.min(Math.max(Math.floor(score / 500), 0), paramSpec.length - 1)];
}
window.addEventListener("load", () => qx.init(main));
</script>
</head>
<body>
</body>
</html>