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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +60 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +270 -0
- data/Rakefile +10 -0
- data/exe/talon +6 -0
- data/lib/dr_talon/cli/engine.rb +274 -0
- data/lib/dr_talon/cli/generate.rb +29 -0
- data/lib/dr_talon/cli.rb +343 -0
- data/lib/dr_talon/config.rb +29 -0
- data/lib/dr_talon/credentials.rb +94 -0
- data/lib/dr_talon/deps/dependency.rb +238 -0
- data/lib/dr_talon/deps/git.rb +69 -0
- data/lib/dr_talon/deps/installer.rb +207 -0
- data/lib/dr_talon/deps/manifest.rb +220 -0
- data/lib/dr_talon/deps/tree.rb +97 -0
- data/lib/dr_talon/deps.rb +62 -0
- data/lib/dr_talon/doctor.rb +161 -0
- data/lib/dr_talon/engine/fetchers.rb +251 -0
- data/lib/dr_talon/engine/git_ignore.rb +63 -0
- data/lib/dr_talon/engine/importer.rb +103 -0
- data/lib/dr_talon/engine/inspector.rb +113 -0
- data/lib/dr_talon/engine/installation.rb +77 -0
- data/lib/dr_talon/engine/materializer.rb +172 -0
- data/lib/dr_talon/engine/spec.rb +38 -0
- data/lib/dr_talon/engine/store.rb +119 -0
- data/lib/dr_talon/engine.rb +50 -0
- data/lib/dr_talon/errors.rb +5 -0
- data/lib/dr_talon/file_copy.rb +49 -0
- data/lib/dr_talon/generators/new_project.rb +289 -0
- data/lib/dr_talon/generators/talon_code.rb +92 -0
- data/lib/dr_talon/http.rb +120 -0
- data/lib/dr_talon/paths.rb +115 -0
- data/lib/dr_talon/project.rb +101 -0
- data/lib/dr_talon/runner.rb +188 -0
- data/lib/dr_talon/talon_framework.rb +103 -0
- data/lib/dr_talon/templates/bare/main_tests.rb.tt +7 -0
- data/lib/dr_talon/templates/bare/rubocop.yml.tt +8 -0
- data/lib/dr_talon/templates/bare/solargraph.yml.tt +24 -0
- data/lib/dr_talon/templates/lib/app/main.rb.tt +13 -0
- data/lib/dr_talon/templates/lib/lib/library.rb.tt +4 -0
- data/lib/dr_talon/templates/lib/lib/version.rb.tt +3 -0
- data/lib/dr_talon/templates/lib/rubocop.yml.tt +9 -0
- data/lib/dr_talon/templates/lib/solargraph.yml.tt +23 -0
- data/lib/dr_talon/templates/lib/test/test_helper.rb.tt +1 -0
- data/lib/dr_talon/templates/lib/test/tests.rb.tt +8 -0
- data/lib/dr_talon/templates/shared/Gemfile.tt +10 -0
- data/lib/dr_talon/templates/shared/README.md.tt +45 -0
- data/lib/dr_talon/templates/shared/editorconfig.tt +9 -0
- data/lib/dr_talon/templates/shared/gitignore.tt +19 -0
- data/lib/dr_talon/templates/talon/game/dependencies.rb.tt +3 -0
- data/lib/dr_talon/templates/talon/game/main.rb.tt +9 -0
- data/lib/dr_talon/templates/talon/game/spin.rb.tt +7 -0
- data/lib/dr_talon/templates/talon/game/title.rb.tt +8 -0
- data/lib/dr_talon/templates/talon/game/title_tests.rb.tt +16 -0
- data/lib/dr_talon/templates/talon/generate/component.rb.tt +19 -0
- data/lib/dr_talon/templates/talon/generate/component_tests.rb.tt +15 -0
- data/lib/dr_talon/templates/talon/generate/prefab.rb.tt +11 -0
- data/lib/dr_talon/templates/talon/generate/prefab_tests.rb.tt +15 -0
- data/lib/dr_talon/templates/talon/generate/scene.rb.tt +14 -0
- data/lib/dr_talon/templates/talon/generate/scene_tests.rb.tt +13 -0
- data/lib/dr_talon/test_report.rb +76 -0
- data/lib/dr_talon/version.rb +5 -0
- data/lib/dr_talon.rb +17 -0
- data/sig/dr_talon.rbs +524 -0
- 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,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,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,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,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,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
|