termfront 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +41 -0
  3. data/LICENSE +21 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +160 -0
  6. data/Rakefile +12 -0
  7. data/data/audio/THIRD_PARTY_NOTICES.md +45 -0
  8. data/data/audio/beep_02.ogg +0 -0
  9. data/data/audio/button1.ogg +0 -0
  10. data/data/audio/complete.ogg +0 -0
  11. data/data/audio/manifest.json +17 -0
  12. data/data/audio/mission_bgm.wav +0 -0
  13. data/data/audio/mission_clear_se.wav +0 -0
  14. data/data/audio/on.ogg +0 -0
  15. data/data/audio/page_se.wav +0 -0
  16. data/data/audio/sector.mp3 +0 -0
  17. data/data/audio/sfx_22b.ogg +0 -0
  18. data/data/audio/shield_alarm_se.wav +0 -0
  19. data/data/audio/shield_regen_se.wav +0 -0
  20. data/data/audio/shoot_01.ogg +0 -0
  21. data/data/audio/terminal_se.wav +0 -0
  22. data/data/audio/title.mp3 +0 -0
  23. data/data/audio/title_bgm.wav +0 -0
  24. data/data/audio/victory.mp3 +0 -0
  25. data/data/events/corridor_sweep.json +27 -0
  26. data/data/events/final_push.json +40 -0
  27. data/data/events/stronghold.json +27 -0
  28. data/data/events/the_gauntlet.json +27 -0
  29. data/data/events/training_grounds.json +31 -0
  30. data/exe/termfront +6 -0
  31. data/exe/termfront-server +7 -0
  32. data/lib/termfront/audio_manager.rb +225 -0
  33. data/lib/termfront/config.rb +38 -0
  34. data/lib/termfront/demo_player.rb +181 -0
  35. data/lib/termfront/drop_item/base.rb +26 -0
  36. data/lib/termfront/drop_item/weapon.rb +38 -0
  37. data/lib/termfront/enemy/base.rb +133 -0
  38. data/lib/termfront/enemy/crawler.rb +18 -0
  39. data/lib/termfront/enemy/executor.rb +18 -0
  40. data/lib/termfront/game.rb +637 -0
  41. data/lib/termfront/input.rb +75 -0
  42. data/lib/termfront/map.rb +72 -0
  43. data/lib/termfront/mission/base.rb +81 -0
  44. data/lib/termfront/mission/corridor_sweep.rb +41 -0
  45. data/lib/termfront/mission/event_loader.rb +87 -0
  46. data/lib/termfront/mission/event_runtime.rb +37 -0
  47. data/lib/termfront/mission/final_push.rb +44 -0
  48. data/lib/termfront/mission/stronghold.rb +45 -0
  49. data/lib/termfront/mission/the_gauntlet.rb +38 -0
  50. data/lib/termfront/mission/training.rb +38 -0
  51. data/lib/termfront/mission/training_grounds.rb +37 -0
  52. data/lib/termfront/network/client.rb +865 -0
  53. data/lib/termfront/network/connection.rb +101 -0
  54. data/lib/termfront/network/server.rb +620 -0
  55. data/lib/termfront/network/wavesfight_client.rb +364 -0
  56. data/lib/termfront/opponent.rb +24 -0
  57. data/lib/termfront/player.rb +147 -0
  58. data/lib/termfront/projectile.rb +44 -0
  59. data/lib/termfront/remote_enemy.rb +21 -0
  60. data/lib/termfront/renderer.rb +707 -0
  61. data/lib/termfront/scene_player.rb +164 -0
  62. data/lib/termfront/sprite.rb +73 -0
  63. data/lib/termfront/terminal_output.rb +63 -0
  64. data/lib/termfront/title_screen.rb +299 -0
  65. data/lib/termfront/version.rb +5 -0
  66. data/lib/termfront/weapon/assault_rifle.rb +15 -0
  67. data/lib/termfront/weapon/base.rb +44 -0
  68. data/lib/termfront/weapon/pistol.rb +15 -0
  69. data/lib/termfront/weapon/shock_pistol.rb +15 -0
  70. data/lib/termfront/weapon/shock_rifle.rb +15 -0
  71. data/lib/termfront.rb +51 -0
  72. data/sig/termfront.rbs +4 -0
  73. metadata +119 -0
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Termfront
4
+ class Map
5
+ attr_reader :grid, :width, :height
6
+
7
+ def initialize(rows)
8
+ @grid = rows.map { |r| r.is_a?(Array) ? r : r.chars }
9
+ @height = @grid.size
10
+ @width = @grid[0].size
11
+ end
12
+
13
+ def wall_at?(fx, fy)
14
+ ix = fx.floor
15
+ iy = fy.floor
16
+ return true if iy < 0 || iy >= @height || ix < 0 || ix >= @width
17
+
18
+ @grid[iy][ix] == "#"
19
+ end
20
+
21
+ def blocked?(px, py, radius = Config::PLAYER_RADIUS)
22
+ wall_at?(px - radius, py - radius) || wall_at?(px + radius, py - radius) ||
23
+ wall_at?(px - radius, py + radius) || wall_at?(px + radius, py + radius)
24
+ end
25
+
26
+ def line_of_sight?(x1, y1, x2, y2)
27
+ dx = x2 - x1
28
+ dy = y2 - y1
29
+ dist = Math.sqrt(dx * dx + dy * dy)
30
+ return true if dist < 0.01
31
+
32
+ dx /= dist
33
+ dy /= dist
34
+
35
+ mx = x1.floor
36
+ my = y1.floor
37
+ ddx = dx == 0 ? 1e30 : (1.0 / dx).abs
38
+ ddy = dy == 0 ? 1e30 : (1.0 / dy).abs
39
+
40
+ if dx < 0
41
+ step_x = -1
42
+ sd_x = (x1 - mx) * ddx
43
+ else
44
+ step_x = 1
45
+ sd_x = (mx + 1.0 - x1) * ddx
46
+ end
47
+ if dy < 0
48
+ step_y = -1
49
+ sd_y = (y1 - my) * ddy
50
+ else
51
+ step_y = 1
52
+ sd_y = (my + 1.0 - y1) * ddy
53
+ end
54
+
55
+ loop do
56
+ if sd_x < sd_y
57
+ return true if sd_x > dist
58
+
59
+ sd_x += ddx
60
+ mx += step_x
61
+ else
62
+ return true if sd_y > dist
63
+
64
+ sd_y += ddy
65
+ my += step_y
66
+ end
67
+ return false if my < 0 || my >= @height || mx < 0 || mx >= @width
68
+ return false if @grid[my][mx] == "#"
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Termfront
4
+ module Mission
5
+ class Base
6
+ def id
7
+ self.class.name.split("::").last
8
+ .gsub(/([a-z0-9])([A-Z])/, '\1_\2')
9
+ .downcase
10
+ end
11
+
12
+ def name = raise(NotImplementedError)
13
+ def briefing = raise(NotImplementedError)
14
+ def map_data = raise(NotImplementedError)
15
+ def spawn = raise(NotImplementedError)
16
+ def weapon_defs = raise(NotImplementedError)
17
+ def enemy_defs = raise(NotImplementedError)
18
+
19
+ def events_path
20
+ File.expand_path("../../../data/events/#{id}.json", __dir__)
21
+ end
22
+
23
+ def event_definitions
24
+ @event_definitions ||= EventLoader.load_file(events_path)
25
+ end
26
+
27
+ def build_terminals
28
+ terminal_ids = event_definitions.filter_map do |event|
29
+ trigger = event[:trigger]
30
+ trigger[:terminal_id] if trigger[:type] == :terminal_used
31
+ end.uniq
32
+
33
+ map_data.each_with_index.filter_map do |row, y|
34
+ row.chars.each_with_index.filter_map do |cell, x|
35
+ next unless cell == "*"
36
+
37
+ { id: (terminal_ids.shift || :"terminal_#{x}_#{y}"), x: x + 0.5, y: y + 0.5 }
38
+ end
39
+ end.flatten
40
+ end
41
+
42
+ def build_map
43
+ Map.new(map_data)
44
+ end
45
+
46
+ def build_weapons
47
+ weapon_defs.map { |type, ammo| Weapon::Base.build(type, ammo) }
48
+ end
49
+
50
+ def build_enemies(difficulty_index)
51
+ enemies = enemy_defs.map do |ed|
52
+ type = ed[4]
53
+ Enemy::Base.build(type, ed, difficulty_index)
54
+ end
55
+ if difficulty_index
56
+ extra = Enemy::Base::DIFFICULTIES[difficulty_index][:extra_enemies]
57
+ enemies += Enemy::Base.generate_extras(enemy_defs, extra, difficulty_index)
58
+ end
59
+ enemies
60
+ end
61
+
62
+ class << self
63
+ def campaign
64
+ @campaign ||= []
65
+ end
66
+
67
+ def wavesfight
68
+ @wavesfight ||= []
69
+ end
70
+
71
+ def register(klass)
72
+ campaign << klass
73
+ end
74
+
75
+ def register_wavesfight(klass)
76
+ wavesfight << klass
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Termfront
4
+ module Mission
5
+ class CorridorSweep < Base
6
+ def name = "Corridor Sweep"
7
+ def briefing = "Sweep the original facility. Expect resistance."
8
+
9
+ def map_data
10
+ [
11
+ "########################",
12
+ "#..........#...........#",
13
+ "#..........#...........#",
14
+ "#..........#...........#",
15
+ "#..........#...........#",
16
+ "#......................#",
17
+ "#..........####........#",
18
+ "#......................#",
19
+ "#..............#.......#",
20
+ "#..............#.......#",
21
+ "########################"
22
+ ]
23
+ end
24
+
25
+ def spawn = [10.0, 6.0, 0.0]
26
+ def weapon_defs = [[:ar, 60], [:pistol, nil]]
27
+
28
+ def enemy_defs
29
+ [
30
+ [16.5, 1.5, 16.5, 4.5, :executor],
31
+ [5.5, 8.5, 9.5, 8.5, :crawler],
32
+ [20.5, 5.5, 20.5, 9.5, :crawler],
33
+ [3.5, 2.5, 3.5, 4.5, :crawler]
34
+ ]
35
+ end
36
+ end
37
+
38
+ Base.register(CorridorSweep)
39
+ Base.register_wavesfight(CorridorSweep)
40
+ end
41
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Termfront
6
+ module Mission
7
+ module EventLoader
8
+ module_function
9
+
10
+ def load_file(path)
11
+ return [] unless File.file?(path)
12
+
13
+ doc = JSON.parse(File.read(path))
14
+ events = doc.fetch("events") do
15
+ raise ArgumentError, "event file #{path} is missing top-level events array"
16
+ end
17
+
18
+ unless events.is_a?(Array)
19
+ raise ArgumentError, "event file #{path} must define events as an array"
20
+ end
21
+
22
+ events.map.with_index do |event, index|
23
+ validate_event!(event, path, index)
24
+ end
25
+ end
26
+
27
+ def validate_event!(event, path, index)
28
+ unless event.is_a?(Hash)
29
+ raise ArgumentError, "event #{index} in #{path} must be an object"
30
+ end
31
+
32
+ id = event["id"]
33
+ trigger = event["trigger"]
34
+ actions = event["actions"]
35
+
36
+ raise ArgumentError, "event #{index} in #{path} is missing id" if blank?(id)
37
+ raise ArgumentError, "event #{id} in #{path} is missing trigger" unless trigger.is_a?(Hash)
38
+ raise ArgumentError, "event #{id} in #{path} is missing actions" unless actions.is_a?(Array) && !actions.empty?
39
+
40
+ trigger_type = trigger["type"]
41
+ raise ArgumentError, "event #{id} in #{path} trigger is missing type" if blank?(trigger_type)
42
+
43
+ normalized_actions = actions.map.with_index do |action, action_index|
44
+ validate_action!(action, path, id, action_index)
45
+ end
46
+
47
+ normalized_trigger = symbolize_keys(trigger).merge(type: trigger_type.to_sym)
48
+ normalized_trigger[:terminal_id] = normalized_trigger[:terminal_id].to_sym if normalized_trigger[:terminal_id].is_a?(String)
49
+
50
+ {
51
+ id: id,
52
+ once: event.fetch("once", true),
53
+ trigger: normalized_trigger,
54
+ actions: normalized_actions
55
+ }
56
+ end
57
+
58
+ def validate_action!(action, path, event_id, index)
59
+ unless action.is_a?(Hash)
60
+ raise ArgumentError, "action #{index} in event #{event_id} (#{path}) must be an object"
61
+ end
62
+
63
+ type = action["type"]
64
+ raise ArgumentError, "action #{index} in event #{event_id} (#{path}) is missing type" if blank?(type)
65
+
66
+ symbolize_keys(action).merge(type: type.to_sym)
67
+ end
68
+
69
+ def symbolize_keys(value)
70
+ case value
71
+ when Array
72
+ value.map { |item| symbolize_keys(item) }
73
+ when Hash
74
+ value.each_with_object({}) do |(key, item), memo|
75
+ memo[key.to_sym] = symbolize_keys(item)
76
+ end
77
+ else
78
+ value
79
+ end
80
+ end
81
+
82
+ def blank?(value)
83
+ value.nil? || value == ""
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Termfront
6
+ module Mission
7
+ class EventRuntime
8
+ def initialize(events)
9
+ @events = events
10
+ @fired = Set.new
11
+ end
12
+
13
+ def trigger(type, payload = {})
14
+ normalized_type = type.to_sym
15
+ normalized_payload = payload.transform_keys(&:to_sym)
16
+
17
+ @events.filter_map do |event|
18
+ next if event[:once] && @fired.include?(event[:id])
19
+ next unless matches?(event[:trigger], normalized_type, normalized_payload)
20
+
21
+ @fired << event[:id] if event[:once]
22
+ event
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def matches?(trigger, type, payload)
29
+ return false unless trigger[:type] == type
30
+
31
+ trigger.all? do |key, value|
32
+ key == :type || payload[key] == value
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Termfront
4
+ module Mission
5
+ class FinalPush < Base
6
+ def name = "Final Push"
7
+ def briefing = "Storm the fortress. Maximum resistance. Good luck."
8
+
9
+ def map_data
10
+ [
11
+ "##########################",
12
+ "#............#...........#",
13
+ "#............#...........#",
14
+ "#............#...........#",
15
+ "#...........*............#",
16
+ "#............#...........#",
17
+ "#............#..####..####",
18
+ "####..########..#........#",
19
+ "#...............#........#",
20
+ "#...........*...#........#",
21
+ "#...............#........#",
22
+ "##########################"
23
+ ]
24
+ end
25
+
26
+ def spawn = [2.5, 2.5, 0.0]
27
+ def weapon_defs = [[:ar, 60], [:pistol, nil]]
28
+
29
+ def enemy_defs
30
+ [
31
+ [8.5, 2.5, 8.5, 5.5, :executor],
32
+ [4.5, 9.5, 4.5, 8.5, :crawler],
33
+ [10.5, 9.5, 10.5, 8.5, :crawler],
34
+ [18.5, 2.5, 18.5, 5.5, :executor],
35
+ [22.5, 8.5, 22.5, 10.5, :executor],
36
+ [16.5, 9.5, 16.5, 8.5, :crawler]
37
+ ]
38
+ end
39
+ end
40
+
41
+ Base.register(FinalPush)
42
+ Base.register_wavesfight(FinalPush)
43
+ end
44
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Termfront
4
+ module Mission
5
+ class Stronghold < Base
6
+ def name = "Stronghold"
7
+ def briefing = "Multi-room stronghold. Executors guard the inner rooms."
8
+
9
+ def map_data
10
+ [
11
+ "####################",
12
+ "#........#.........#",
13
+ "#........#.........#",
14
+ "#........#.........#",
15
+ "#...........*......#",
16
+ "#........#.........#",
17
+ "#........#.........#",
18
+ "####..####.........#",
19
+ "#........#.........#",
20
+ "#........#.........#",
21
+ "#................*.#",
22
+ "#........#.........#",
23
+ "#........#.........#",
24
+ "####################"
25
+ ]
26
+ end
27
+
28
+ def spawn = [2.5, 2.5, 0.0]
29
+ def weapon_defs = [[:ar, 60], [:pistol, nil]]
30
+
31
+ def enemy_defs
32
+ [
33
+ [5.5, 5.5, 5.5, 2.5, :crawler],
34
+ [14.5, 2.5, 14.5, 5.5, :executor],
35
+ [3.5, 10.5, 3.5, 12.5, :crawler],
36
+ [14.5, 9.5, 14.5, 12.5, :executor],
37
+ [10.5, 11.5, 15.5, 11.5, :crawler]
38
+ ]
39
+ end
40
+ end
41
+
42
+ Base.register(Stronghold)
43
+ Base.register_wavesfight(Stronghold)
44
+ end
45
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Termfront
4
+ module Mission
5
+ class TheGauntlet < Base
6
+ def name = "The Gauntlet"
7
+ def briefing = "A long corridor with enemies in every room."
8
+
9
+ def map_data
10
+ [
11
+ "##############################",
12
+ "#.....#.....#.....#.....#....#",
13
+ "#.....#.....#.....#.....#....#",
14
+ "#...........*...........*....#",
15
+ "#.....#.....#.....#.....#....#",
16
+ "#.....#.....#.....#.....#....#",
17
+ "#.....#.....#.....#.....#....#",
18
+ "##############################"
19
+ ]
20
+ end
21
+
22
+ def spawn = [2.5, 3.5, 0.0]
23
+ def weapon_defs = [[:ar, 60], [:pistol, nil]]
24
+
25
+ def enemy_defs
26
+ [
27
+ [4.5, 2.5, 4.5, 5.5, :crawler],
28
+ [8.5, 5.5, 8.5, 2.5, :crawler],
29
+ [14.5, 2.5, 14.5, 5.5, :crawler],
30
+ [20.5, 5.5, 20.5, 2.5, :crawler],
31
+ [26.5, 2.5, 26.5, 5.5, :crawler]
32
+ ]
33
+ end
34
+ end
35
+
36
+ Base.register(TheGauntlet)
37
+ end
38
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Termfront
4
+ module Mission
5
+ class Training < Base
6
+ def name = "Training"
7
+ def briefing = "Practice mode. Sweep the facility."
8
+
9
+ def map_data
10
+ [
11
+ "########################",
12
+ "#..........#...........#",
13
+ "#..........#...........#",
14
+ "#..........#...........#",
15
+ "#..........#...........#",
16
+ "#......................#",
17
+ "#..........####........#",
18
+ "#......................#",
19
+ "#..............#.......#",
20
+ "#..............#.......#",
21
+ "########################"
22
+ ]
23
+ end
24
+
25
+ def spawn = [10.0, 6.0, 0.0]
26
+ def weapon_defs = [[:ar, 60], [:pistol, nil]]
27
+
28
+ def enemy_defs
29
+ [
30
+ [16.5, 1.5, 16.5, 4.5, :executor],
31
+ [5.5, 8.5, 9.5, 8.5, :crawler],
32
+ [20.5, 5.5, 20.5, 9.5, :crawler],
33
+ [3.5, 2.5, 3.5, 4.5, :crawler]
34
+ ]
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Termfront
4
+ module Mission
5
+ class TrainingGrounds < Base
6
+ def name = "Training Grounds"
7
+ def briefing = "Clear a small compound. Learn the basics."
8
+
9
+ def map_data
10
+ [
11
+ "################",
12
+ "#..............#",
13
+ "#..............#",
14
+ "#......##......#",
15
+ "#......##......#",
16
+ "#..............#",
17
+ "#..............#",
18
+ "#..............#",
19
+ "#..............#",
20
+ "################"
21
+ ]
22
+ end
23
+
24
+ def spawn = [2.5, 5.0, 0.0]
25
+ def weapon_defs = [[:pistol, nil]]
26
+
27
+ def enemy_defs
28
+ [
29
+ [10.5, 3.5, 10.5, 6.5, :crawler],
30
+ [13.5, 7.5, 13.5, 2.5, :crawler]
31
+ ]
32
+ end
33
+ end
34
+
35
+ Base.register(TrainingGrounds)
36
+ end
37
+ end