/
aw.go
288 lines (258 loc) · 7.27 KB
/
aw.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
/*
Copyright 2021 Josh Deprez
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package example
import (
"encoding/gob"
"fmt"
"math"
"github.com/DrJosh9000/ichigo/engine"
"github.com/DrJosh9000/ichigo/geom"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
const awakemanProducesBubbles = true
var _ interface {
engine.Identifier
engine.Disabler
engine.Prepper
engine.Scanner
engine.Updater
} = &Awakeman{}
func init() {
gob.Register(&Awakeman{})
}
// Awakeman is a bit of a god object for now...
type Awakeman struct {
engine.Disables
Sprite engine.Sprite
CameraID string
ToastID string
game *engine.Game
camera *engine.Camera
toast *engine.DebugToast
vel geom.Float3
facingLeft bool
coyoteTimer int
jumpBuffer int
noclip bool
spawnPoint geom.Int3
bubbleTimer int
anims map[string]*engine.Anim
}
// Ident returns "awakeman". There should be only one!
func (aw *Awakeman) Ident() string { return "awakeman" }
// Update updates Awakeman, including capturing input, applying gravity and
// movement, and repositioning the camera.
func (aw *Awakeman) Update() error {
// TODO: better cheat for noclip
if inpututil.IsKeyJustPressed(ebiten.KeyN) {
aw.noclip = !aw.noclip
aw.vel = geom.Float3{}
if aw.toast != nil {
if aw.noclip {
aw.toast.Toast("noclip enabled")
} else {
aw.toast.Toast("noclip disabled")
}
}
}
upd := aw.realUpdate
if aw.noclip {
upd = aw.noclipUpdate
}
if err := upd(); err != nil {
return err
}
// Update the camera
// aw.Pos is top-left corner, so add half size to get centre
z := 1.0
if ebiten.IsKeyPressed(ebiten.KeyShift) {
z = 2.0
}
aw.camera.PointAt(aw.Sprite.Actor.BoundingBox().Centre(), z)
return nil
}
func (aw *Awakeman) noclipUpdate() error {
if ebiten.IsKeyPressed(ebiten.KeyUp) {
aw.Sprite.Actor.Pos.Y--
}
if ebiten.IsKeyPressed(ebiten.KeyDown) {
aw.Sprite.Actor.Pos.Y++
}
if ebiten.IsKeyPressed(ebiten.KeyLeft) {
aw.Sprite.Actor.Pos.X--
}
if ebiten.IsKeyPressed(ebiten.KeyRight) {
aw.Sprite.Actor.Pos.X++
}
return nil
}
func (aw *Awakeman) realUpdate() error {
const (
ε = 0.2
restitution = -0.3
gravity = 0.25
airResistance = -0.005
jumpVelocity = -3.3
sqrt2 = 1.414213562373095
runVelocity = sqrt2
coyoteTime = 5
jumpBufferTime = 5
respawnY = 1000
bubblePeriod = 6
)
if awakemanProducesBubbles {
// Add a bubble?
aw.bubbleTimer--
if aw.bubbleTimer <= 0 {
aw.bubbleTimer = bubblePeriod
bubble := NewBubble(aw.Sprite.Actor.Pos.Add(geom.Pt3(-3, -20, -1)))
if err := aw.game.Load(bubble, Assets); err != nil {
return err
}
// Add bubble to same parent as aw
aw.game.PathRegister(bubble, aw.game.Parent(aw))
if err := aw.game.Prepare(bubble); err != nil {
return err
}
bubble.Sprite.SetAnim(bubble.Sprite.Sheet.NewAnim("bubble"))
}
}
// Fell below some threshold?
if aw.Sprite.Actor.Pos.Y > respawnY {
aw.Sprite.Actor.Pos = aw.spawnPoint
aw.vel = geom.Float3{}
}
// High-school physics time! Under constant acceleration:
// v = v_0 + a*t
// and
// s = t * (v_0 + v) / 2
// (note t is in ticks and s is in world units)
// and since we get one Update per tick (t = 1),
// v = v_0 + a,
// and
// s = (v_0 + v) / 2.
// Capture current v_0 to use later.
v0 := aw.vel
// Has traction?
if aw.vel.Y >= 0 && aw.Sprite.Actor.CollidesAt(aw.Sprite.Actor.Pos.Add(geom.Pt3(0, 1, 0))) {
// Not falling.
// Instantly decelerate (AW absorbs all kinetic E in legs, or something)
if aw.jumpBuffer > 0 {
// Tried to jump recently -- so jump
aw.vel.Y = jumpVelocity
aw.jumpBuffer = 0
} else {
// Can jump now or soon.
aw.vel.Y = 0
aw.coyoteTimer = coyoteTime
}
} else {
// Falling. v = v_0 + a, and a = gravity + airResistance(v_0)
aw.vel.Y += gravity + airResistance*aw.vel.Y
if aw.coyoteTimer > 0 {
aw.coyoteTimer--
}
if aw.jumpBuffer > 0 {
aw.jumpBuffer--
}
}
// Handle controls
// NB: spacebar sometimes does things on web pages (scrolls down)
if inpututil.IsKeyJustPressed(ebiten.KeySpace) || inpututil.IsKeyJustPressed(ebiten.KeyZ) {
// On ground or recently on ground?
if aw.coyoteTimer > 0 {
// Jump. One frame of v = jumpVelocity (ignoring any gravity already applied this tick).
aw.vel.Y = jumpVelocity
} else {
// Buffer the jump in case aw hits the ground soon.
aw.jumpBuffer = jumpBufferTime
}
}
// Left, right, away, toward
aw.vel.X, aw.vel.Z = 0, 0
switch {
case ebiten.IsKeyPressed(ebiten.KeyLeft) || ebiten.IsKeyPressed(ebiten.KeyJ):
aw.vel.X = -runVelocity
case ebiten.IsKeyPressed(ebiten.KeyRight) || ebiten.IsKeyPressed(ebiten.KeyL):
aw.vel.X = runVelocity
}
switch {
case ebiten.IsKeyPressed(ebiten.KeyUp) || ebiten.IsKeyPressed(ebiten.KeyI):
aw.vel.Z = -runVelocity
case ebiten.IsKeyPressed(ebiten.KeyDown) || ebiten.IsKeyPressed(ebiten.KeyK):
aw.vel.Z = runVelocity
}
// Animations and velocity correction
switch {
case aw.vel.X != 0 && aw.vel.Z != 0: // Diagonal
aw.Sprite.SetAnim(aw.anims["run_vert"])
// Pythagorean theorem; |vx| = |vz|, so the hypotenuse is √2 too big
// if we want to run at runVelocity always
aw.vel.X /= sqrt2
aw.vel.Z /= sqrt2
case aw.vel.X == 0 && aw.vel.Z != 0: // Vertical
aw.Sprite.SetAnim(aw.anims["run_vert"])
// vz == 0 for all remaining cases
case aw.vel.X < 0: // Left
aw.Sprite.SetAnim(aw.anims["run_left"])
aw.facingLeft = true
case aw.vel.X > 0: // Right
aw.Sprite.SetAnim(aw.anims["run_right"])
aw.facingLeft = false
default: // aw.velocity.X == 0; Idle
aw.Sprite.SetAnim(aw.anims["idle_right"])
if aw.facingLeft {
aw.Sprite.SetAnim(aw.anims["idle_left"])
}
}
// s = (v_0 + v) / 2.
aw.Sprite.Actor.MoveX((v0.X+aw.vel.X)/2, nil)
// For Y, on collision from going upwards, bounce a little bit.
// Does not apply to X because controls override it anyway.
aw.Sprite.Actor.MoveY((v0.Y+aw.vel.Y)/2, func() {
if aw.vel.Y > 0 {
return
}
aw.vel.Y *= restitution
if math.Abs(aw.vel.Y) < ε {
aw.vel.Y = 0
}
})
aw.Sprite.Actor.MoveZ((v0.Z+aw.vel.Z)/2, nil)
return nil
}
// Prepare captures necessary references to other game components.
func (aw *Awakeman) Prepare(game *engine.Game) error {
aw.game = game
cam, ok := game.Component(aw.CameraID).(*engine.Camera)
if !ok {
return fmt.Errorf("component %q not *engine.Camera", aw.CameraID)
}
aw.camera = cam
tst, ok := game.Component(aw.ToastID).(*engine.DebugToast)
if !ok {
return fmt.Errorf("component %q not *engine.DebugToast", aw.ToastID)
}
aw.toast = tst
aw.anims = aw.Sprite.Sheet.NewAnims()
aw.spawnPoint = aw.Sprite.Actor.Pos
return nil
}
// Scan visits &aw.Sprite.
func (aw *Awakeman) Scan(visit engine.VisitFunc) error {
return visit(&aw.Sprite)
}
func (aw *Awakeman) String() string {
return fmt.Sprintf("Awakeman@%v", aw.Sprite.Actor.Pos)
}