dr_talon 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 (67) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +60 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +270 -0
  6. data/Rakefile +10 -0
  7. data/exe/talon +6 -0
  8. data/lib/dr_talon/cli/engine.rb +274 -0
  9. data/lib/dr_talon/cli/generate.rb +29 -0
  10. data/lib/dr_talon/cli.rb +343 -0
  11. data/lib/dr_talon/config.rb +29 -0
  12. data/lib/dr_talon/credentials.rb +94 -0
  13. data/lib/dr_talon/deps/dependency.rb +238 -0
  14. data/lib/dr_talon/deps/git.rb +69 -0
  15. data/lib/dr_talon/deps/installer.rb +207 -0
  16. data/lib/dr_talon/deps/manifest.rb +220 -0
  17. data/lib/dr_talon/deps/tree.rb +97 -0
  18. data/lib/dr_talon/deps.rb +62 -0
  19. data/lib/dr_talon/doctor.rb +161 -0
  20. data/lib/dr_talon/engine/fetchers.rb +251 -0
  21. data/lib/dr_talon/engine/git_ignore.rb +63 -0
  22. data/lib/dr_talon/engine/importer.rb +103 -0
  23. data/lib/dr_talon/engine/inspector.rb +113 -0
  24. data/lib/dr_talon/engine/installation.rb +77 -0
  25. data/lib/dr_talon/engine/materializer.rb +172 -0
  26. data/lib/dr_talon/engine/spec.rb +38 -0
  27. data/lib/dr_talon/engine/store.rb +119 -0
  28. data/lib/dr_talon/engine.rb +50 -0
  29. data/lib/dr_talon/errors.rb +5 -0
  30. data/lib/dr_talon/file_copy.rb +49 -0
  31. data/lib/dr_talon/generators/new_project.rb +289 -0
  32. data/lib/dr_talon/generators/talon_code.rb +92 -0
  33. data/lib/dr_talon/http.rb +120 -0
  34. data/lib/dr_talon/paths.rb +115 -0
  35. data/lib/dr_talon/project.rb +101 -0
  36. data/lib/dr_talon/runner.rb +188 -0
  37. data/lib/dr_talon/talon_framework.rb +103 -0
  38. data/lib/dr_talon/templates/bare/main_tests.rb.tt +7 -0
  39. data/lib/dr_talon/templates/bare/rubocop.yml.tt +8 -0
  40. data/lib/dr_talon/templates/bare/solargraph.yml.tt +24 -0
  41. data/lib/dr_talon/templates/lib/app/main.rb.tt +13 -0
  42. data/lib/dr_talon/templates/lib/lib/library.rb.tt +4 -0
  43. data/lib/dr_talon/templates/lib/lib/version.rb.tt +3 -0
  44. data/lib/dr_talon/templates/lib/rubocop.yml.tt +9 -0
  45. data/lib/dr_talon/templates/lib/solargraph.yml.tt +23 -0
  46. data/lib/dr_talon/templates/lib/test/test_helper.rb.tt +1 -0
  47. data/lib/dr_talon/templates/lib/test/tests.rb.tt +8 -0
  48. data/lib/dr_talon/templates/shared/Gemfile.tt +10 -0
  49. data/lib/dr_talon/templates/shared/README.md.tt +45 -0
  50. data/lib/dr_talon/templates/shared/editorconfig.tt +9 -0
  51. data/lib/dr_talon/templates/shared/gitignore.tt +19 -0
  52. data/lib/dr_talon/templates/talon/game/dependencies.rb.tt +3 -0
  53. data/lib/dr_talon/templates/talon/game/main.rb.tt +9 -0
  54. data/lib/dr_talon/templates/talon/game/spin.rb.tt +7 -0
  55. data/lib/dr_talon/templates/talon/game/title.rb.tt +8 -0
  56. data/lib/dr_talon/templates/talon/game/title_tests.rb.tt +16 -0
  57. data/lib/dr_talon/templates/talon/generate/component.rb.tt +19 -0
  58. data/lib/dr_talon/templates/talon/generate/component_tests.rb.tt +15 -0
  59. data/lib/dr_talon/templates/talon/generate/prefab.rb.tt +11 -0
  60. data/lib/dr_talon/templates/talon/generate/prefab_tests.rb.tt +15 -0
  61. data/lib/dr_talon/templates/talon/generate/scene.rb.tt +14 -0
  62. data/lib/dr_talon/templates/talon/generate/scene_tests.rb.tt +13 -0
  63. data/lib/dr_talon/test_report.rb +76 -0
  64. data/lib/dr_talon/version.rb +5 -0
  65. data/lib/dr_talon.rb +17 -0
  66. data/sig/dr_talon.rbs +524 -0
  67. metadata +142 -0
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DrTalon
4
+ # A DragonRuby game (or library) directory managed by talon.
5
+ #
6
+ # Two layouts are supported:
7
+ # - +:full+, DragonRuby's own: engine files at the root, game in +mygame/+
8
+ # - +:game_only+: the root is the game directory (+app/main.rb+), as used
9
+ # by libraries; only the engine binary and font sit beside it
10
+ class Project
11
+ PIN_FILE = ".dragonruby-version"
12
+
13
+ # No project at or above the working directory.
14
+ class NotFound < Error; end
15
+
16
+ # Walks up from +dir+ to the nearest directory with a {PIN_FILE}.
17
+ #
18
+ # @param dir [String]
19
+ # @return [Project, nil]
20
+ def self.find(dir = Dir.pwd)
21
+ dir = File.expand_path(dir)
22
+
23
+ loop do
24
+ return new(dir) if File.file?(File.join(dir, PIN_FILE))
25
+
26
+ parent = File.dirname(dir)
27
+ return nil if parent == dir
28
+
29
+ dir = parent
30
+ end
31
+ end
32
+
33
+ # The pinned project at or above +dir+, or else +dir+ itself when it
34
+ # looks like a DragonRuby game. Refuses other directories so engine files
35
+ # never get copied somewhere unexpected, such as a home directory.
36
+ #
37
+ # @param dir [String]
38
+ # @return [Project]
39
+ # @raise [NotFound]
40
+ def self.locate(dir = Dir.pwd)
41
+ project = find(dir) || new(File.expand_path(dir))
42
+ return project if project.layout
43
+
44
+ raise NotFound, "#{project.root} doesn't look like a DragonRuby project " \
45
+ "(no #{PIN_FILE}, mygame/app/main.rb or app/main.rb)."
46
+ end
47
+
48
+ # @return [String]
49
+ attr_reader :root
50
+
51
+ # @param root [String]
52
+ def initialize(root)
53
+ @root = root
54
+ end
55
+
56
+ # @return [Symbol, nil] +:full+, +:game_only+, or nil if neither fits
57
+ def layout
58
+ if File.file?(File.join(root, "mygame", "app", "main.rb"))
59
+ :full
60
+ elsif File.file?(File.join(root, "app", "main.rb"))
61
+ :game_only
62
+ end
63
+ end
64
+
65
+ # @return [String] the directory passed to +dragonruby+
66
+ def game_dir
67
+ (layout == :game_only) ? root : File.join(root, "mygame")
68
+ end
69
+
70
+ # @return [String]
71
+ def pin_path
72
+ File.join(root, PIN_FILE)
73
+ end
74
+
75
+ # @return [Engine::Spec, nil] the pinned engine, without a platform
76
+ # @raise [Error] if the pin file can't be parsed
77
+ def pin
78
+ return nil unless File.file?(pin_path)
79
+
80
+ line = File.readlines(pin_path, chomp: true).map(&:strip).find { |l| !l.empty? && !l.start_with?("#") }
81
+ raise Error, "#{PIN_FILE} is empty." unless line
82
+
83
+ Engine::Spec.parse(line)
84
+ end
85
+
86
+ # Pins +installation+'s version and tier. The platform is left out so the
87
+ # pin works on every developer's machine.
88
+ #
89
+ # @param installation [Engine::Installation]
90
+ # @return [void]
91
+ def pin!(installation)
92
+ File.write(pin_path, "#{installation.version}-#{installation.tier}\n")
93
+ end
94
+
95
+ # @param relative [String]
96
+ # @return [String]
97
+ def path(*relative)
98
+ File.join(root, *relative)
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DrTalon
4
+ # Builds and runs the commands of a project's installed engine:
5
+ # +dragonruby+, +dragonruby-publish+ and +dragonruby-httpd+.
6
+ class Runner
7
+ # The project doesn't have the needed engine binary installed.
8
+ class MissingBinary < Error; end
9
+
10
+ # DragonRuby writes these while running and building.
11
+ GENERATED_DIRS = %w[builds logs tmp errors].freeze
12
+
13
+ TEST_PATTERN = "*_tests.rb"
14
+ TEST_DIRS = %w[test tests].freeze
15
+
16
+ # Environment for running without a window, e.g. in CI.
17
+ HEADLESS_ENV = {"SDL_VIDEO_DRIVER" => "dummy", "SDL_AUDIO_DRIVER" => "dummy"}.freeze
18
+
19
+ # Replaces the current process. Separate so tests can stub it.
20
+ #
21
+ # @param argv [Array<String>]
22
+ # @param chdir [String]
23
+ def self.exec_process(argv, chdir:)
24
+ Process.exec(*argv, chdir: chdir)
25
+ end
26
+
27
+ # @param project [Project]
28
+ def initialize(project)
29
+ @project = project
30
+ end
31
+
32
+ # @param name [String]
33
+ # @return [String] absolute path to the binary in the project root
34
+ # @raise [MissingBinary]
35
+ def binary(name = "dragonruby")
36
+ path = [name, "#{name}.exe"].map { |candidate| @project.path(candidate) }.find { |p| File.file?(p) }
37
+ return path if path
38
+
39
+ hint =
40
+ if name != "dragonruby" && @project.layout == :game_only
41
+ "Game-only projects only get the dragonruby binary; build and serve need DragonRuby's full layout."
42
+ else
43
+ "Install the engine with `talon engine use`."
44
+ end
45
+ raise MissingBinary, "No #{name} in #{@project.root}. #{hint}"
46
+ end
47
+
48
+ # @return [String] the game directory as +dragonruby+ expects it
49
+ def game_arg
50
+ (@project.layout == :game_only) ? "." : "mygame"
51
+ end
52
+
53
+ # @param args [Array<String>] passed through, e.g. --record
54
+ # @return [Array<String>]
55
+ def run_command(args = [])
56
+ [binary, game_arg, *args]
57
+ end
58
+
59
+ # Packages every platform into builds/ without uploading.
60
+ #
61
+ # @param args [Array<String>] passed through to dragonruby-publish
62
+ # @return [Array<String>]
63
+ def build_command(args = [])
64
+ [binary("dragonruby-publish"), "--only-package", *args, game_arg]
65
+ end
66
+
67
+ # @param dir [String] directory to serve
68
+ # @param port [Integer]
69
+ # @return [Array<String>]
70
+ def serve_command(dir, port)
71
+ [binary("dragonruby-httpd"), dir, port.to_s]
72
+ end
73
+
74
+ # @param file [String] relative to the game directory
75
+ # @return [Array<String>]
76
+ def test_command(file)
77
+ [binary, game_arg, "--test", file]
78
+ end
79
+
80
+ # @return [String, nil] the newest HTML5 build, relative to the project root
81
+ def html5_build
82
+ Dir.glob("builds/*-html5-*/", base: @project.root)
83
+ .select { |dir| File.file?(@project.path(dir, "index.html")) }
84
+ .max_by { |dir| File.mtime(@project.path(dir)) }
85
+ &.chomp("/")
86
+ end
87
+
88
+ # Test files to run, relative to the game directory.
89
+ #
90
+ # @param paths [Array<String>] files or directories, relative to +base+;
91
+ # all of test/ and tests/ when empty
92
+ # @param base [String] what +paths+ are relative to (the working directory)
93
+ # @return [Array<String>]
94
+ # @raise [Error] if a path is missing or outside the game directory
95
+ def test_files(paths = [], base: Dir.pwd)
96
+ game_dir = File.expand_path(@project.game_dir)
97
+
98
+ files =
99
+ if paths.empty?
100
+ TEST_DIRS.flat_map { |dir| Dir.glob(File.join(dir, "**", TEST_PATTERN), base: game_dir) }
101
+ else
102
+ paths.flat_map { |path| expand_test_path(File.expand_path(path, base), game_dir) }
103
+ end
104
+
105
+ files.uniq.sort
106
+ end
107
+
108
+ # Runs one test file and reads the outcome from DragonRuby's output.
109
+ #
110
+ # @param file [String] relative to the game directory
111
+ # @param timeout [Numeric] seconds before the run is killed
112
+ # @param headless [Boolean] run without a window
113
+ # @return [TestReport]
114
+ def run_test(file, timeout: 120, headless: true)
115
+ env = headless ? HEADLESS_ENV.reject { |key, _| ENV.key?(key) } : {}
116
+ reader, writer = IO.pipe
117
+ # Its own process group, so a timeout can stop anything it started that
118
+ # still holds the output pipe open.
119
+ group = Gem.win_platform? ? {new_pgroup: true} : {pgroup: true}
120
+ pid = Process.spawn(env, *test_command(file), chdir: @project.root, out: writer, err: writer, in: File::NULL, **group)
121
+ writer.close
122
+ output = Thread.new { reader.read }
123
+
124
+ exited = wait(pid, timeout)
125
+ stop(pid) unless exited
126
+ report = exited ? TestReport.parse(output.value) : TestReport.timeout(output.value)
127
+ pid = nil
128
+ report
129
+ ensure
130
+ # Interrupted (e.g. Ctrl-C, which the separate process group doesn't
131
+ # receive) or failed: don't leave DragonRuby running.
132
+ stop(pid) if pid
133
+ writer&.close unless writer&.closed?
134
+ reader&.close
135
+ end
136
+
137
+ # Deletes what DragonRuby generates while running and building.
138
+ #
139
+ # @return [Array<String>] removed paths, relative to the project root
140
+ def clean
141
+ dirs = GENERATED_DIRS.dup
142
+ dirs += GENERATED_DIRS.map { |dir| "mygame/#{dir}" } if @project.layout == :full
143
+
144
+ dirs.select { |dir| File.directory?(@project.path(dir)) }.each do |dir|
145
+ FileUtils.rm_rf(@project.path(dir))
146
+ end
147
+ end
148
+
149
+ private
150
+
151
+ def expand_test_path(path, game_dir)
152
+ unless path == game_dir || path.start_with?(game_dir + File::SEPARATOR)
153
+ raise Error, "#{path} is outside the game directory (#{game_dir})."
154
+ end
155
+
156
+ relative = path.delete_prefix(game_dir).delete_prefix(File::SEPARATOR)
157
+ if File.directory?(path)
158
+ Dir.glob(File.join("**", TEST_PATTERN), base: path).map { |file| relative.empty? ? file : File.join(relative, file) }
159
+ elsif File.file?(path)
160
+ [relative]
161
+ else
162
+ raise Error, "#{path} doesn't exist."
163
+ end
164
+ end
165
+
166
+ # @return [Boolean] whether the process exited before the timeout
167
+ def wait(pid, timeout)
168
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
169
+ loop do
170
+ return true if Process.waitpid(pid, Process::WNOHANG)
171
+ return false if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
172
+
173
+ sleep 0.05
174
+ end
175
+ end
176
+
177
+ def stop(pid)
178
+ target = Gem.win_platform? ? pid : -pid
179
+ Process.kill(:TERM, target)
180
+ return if wait(pid, 5)
181
+
182
+ Process.kill(:KILL, target)
183
+ Process.waitpid(pid)
184
+ rescue Errno::ESRCH, Errno::ECHILD
185
+ nil
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DrTalon
4
+ # Support for projects built on the Talon game framework
5
+ # (https://github.com/asimmons91/talon): detecting it, and keeping the
6
+ # requires of generated scenes, components and prefabs in app/main.rb.
7
+ module TalonFramework
8
+ REPOSITORY = "asimmons91/talon"
9
+ GITHUB_KEY = %r{\Agithub:#{Regexp.escape(REPOSITORY)}(/|\z)}
10
+ # Where Talon's entry file is vendored by default.
11
+ REQUIRE = "vendor/talon/talon.rb"
12
+
13
+ BEGIN_MARKER = "# >>> talon generate: keep this block; `talon generate` adds requires to it"
14
+ END_MARKER = "# <<< talon generate"
15
+ BLOCK = /^#{Regexp.escape(BEGIN_MARKER)}\n(.*?)^#{Regexp.escape(END_MARKER)}$/m
16
+
17
+ # Load order inside the block: superclasses and helpers before the
18
+ # scenes that use them.
19
+ KIND_ORDER = %w[components prefabs scenes].freeze
20
+
21
+ # Raised when generating Talon code in a project that doesn't use it.
22
+ class NotUsed < Error; end
23
+
24
+ module_function
25
+
26
+ # The Talon entry in dependencies.rb: from its GitHub repository, named
27
+ # talon, or a local path through a talon checkout.
28
+ #
29
+ # @param project [Project]
30
+ # @return [Deps::Dependency, nil]
31
+ def dependency(project)
32
+ Deps::Manifest.for(project).dependencies.find do |dependency|
33
+ dependency.key.match?(GITHUB_KEY) ||
34
+ dependency.name == "talon" ||
35
+ (dependency.is_a?(Deps::PathDependency) && dependency.path.split("/").include?("talon"))
36
+ end
37
+ rescue Deps::ManifestError
38
+ nil
39
+ end
40
+
41
+ # @param project [Project]
42
+ # @return [Boolean] whether dependencies.rb declares Talon
43
+ def used_by?(project)
44
+ !dependency(project).nil?
45
+ end
46
+
47
+ # @param project [Project]
48
+ # @return [String] the game-relative require for Talon's entry file,
49
+ # wherever the project vendors it
50
+ def require_path(project)
51
+ found = dependency(project)
52
+ found ? "#{found.destination}/talon.rb" : REQUIRE
53
+ end
54
+
55
+ # @param project [Project]
56
+ # @return [void]
57
+ # @raise [NotUsed]
58
+ def require_used!(project)
59
+ return if used_by?(project)
60
+
61
+ raise NotUsed, "This project doesn't use Talon. Add it with `talon add github:#{REPOSITORY} --path lib`, " \
62
+ "or start from the Talon template with `talon new NAME --template talon`."
63
+ end
64
+
65
+ # Adds +line+ to the requires block in +main_rb+, keeping the block in
66
+ # {KIND_ORDER}.
67
+ #
68
+ # @param main_rb [String] path to app/main.rb
69
+ # @param line [String] e.g. require "app/scenes/title.rb"
70
+ # @return [Symbol] +:added+, +:present+ or +:no_block+ (the caller should
71
+ # tell the user to add the line themselves)
72
+ def add_require(main_rb, line)
73
+ content = File.file?(main_rb) ? File.read(main_rb) : ""
74
+ match = BLOCK.match(content)
75
+ return :no_block unless match
76
+
77
+ lines = match[1].lines(chomp: true).reject(&:empty?)
78
+ return :present if lines.include?(line)
79
+
80
+ # Other lines keep their order at the top; generated ones follow,
81
+ # grouped by kind and sorted by name.
82
+ sorted = (lines + [line]).each_with_index.sort_by do |entry, index|
83
+ kind = entry[%r{"app/([^/]+)/}, 1]
84
+ kind ? [KIND_ORDER.index(kind) || KIND_ORDER.size, entry] : [-1, index]
85
+ end.map(&:first)
86
+ block = [BEGIN_MARKER, *sorted, END_MARKER].join("\n")
87
+ File.write(main_rb, content.sub(BLOCK) { block })
88
+ :added
89
+ end
90
+
91
+ # @param name [String] e.g. "main_menu", "MainMenu", "main-menu"
92
+ # @return [Array(String, String)] the class name and file name
93
+ # @raise [Error] for names that aren't a plain identifier
94
+ def names(name)
95
+ unless name.match?(/\A[A-Za-z][A-Za-z0-9_-]*\z/)
96
+ raise Error, "Invalid name \"#{name}\". Use letters, digits, - and _, starting with a letter (e.g. MainMenu)."
97
+ end
98
+
99
+ words = name.gsub(/([a-z\d])([A-Z])/, '\1_\2').split(/[-_]/).reject(&:empty?)
100
+ [words.map { |word| word[0].upcase + word[1..] }.join, words.map(&:downcase).join("_")]
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,7 @@
1
+ # DragonRuby runs each method named test_* that takes (args, assert).
2
+ # Run them with `talon test`; add files named *_tests.rb to this directory.
3
+ class MainTests
4
+ def test_tests_get_dragonruby_args(args, assert)
5
+ assert.true! args.respond_to?(:outputs), "tests receive the same args as tick"
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ inherit_gem:
2
+ rubocop-rails-omakase: rubocop.yml
3
+
4
+ AllCops:
5
+ Include:
6
+ - "mygame/**/*.rb"
7
+ Exclude:
8
+ - "mygame/vendor/**/*"
@@ -0,0 +1,24 @@
1
+ ---
2
+ include:
3
+ - "mygame/**/*.rb"
4
+ exclude:
5
+ - "mygame/tmp/**/*"
6
+ require: []
7
+ domains: []
8
+ reporters:
9
+ - rubocop
10
+ formatter:
11
+ rubocop:
12
+ cops: safe
13
+ except: []
14
+ only: []
15
+ extra_args: []
16
+ type_checker:
17
+ rules: {}
18
+ require_paths: []
19
+ plugins:
20
+ - solargraph-dragonruby
21
+ <% if talon? -%>
22
+ - solargraph-dr_talon
23
+ <% end -%>
24
+ max_files: 5000
@@ -0,0 +1,13 @@
1
+ require "lib/<%= file_name %>.rb"
2
+
3
+ # A small demo of the library; the game DragonRuby runs from this directory.
4
+ def tick(args)
5
+ args.outputs.labels << {
6
+ x: 640,
7
+ y: 360,
8
+ text: "<%= module_name %> #{<%= module_name %>::VERSION}",
9
+ size_px: 30,
10
+ anchor_x: 0.5,
11
+ anchor_y: 0.5
12
+ }
13
+ end
@@ -0,0 +1,4 @@
1
+ require_relative "<%= file_name %>/version"
2
+
3
+ module <%= module_name %>
4
+ end
@@ -0,0 +1,3 @@
1
+ module <%= module_name %>
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,9 @@
1
+ inherit_gem:
2
+ rubocop-rails-omakase: rubocop.yml
3
+
4
+ AllCops:
5
+ Include:
6
+ - "**/*.rb"
7
+ Exclude:
8
+ - "vendor/**/*"
9
+ - "tmp/**/*"
@@ -0,0 +1,23 @@
1
+ ---
2
+ include:
3
+ - "app/**/*.rb"
4
+ - "lib/**/*.rb"
5
+ exclude:
6
+ - "test/**/*"
7
+ - "vendor/**/*"
8
+ require: []
9
+ domains: []
10
+ reporters:
11
+ - rubocop
12
+ formatter:
13
+ rubocop:
14
+ cops: safe
15
+ except: []
16
+ only: []
17
+ extra_args: []
18
+ type_checker:
19
+ rules: {}
20
+ require_paths: []
21
+ plugins:
22
+ - solargraph-dragonruby
23
+ max_files: 5000
@@ -0,0 +1 @@
1
+ require "lib/<%= file_name %>.rb"
@@ -0,0 +1,8 @@
1
+ require "test/test_helper.rb"
2
+
3
+ # Run with: talon test
4
+ class <%= module_name %>Tests
5
+ def test_has_a_version(args, assert)
6
+ assert.true! <%= module_name %>::VERSION.is_a?(String), "VERSION should be a String"
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Editor and lint tooling only. DragonRuby doesn't load gems; game
4
+ # dependencies go in <%= dependencies_hint %>.
5
+ gem "rubocop-rails-omakase", require: false
6
+ gem "solargraph", require: false
7
+ gem "solargraph-dragonruby", require: false
8
+ <% if talon? -%>
9
+ gem "solargraph-dr_talon", require: false
10
+ <% end -%>
@@ -0,0 +1,45 @@
1
+ # <%= title %>
2
+
3
+ <% if talon? -%>
4
+ A [DragonRuby](https://dragonruby.org) game built on [Talon](https://github.com/<%= talon_repository %>).
5
+ <% elsif layout == :full -%>
6
+ A [DragonRuby](https://dragonruby.org) game.
7
+ <% else -%>
8
+ A [DragonRuby](https://dragonruby.org) library.
9
+ <% end -%>
10
+
11
+ ## Setup
12
+
13
+ <% if commit_engine? -%>
14
+ The DragonRuby engine (<%= engine.version %> <%= engine.tier %>) is committed with the project.
15
+ <% else -%>
16
+ The project pins DragonRuby <%= engine.version %> (<%= engine.tier %>) in `.dragonruby-version`; the engine itself isn't committed. Install it with [talon](https://github.com/asimmons91/dr_talon):
17
+
18
+ ```bash
19
+ talon engine import path/to/dragonruby-<%= engine.tier %>-<%= engine.platform %>.zip # once per machine
20
+ talon engine use
21
+ ```
22
+ <% end -%>
23
+
24
+ Editor tooling (RuboCop, Solargraph):
25
+
26
+ ```bash
27
+ bundle install
28
+ ```
29
+
30
+ ## Running
31
+
32
+ ```bash
33
+ <% if layout == :full -%>
34
+ talon run # ./dragonruby mygame
35
+ talon test # *_tests.rb in mygame/tests/
36
+ talon build # package every platform into builds/
37
+ talon serve # play the HTML5 build at http://localhost:8080/
38
+ <% if talon? -%>
39
+ talon generate scene|component|prefab NAME
40
+ <% end -%>
41
+ <% else -%>
42
+ talon run # ./dragonruby .
43
+ talon test # ./dragonruby . --test for each test/*_tests.rb
44
+ <% end -%>
45
+ ```
@@ -0,0 +1,9 @@
1
+ root = true
2
+
3
+ [*]
4
+ end_of_line = lf
5
+ charset = utf-8
6
+ indent_style = space
7
+ indent_size = 2
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
@@ -0,0 +1,19 @@
1
+ # DragonRuby
2
+ /builds/
3
+ /logs/
4
+ /tmp/
5
+ /errors/
6
+ <% if layout == :full -%>
7
+ /mygame/errors/
8
+ <% end -%>
9
+
10
+ # Ruby tooling
11
+ /.bundle/
12
+
13
+ # Editors
14
+ .vscode/*
15
+ !.vscode/settings.json
16
+ !.vscode/tasks.json
17
+ !.vscode/launch.json
18
+ !.vscode/extensions.json
19
+ !.vscode/*.code-snippets
@@ -0,0 +1,3 @@
1
+ # DragonRuby dependencies, vendored into vendor/ by `talon install`.
2
+ # Add more with `talon add github:owner/repo`.
3
+ github "<%= talon_repository %>", path: "lib"
@@ -0,0 +1,9 @@
1
+ require "<%= talon_require %>"
2
+
3
+ <%= requires_block %>
4
+
5
+ def tick(args)
6
+ $game ||= Talon::Game.new(Title.new)
7
+ $game.args = args
8
+ $game.tick
9
+ end
@@ -0,0 +1,7 @@
1
+ # Turns its game object and draws it as a sprite.
2
+ class Spin < Talon::Component
3
+ prop :speed, default: 1
4
+
5
+ on_update { go.angle = (go.angle || 0) + speed }
6
+ on_draw { outputs.sprites << go }
7
+ end
@@ -0,0 +1,8 @@
1
+ # The first scene. Add more with `talon generate scene NAME` and switch with
2
+ # game.change_scene(OtherScene.new)
3
+ class Title < Talon::Scene
4
+ on_enter do
5
+ spawn(:logo, x: 640, y: 360, w: 128, h: 128, anchor_x: 0.5, anchor_y: 0.5,
6
+ path: "sprites/square/blue.png") { |go| go.add(Spin) }
7
+ end
8
+ end
@@ -0,0 +1,16 @@
1
+ require "<%= talon_require %>"
2
+ require "app/components/spin.rb"
3
+ require "app/scenes/title.rb"
4
+
5
+ class TitleTests
6
+ def test_title_spins_the_logo(args, assert)
7
+ scene = Title.new
8
+ game = Talon::Game.new(scene)
9
+ game.args = args
10
+ 3.times { game.tick }
11
+
12
+ logo = scene.find { |go| go.name == :logo }
13
+ assert.true! logo, "the title scene should spawn a logo"
14
+ assert.equal! logo.angle, 3
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ # A component: behaviour attached to a game object with go.add(<%= class_name %>).
2
+ class <%= class_name %> < Talon::Component
3
+ # Props are plain attributes set from go.add(<%= class_name %>, name: value).
4
+ # prop :speed, default: 1
5
+
6
+ # State lives in args.state, so it survives hot reloads.
7
+ # state :count, default: 0
8
+
9
+ on_start do
10
+ end
11
+
12
+ on_update do
13
+ # Runs every tick. go is the game object; args, inputs and outputs are available.
14
+ end
15
+
16
+ on_draw do
17
+ # Runs every tick after all updates, e.g. outputs.sprites << go
18
+ end
19
+ end