-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.ts
381 lines (337 loc) · 13.2 KB
/
main.ts
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
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
const textures = [
assets.image`pirate`, // map color 1
assets.image`wood`, // map color 2
assets.image`wood`, // map color 3
assets.image`brick`, // map color 4
assets.image`pirate`, // map color 5
assets.image`stone`, // map color 6
assets.image`wood`, // map color 7
]
const numTexture = textures.length;
const animTextures = [
assets.animation`myAnim`, // map color 8
]
game.stats = true
const fpx = 10 // Fixed point position (10 bit)
const fpx_scale = 1 << fpx // Fixed point scaling (2^fpx)
const one_fp = 1 << fpx // One unit in fixed point.
const one2 = 1 << (fpx + fpx)
// Convert from integer to fixed point number
function tofpx(n: number) {
return (n * fpx_scale) | 0
}
const fov = 0.6 // Field of view
const fov_fp = tofpx(fov)
class SpriteObject {
img: Image
x_fp: number // x position
y_fp: number // y position
xVel_fp: number // x-Velocity
yVel_fp: number // y-Velocity
uDiv_fp: number // Texture scaling x-axis
vDiv_fp: number // Texture scaling y-axis
onGround: number // Sprite stands on the ground
constructor(img: Image, x_fp: number, y_fp: number) {
this.img = img
this.x_fp = x_fp
this.y_fp = y_fp
this.xVel_fp = tofpx(0);
this.yVel_fp = tofpx(0);
this.uDiv_fp = tofpx(1);
this.vDiv_fp = tofpx(1);
}
}
let allSprites: SpriteObject[] = []
allSprites.push(new SpriteObject(assets.image`sprite`, tofpx(22), tofpx(8)))
allSprites.push(new SpriteObject(assets.image`sprite`, tofpx(17), tofpx(8)))
class State {
// Variables which ends in '_fp' are fixed point. (integer scaled by fpx_scale)
x_fp: number
y_fp: number
map: Image
dirX_fp: number
dirY_fp: number
planeX_fp: number
planeY_fp: number
angle: number
horizon_offset: number
timeStamp_ms: number
elapsed_ms: number
constructor() {
this.angle = 0
this.x_fp = tofpx(18)
this.y_fp = tofpx(8)
this.setVectors()
this.map = assets.image`map`
this.horizon_offset = 0
}
private setVectors() {
const sin = Math.sin(this.angle)
const cos = Math.cos(this.angle)
// Direction vector (camera view direction)
this.dirX_fp = tofpx(cos)
this.dirY_fp = tofpx(sin)
// Screen view plane vector (perpendicular to camera view)
this.planeX_fp = tofpx(sin * fov)
this.planeY_fp = tofpx(cos * -fov)
}
// Check map if x, y is not a wall.
private canGo(x_fp: number, y_fp: number) {
return this.map.getPixel(x_fp >> fpx, y_fp >> fpx) == 0
}
updateTime()
{
let now = game.runtime();
this.elapsed_ms = now - st.timeStamp_ms
this.timeStamp_ms = now
}
updateControls() {
const dx = controller.dx(2)
if (dx) {
if (controller.A.isPressed()) {
// Side ways - strafing
const nx_fp = this.x_fp + Math.round(dx * this.planeX_fp)
const ny_fp = this.y_fp + Math.round(dx * this.planeY_fp)
if (this.canGo(nx_fp, ny_fp)) {
this.x_fp = nx_fp
this.y_fp = ny_fp
}
}
else {
this.angle -= dx
if (this.angle < -2 * Math.PI)
this.angle += 2 * Math.PI
if (this.angle > 2 * Math.PI)
this.angle -= 2 * Math.PI
this.setVectors()
}
}
const dy = controller.dy(2)
if (dy) {
// Calculate a new position.
const nx_fp = this.x_fp - Math.round(this.dirX_fp * dy)
const ny_fp = this.y_fp - Math.round(this.dirY_fp * dy)
if (!this.canGo(nx_fp, ny_fp) && this.canGo(this.x_fp, this.y_fp)) {
if (this.canGo(this.x_fp, ny_fp))
this.y_fp = ny_fp
else if (this.canGo(nx_fp, this.y_fp))
this.x_fp = nx_fp
} else {
this.x_fp = nx_fp
this.y_fp = ny_fp
}
}
if (dx || dy) {
const bobbing = 8
this.horizon_offset = Math.abs(Math.idiv(this.timeStamp_ms, 50) % bobbing - (bobbing>>1) )
}
}
trace() {
// based on https://lodev.org/cgtutor/raycasting.html
const w = screen.width
const h = screen.height
/////////////////////
// Draw sky color
/////////////////////
screen.fillRect(0, 0, w, (h >> 1) + this.horizon_offset, 13)
/////////////////////
// Draw walls
/////////////////////
let depth_map_fp: number[] = [] // Depth map for each ray cast.
// For each pixel column along the width of the screen, cast a ray onto the map and test for ray intersections.
for (let x = 0; x < w; x++) {
const cameraX_fp: number = Math.idiv((x << fpx) << 1, w) - one_fp
// Direction of the ray.
let rayDirX_fp = this.dirX_fp + (this.planeX_fp * cameraX_fp >> fpx)
let rayDirY_fp = this.dirY_fp + (this.planeY_fp * cameraX_fp >> fpx)
// Map square (initialize to player/camera position)
let mapX = this.x_fp >> fpx
let mapY = this.y_fp >> fpx
// length of ray from current position to next x or y-side
let sideDistX = 0, sideDistY = 0
// avoid division by zero
if (rayDirX_fp == 0) rayDirX_fp = 1
if (rayDirY_fp == 0) rayDirY_fp = 1
// length of ray from one x or y-side to next x or y-side
const deltaDistX = Math.abs(Math.idiv(one2, rayDirX_fp));
const deltaDistY = Math.abs(Math.idiv(one2, rayDirY_fp));
let mapStepX = 0, mapStepY = 0
let sideWallHit = false;
//calculate step and initial sideDist
if (rayDirX_fp < 0) {
mapStepX = -1;
sideDistX = ((this.x_fp - (mapX << fpx)) * deltaDistX) >> fpx;
} else {
mapStepX = 1;
sideDistX = (((mapX << fpx) + one_fp - this.x_fp) * deltaDistX) >> fpx;
}
if (rayDirY_fp < 0) {
mapStepY = -1;
sideDistY = ((this.y_fp - (mapY << fpx)) * deltaDistY) >> fpx;
} else {
mapStepY = 1;
sideDistY = (((mapY << fpx) + one_fp - this.y_fp) * deltaDistY) >> fpx;
}
let color = 0
while (true) {
//jump to next map square, OR in x-direction, OR in y-direction
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += mapStepX;
sideWallHit = false;
} else {
sideDistY += deltaDistY;
mapY += mapStepY;
sideWallHit = true;
}
color = this.map.getPixel(mapX, mapY)
if (color)
{
color--
break; // hit!
}
}
let perpWallDist_fp = 0
let wallX = 0
if (!sideWallHit) {
perpWallDist_fp = Math.idiv(((mapX << fpx) - this.x_fp + (1 - mapStepX << fpx - 1)) << fpx, rayDirX_fp)
wallX = this.y_fp + (perpWallDist_fp * rayDirY_fp >> fpx);
} else {
perpWallDist_fp = Math.idiv(((mapY << fpx) - this.y_fp + (1 - mapStepY << fpx - 1)) << fpx, rayDirY_fp)
wallX = this.x_fp + (perpWallDist_fp * rayDirX_fp >> fpx);
}
wallX &= (1 << fpx) - 1
depth_map_fp.push(perpWallDist_fp)
let tex: Image
if (color < numTexture) {
// Normal texture.
tex = textures[color]
}
else {
// Animated textures.
let anim = animTextures[color - numTexture]
const frameTime_ms = 200 // ms per frame.
tex = anim[Math.idiv(this.timeStamp_ms, frameTime_ms)% anim.length]
}
if (!tex)
continue
// textures look much better when lineHeight is odd
let lineHeight = Math.idiv(h << fpx, perpWallDist_fp) | 1;
let drawStart = ((-lineHeight + h) >> 1) + this.horizon_offset
let texX = (wallX * tex.width) >> fpx;
if ((!sideWallHit && rayDirX_fp > 0) || (sideWallHit && rayDirY_fp < 0))
texX = tex.width - texX - 1;
screen.blitRow(x, drawStart, tex, texX, lineHeight)
}
/////////////////////
// Draw sprites
/////////////////////
// Sort all sprites based on distance to the camera.
const mapped = allSprites.map((v, i) => {
// No need to take sqrt when just sorting.
return { i:i, value: (this.x_fp - v.x_fp) ** 2 + (this.y_fp - v.y_fp) ** 2 };
});
mapped.sort((a, b) => {
if (a.value > b.value) { return -1; }
if (a.value < b.value) { return 1; }
return 0;
});
// Draw the sorted sprite list
mapped.forEach(mapIdx => {
let s = allSprites[mapIdx.i]
this.drawSprite(s, depth_map_fp)
});
}
updateSprites()
{
let w_fp = screen.width << fpx
let h_fp = screen.height << fpx
for (let i = allSprites.length - 1; i >= 0; i--)
{
let s = allSprites[i]
s.x_fp += Math.idiv(this.elapsed_ms * s.xVel_fp, 1000)
s.y_fp += Math.idiv(this.elapsed_ms * s.yVel_fp, 1000)
if (s.x_fp < 0 || s.y_fp < 0 || s.x_fp > w_fp || s.y_fp > h_fp)
{
allSprites.removeAt(i);
console.log("destroying sprite")
}
}
}
drawSprite(s: SpriteObject, depth_map_fp: number[]) {
const w = screen.width
const h = screen.height
let spriteX_fp = s.x_fp - this.x_fp
let spriteY_fp = s.y_fp - this.y_fp
// Transform the sprite coordinates to the camera coordinate system.
// Project sprite vector onto normalized camera axis: ~ (dirX, dirY)
let transformY_fp = (spriteX_fp * this.dirX_fp + spriteY_fp * this.dirY_fp) >> fpx
if (transformY_fp < 0){
return // Behind camera.
}
// Project sprite vector onto normalized plane axis: ~ (dirY, -dirX)
let transformX_fp =Math.idiv(spriteX_fp * this.dirY_fp - spriteY_fp * this.dirX_fp, fov_fp)
// Calculate screen X position of the sprite,
let spriteScreenX = (w * (one_fp + Math.idiv(transformX_fp << fpx, transformY_fp))) >> (fpx + 1)
//calculate height and width of the sprite on screen
let spriteScreenHeight = Math.idiv(h << fpx, Math.abs(transformY_fp)) //using 'transformY' instead of the real distance prevents fisheye
let spriteScreenWidth = spriteScreenHeight
// Scale the sprite in x and y direction.
spriteScreenHeight = Math.idiv(spriteScreenHeight << fpx, s.vDiv_fp) | 1;
spriteScreenWidth = Math.idiv(spriteScreenWidth << fpx, s.uDiv_fp) | 1;
// Place sprite on ground?
let vMoveScreen = s.onGround ? Math.idiv(s.vDiv_fp * s.img.height, transformY_fp) : 0
// Determine the drawing bounds on the screen.
let drawStartY = ((h - spriteScreenHeight) >> 1) + this.horizon_offset + vMoveScreen
let drawEndY = drawStartY + spriteScreenHeight
if (drawEndY >= h) {
drawEndY = h - 1
}
let drawStartX = spriteScreenX - (spriteScreenWidth >> 1)
let drawStartX_offset = -drawStartX
if (drawStartX < 0) {
drawStartX = 0;
}
let drawEndX = spriteScreenX + (spriteScreenWidth >> 1)
if (drawEndX >= w) {
drawEndX = w - 1
}
// loop through every vertical stripe of the sprite on screen
for(let x = drawStartX; x <= drawEndX; x++)
{
if (transformY_fp >= depth_map_fp[x]) {
continue // behind a wall
}
let texX = Math.idiv(s.img.width * (x + drawStartX_offset) << fpx, spriteScreenWidth) >> fpx
//screen.blitRow(x, drawStartY, s.img, texX, spriteScreenHeight)
// blitRow does not support transparency - so we draw the pixels manually.
for (let y = Math.max(0,drawStartY); y <= drawEndY; y++)
{
let texY = Math.idiv( (y - drawStartY) * s.img.height << fpx, spriteScreenHeight) >> fpx
let c = s.img.getPixel(texX, texY)
if (c) { // Only draw when not transparent.
screen.setPixel(x, y, c)
}
}
}
}
}
const st = new State()
game.onUpdate(function () {
st.updateTime()
st.updateControls()
st.updateSprites()
})
game.onPaint(function () {
st.trace()
})
controller.B.onEvent(ControllerButtonEvent.Pressed, () =>
{
let s = new SpriteObject(animTextures[0][0], st.x_fp, st.y_fp)
s.uDiv_fp = tofpx(3)
s.vDiv_fp = tofpx(3)
s.xVel_fp = st.dirX_fp << 2
s.yVel_fp = st.dirY_fp << 2
allSprites.push(s)
}
)