summaryrefslogtreecommitdiffhomepage
path: root/samples/99_genre_rpg_roguelike/roguelike_starting_point/app/main.rb
blob: 7ed8d607b648fbc5c48f67e6db9ef6924d993026 (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
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
=begin

 APIs listing that haven't been encountered in previous sample apps:

 - lambda: A way to define a block and its parameters with special syntax.
   For example, the syntax of lambda looks like this:
   my_lambda = -> { puts "This is my lambda" }

 Reminders:
 - args.outputs.labels: An array. The values generate a label.
   The parameters are [X, Y, TEXT, SIZE, ALIGNMENT, RED, GREEN, BLUE, ALPHA, FONT STYLE]

 - ARRAY#inside_rect?: Returns whether or not the point is inside a rect.

 - product: Returns an array of all combinations of elements from all arrays.

 - find: Finds all elements of a collection that meet requirements.

 - abs: Returns the absolute value.

=end

# This sample app allows the player to move around in the dungeon, which becomes more or less visible
# depending on the player's location, and also has enemies.

class Game
  attr_accessor :args, :state, :inputs, :outputs, :grid

  # Calls all the methods needed for the game to run properly.
  def tick
    defaults
    render_canvas
    render_dungeon
    render_player
    render_enemies
    print_cell_coordinates
    calc_canvas
    input_move
    input_click_map
  end

  # Sets default values and initializes variables
  def defaults
    outputs.background_color = [0, 0, 0] # black background

    # Initializes empty canvas, dungeon, and enemies collections.
    state.canvas   ||= []
    state.dungeon  ||= []
    state.enemies  ||= []

    # If state.area doesn't have value, load_area_one and derive_dungeon_from_area methods are called
    if !state.area
      load_area_one
      derive_dungeon_from_area

      # Changing these values will change the position of player
      state.x = 7
      state.y = 5

      # Creates new enemies, sets their values, and adds them to the enemies collection.
      state.enemies << state.new_entity(:enemy) do |e| # declares each enemy as new entity
        e.x           = 13 # position
        e.y           = 5
        e.previous_hp = 3
        e.hp          = 3
        e.max_hp      = 3
        e.is_dead     = false # the enemy is alive
      end

      update_line_of_sight # updates line of sight by adding newly visible cells
    end
  end

  # Adds elements into the state.area collection
  # The dungeon is derived using the coordinates of this collection
  def load_area_one
    state.area ||= []
    state.area << [8, 6]
    state.area << [7, 6]
    state.area << [7, 7]
    state.area << [8, 9]
    state.area << [7, 8]
    state.area << [7, 9]
    state.area << [6, 4]
    state.area << [7, 3]
    state.area << [7, 4]
    state.area << [6, 5]
    state.area << [7, 5]
    state.area << [8, 5]
    state.area << [8, 4]
    state.area << [1, 1]
    state.area << [0, 1]
    state.area << [0, 2]
    state.area << [1, 2]
    state.area << [2, 2]
    state.area << [2, 1]
    state.area << [2, 3]
    state.area << [1, 3]
    state.area << [1, 4]
    state.area << [2, 4]
    state.area << [2, 5]
    state.area << [1, 5]
    state.area << [2, 6]
    state.area << [3, 6]
    state.area << [4, 6]
    state.area << [4, 7]
    state.area << [4, 8]
    state.area << [5, 8]
    state.area << [5, 9]
    state.area << [6, 9]
    state.area << [7, 10]
    state.area << [7, 11]
    state.area << [7, 12]
    state.area << [7, 12]
    state.area << [7, 13]
    state.area << [8, 13]
    state.area << [9, 13]
    state.area << [10, 13]
    state.area << [11, 13]
    state.area << [12, 13]
    state.area << [12, 12]
    state.area << [8, 12]
    state.area << [9, 12]
    state.area << [10, 12]
    state.area << [11, 12]
    state.area << [12, 11]
    state.area << [13, 11]
    state.area << [13, 10]
    state.area << [13, 9]
    state.area << [13, 8]
    state.area << [13, 7]
    state.area << [13, 6]
    state.area << [12, 6]
    state.area << [14, 6]
    state.area << [14, 5]
    state.area << [13, 5]
    state.area << [12, 5]
    state.area << [12, 4]
    state.area << [13, 4]
    state.area << [14, 4]
    state.area << [1, 6]
    state.area << [6, 6]
  end

  # Starts with an empty dungeon collection, and adds dungeon cells into it.
  def derive_dungeon_from_area
    state.dungeon = [] # starts as empty collection

    state.area.each do |a| # for each element of the area collection
      state.dungeon << state.new_entity(:dungeon_cell) do |d| # declares each dungeon cell as new entity
        d.x = a.x # dungeon cell position using coordinates from area
        d.y = a.y
        d.is_visible = false # cell is not visible
        d.alpha = 0 # not transparent at all
        d.border = [left_margin   + a.x * grid_size,
                    bottom_margin + a.y * grid_size,
                    grid_size,
                    grid_size,
                    *blue,
                    255] # sets border definition for dungeon cell
        d # returns dungeon cell
      end
    end
  end

  def left_margin
    40  # sets left margin
  end

  def bottom_margin
    60 # sets bottom margin
  end

  def grid_size
    40 # sets size of grid square
  end

  # Updates the line of sight by calling the thick_line_of_sight method and
  # adding dungeon cells to the newly_visible collection
  def update_line_of_sight
    variations = [-1, 0, 1]
    # creates collection of newly visible dungeon cells
    newly_visible = variations.product(variations).flat_map do |rise, run| # combo of all elements
      thick_line_of_sight state.x, state.y, rise, run, 15, # calls thick_line_of_sight method
                          lambda { |x, y| dungeon_cell_exists? x, y } # checks whether or not cell exists
    end.uniq# removes duplicates

    state.dungeon.each do |d| # perform action on each element of dungeons collection
      d.is_visible = newly_visible.find { |v| v.x == d.x && v.y == d.y } # finds match inside newly_visible collection
    end
  end

  #Returns a boolean value
  def dungeon_cell_exists? x, y
    # Finds cell coordinates inside dungeon collection to determine if dungeon cell exists
    state.dungeon.find { |d| d.x == x && d.y == y }
  end

  # Calls line_of_sight method to add elements to result collection
  def thick_line_of_sight start_x, start_y, rise, run, distance, cell_exists_lambda
    result = []
    result += line_of_sight start_x, start_y, rise, run, distance, cell_exists_lambda
    result += line_of_sight start_x - 1, start_y, rise, run, distance, cell_exists_lambda # one left
    result += line_of_sight start_x + 1, start_y, rise, run, distance, cell_exists_lambda # one right
    result
  end

  # Adds points to the result collection to create the player's line of sight
  def line_of_sight start_x, start_y, rise, run, distance, cell_exists_lambda
    result = [] # starts as empty collection
    points = points_on_line start_x, start_y, rise, run, distance # calls points_on_line method
    points.each do |p| # for each point in collection
      if cell_exists_lambda.call(p.x, p.y) # if the cell exists
        result << p # add it to result collection
      else # if cell does not exist
        return result # return result collection as it is
      end
    end

    result # return result collection
  end

  # Finds the coordinates of the points on the line by performing calculations
  def points_on_line start_x, start_y, rise, run, distance
    distance.times.map do |i| # perform an action
      [start_x + run * i, start_y + rise * i] # definition of point
    end
  end

  def render_canvas
    return
    outputs.borders << state.canvas.map do |c| # on each element of canvas collection
      c.border # outputs border
    end
  end

  # Outputs the dungeon cells.
  def render_dungeon
    outputs.solids << [0, 0, grid.w, grid.h] # outputs black background for grid

    # Sets the alpha value (opacity) for each dungeon cell and calls the cell_border method.
    outputs.borders << state.dungeon.map do |d| # for each element in dungeon collection
      d.alpha += if d.is_visible # if cell is visible
                 255.fdiv(30) # increment opacity (transparency)
               else # if cell is not visible
                 255.fdiv(600) * -1 # decrease opacity
               end
      d.alpha = d.alpha.cap_min_max(0, 255)
      cell_border d.x, d.y, [*blue, d.alpha] # sets blue border using alpha value
    end.reject_nil
  end

  # Sets definition of a cell border using the parameters
  def cell_border x, y, color = nil
    [left_margin   + x * grid_size,
    bottom_margin + y * grid_size,
    grid_size,
    grid_size,
    *color]
  end

  # Sets the values for the player and outputs it as a label
  def render_player
    outputs.labels << [grid_x(state.x) + 20, # positions "@" text in center of grid square
                     grid_y(state.y) + 35,
                     "@", # player is represented by a white "@" character
                     1, 1, *white]
  end

  def grid_x x
    left_margin + x * grid_size # positions horizontally on grid
  end

  def grid_y y
    bottom_margin + y * grid_size # positions vertically on grid
  end

  # Outputs enemies onto the screen.
  def render_enemies
    state.enemies.map do |e| # for each enemy in the collection
      alpha = 255 # set opacity (full transparency)

      # Outputs an enemy using a label.
      outputs.labels << [
                   left_margin + 20 +  e.x * grid_size, # positions enemy's "r" text in center of grid square
                   bottom_margin + 35 + e.y * grid_size,
                   "r", # enemy's text
                   1, 1, *white, alpha]

      # Creates a red border around an enemy.
      outputs.borders << [grid_x(e.x), grid_y(e.y), grid_size, grid_size, *red]
    end
  end

  #White labels are output for the cell coordinates of each element in the dungeon collection.
  def print_cell_coordinates
    return unless state.debug
    state.dungeon.each do |d|
      outputs.labels << [grid_x(d.x) + 2,
                         grid_y(d.y) - 2,
                         "#{d.x},#{d.y}",
                         -2, 0, *white]
    end
  end

  # Adds new elements into the canvas collection and sets their values.
  def calc_canvas
    return if state.canvas.length > 0 # return if canvas collection has at least one element
    15.times do |x| # 15 times perform an action
      15.times do |y|
        state.canvas << state.new_entity(:canvas) do |c| # declare canvas element as new entity
          c.x = x # set position
          c.y = y
          c.border = [left_margin   + x * grid_size,
                      bottom_margin + y * grid_size,
                      grid_size,
                      grid_size,
                      *white, 30] # sets border definition
        end
      end
    end
  end

  # Updates x and y values of the player, and updates player's line of sight
  def input_move
    x, y, x_diff, y_diff = input_target_cell

    return unless dungeon_cell_exists? x, y # player can't move there if a dungeon cell doesn't exist in that location
    return if enemy_at x, y # player can't move there if there is an enemy in that location

    state.x += x_diff # increments x by x_diff (so player moves left or right)
    state.y += y_diff # same with y and y_diff ( so player moves up or down)
    update_line_of_sight # updates visible cells
  end

  def enemy_at x, y
    # Finds if coordinates exist in enemies collection and enemy is not dead
    state.enemies.find { |e| e.x == x && e.y == y && !e.is_dead }
  end

  #M oves the user based on their keyboard input and sets values for target cell
  def input_target_cell
    if inputs.keyboard.key_down.up # if "up" key is in "down" state
      [state.x, state.y + 1,  0,  1] # user moves up
    elsif inputs.keyboard.key_down.down # if "down" key is pressed
      [state.x, state.y - 1,  0, -1] # user moves down
    elsif inputs.keyboard.key_down.left # if "left" key is pressed
      [state.x - 1, state.y, -1,  0] # user moves left
    elsif inputs.keyboard.key_down.right # if "right" key is pressed
      [state.x + 1, state.y,  1,  0] # user moves right
    else
      nil  # otherwise, empty
    end
  end

  # Goes through the canvas collection to find if the mouse was clicked inside of the borders of an element.
  def input_click_map
    return unless inputs.mouse.click # return unless the mouse is clicked
    canvas_entry = state.canvas.find do |c| # find element from canvas collection that meets requirements
      inputs.mouse.click.inside_rect? c.border # find border that mouse was clicked inside of
    end
    puts canvas_entry # prints canvas_entry value
  end

  # Sets the definition of a label using the parameters.
  def label text, x, y, color = nil
    color ||= white # color is initialized to white
    [x, y, text, 1, 1, *color] # sets label definition
  end

  def green
    [60, 200, 100] # sets color saturation to shade of green
  end

  def blue
    [50, 50, 210] # sets color saturation to shade of blue
  end

  def white
    [255, 255, 255] # sets color saturation to white
  end

  def red
    [230, 80, 80] # sets color saturation to shade of red
  end

  def orange
    [255, 80, 60] # sets color saturation to shade of orange
  end

  def pink
    [255, 0, 200] # sets color saturation to shade of pink
  end

  def gray
    [75, 75, 75] # sets color saturation to shade of gray
  end

  # Recolors the border using the parameters.
  def recolor_border border, r, g, b
    border[4] = r
    border[5] = g
    border[6] = b
    border
  end

  # Returns a boolean value.
  def visible? cell
    # finds cell's coordinates inside visible_cells collections to determine if cell is visible
    state.visible_cells.find { |c| c.x == cell.x && c.y == cell.y}
  end

  # Exports dungeon by printing dungeon cell coordinates
  def export_dungeon
    state.dungeon.each do |d| # on each element of dungeon collection
      puts "state.dungeon << [#{d.x}, #{d.y}]" # prints cell coordinates
    end
  end

  def distance_to_cell cell
    distance_to state.x, cell.x, state.y, cell.y # calls distance_to method
  end

  def distance_to from_x, x, from_y, y
    (from_x - x).abs + (from_y - y).abs # finds distance between two cells using coordinates
  end
end

$game = Game.new

def tick args
  $game.args    = args
  $game.state   = args.state
  $game.inputs  = args.inputs
  $game.outputs = args.outputs
  $game.grid    = args.grid
  $game.tick
end