This repository was archived by the owner on Mar 23, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathgame.go
479 lines (433 loc) · 14.5 KB
/
game.go
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
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
// Copyright © 2013-2016 Galvanized Logic Inc.
// Use is governed by a BSD-style license found in the LICENSE file.
package main
import (
"container/list"
"math"
"github.com/gazed/vu"
"github.com/gazed/vu/math/lin"
)
// game keeps track of the game play screen. This includes all game levels
// and the heads up display (hud).
type game struct {
mp *bampf // Main program.
levels map[int]*level // Game levels.
cl *level // Current level.
dt float64 // Delta time updated per game tick.
keys []int // Key bindings.
lens *cam // Dictates how the camera moves.
ww, wh int // Window size.
mxp, myp int // Previous mouse locations.
procDebug func(*vu.Input) // Debugging commands in debug loads.
evolving bool // True when player is moving between levels.
dir *lin.Q // Movement direction.
// Debug variables
fly bool // Debug flying ability switch, see game_debug.go
last lastSpot // Keeps the last valid player position when debugging.
// Static state.
run float64 // Run speed.
spin float64 // Spin speed.
vr float64 // Visible radius.
}
// Implement the screen interface.
func (g *game) fadeIn() animation { return g.newStartGameAnimation() }
func (g *game) fadeOut() animation { return g.newEndGameAnimation() }
func (g *game) resize(width, height int) { g.handleResize(width, height) }
func (g *game) activate(state int) {
switch state {
case screenActive:
g.mp.eng.Set(vu.CursorOn(false))
g.cl.setVisible(true)
g.setKeys(g.keys)
g.evolving = false
case screenDeactive:
g.mp.eng.Set(vu.CursorOn(true))
g.cl.setVisible(false)
g.evolving = false
case screenPaused:
g.mp.eng.Set(vu.CursorOn(true))
case screenEvolving:
g.evolving = true
}
}
// User input to game events. Implements screen interface.
func (g *game) processInput(in *vu.Input, eventq *list.List) {
if g.cl == nil { // no current level just yet... still starting.
return
}
// update game state if the game is active and not transitioning between levels.
// Do the evolve check before processing any other input.
g.spinView(in.Mx, in.My, g.dt)
if !g.evolving {
g.lens.update(g.cl.cam) // smooth camera.
g.cl.update() // level per-tick updates.
g.evolveCheck(eventq) // kick off any necessary level transitions.
}
g.centerMouse(in.Mx, in.My) // keep centering the mouse.
// process any new input.
g.dt = in.Dt
for press, down := range in.Down {
switch {
case press == vu.KEsc && down == 1 && !g.evolving:
publish(eventq, toggleOptions, nil)
case press == vu.KSpace && down == 1:
publish(eventq, skipAnim, nil)
case press == g.keys[0] && !g.evolving: // rebindable keys from here on.
publish(eventq, goForward, down)
case press == g.keys[1] && !g.evolving:
publish(eventq, goBack, down)
case press == g.keys[2] && !g.evolving:
publish(eventq, goLeft, down)
case press == g.keys[3] && !g.evolving:
publish(eventq, goRight, down)
case press == g.keys[4] && down == 1 && !g.evolving:
publish(eventq, cloak, nil)
case press == g.keys[5] && down == 1 && !g.evolving:
publish(eventq, teleport, nil)
}
}
g.procDebug(in) // noop method call in production loads.
}
// Process game events. Implements screen interface.
func (g *game) processEvents(eventq *list.List) (transition int) {
for e := eventq.Front(); e != nil; e = e.Next() {
eventq.Remove(e)
event := e.Value.(*event)
switch event.id {
case toggleOptions:
return configGame
case goForward:
if dwn, ok := event.data.(int); ok {
g.goForward(g.dt, dwn)
} else {
logf("game.processEvents: did not receive goForward down")
}
case goBack:
if dwn, ok := event.data.(int); ok {
g.goBack(g.dt, dwn)
} else {
logf("game.processEvents: did not receive goBack down")
}
case goLeft:
if dwn, ok := event.data.(int); ok {
g.goLeft(g.dt, dwn)
} else {
logf("game.processEvents: did not receive goLeft down")
}
case goRight:
if dwn, ok := event.data.(int); ok {
g.goRight(g.dt, dwn)
} else {
logf("game.processEvents: did not receive goRight down")
}
case cloak:
g.cl.cloak()
case teleport:
g.lens.reset(g.cl.cam)
g.cl.teleport()
case keysRebound:
if keys, ok := event.data.([]int); ok {
g.setKeys(keys)
} else {
logf("game.processEvents: did not receive keysRebound keys")
}
case skipAnim:
g.mp.ani.skip()
case wonGame:
g.activate(screenDeactive)
return finishGame
}
}
return playGame
}
// newGameScreen initializes the gameplay screen.
func newGameScreen(mp *bampf) (scr *game) {
g := &game{}
g.mp = mp
g.lens = &cam{}
g.ww, g.wh = mp.ww, mp.wh
g.run = 10 // shared constant
g.spin = 25 // shared constant
g.vr = 25 // shared constant
g.levels = make(map[int]*level)
g.procDebug = g.setDebugProcessor(g)
return g
}
// setDebugProcessor checks if the optional processDebugInput method
// is present in the build.
func (g *game) setDebugProcessor(gi interface{}) func(*vu.Input) {
if fn, ok := gi.(interface {
processDebugInput(*vu.Input)
}); ok {
return fn.processDebugInput // debugging is on.
}
return func(in *vu.Input) {} // debugging is off.
}
// handleResize affects all levels, not just the current one.
func (g *game) handleResize(width, height int) {
g.ww, g.wh = width, height
for _, stage := range g.levels {
stage.resize(width, height)
}
}
// spinView updates the camera look direction based on amount of mouse movement
// from the previous call.
func (g *game) spinView(mx, my int, dt float64) {
xdiff, ydiff := float64(mx-g.mxp), float64(my-g.myp)
g.lens.look(g.spin, dt, xdiff, ydiff)
g.mxp, g.myp = mx, my
}
// centerMouse pops the mouse back to the center of the window, but only
// when the mouse starts to stray too far away.
func (g *game) centerMouse(mx, my int) {
cx, cy := g.ww/2, g.wh/2
if math.Abs(float64(cx-mx)) > 200 || math.Abs(float64(cy-my)) > 200 {
g.mp.eng.Set(vu.CursorAt(g.ww/2, g.wh/2))
g.mxp, g.myp = cx, cy
}
}
// limitWandering puts a limit on how far the player can get from the center
// of the level. This allows the player to feel like they are traveling away
// forever, but they can then return to the center in very little time.
func (g *game) limitWandering(down int) {
maxd := g.vr * 3 // max allowed distance from center
cx, _, cz := g.cl.center.At() // center location
x, y, z := g.cl.body.At() // player location
toc := &lin.V3{X: x - cx, Y: y, Z: z - cz} // vector to center
dtoc := toc.Len() // distance to center
if dtoc > maxd {
// stop moving forward and move a bit back to center.
if body := g.cl.body.Body(); body != nil {
body.Stop()
body.Rest()
body.Push(-toc.X/100, 0, -toc.Z/100)
}
}
if down < 0 {
if body := g.cl.body.Body(); body != nil {
body.Stop()
body.Rest()
}
}
g.cl.player.part.SetListener()
}
// Player movement handlers.
func (g *game) goForward(dt float64, down int) {
g.lens.forward(g.cl.body, dt, g.run, g.dir)
g.limitWandering(down)
}
func (g *game) goBack(dt float64, down int) {
g.lens.back(g.cl.body, dt, g.run, g.dir)
g.limitWandering(down)
}
func (g *game) goLeft(dt float64, down int) {
g.lens.left(g.cl.body, dt, g.run, g.dir)
g.limitWandering(down)
}
func (g *game) goRight(dt float64, down int) {
g.lens.right(g.cl.body, dt, g.run, g.dir)
g.limitWandering(down)
}
// evolveCheck looks for a player at full health that is at the center
// of the level. This is the trigger to complete the level.
func (g *game) evolveCheck(eventq *list.List) {
if g.cl.isPlayerWorthy() {
x, y, z := g.cl.cam.At()
gridx, gridy := toGrid(x, y, z, float64(g.cl.units))
if gridx == g.cl.gcx && gridy == g.cl.gcy {
if g.cl.num < 4 {
g.mp.ani.addAnimation(g.newEvolveAnimation(1))
} else if g.cl.num == 4 {
publish(eventq, wonGame, nil)
}
}
}
}
// healthUpdated is a callback whenever player health changes.
// Players that have full health are worthy to descend to the
// next level, they just have to reach the center first.
func (g *game) healthUpdated(health, warn, high int) {
if health <= 0 {
if g.cl.num > 0 {
g.mp.ani.addAnimation(g.newEvolveAnimation(-1))
}
}
// increase the center block scale when player is ready to evolve.
if g.cl.isPlayerWorthy() {
g.cl.center.SetScale(1, 50, 1)
} else {
g.cl.center.SetScale(1, 1, 1)
}
}
// setKeys sets the rebindable keys.
func (g *game) setKeys(keys []int) {
g.keys = keys
if g.cl != nil {
g.cl.updateKeys(g.keys)
}
}
// setLevel updates to the requested level,
// generating a new level if necessary.
func (g *game) setLevel(lvl int) {
if g.cl != nil {
g.cl.deactivate()
}
if _, ok := g.levels[lvl]; !ok {
g.levels[lvl] = newLevel(g, lvl)
} else {
g.levels[lvl].player.reset()
}
g.cl = g.levels[lvl]
g.lens.reset(g.cl.cam)
g.cl.activate(g)
g.cl.updateKeys(g.keys)
g.dir = g.cl.cam.Look
}
// newStartGameAnimation descends to the initial level from
// the launch screen.
func (g *game) newStartGameAnimation() animation {
return &fadeLevelAnimation{g: g, gameState: screenActive, dir: 1, out: false, ticks: 100}
}
// newEndGameAnimation descends from the final level to the end screen.
func (g *game) newEndGameAnimation() animation {
return &fadeLevelAnimation{g: g, gameState: screenDeactive, dir: 1, out: true, ticks: 100}
}
// newEvolveAnimation descends or ascends from one game level to another.
func (g *game) newEvolveAnimation(dir int) animation {
g.activate(screenEvolving)
fadeOut := &fadeLevelAnimation{g: g, gameState: screenDeactive, dir: dir, out: true, ticks: 100}
fadeIn := &fadeLevelAnimation{g: g, gameState: screenActive, dir: dir, out: false, ticks: 100}
transition := func() { g.switchLevel(fadeOut, fadeIn) }
return newTransitionAnimation(fadeOut, fadeIn, transition)
}
// switchLevel resets any changes to the center of the current level
// and then switches to the next level.
func (g *game) switchLevel(fo, fi *fadeLevelAnimation) {
g.cl.setBackgroundColour(1)
g.cl.center.SetScale(1, 1, 1).SetUniform("spin", 1.0)
// switch to the new level.
g.setLevel(g.cl.num + fo.dir)
}
// game
// ===========================================================================
// fadeLevelAnimation animates the transition between levels.
// Animation to fade a level. This does both up and down evovle directions
// and does fade ins and fade outs.
type fadeLevelAnimation struct {
g *game // All the state needed to do the fade.
gameState int // Will be set after finishing the second of two animations.
dir int // Which way the level is fading (up or down).
out bool // true if the level is fading out, false otherwise.
ticks int // Animation run rate - number of animation steps.
tickCnt int // Current step.
distA float64 // Animation start height.
distB float64 // The height where the animation stops.
tiltA float64 // Animation start tilt.
tiltB float64 // Animation end tilt.
state int // Track animation progress 0:start, 1:run, 2:done.
colr float32 // Amount needed to change colour.
}
// fade in/out the level.
func (f *fadeLevelAnimation) Animate(dt float64) bool {
switch f.state {
case 0:
g := f.g
g.evolving = true
g.cl.body.DisposeBody()
x, z := 4.0, 10.0 // standard starting spot.
if f.out {
// fading out:
// start level drop below if dir == 1
// cam tilt from 0 to 75
// location goes down from 0 to -g.vr.
// start level and rise if dir == -1
// cam tilt from 0 to -75
// location goes up from 0 to g.vr.
f.tiltA, f.tiltB = 0.0, float64(75*f.dir)
f.distA, f.distB = 0.0, float64(f.dir)*-g.vr
x, _, z = g.cl.cam.At() // start from player location.
} else {
// fading in:
// start high and drop to level if dir == 1
// cam tilt from -75 to 0
// location goes from g.vr down to 0.
// start low and rise to level if dir == -1
// cam tilt from 75 to 0
// location goes from -g.vr down to 0.
f.tiltA, f.tiltB = float64(-75*f.dir), 0.0
f.distA, f.distB = float64(f.dir)*g.vr, 0.0
}
g.lens.pitch = f.tiltA
g.cl.cam.SetAt(x, f.distA, z)
f.colr = (float32(1) - g.cl.colour) / float32(f.ticks)
g.cl.setVisible(true)
g.cl.setHudVisible(false)
f.state = 1
return true
case 1:
g := f.g
g.cl.colour += f.colr
g.cl.setBackgroundColour(g.cl.colour)
move := (f.distB - f.distA) / float64(f.ticks)
g.cl.cam.Move(0, move, 0, lin.QI)
tilt := (f.tiltB - f.tiltA) / float64(f.ticks) * 2
g.lens.pitch = g.lens.updatePitch(g.lens.pitch, tilt, g.spin, g.dt)
g.cl.cam.SetPitch(g.lens.pitch)
if f.tickCnt >= f.ticks {
f.Wrap()
return false // animation done.
}
f.tickCnt++
return true
default:
return false // animation done.
}
}
// Wrap finishes the fade level animation and sets the player position to
// a safe and stable location.
func (f *fadeLevelAnimation) Wrap() {
g := f.g
g.lens = &cam{}
g.cl.setHudVisible(true)
g.cl.body.DisposeBody()
g.cl.body.MakeBody(vu.Sphere(0.25))
g.cl.body.SetSolid(1, 0)
x, _, z := g.cl.cam.At()
g.cl.cam.SetAt(x, 0.5, z)
g.lens.pitch = 0
g.cl.cam.SetPitch(g.lens.pitch)
g.cl.body.SetAt(x, 0.5, z)
g.cl.body.SetView(lin.QI)
// set the new game state if appropriate.
if f.gameState == screenDeactive || f.gameState == screenActive {
g.activate(f.gameState)
}
f.state = 2
}
// fadeLevelAnimation
// ===========================================================================
// Various game algorithms
// gameMapSize gives the grid size for a given level.
func gameMapSize(lvl int) int { return lvl*6 + 9 }
// gameCcol is the inverse background colour for the center of the given level.
func gameCcol(lvl int) float64 { return float64(lvl+1) * 0.15 }
// gameMuster is the number of sentinels generated for a given level.
var gameMuster = []int{1, 5, 25, 50, 100}
// gameCellGain gives the per-level number of cells gained for each core
// collected.
var gameCellGain = []int{1, 2, 4, 8, 8}
// gameCellLoss gives the per-level number of cells lost for each collision
// with a sentinel. These are multiples of the corresponding cell gains.
var gameCellLoss = []int{1, 12, 24, 48, 64}
// lastSpot is used during debug to return the player to their previous
// position when debug fly mode is turned off.
type lastSpot struct {
lx, ly, lz float64 // location
// dx, dy, dz, dw float64 // direction
pitch float64 // up/down.
yaw float64 // spin.
}
// calculate a unique id for an x, y coordinate.
func id(x, y, size int) int { return x*size + y }
// get the x, y coordinate for a unique identifier.
func at(id, size int) (x, y int) { return id % size, id / size }