summaryrefslogtreecommitdiffhomepage
path: root/samples/99_genre_arcade/twinstick/app/main.rb
blob: 2ce1d50b8c574fada3f9a967f673d1b9efa1156b (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
def tick args
  args.state.player         ||= {x: 600, y: 320, w: 80, h: 80, path: 'sprites/circle-white.png', vx: 0, vy: 0, health: 10, cooldown: 0, score: 0}
  args.state.enemies        ||= []
  args.state.player_bullets ||= []
  args.state.tick_count     ||= -1
  args.state.tick_count     += 1
  spawn_enemies args
  kill_enemies args
  move_enemies args
  move_bullets args
  move_player args
  fire_player args
  args.state.player[:r] = args.state.player[:g] = args.state.player[:b] = (args.state.player[:health] * 25.5).clamp(0, 255)
  label_color           = args.state.player[:health] <= 5 ? 255 : 0
  args.outputs.labels << [
      {
          x: args.state.player.x + 40, y: args.state.player.y + 60, alignment_enum: 1, text: "#{args.state.player[:health]} HP",
          r: label_color, g: label_color, b: label_color
      }, {
          x: args.state.player.x + 40, y: args.state.player.y + 40, alignment_enum: 1, text: "#{args.state.player[:score]} PTS",
          r: label_color, g: label_color, b: label_color, size_enum: 2 - args.state.player[:score].to_s.length,
      }
  ]
  args.outputs.sprites << [args.state.player, args.state.enemies, args.state.player_bullets]
  args.state.clear! if args.state.player[:health] < 0 # Reset the game if the player's health drops below zero
end

def spawn_enemies args
  # Spawn enemies more frequently as the player's score increases.
  if rand < (100+args.state.player[:score])/(10000 + args.state.player[:score]) || args.state.tick_count.zero?
    theta = rand * Math::PI * 2
    args.state.enemies << {
        x: 600 + Math.cos(theta) * 800, y: 320 + Math.sin(theta) * 800, w: 80, h: 80, path: 'sprites/circle-white.png',
        r: (256 * rand).floor, g: (256 * rand).floor, b: (256 * rand).floor
    }
  end
end

def kill_enemies args
  args.state.enemies.reject! do |enemy|
    # Check if enemy and player are within 80 pixels of each other (i.e. overlapping)
    if 6400 > (enemy.x - args.state.player.x) ** 2 + (enemy.y - args.state.player.y) ** 2
      # Enemy is touching player. Kill enemy, and reduce player HP by 1.
      args.state.player[:health] -= 1
    else
      args.state.player_bullets.any? do |bullet|
        # Check if enemy and bullet are within 50 pixels of each other (i.e. overlapping)
        if 2500 > (enemy.x - bullet.x + 30) ** 2 + (enemy.y - bullet.y + 30) ** 2
          # Increase player health by one for each enemy killed by a bullet after the first enemy, up to a maximum of 10 HP
          args.state.player[:health] += 1 if args.state.player[:health] < 10 && bullet[:kills] > 0
          # Keep track of how many enemies have been killed by this particular bullet
          bullet[:kills]             += 1
          # Earn more points by killing multiple enemies with one shot.
          args.state.player[:score]  += bullet[:kills]
        end
      end
    end
  end
end

def move_enemies args
  args.state.enemies.each do |enemy|
    # Get the angle from the enemy to the player
    theta   = Math.atan2(enemy.y - args.state.player.y, enemy.x - args.state.player.x)
    # Convert the angle to a vector pointing at the player
    dx, dy  = theta.to_degrees.vector 5
    # Move the enemy towards the player
    enemy.x -= dx
    enemy.y -= dy
  end
end

def move_bullets args
  args.state.player_bullets.each do |bullet|
    # Move the bullets according to the bullet's velocity
    bullet.x += bullet[:vx]
    bullet.y += bullet[:vy]
  end
  args.state.player_bullets.reject! do |bullet|
    # Despawn bullets that are outside the screen area
    bullet.x < -20 || bullet.y < -20 || bullet.x > 1300 || bullet.y > 740
  end
end

def move_player args
  # Get the currently held direction.
  dx, dy                 = move_directional_vector args
  # Take the weighted average of the old velocities and the desired velocities. 
  # Since move_directional_vector returns values between -1 and 1, 
  #   and we want to limit the speed to 7.5, we multiply dx and dy by 7.5*0.1 to get 0.75
  args.state.player[:vx] = args.state.player[:vx] * 0.9 + dx * 0.75
  args.state.player[:vy] = args.state.player[:vy] * 0.9 + dy * 0.75
  # Move the player
  args.state.player.x    += args.state.player[:vx]
  args.state.player.y    += args.state.player[:vy]
  # If the player is about to go out of bounds, put them back in bounds.
  args.state.player.x    = args.state.player.x.clamp(0, 1201)
  args.state.player.y    = args.state.player.y.clamp(0, 640)
end


def fire_player args
  # Reduce the firing cooldown each tick
  args.state.player[:cooldown] -= 1
  # If the player is allowed to fire
  if args.state.player[:cooldown] <= 0
    dx, dy = shoot_directional_vector args # Get the bullet velocity
    return if dx == 0 && dy == 0 # If the velocity is zero, the player doesn't want to fire. Therefore, we just return early.
    # Add a new bullet to the list of player bullets.
    args.state.player_bullets << {
        x:     args.state.player.x + 30 + 40 * dx,
        y:     args.state.player.y + 30 + 40 * dy,
        w:     20, h: 20,
        path:  'sprites/circle-white.png',
        r:     0, g: 0, b: 0,
        vx:    10 * dx + args.state.player[:vx] / 7.5, vy: 10 * dy + args.state.player[:vy] / 7.5, # Factor in a bit of the player's velocity
        kills: 0
    }
    args.state.player[:cooldown] = 30 # Reset the cooldown
  end
end

# Custom function for getting a directional vector just for movement using WASD
def move_directional_vector args
  dx = 0
  dx += 1 if args.inputs.keyboard.d
  dx -= 1 if args.inputs.keyboard.a
  dy = 0
  dy += 1 if args.inputs.keyboard.w
  dy -= 1 if args.inputs.keyboard.s
  if dx != 0 && dy != 0
    dx *= 0.7071
    dy *= 0.7071
  end
  [dx, dy]
end

# Custom function for getting a directional vector just for shooting using the arrow keys
def shoot_directional_vector args
  dx = 0
  dx += 1 if args.inputs.keyboard.key_down.right || args.inputs.keyboard.key_held.right
  dx -= 1 if args.inputs.keyboard.key_down.left || args.inputs.keyboard.key_held.left
  dy = 0
  dy += 1 if args.inputs.keyboard.key_down.up || args.inputs.keyboard.key_held.up
  dy -= 1 if args.inputs.keyboard.key_down.down || args.inputs.keyboard.key_held.down
  if dx != 0 && dy != 0
    dx *= 0.7071
    dy *= 0.7071
  end
  [dx, dy]
end