summaryrefslogtreecommitdiffhomepage
path: root/samples/99_genre_arcade/dueling_starships/app/main.rb
blob: 8adcf3d1be8df75fcb7152856bb1369417940ff5 (plain)
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
class DuelingSpaceships
  attr_accessor :state, :inputs, :outputs, :grid

  def tick
    defaults
    render
    calc
    input
  end

  def defaults
    outputs.background_color = [0, 0, 0]
    state.ship_blue       ||= new_blue_ship
    state.ship_red        ||= new_red_ship
    state.flames          ||= []
    state.bullets         ||= []
    state.ship_blue_score ||= 0
    state.ship_red_score  ||= 0
    state.stars           ||= 100.map do
      [rand.add(2).to_square(grid.w_half.randomize(:sign, :ratio),
                             grid.h_half.randomize(:sign, :ratio)),
       128 + 128.randomize(:ratio), 255, 255]
    end
  end

  def default_ship x, y, angle, sprite_path, bullet_sprite_path, color
    state.new_entity(:ship,
                    { x: x,
                      y: y,
                      dy: 0,
                      dx: 0,
                      damage: 0,
                      dead: false,
                      angle: angle,
                      max_alpha: 255,
                      sprite_path: sprite_path,
                      bullet_sprite_path: bullet_sprite_path,
                      color: color })
  end

  def new_red_ship
    default_ship(400, 250.randomize(:sign, :ratio),
                 180, 'sprites/ship_red.png', 'sprites/red_bullet.png',
                 [255, 90, 90])
  end

  def new_blue_ship
    default_ship(-400, 250.randomize(:sign, :ratio),
                 0, 'sprites/ship_blue.png', 'sprites/blue_bullet.png',
                 [110, 140, 255])
  end

  def render
    render_instructions
    render_score
    render_universe
    render_flames
    render_ships
    render_bullets
  end

  def render_ships
    update_ship_outputs(state.ship_blue)
    update_ship_outputs(state.ship_red)
    outputs.sprites << [state.ship_blue.sprite, state.ship_red.sprite]
    outputs.labels  << [state.ship_blue.label, state.ship_red.label]
  end

  def render_instructions
    return if state.ship_blue.dx  > 0  || state.ship_blue.dy > 0  ||
              state.ship_red.dx   > 0  || state.ship_red.dy  > 0  ||
              state.flames.length > 0

    outputs.labels << [grid.left.shift_right(30),
                       grid.bottom.shift_up(30),
                       "Two gamepads needed to play. R1 to accelerate. Left and right on D-PAD to turn ship. Hold A to shoot. Press B to drop mines.",
                       0, 0, 255, 255, 255]
  end

  def calc
    calc_thrusts
    calc_ships
    calc_bullets
    calc_winner
  end

  def input
    input_accelerate
    input_turn
    input_bullets_and_mines
  end

  def render_score
    outputs.labels << [grid.left.shift_right(80),
                       grid.top.shift_down(40),
                       state.ship_blue_score, 30, 1, state.ship_blue.color]

    outputs.labels << [grid.right.shift_left(80),
                       grid.top.shift_down(40),
                       state.ship_red_score,  30, 1, state.ship_red.color]
  end

  def render_universe
    return if outputs.static_solids.any?
    outputs.static_solids << grid.rect
    outputs.static_solids << state.stars
  end

  def apply_round_finished_alpha entity
    return entity unless state.round_finished_debounce
    entity.a *= state.round_finished_debounce.percentage_of(2.seconds)
    return entity
  end

  def update_ship_outputs ship, sprite_size = 66
    ship.sprite =
      apply_round_finished_alpha [sprite_size.to_square(ship.x, ship.y),
                                  ship.sprite_path,
                                  ship.angle,
                                  ship.dead ? 0 : 255 * ship.created_at.ease(2.seconds)].sprite
    ship.label =
      apply_round_finished_alpha [ship.x,
                                  ship.y + 100,
                                  "." * 5.minus(ship.damage).greater(0), 20, 1, ship.color, 255].label
  end

  def render_flames sprite_size = 6
    outputs.sprites << state.flames.map do |p|
      apply_round_finished_alpha [sprite_size.to_square(p.x, p.y),
                                  'sprites/flame.png', 0,
                                  p.max_alpha * p.created_at.ease(p.lifetime, :flip)].sprite
    end
  end

  def render_bullets sprite_size = 10
    outputs.sprites << state.bullets.map do |b|
      apply_round_finished_alpha [b.sprite_size.to_square(b.x, b.y),
                                  b.owner.bullet_sprite_path,
                                  0, b.max_alpha].sprite
    end
  end

  def wrap_location! location
    location.x = grid.left    if location.x > grid.right
    location.x = grid.right   if location.x < grid.left
    location.y = grid.top     if location.y < grid.bottom
    location.y = grid.bottom  if location.y > grid.top
    location
  end

  def calc_thrusts
    state.flames =
      state.flames
        .reject(&:old?)
        .map do |p|
          p.speed *= 0.9
          p.y += p.angle.vector_y(p.speed)
          p.x += p.angle.vector_x(p.speed)
          wrap_location! p
      end
  end

  def all_ships
    [state.ship_blue, state.ship_red]
  end

  def alive_ships
    all_ships.reject { |s| s.dead }
  end

  def calc_bullet bullet
    bullet.y += bullet.angle.vector_y(bullet.speed)
    bullet.x += bullet.angle.vector_x(bullet.speed)
    wrap_location! bullet
    explode_bullet! bullet if bullet.old?
    return if bullet.exploded
    return if state.round_finished
    alive_ships.each do |s|
      if s != bullet.owner &&
         s.sprite.intersect_rect?(bullet.sprite_size.to_square(bullet.x, bullet.y))
        explode_bullet! bullet, 10, 5, 30
        s.damage += 1
      end
    end
  end

  def calc_bullets
    state.bullets.each    { |b| calc_bullet b }
    state.bullets.reject! { |b| b.exploded }
  end

  def create_explosion! type, entity, flame_count, max_speed, lifetime, max_alpha = 255
    flame_count.times do
      state.flames << state.new_entity(type,
                                     { angle: 360.randomize(:ratio),
                                       speed: max_speed.randomize(:ratio),
                                       lifetime: lifetime,
                                       x: entity.x,
                                       y: entity.y,
                                       max_alpha: max_alpha })
    end
  end

  def explode_bullet! bullet, flame_override = 5, max_speed = 5, lifetime = 10
    bullet.exploded = true
    create_explosion! :bullet_explosion,
                      bullet,
                      flame_override,
                      max_speed,
                      lifetime,
                      bullet.max_alpha
  end

  def calc_ship ship
    ship.x += ship.dx
    ship.y += ship.dy
    wrap_location! ship
  end

  def calc_ships
    all_ships.each { |s| calc_ship s }
    return if all_ships.any? { |s| s.dead }
    return if state.round_finished
    return unless state.ship_blue.sprite.intersect_rect?(state.ship_red.sprite)
    state.ship_blue.damage = 5
    state.ship_red.damage  = 5
  end

  def create_thruster_flames! ship
    state.flames << state.new_entity(:ship_thruster,
                                   { angle: ship.angle + 180 + 60.randomize(:sign, :ratio),
                                     speed: 5.randomize(:ratio),
                                     max_alpha: 255 * ship.created_at_elapsed.percentage_of(2.seconds),
                                     lifetime: 30,
                                     x: ship.x - ship.angle.vector_x(40) + 5.randomize(:sign, :ratio),
                                     y: ship.y - ship.angle.vector_y(40) + 5.randomize(:sign, :ratio) })
  end

  def input_accelerate_ship should_move_ship, ship
    return if ship.dead

    should_move_ship &&= (ship.dx + ship.dy).abs < 5

    if should_move_ship
      create_thruster_flames! ship
      ship.dx += ship.angle.vector_x 0.050
      ship.dy += ship.angle.vector_y 0.050
    else
      ship.dx *= 0.99
      ship.dy *= 0.99
    end
  end

  def input_accelerate
    input_accelerate_ship inputs.controller_one.key_held.r1 || inputs.keyboard.up, state.ship_blue
    input_accelerate_ship inputs.controller_two.key_held.r1, state.ship_red
  end

  def input_turn_ship direction, ship
    ship.angle -= 3 * direction
  end

  def input_turn
    input_turn_ship inputs.controller_one.left_right + inputs.keyboard.left_right, state.ship_blue
    input_turn_ship inputs.controller_two.left_right, state.ship_red
  end

  def input_bullet create_bullet, ship
    return unless create_bullet
    return if ship.dead

    state.bullets << state.new_entity(:ship_bullet,
                                    { owner: ship,
                                      angle: ship.angle,
                                      max_alpha: 255 * ship.created_at_elapsed.percentage_of(2.seconds),
                                      speed: 5 + ship.dx.mult(ship.angle.vector_x) + ship.dy.mult(ship.angle.vector_y),
                                      lifetime: 120,
                                      sprite_size: 10,
                                      x: ship.x + ship.angle.vector_x * 32,
                                      y: ship.y + ship.angle.vector_y * 32 })
  end

  def input_mine create_mine, ship
    return unless create_mine
    return if ship.dead

    state.bullets << state.new_entity(:ship_bullet,
                                    { owner: ship,
                                      angle: 360.randomize(:sign, :ratio),
                                      max_alpha: 255 * ship.created_at_elapsed.percentage_of(2.seconds),
                                      speed: 0.02,
                                      sprite_size: 10,
                                      lifetime: 600,
                                      x: ship.x + ship.angle.vector_x * -50,
                                      y: ship.y + ship.angle.vector_y * -50 })
  end

  def input_bullets_and_mines
    return if state.bullets.length > 100

    [
      [inputs.controller_one.key_held.a || inputs.keyboard.key_held.space,
       inputs.controller_one.key_down.b || inputs.keyboard.key_down.down,
       state.ship_blue],
      [inputs.controller_two.key_held.a, inputs.controller_two.key_down.b, state.ship_red]
    ].each do |a_held, b_down, ship|
      input_bullet(a_held && state.tick_count.mod_zero?(10).or(a_held == 0), ship)
      input_mine(b_down, ship)
    end
  end

  def calc_kill_ships
    alive_ships.find_all { |s| s.damage >= 5 }.each do |s|
      s.dead = true
      create_explosion! :ship_explosion, s, 20, 20, 30, s.max_alpha
    end
  end

  def calc_score
    return if state.round_finished
    return if alive_ships.length > 1

    if alive_ships.first == state.ship_red
      state.ship_red_score += 1
    elsif alive_ships.first == state.ship_blue
      state.ship_blue_score += 1
    end

    state.round_finished = true
  end

  def calc_reset_ships
    return unless state.round_finished
    state.round_finished_debounce ||= 2.seconds
    state.round_finished_debounce -= 1
    return if state.round_finished_debounce > 0
    start_new_round!
  end

  def start_new_round!
    state.ship_blue = new_blue_ship
    state.ship_red  = new_red_ship
    state.round_finished = false
    state.round_finished_debounce = nil
    state.flames.clear
    state.bullets.clear
  end

  def calc_winner
    calc_kill_ships
    calc_score
    calc_reset_ships
  end
end

$dueling_spaceship = DuelingSpaceships.new

def tick args
  args.grid.origin_center!
  $dueling_spaceship.inputs  = args.inputs
  $dueling_spaceship.outputs = args.outputs
  $dueling_spaceship.state    = args.state
  $dueling_spaceship.grid    = args.grid
  $dueling_spaceship.tick
end