Initial commit

This commit is contained in:
Gordon Pedersen 2023-03-08 14:06:29 +11:00 committed by GitHub
commit 870a4c5ad8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 1753 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
settings-debug.txt

108
README.md Normal file
View file

@ -0,0 +1,108 @@
# Scale
![repeating red dragon scale pixel art](https://user-images.githubusercontent.com/928367/204090457-0d096cbe-21cc-4753-9c63-f7786d165cfa.png)
**Simple DragonRuby Game Toolkit Game Starter Template**
Status: usable but not yet stable! This is pre-v1 software.
Quickly start a new DragonRuby Game Toolkit game with useful conventions and helpful extensions.
Looking for a simpler version? Check out [the `simple` release](https://github.com/DragonRidersUnite/scale/releases/tag/simple) that's just `app/main.rb` with some constants and helper methods.
[Check out the CHANGELOG for the summary of recent changes!](https://github.com/DragonRidersUnite/scale/wiki/CHANGELOG)
## Bugs / Features
Last tested against DragonRuby Game Toolkit v4.3.
- Functional approach to the code, namespaced in modules
- Use the DragonRuby GTK methods and data structures you know and love
- Driven by `args.state`
- Menus and pause screen
- Sensible default controls
- Defined location for where to put scenes
- Settings that persist to disk
- Displays framerate in the upper-right hand corner of the game when running in development mode
- `#debug?` helper to easily check if the game is running in development mode; useful for custom commands
- `#mobile?` to easily check when on mobile and <kbd>M</kbd> to simulate mobile
- Reload all sprites in development using the `i` key
- Reset the game with `r` key, calls `$gtk.reset`
- Put all debug-only code in `#debug_tick`
- `#init` method that gets run once on game boot
- `#version` to get the version of your game
- Constants for various values and enums: `FPS`, `BLEND_*`, `ALIGN_*`
- Tests for the methods!
- See more in [SCALE_DOCS.md](./SCALE_DOCS.md)
## Use It
There are two main ways you can use the Scale template for your games.
[📺 Video demo showing how to get started](https://www.youtube.com/watch?v=eek3a3aO-zo)
### Download the Zip
The fastest way to get started is to download the template zip file and put it into your unzipped DragonRuby Game Toolkit folder.
1. Download and unzip the DragonRuby Game Toolkit engine zip
2. Delete the `mygame` directory
3. [Download Scale](https://github.com/DragonRidersUnite/scale/archive/refs/heads/main.zip)
4. Unzip the `scale-main.zip`
5. Move the `scale-main` folder into the DRGTK folder
6. Rename `scale-main` to `mygame`
7. Start DragonRuby, and make an awesome game!
### Use GitHub's Template System
If you're going to track your game with Git and use GitHub, the baked-in template system will get you going quickly.
1. View the project on GitHub: https://github.com/DragonRidersUnite/scale
2. Click "Use this template"
3. Click "Create a new repository"
4. Fill out the details and create the repository
5. Unzip the DragonRuby Game Toolkit engine zip
6. Delete the `mygame` directory
7. Clone your new repository into the DRGTK engine folder with the folder name `mygame`, example: `git clone git@github.com:USERNAME/REPO.git mygame`
7. Start DragonRuby, and make an awesome game!
### Updating
Because Scale is a template with all of its source as part of your game, updating the framework's source code in your game isn't an easy thing to do.
I generally would say: don't worry about it! Take ownership over the code in your game and change what Scale provides you without concern. When it comes time to start your next game, Scale will be updated and improved.
But if you do find yourself wanting to keep it updated, [watch the GitHub repo](https://github.com/DragonRidersUnite/scale) for releases. You could pull in just the changes you want. Or you could set an upstream in your repo to the template and merge the changes in.
## Documentation
Every game that uses Scale comes with the [SCALE_DOCS.md](./SCALE_DOCS.md) file. Read through that document to find a quickstart guide and information about useful methods.
## Release Approach & Versioning
Code on the `main` branch is intended to be stable because Scale is a template. When significant changes have been made, a tag and release are created. This allows progress to be tracked and previous versions to be easily downloaded.
Scale uses a simplified major.minor versioning scheme. Major version bumps means there are breaking changes to the API (the methods and structure). Minor bumps mean non-breaking additions and fixes.
## Template License
The template source code falls under the [Unlicense](https://unlicense.org/), meaning it is dedicated to the public domain and you are free to do with it what you want.
## Contribute
Conributions are welcome!
Open an issue or submit PRs if you notice something isn't working.
If you find yourself adding the same files, methods, constants, etc. to your DRGTK games, submit a PR to add it to Scale.
---
[Clear out what's above in this README out and add your own details for making your game!]
## Debug Shortcuts
- <kbd>0</kbd> — display debug details (ex: framerate)
- <kbd>i</kbd> — reload sprites from disk
- <kbd>r</kbd> — reset the entire game state
- <kbd>m</kbd> — toggle mobile simulation

298
SCALE_DOCS.md Normal file
View file

@ -0,0 +1,298 @@
# Scale Docs
Everything you need to get started with using [Scale](https://github.com/DragonRidersUnite/scale).
[Check out the CHANGELOG for the summary of recent changes!](https://github.com/DragonRidersUnite/scale/wiki/CHANGELOG)
## Quickstart
Once you've got your game from the Scale template dropped into your `mygame` directory in your DragonRuby GTK game, here's what you'll want to do next.
### Configure
Start by specifying your name and game title in `metadata/game_metadata.txt`. The following properties are useful:
- `devid` — your itch.io username, no spaces
- `devtitle` — your name, spaces are okay
- `gameid` — the URL slug of your game on itch.io that you must already create, no spaces
- `gametitle` — the name of your game, spaces are okay!
Now when you start the DragonRuby engine for your game, you'll see what you've set.
### Lay of the Land
Because all of Scale is included with your new game, you can just browse through the source code to get the lay of the land. You'll notice in the `app/` directory a bunch of files! It's okay, don't worry. It's pretty sensibly organized, and there are plenty of comments.
Explore, be curious, and change things to suit your needs.
Scale is structured to follow the default DragonRuby GTK way of working with methods and `args.state`. That drives how much of it works. Most of Scale is organized into modules so that there aren't method name conflicts in the global space.
### Gameplay
Open up `app/scenes/gameplay.rb`, as that's where you'll actually add the fun parts of your game. `Scene.tick_gameplay` is just like the regular ole `#tick` in DragonRuby GTK. It takes args, and you go from there. It does include some handy stuff though, but just let that be.
You'll code your game just as you normally would. If you're new to DragonRuby GTK, [check out the book](https://book.dragonriders.community/) to get started.
### Input
Scale comes with some helper methods to make checking for input from multiple sources (multiple keyboard keys and gamepad buttons) easy. That code lives in `app/input.rb`, and out of the box, you get:
- `primary_down?` — check if J, Z, Space, or Gamepad A button was just pressed
- `primary_down_or_held?` — check if J, Z, Space, or Gamepad A button was just pressed or is being held
- `secondary_down?` — check if K, X, Backspace, or Gamepad B button was just pressed
- `secondary_down_or_held?` — check if K, X, Backspace, or Gamepad B button was just pressed or is being held
- `pause_down?` — check if Escape, P, or Gamepad Start button was just pressed, which pauses the game
You could add more methods for various inputs in your game. Maybe there's a secondary button you use. Or any number of them! These input methods make it really easy to support various input methods without worrying about the keys/buttons that are being pressed. Want to add a new key for a given layout? Just change it.
Here's an example of how you'd use it if you wanted to have your player swing their sword:
``` ruby
if primary_down?(args.inputs)
swing_sword(args, args.state.player)
end
```
### Text
You'll very likely need to display text in your game to show health or the player's level or to give instruction. It's a fundamental part of game user interfaces. `app/text.rb` contains a few helpful constructs to make working with text easier.
First is the `TEXT` Hash constant. It contains key values of the text to display in your game. This is preferable to putting strings everywhere because you can more easily review the text for typos and eventually translate your game much more easily. It takes a little time to get used to putting your text here first, but it'll become second nature quick enough. You'll see text for what already exists in Scale.
`#text` is a method you call to access the values in `TEXT`. Example:
``` ruby
text(:start)
```
returns `"Start"`.
Then we've got the `#label` method. It contains sensible defaults for outputting labels quickly with DragonRuby GTK. It's got a friendlier API than the default data structure in DRGTK. There are multiple ways to use it.
In its simplest form, you can pass in a symbol key for `TEXT` and specify the position to render it:
``` ruby
args.outputs.labels << label(:start, x: 100, y: 100)
```
You can also pass in a value, like a number that will display:
``` ruby
args.outputs.labels << label(args.state.player.level, x: 100, y: 100)
```
Additional parameters are supported too, all the way up to something like this:
``` ruby
args.outputs.labels << label(
:restart, x: 100, y: 100, align: ALIGN_CENTER,
size: SIZE_SM, color: RED, font: FONT_ITALIC
)
```
In `app/text.rb` you'll find the constants for text size and the various fonts. Change them as you'd like!
### Sprites
A lot of games need sprites, and when making them, it's so helpful to see them in the context of your game. DragonRuby GTK allows you to reload sprites in your game, but you need to specify which ones to reload. To get around this, in Scale you track your sprites in `app/sprite.rb`'s `SPRITES` hash. It's a little tedious, but by having a dictionary of sprites, they can easily be reloaded with <kbd>i</kbd> when developing your game.
Get the path for your sprite with `Sprite.for(:key)`:
``` ruby
args.outputs.sprites << {
x: 100, y: 100, w: 32, h: 32, path: Sprite.for(:player),
}
```
### Scenes
The `app/scenes/` directory is where different scenes of your game go. There are scenes for Paused, Main Menu, Settings, and Gameplay. Scale uses `args.state.scene` to know which scene the game is in.
You can switch scenes by using `Scene.switch(args, :gameplay)` where the parameter is the symbol of the scene you want. It must match the name of your scene method after the `tick_` prefix. So if you wanted a `:credits` scene, you'd define it like this in `app/scenes/credits.rb`:
``` ruby
module Scene
class << self
def tick_credits(args)
args.outputs.labels << label(:credits, x: args.grid.w / 2, y: args.grid.top - 200, align: ALIGN_CENTER, size: SIZE_LG, font: FONT_BOLD)
end
end
end
```
(Don't forget to require it in `app/main.rb` too!)
Then when you want to show the Credits scene, call `Scene.switch(args, :credits)`.
### Collision Detection
Scale provides you with a collision method that executes the passed in block for each element that intersects:
``` ruby
collide(tiles, bullets) do |tile, bullet|
bullet.dead = true
end
```
It takes arrays or single objects as parameters.
### Menus
You'll find examples of menus throughout the Scale codebase for common scenes like Settings and the Main Menu.
Menus support cursor-based navigation or buttons for mouse/touch.
How they work is pretty simple, you just call `Menu.tick` and pass in an array of menu options.
``` ruby
options = [
{
key: :attack,
on_select: -> (args) do
# do the attack
end
},
{
key: :defend,
on_select: -> (args) do
# defend
end
},
]
Menu.tick(args, :your_menu_name, options)
```
The options array is just a collection of hashes with a key and an `on_select` lambda that gets called with `args` for doing anything in your game that you need to have happen.
`Menu.tick` also supports an optional `menu_y` for positioning the menu.
### Colors
A simple color palette is provided in `app/constants.rb`. Most of the primary colors are present, along with some variants. They return Hashes with `r`, `g`, and `b` keys. If you have a sprite that you want to change red, you'd do this:
``` ruby
args.outputs.sprites << {
x: 100, y: 100, w: 32, h: 32, path: Sprite.for(:player)
}.merge(RED)
```
### Sound Effects & Music
Scale comes with a few helper methods to make playing music and sound effects easy, as well as settings to enable or disable their playback.
Play a sound effect:
``` ruby
play_sfx(args, :enemy_hit)
```
the symbol (or string) file key for the file must correspond to the file's name in `sounds/`.
Here's how to play music:
``` ruby
play_music(args, :menu)
```
the symbol (or string) file key for the file must correspond to the file's name in `sounds/`.
You can pause and resume music easily with:
``` ruby
pause_music(args)
resume_music(args)
```
Scale assumes only one music track can be playing at a time.
### Adding New Files
When you add new code files to `app/`, just be sure to require them in `app/main.rb`.
### Debug Tick & Development Shortcuts
When you're making a game, you often want to have easy shortcuts to toggle settings. Maybe you want to make your player invincible so you can easily test changes out. `app/tick.rb` contains a method called `#debug_tick` that's only called when you're game is in debug mode (a.k.a. not the version shipped to players). Anything you put in there will run every tick but only while you make the game.
You'll see there are already three shortcuts Scale gives you:
- <kbd>i</kbd> — reloads sprites from disk
- <kbd>r</kbd> — resets game state
- <kbd>0</kbd> — displays framerate and other debug details
Use those as templates to add your own development shortcuts.
You can check for Debug mode in your game anywhere with the `debug?` method.
### `#debug_label` Method
It's common to need to display debug-only information about entities in your game. Maybe you want to see a value that changes over time. This is what `#debug_label` is for. It putputs text that's shown when you toggle on debug details with the <kbd>0</kbd> key.
Here's an example of how to use it to track the player's current coordinates:
``` ruby
# assume we have args.state.player that can move
player = args.state.player
debug_label(args, player.x, player.y - 4, "#{player.x}, #{player.y}")
```
### Tests
The `test/tests.rb` is where you can put tests for your game. It also includes tests for methods provided by Scale. Tests are powered by [DragonTest](https://github.com/DragonRidersUnite/dragon_test), a simple testing library for DragonRuby GTK. You'll see plenty of examples in there, but here's a quick overview:
Write tests:
``` ruby
test :text_for_setting_val do |args, assert|
it "returns proper text for boolean vals" do
assert.equal!(text_for_setting_val(true), "ON")
assert.equal!(text_for_setting_val(false), "OFF")
end
it "passes the value through when not a boolean" do
assert.equal!(text_for_setting_val("other"), "other")
end
end
```
You've got these assertions:
- `assert.true!` - whether or not what's passed in is truthy, ex: `assert.true!(5 + 5 == 10)`
- `assert.false!` - whether or not what's passed in is falsey, ex: `assert.false!(5 + 5 != 10)`
- `assert.equal!` - whether or not what's passed into param 1 is equal to param 2, ex: `assert.equal!(5, 2 + 3)`
- `assert.exception!` - expect the called code to raise an error with optional message, ex: `assert.exception!(KeyError, "Key not found: :not_present") { text(args, :not_present) }`
- `assert.includes!` - whether or not the array includes the value, ex: `assert.includes!([1, 2, 3], 2)`
Run your tests with: `./run_tests` — test runner script for Unix-like environments with a shell that has proper exit codes on success and fail
Your tests will also run when you save test files and be output to your running game's logs. Nifty!
## Debug Shortcuts
You'll find these in the README, too. Scale comes with some handy keyboard shortcuts that only run in debug mode to make building your game easier.
- <kbd>0</kbd> — display debug details (ex: framerate)
- <kbd>i</kbd> — reload sprites from disk
- <kbd>r</kbd> — reset the entire game state
- <kbd>m</kbd> — toggle mobile simulation
## Mobile Development
Use the `#mobile?` method to check to add logic specifically for mobile devices. Press <kbd>m</kbd> to simulate this on desktop so you can easily check how your game will look on those platforms.
Example:
``` ruby
text_key = mobile? ? :instructions_mobile : :instructions
```
There are convenient methods and tracking for swipe inputs on touch devices. Scale automatically keeps track of them, and if you use the `up?(args)`, `down?(args)`, `left?(args)`, `right?(args)` methods, they're automatically checked. Otherwise, you can check to see if a swipe occurred with:
``` ruby
if args.state.swipe.up
# do the thing
end
```
## Make Scale Yours!
This is your game now. Scale is just here to help you out. Change Scale to meet your game's needs.

41
app/constants.rb Normal file
View file

@ -0,0 +1,41 @@
FPS = 60
# for use with label alignment_enum
ALIGN_LEFT = 0
ALIGN_CENTER = 1
ALIGN_RIGHT = 2
# for use with blendmode_enum
BLEND_NONE = 0
BLEND_ALPHA = 1
BLEND_ADDITIVE = 2
BLEND_MODULO = 3
BLEND_MULTIPLY = 4
# A basic color palette; customize for your needs!
# Use with `#merge!` into your Hash data structures or pass as a parameter into
# methods that support color
TRUE_BLACK = { r: 0, g: 0, b: 0 }
BLACK = { r: 25, g: 25, b: 25 }
GRAY = { r: 157, g: 157, b: 157 }
WHITE = { r: 255, g: 255, b: 255 }
BLUE = { r: 42, g: 133, b: 216 }
GREEN = { r: 42, g: 216, b: 78 }
ORANGE = { r: 255, g: 173, b: 31 }
PINK = { r: 245, g: 146, b: 198 }
PURPLE = { r: 133, g: 42, b: 216 }
RED = { r: 231, g: 89, b: 82 }
YELLOW = { r: 240, g: 232, b: 89 }
DARK_BLUE = { r: 22, g: 122, b: 188 }
DARK_GREEN = { r: 5, g: 84, b: 12 }
DARK_PURPLE = { r: 66, g: 12, b: 109 }
DARK_PURPLE = { r: 103, g: 5, b: 98 }
DARK_RED = { r: 214, g: 26, b: 12 }
DARK_YELLOW = { r: 120, g: 97, b: 7 }
DIR_DOWN = :down
DIR_UP = :up
DIR_LEFT = :left
DIR_RIGHT = :right

58
app/game_setting.rb Normal file
View file

@ -0,0 +1,58 @@
# different than the Settings scene, this module contains methods for things
# like fullscreen on/off, sfx on/off, etc.
module GameSetting
class << self
# returns a string of a hash of settings in the following format:
# key1=val1,key2=val2
# `settings` should be a hash of keys and vals to be saved
def settings_for_save(settings)
settings.map do |k, v|
"#{k}:#{v}"
end.join(",")
end
# we don't want to accidentally ship our debug preferences to our players
def settings_file
"settings#{ debug? ? '-debug' : nil}.txt"
end
# useful when wanting to save settings after the code in the block is
# executed, ex: `GameSetting.save_after(args) { |args| args.state.setting.big_head_mode = true }
def save_after(args)
yield(args)
save_settings(args)
end
# loads settings from disk and puts them into `args.state.setting`
def load_settings(args)
settings = args.gtk.read_file(settings_file)&.chomp
if settings
settings.split(",").map { |s| s.split(":") }.to_h.each do |k, v|
if v == "true"
v = true
elsif v == "false"
v = false
end
args.state.setting[k.to_sym] = v
end
else
args.state.setting.sfx = true
args.state.setting.music = true
args.state.setting.fullscreen = false
end
if args.state.setting.fullscreen
args.gtk.set_window_fullscreen(args.state.setting.fullscreen)
end
end
# saves settings from `args.state.setting` to disk
def save_settings(args)
args.gtk.write_file(
settings_file,
settings_for_save(open_entity_to_hash(args.state.setting))
)
end
end
end

110
app/input.rb Normal file
View file

@ -0,0 +1,110 @@
# efficient input helpers that all take `args.inputs`
PRIMARY_KEYS = [:j, :z, :space]
def primary_down?(inputs)
PRIMARY_KEYS.any? { |k| inputs.keyboard.key_down.send(k) } ||
inputs.controller_one.key_down&.a
end
def primary_down_or_held?(inputs)
primary_down?(inputs) ||
PRIMARY_KEYS.any? { |k| inputs.keyboard.key_held.send(k) } ||
(inputs.controller_one.connected &&
inputs.controller_one.key_held.a)
end
SECONDARY_KEYS = [:k, :x, :backspace]
def secondary_down?(inputs)
SECONDARY_KEYS.any? { |k| inputs.keyboard.key_down.send(k) } ||
(inputs.controller_one.connected &&
inputs.controller_one.key_down.b)
end
def secondary_down_or_held?(inputs)
secondary_down?(inputs) ||
SECONDARY_KEYS.any? { |k| inputs.keyboard.key_held.send(k) } ||
(inputs.controller_one.connected &&
inputs.controller_one.key_held.b)
end
PAUSE_KEYS= [:escape, :p]
def pause_down?(inputs)
PAUSE_KEYS.any? { |k| inputs.keyboard.key_down.send(k) } ||
inputs.controller_one.key_down&.start
end
# check for arrow keys, WASD, gamepad, and swipe up
def up?(args)
args.inputs.up || args.state.swipe.up
end
# check for arrow keys, WASD, gamepad, and swipe down
def down?(args)
args.inputs.down || args.state.swipe.down
end
# check for arrow keys, WASD, gamepad, and swipe left
def left?(args)
args.inputs.left || args.state.swipe.left
end
# check for arrow keys, WASD, gamepad, and swipe right
def right?(args)
args.inputs.right || args.state.swipe.right
end
# called by the main #tick method to keep track of swipes, you likely don't
# need to call this yourself
#
# to check for swipes outside of the directional methods above, use it like
# this:
#
# if args.state.swipe.up
# # do the thing
# end
#
def track_swipe(args)
return unless mobile?
reset_swipe(args) if args.state.swipe.nil? || args.state.swipe.stop_tick
swipe = args.state.swipe
if args.inputs.mouse.down
swipe.merge!({
start_tick: args.state.tick_count,
start_x: args.inputs.mouse.x,
start_y: args.inputs.mouse.y,
})
end
if swipe.start_tick && swipe.start_x && swipe.start_y
p1 = [swipe.start_x, swipe.start_y]
p2 = [args.inputs.mouse.x, args.inputs.mouse.y]
dist = args.geometry.distance(p1, p2)
if dist > 50 # min distance threshold
swipe.merge!({
stop_x: p2[0],
stop_y: p2[1],
})
angle = args.geometry.angle_from(p1, p2)
swipe.angle = angle
swipe.dist = dist
swipe.stop_tick = args.state.tick_count
if angle > 315 || swipe.angle < 45
swipe.left = true
elsif angle >= 45 && angle <= 135
swipe.down = true
elsif angle > 135 && angle < 225
swipe.right = true
elsif angle >= 225 && angle <= 315
swipe.up = true
end
end
end
end
# reset the currently tracked swipe
def reset_swipe(args)
args.state.swipe = { up: false, down: false, right: false, left: false }
end

19
app/main.rb Normal file
View file

@ -0,0 +1,19 @@
require "app/input.rb"
require "app/sprite.rb"
require "app/util.rb"
require "app/constants.rb"
require "app/menu.rb"
require "app/scene.rb"
require "app/game_setting.rb"
require "app/sound.rb"
require "app/text.rb"
require "app/scenes/gameplay.rb"
require "app/scenes/main_menu.rb"
require "app/scenes/paused.rb"
require "app/scenes/settings.rb"
# NOTE: add all requires above this
require "app/tick.rb"

113
app/menu.rb Normal file
View file

@ -0,0 +1,113 @@
module Menu
class << self
# Updates and renders a list of options that get passed through.
#
# +options+ data structure:
# [
# {
# text: "some string",
# on_select: -> (args) { "do some stuff in this lambda" }
# }
# ]
def tick(args, state_key, options, menu_y: 420)
args.state.send(state_key).current_option_i ||= 0
args.state.send(state_key).hold_delay ||= 0
menu_state = args.state.send(state_key)
labels = []
spacer = mobile? ? 100 : 60
options.each.with_index do |option, i|
text = case option.kind
when :toggle
"#{text(option[:key])}: #{text_for_setting_val(option[:setting_val])}"
else
text(option[:key])
end
label = label(
text,
x: args.grid.w / 2,
y: menu_y + (options.length - i * spacer),
align: ALIGN_CENTER,
size: SIZE_MD
)
label.key = option[:key]
label_size = args.gtk.calcstringbox(label.text, label.size_enum)
labels << label
if menu_state.current_option_i == i
if !mobile? || (mobile? && args.inputs.controller_one.connected)
args.outputs.solids << {
x: label.x - (label_size[0] / 1.4) - 24 + (Math.sin(args.state.tick_count / 8) * 4),
y: label.y - 22,
w: 16,
h: 16,
}.merge(WHITE)
end
end
end
labels.each do |l|
button_border = { w: 340, h: 80, x: l.x - 170, y: l.y - 55 }.merge(WHITE)
if mobile?
args.outputs.borders << button_border
end
if args.inputs.mouse.up && args.inputs.mouse.inside_rect?(button_border)
o = options.find { |o| o[:key] == l[:key] }
play_sfx(args, :menu)
o[:on_select].call(args) if o
end
end
args.outputs.labels << labels
move = nil
if args.inputs.down
move = :down
elsif args.inputs.up
move = :up
else
menu_state.hold_delay = 0
end
if move
menu_state.hold_delay -= 1
if menu_state.hold_delay <= 0
play_sfx(args, :menu)
index = menu_state.current_option_i
if move == :up
index -= 1
else
index += 1
end
if index < 0
index = options.length - 1
elsif index > options.length - 1
index = 0
end
menu_state.current_option_i = index
menu_state.hold_delay = 10
end
end
if primary_down?(args.inputs)
play_sfx(args, :select)
options[menu_state.current_option_i][:on_select].call(args)
end
end
def text_for_setting_val(val)
case val
when true
text(:on)
when false
text(:off)
else
val
end
end
end
end

11
app/repl.rb Normal file
View file

@ -0,0 +1,11 @@
# ===============================================================
# Welcome to repl.rb
# ===============================================================
# You can experiement with code within this file. Code in this
# file is only executed when you save (and only excecuted ONCE).
# ===============================================================
# Remove the x from xrepl to run the code. Add the x back to ignore to code.
xrepl do
puts "The result of 1 + 2 is: #{1 + 2}"
end

36
app/scene.rb Normal file
View file

@ -0,0 +1,36 @@
# A scene represents a discreet state of gameplay. Things like the main menu,
# game over screen, and gameplay.
#
# Define a new scene by adding one to `app/scenes/` and defining a
# `Scene.tick_SCENE_NAME` class method.
#
# The main `#tick` of the game handles delegating to the current scene based on
# the `args.state.scene` value, which is a symbol of the current scene, ex:
# `:gameplay`
module Scene
class << self
# Change the current scene, and optionally reset the scene that's begin
# changed to so any data is cleared out
# ex:
# Scene.switch(args, :gameplay)
def switch(args, scene, reset: false, return_to: nil)
args.state.scene_to_return_to = return_to if return_to
if scene == :back && args.state.scene_to_return_to
scene = args.state.scene_to_return_to
args.state.scene_to_return_to = nil
end
if reset
args.state.send(scene)&.current_option_i = nil
args.state.send(scene)&.hold_delay = nil
# you can also add custom reset logic as-needed for specific scenes
# here
end
args.state.scene = scene
raise FinishTick.new
end
end
end

54
app/scenes/gameplay.rb Normal file
View file

@ -0,0 +1,54 @@
module Scene
class << self
# This is your main entrypoint into the actual fun part of your game!
def tick_gameplay(args)
labels = []
sprites = []
# focus tracking
if !args.state.has_focus && args.inputs.keyboard.has_focus
args.state.has_focus = true
elsif args.state.has_focus && !args.inputs.keyboard.has_focus
args.state.has_focus = false
end
# auto-pause & input-based pause
if !args.state.has_focus || pause_down?(args)
return pause(args)
end
tick_pause_button(args, sprites) if mobile?
draw_bg(args, BLACK)
labels << label("GAMEPLAY", x: 40, y: args.grid.top - 40, size: SIZE_LG, font: FONT_BOLD)
args.outputs.labels << labels
args.outputs.sprites << sprites
end
def pause(args)
play_sfx(args, :select)
return Scene.switch(args, :paused, reset: true)
end
def tick_pause_button(args, sprites)
pause_button = {
x: 72.from_right,
y: 72.from_top,
w: 52,
h: 52,
path: Sprite.for(:pause),
}
pause_rect = pause_button.dup
pause_padding = 12
pause_rect.x -= pause_padding
pause_rect.y -= pause_padding
pause_rect.w += pause_padding * 2
pause_rect.h += pause_padding * 2
if args.inputs.mouse.down && args.inputs.mouse.inside_rect?(pause_rect)
return pause(args)
end
sprites << pause_button
end
end
end

51
app/scenes/main_menu.rb Normal file
View file

@ -0,0 +1,51 @@
module Scene
class << self
# what's displayed when your game starts
def tick_main_menu(args)
draw_bg(args, DARK_PURPLE)
options = [
{
key: :start,
on_select: -> (args) { Scene.switch(args, :gameplay, reset: true) }
},
{
key: :settings,
on_select: -> (args) { Scene.switch(args, :settings, reset: true, return_to: :main_menu) }
},
]
if args.gtk.platform?(:desktop)
options << {
key: :quit,
on_select: -> (args) { args.gtk.request_quit }
}
end
Menu.tick(args, :main_menu, options)
labels = []
labels << label(
"v#{version}",
x: 32.from_left, y: 32.from_top,
size: SIZE_XS, align: ALIGN_LEFT)
labels << label(
title.upcase, x: args.grid.w / 2, y: args.grid.top - 100,
size: SIZE_LG, align: ALIGN_CENTER, font: FONT_BOLD_ITALIC)
labels << label(
"#{text(:made_by)} #{dev_title}",
x: args.grid.left + 24, y: 48,
size: SIZE_XS, align: ALIGN_LEFT)
labels << label(
:controls_title,
x: args.grid.right - 24, y: 84,
size: SIZE_SM, align: ALIGN_RIGHT)
labels << label(
args.inputs.controller_one.connected ? :controls_gamepad : :controls_keyboard,
x: args.grid.right - 24, y: 48,
size: SIZE_XS, align: ALIGN_RIGHT)
args.outputs.labels << labels
end
end
end

39
app/scenes/paused.rb Normal file
View file

@ -0,0 +1,39 @@
module Scene
class << self
# scene reached from gameplay when the player needs a break
def tick_paused(args)
draw_bg(args, DARK_YELLOW)
options = [
{
key: :resume,
on_select: -> (args) { Scene.switch(args, :gameplay) }
},
{
key: :settings,
on_select: -> (args) { Scene.switch(args, :settings, reset: true, return_to: :paused) }
},
{
key: :return_to_main_menu,
on_select: -> (args) { Scene.switch(args, :main_menu) }
},
]
if args.gtk.platform?(:desktop)
options << {
key: :quit,
on_select: -> (args) { args.gtk.request_quit }
}
end
Menu.tick(args, :paused, options)
if secondary_down?(args.inputs)
play_sfx(args, :select)
options.find { |o| o[:key] == :resume }[:on_select].call(args)
end
args.outputs.labels << label(:paused, x: args.grid.w / 2, y: args.grid.top - 200, align: ALIGN_CENTER, size: SIZE_LG, font: FONT_BOLD)
end
end
end

60
app/scenes/settings.rb Normal file
View file

@ -0,0 +1,60 @@
module Scene
class << self
# reachable via main menu or pause menu, allows for configuring the game
# for the player's preferences.
def tick_settings(args)
draw_bg(args, DARK_GREEN)
options = [
{
key: :sfx,
kind: :toggle,
setting_val: args.state.setting.sfx,
on_select: -> (args) do
GameSetting.save_after(args) do |args|
args.state.setting.sfx = !args.state.setting.sfx
end
end
},
{
key: :music,
kind: :toggle,
setting_val: args.state.setting.music,
on_select: -> (args) do
GameSetting.save_after(args) do |args|
args.state.setting.music = !args.state.setting.music
set_music_vol(args)
end
end
},
{
key: :back,
on_select: -> (args) { Scene.switch(args, :back) }
},
]
if args.gtk.platform?(:desktop)
options.insert(options.length - 1, {
key: :fullscreen,
kind: :toggle,
setting_val: args.state.setting.fullscreen,
on_select: -> (args) do
GameSetting.save_after(args) do |args|
args.state.setting.fullscreen = !args.state.setting.fullscreen
args.gtk.set_window_fullscreen(args.state.setting.fullscreen)
end
end
})
end
Menu.tick(args, :settings, options)
if secondary_down?(args.inputs)
play_sfx(args, :select)
options.find { |o| o[:key] == :back }[:on_select].call(args)
end
args.outputs.labels << label(:settings, x: args.grid.w / 2, y: args.grid.top - 200, align: ALIGN_CENTER, size: SIZE_LG, font: FONT_BOLD)
end
end
end

30
app/sound.rb Normal file
View file

@ -0,0 +1,30 @@
# play a sound effect. the file in sounds/ must match the key name. ex:
# play_sfx(args, :select)
def play_sfx(args, key)
if args.state.setting.sfx
args.outputs.sounds << "sounds/#{key}.wav"
end
end
# play the specified music track, the key must correspond to the
# `sounds/#{key}.ogg` file naming scheme.
def play_music(args, key)
args.audio[:music] = { input: "sounds/#{key}.ogg", looping: true, }
set_music_vol(args)
end
# sets the music vol based on whether or not music is enabled or disabled
def set_music_vol(args)
vol = args.state.setting.music ? 0.8 : 0.0
args.audio[:music]&.gain = vol
end
# pause the currently playing music track
def pause_music(args)
args.audio[:music].paused = true
end
# pause the current music track
def resume_music(args)
args.audio[:music].paused = false
end

25
app/sprite.rb Normal file
View file

@ -0,0 +1,25 @@
module Sprite
# annoying to track but useful for reloading with +i+ in debug mode; would be
# nice to define a different way
SPRITES = {
bullet: "sprites/bullet.png",
enemy: "sprites/enemy.png",
enemy_king: "sprites/enemy_king.png",
enemy_super: "sprites/enemy_super.png",
exp_chip: "sprites/exp_chip.png",
familiar: "sprites/familiar.png",
player: "sprites/player.png",
pause: "sprites/pause.png",
}
class << self
def reset_all(args)
SPRITES.each { |_, v| args.gtk.reset_sprite(v) }
end
def for(key)
SPRITES.fetch(key)
end
end
end

8
app/tests.rb Normal file
View file

@ -0,0 +1,8 @@
# NOTE: don't write tests in this file, instead put them in `test/main_test.rb`.
require "lib/dragon_test.rb"
# add requires for additional test files here
# this must be required last
require "test/tests.rb"

58
app/text.rb Normal file
View file

@ -0,0 +1,58 @@
# Why put our text in a Hash? It makes it easier to proofread when near each
# other, makes the game easier to localize, and it's easier to manage than
# scouring the codebase.
#
# Don't access via this constant! Use the `#text` method instead.
TEXT = {
back: "Back",
controls_title: "Controls",
controls_keyboard: "WASD/Arrows to move | J/Z/Space to confirm | Esc/P to pause",
controls_gamepad: "Stick/D-Pad to move | A to confirm | Start to pause",
fullscreen: "Fullscreen",
made_by: "A game by",
music: "Music",
off: "OFF",
on: "ON",
paused: "Paused",
quit: "Quit",
resume: "Resume",
return_to_main_menu: "Return to Main Menu",
settings: "Settings",
sfx: "Sound Effects",
start: "Start",
}
# Gets the text for the passed in `key`. Raises if it does not exist. We don't
# want missing text!
def text(key)
TEXT.fetch(key)
end
SIZE_XS = 0
SIZE_SM = 4
SIZE_MD = 6
SIZE_LG = 10
FONT_REGULAR = "fonts/Atkinson-Hyperlegible-Regular-102.ttf"
FONT_ITALIC = "fonts/Atkinson-Hyperlegible-Italic-102.ttf"
FONT_BOLD = "fonts/Atkinson-Hyperlegible-Bold-102.ttf"
FONT_BOLD_ITALIC = "fonts/Atkinson-Hyperlegible-BoldItalic-102.ttf"
# Friendly method with sensible defaults for creating DRGTK label data
# structures.
def label(value_or_key, x:, y:, align: ALIGN_LEFT, size: SIZE_MD, color: WHITE, font: FONT_REGULAR)
text = if value_or_key.is_a?(Symbol)
text(value_or_key)
else
value_or_key
end
{
text: text,
x: x,
y: y,
alignment_enum: align,
size_enum: size,
font: font,
}.merge(color)
end

79
app/tick.rb Normal file
View file

@ -0,0 +1,79 @@
# Code that only gets run once on game start
def init(args)
reset_swipe(args)
GameSetting.load_settings(args)
end
def tick(args)
init(args) if args.state.tick_count == 0
# this looks good on non 16:9 resolutions; game background is different
args.outputs.background_color = TRUE_BLACK.values
args.state.has_focus ||= true
args.state.scene ||= :main_menu
track_swipe(args) if mobile?
Scene.send("tick_#{args.state.scene}", args)
debug_tick(args)
rescue FinishTick
end
# raise this as an easy way to end the current tick early
class FinishTick < StandardError; end
# code that only runs while developing
# put shortcuts and helpful info here
def debug_tick(args)
return unless debug?
debug_label(
args, 24.from_right, 24.from_top,
"v#{version} | DR v#{$gtk.version} (#{$gtk.platform}) | Ticks: #{args.state.tick_count} | FPS: #{args.gtk.current_framerate.round}",
ALIGN_RIGHT)
if args.inputs.keyboard.key_down.zero
play_sfx(args, :select)
args.state.render_debug_details = !args.state.render_debug_details
end
if args.inputs.keyboard.key_down.i
play_sfx(args, :select)
Sprite.reset_all(args)
args.gtk.notify!("Sprites reloaded")
end
if args.inputs.keyboard.key_down.r
play_sfx(args, :select)
$gtk.reset
end
if args.inputs.keyboard.key_down.m
play_sfx(args, :select)
args.state.simulate_mobile = !args.state.simulate_mobile
msg = if args.state.simulate_mobile
"Mobile simulation on"
else
"Mobile simulation off"
end
args.gtk.notify!(msg)
end
end
# render a label that is only shown when in debug mode and the debug details
# are shown; toggle with +0+ key
def debug_label(args, x, y, text, align=ALIGN_LEFT)
return unless debug?
return unless args.state.render_debug_details
args.outputs.debug << { x: x, y: y, text: text, alignment_enum: align }.merge(WHITE).label!
end
# different than background_color... use this to change the bg color for the
# visible portion of the game
def draw_bg(args, color)
args.outputs.solids << { x: args.grid.left, y: args.grid.bottom, w: args.grid.w, h: args.grid.h }.merge(color)
end

165
app/util.rb Normal file
View file

@ -0,0 +1,165 @@
###########
# utility, math, and misc methods
# returns random val between min & max, inclusive
# needs integers, use rand if you don't need min/max and don't care much
def random(min, max)
min = Integer(min)
max = Integer(max)
rand((max + 1) - min) + min
end
# returns true the passed in % of the time
# ex: `percent_chance?(25)` -- 1/4 chance of returning true
def percent_chance?(percent)
error("percent param (#{percent}) can't be above 100!") if percent > 100.0
return false if percent == 0.0
return true if percent == 100.0
rand() < (percent / 100.0)
end
# strips away the junk added by GTK::OpenEntity
def open_entity_to_hash(open_entity)
open_entity.as_hash.except(:entity_id, :entity_name, :entity_keys_by_ref, :__thrash_count__)
end
# Executes the block for each intersection of the collections. Doesn't check
# intersections within a parameter
#
# Block arguments are an instance of each collection, ordered by the parameter
# order.
#
# ex:
# collide(tiles, enemies) do |tile, enemy|
# # do stuff!
# end
#
# collide(player, enemies) do |player, enemy|
# player.health -= enemy.power
# end
def collide(col1, col2, &block)
col1 = [col1] unless col1.is_a?(Array)
col2 = [col2] unless col2.is_a?(Array)
col1.each do |i|
col2.each do |j|
if i.intersect_rect?(j)
block.call(i, j)
end
end
end
end
# +angle+ is expected to be in degrees with 0 being facing right
def vel_from_angle(angle, speed)
[speed * Math.cos(deg_to_rad(angle)), speed * Math.sin(deg_to_rad(angle))]
end
# returns diametrically opposed angle
# uses degrees
def opposite_angle(angle)
add_to_angle(angle, 180)
end
# returns a new angle from the og `angle` one summed with the `diff`
# degrees! of course
def add_to_angle(angle, diff)
((angle + diff) % 360).abs
end
def deg_to_rad(deg)
(deg * Math::PI / 180).round(4)
end
# Returns degrees
def angle_for_dir(dir)
case dir
when DIR_RIGHT
0
when DIR_LEFT
180
when DIR_UP
90
when DIR_DOWN
270
else
error("invalid dir: #{dir}")
end
end
# checks if the passed in `rect` is outside of the `container`
# `container` can be any rectangle-ish data structure
def out_of_bounds?(container, rect)
rect.x > container.right ||
rect.x + rect.w < container.left ||
rect.y > container.top ||
rect.y + rect.h < container.bottom
end
# Raises an exception with the passing in error message
# `msg` - String
def error(msg)
raise StandardError.new(msg)
end
# The version of your game defined in `metadata/game_metadata.txt`
def version
$gtk.args.cvars['game_metadata.version'].value
end
# Name of who make the game
def dev_title
$gtk.args.cvars['game_metadata.devtitle'].value
end
# Title of the game
def title
$gtk.args.cvars['game_metadata.gametitle'].value
end
# debug mode is what's running when you're making the game
# when you build and ship your game, it's in production mode
def debug?
@debug ||= !$gtk.production
end
# whether or not you're on a mobile device (or simulating one)
def mobile?
$gtk.platform?(:mobile) || $gtk.args.state.simulate_mobile
end
# sets the passed in entity's color for the specified number of ticks
def flash(entity, color, tick_count)
entity.flashing = true
entity.flash_ticks_remaining = tick_count
entity.flash_color = color
end
def tick_flasher(entity)
if entity.flashing
entity.flash_ticks_remaining -= 1
entity.merge!(entity.flash_color)
if entity.flash_ticks_remaining <= 0
entity.flashing = false
reset_color(entity)
end
end
end
def reset_color(entity)
entity.a = nil
entity.r = nil
entity.g = nil
entity.b = nil
end
# Returns a hash of the x and y position coords of the center of the entity.
# The passed in `entity` must have x, y, h, and w attributes
#
# Ex:
# center_of({ x: 100, y: 100, w: 200, h: 250 })
# # => { x: 200.0, y: 225.0 }
def center_of(entity)
raise StandardError.new("entity does not have needed properties to find center; must have x, y, w, and h properties") unless entity.x && entity.y && entity.h && entity.w
{ x: entity.x + entity.w / 2, y: entity.y + entity.h / 2 }
end

9
concom-config.json Normal file
View file

@ -0,0 +1,9 @@
{
"types": [
"feat",
"adjust",
"fix",
"chore"
],
"breaking_changes": true
}

1
data/.gitkeep Normal file
View file

@ -0,0 +1 @@
Put level data and other txt files here.

1
fonts/.gitkeep Normal file
View file

@ -0,0 +1 @@
Put your custom fonts here.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

96
lib/dragon_test.rb Normal file
View file

@ -0,0 +1,96 @@
def run_tests
$gtk.tests&.passed.clear
$gtk.tests&.inconclusive.clear
$gtk.tests&.failed.clear
puts "💨 running tests"
$gtk.reset 100
$gtk.log_level = :on
$gtk.tests.start
if $gtk.tests.failed.any?
puts "🙀 tests failed!"
failures = $gtk.tests.failed.uniq.map do |failure|
"🔴 ##{failure[:m]} - #{failure[:e]}"
end
if $gtk.cli_arguments.keys.include?(:"exit-on-fail")
$gtk.write_file("test-failures.txt", failures.join("\n"))
exit(1)
end
else
puts "🪩 tests passed!"
end
end
# an optional BDD-like method to use to group and document tests
def it(message)
yield
end
class GTK::Assert
# define custom assertions here!
# def rect!(obj)
# true!(obj.x && obj.y && obj.w && obj.h, "doesn't have needed properties")
# end
# takes three params: lambda that gets called, the error class, and the
# expected message.
# usage: assert.exception!(KeyError, "Key not found: :not_present") { text(args, :not_present) }
def exception!(error_class, message=nil)
begin
yield
rescue StandardError => e
equal!(e.class, error_class)
if message
equal!(e.message, message)
end
end
end
# usage: assert.includes!([1, 2, 3], 3)
def includes!(arr, val)
true!(arr.include?(val), "array: #{arr} does not include the val: #{val}")
end
# usage: assert.not_includes!([1, 2, 3], 4)
def not_includes!(arr, val)
false!(arr.include?(val), "array: #{arr} does include the val: #{val}")
end
# usage: assert.int!(2 + 3)
def int!(obj)
true!(obj.is_a?(Integer), "that's no integer!")
end
end
def test(method)
test_name = "test_#{method}"
define_method(test_name) do |args, assert|
yield(args, assert)
end
end
test :assert_includes do |args, assert|
it "works!" do
assert.includes!([1, 2, 3], 3)
end
end
test :assert_not_includes do |args, assert|
it "works!" do
assert.not_includes!([1, 2, 3], 4)
end
end
test :assert_int do |args, assert|
it "works!" do
assert.int!(2 + 3)
end
end
test :assert_exception do |args, assert|
class MyError < StandardError; end
assert.exception!(MyError, "oh no") { raise MyError.new("oh no") }
end

7
metadata/cvars.txt Normal file
View file

@ -0,0 +1,7 @@
log.filter_subsystems=HTTPServer
# Should use the whole display
# renderer.fullscreen=true
# Milliseconds to sleep per frame when the game is in the background (zero to disable)
# renderer.background_sleep=0

View file

@ -0,0 +1,73 @@
devid=myitchusername
devtitle=My Name
gameid=my-game-title
gametitle=My Game Title
version=0.1-dev
icon=metadata/icon.png
# === Flags available at all licensing tiers ===
# Defines the render scale quality for sprites. scale_quality=0 (default) is nearest neighbor, scale_quality=1 is linear, scale_quality=2 is antialiased.
# scale_quality=0
# === Flags available in DragonRuby Game Toolkit Pro ====
# Uncomment the entry below to bytecode compile your Ruby code
# compile_ruby=false
# Uncomment the entry below to specify the package name for your APK
# packageid=org.dev.gamename
# Setting this property to true will enable High DPI rendering (try in combination with scale_quality to see what looks best)
# highdpi=false
# === Portrait Mode ===
# The orientation can be set to either landscape (1280x720) or portrait (720x1280)
# orientation=landscape
# === HD Mode ===
# HD Mode: when enabled, will give you 720p, 1080p, 1440p, 4k, and 5k rendering options
# Check out the following YouTube Video for a demo of DragonRuby's HD Capabilities
# https://youtu.be/Rnc6z84zaa4
# hd=false
# === Texture Atlases ===
# See sample app for texture atlas usage: =./samples/07_advanced_rendering_hd/02_texture_atlases=
# DragonRuby will recursively search the following directory for texture atlases.
# sprites_directory=sprites
# === All Screen Mode ===
# All Screen Mode: when enabled, removes the letter box and lets you render outside of the 16:9 safe area
# NOTE: requires hd=true
# allscreen=false
# All Screen Mode's Max Scale: You can specify the maximum scale for your game. Any resolution higher than your max scale will give more area outside of your resolutions safe area:
# default value is 100 (which keeps the baseline 720p and draws to all screen area from there)
# allscreen_max_scale=100
# Supported values for max scale:
# 720p: scales up to 1280x720 (and draws to all screen area from there)
# allscreen_max_scale=100
# HD+: scales up to 1600x900
# allscreen_max_scale=125
# 1080p: scales up to 1920x1080
# allscreen_max_scale=150
# 1440p: scales up to 2560x1440
# allscreen_max_scale=200
# 1800p: scales up to 3200x1800
# allscreen_max_scale=250
# 4k: scales up to 3200x2160
# allscreen_max_scale=300
# 5k: scales up to 6400x2880
# allscreen_max_scale=400

BIN
metadata/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

13
metadata/ios_metadata.txt Normal file
View file

@ -0,0 +1,13 @@
# ios_metadata.txt is used by the Pro version of DragonRuby Game Toolkit to create iOS apps.
# Information about the Pro version can be found at: http://dragonruby.org/toolkit/game#purchase
# teamid needs to be set to your assigned Team Id which can be found at https://developer.apple.com/account/#/membership/
teamid=
# appid needs to be set to your application identifier which can be found at https://developer.apple.com/account/resources/identifiers/list
appid=
# appname is the name you want to show up underneath the app icon on the device. Keep it under 10 characters.
appname=
# devcert is the certificate to use for development/deploying to your local device. This is the NAME of the certificate as it's displayed in Keychain Access.
devcert=
# prodcert is the certificate to use for distribution to the app store. This is the NAME of the certificate as it's displayed in Keychain Access.
prodcert=

10
run_tests Executable file
View file

@ -0,0 +1,10 @@
#!/usr/bin/env sh
set -e
rm -f test-failures.txt
if ! ../dragonruby . --eval app/tests.rb --no-tick --exit-on-fail; then
echo "🙀 tests failed!"
cat test-failures.txt
exit 1
else
echo "🪩 tests passed!"
fi

1
sounds/.gitkeep Normal file
View file

@ -0,0 +1 @@
Put your sounds here.

BIN
sounds/menu.wav Normal file

Binary file not shown.

BIN
sounds/select.wav Normal file

Binary file not shown.

0
sprites/.gitkeep Normal file
View file

BIN
sprites/pause.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

178
test/tests.rb Normal file
View file

@ -0,0 +1,178 @@
# To run the tests: ./run_tests
#
# Available assertions:
# assert.true!
# assert.false!
# assert.equal!
# assert.exception!
# assert.includes!
# assert.not_includes!
# assert.int!
# + any that you define
#
# Powered by Dragon Test: https://github.com/DragonRidersUnite/dragon_test
return unless debug?
test :menu_text_for_setting_val do |args, assert|
assert.equal!(Menu.text_for_setting_val(true), "ON")
assert.equal!(Menu.text_for_setting_val(false), "OFF")
assert.equal!(Menu.text_for_setting_val("other"), "other")
end
test :out_of_bounds do |args, assert|
grid = {
x: 0,
y: 0,
w: 1280,
h: 720,
}
assert.true!(out_of_bounds?(grid, { x: -30, y: 30, w: 24, h: 24 }))
assert.true!(out_of_bounds?(grid, { x: 30, y: -50, w: 24, h: 24 }))
assert.false!(out_of_bounds?(grid, { x: 30, y: 30, w: 24, h: 24 }))
end
test :angle_for_dir do |args, assert|
assert.equal!(angle_for_dir(DIR_RIGHT), 0)
assert.equal!(angle_for_dir(DIR_LEFT), 180)
assert.equal!(angle_for_dir(DIR_UP), 90)
assert.equal!(angle_for_dir(DIR_DOWN), 270)
end
test :vel_from_angle do |args, assert|
it "calculates core four angles properly" do
assert.equal!(vel_from_angle(0, 5), [5.0, 0.0])
assert.equal!(vel_from_angle(90, 5).map { |v| v.round(2) }, [0.0, 5.0])
assert.equal!(vel_from_angle(180, 5).map { |v| v.round(2) }, [-5.0, 0.0])
assert.equal!(vel_from_angle(270, 5).map { |v| v.round(2) }, [0.0, -5.0])
end
it "calculates other values as expected" do
assert.equal!(vel_from_angle(12, 5).map { |v| v.round(2) }, [4.89, 1.04])
end
end
test :open_entity_to_hash do |args, assert|
it "strips OpenEntity keys" do
args.state.foo.bar = true
args.state.foo.biz = false
assert.equal!(open_entity_to_hash(args.state.foo), { bar: true, biz: false })
end
end
test :game_setting_settings_for_save do |args, assert|
it "joins hash keys and values" do
assert.equal!(GameSetting.settings_for_save({ fullscreen: true, sfx: false}), "fullscreen:true,sfx:false")
end
end
test :text do |args, assert|
it "returns the value for the passed in key" do
assert.equal!(text(:fullscreen), "Fullscreen")
end
it "raises when the key isn't present" do
assert.exception!(KeyError, "Key not found: :not_present") { text(:not_present) }
end
end
test :opposite_angle do |args, assert|
it "returns the diametrically opposed angle" do
assert.equal!(opposite_angle(0), 180)
assert.equal!(opposite_angle(180), 0)
assert.equal!(opposite_angle(360), 180)
assert.equal!(opposite_angle(90), 270)
assert.equal!(opposite_angle(270), 90)
end
end
test :add_to_angle do |args, assert|
it "returns the new angle on the circle" do
assert.equal!(add_to_angle(0, 30), 30)
assert.equal!(add_to_angle(0, -30), 330)
assert.equal!(add_to_angle(180, -30), 150)
assert.equal!(add_to_angle(320, 60), 20)
assert.equal!(add_to_angle(320, -60), 260)
end
end
test :percent_chance? do |args, assert|
it "returns false if the percent is 0" do
assert.false!(percent_chance?(0))
end
it "returns true if the percent is 100" do
assert.true!(percent_chance?(100))
end
it "returns a boolean" do
assert.true!([TrueClass, FalseClass].include?(percent_chance?(50).class))
end
end
test :collide do |args, assert|
it "calls the block for every intersection of the two collections" do
counter = 0
enemies = [{ x: 0, y: 0, w: 8, h: 8, type: :e}, { x: 8, y: 0, w: 8, h: 8, type: :e}]
# 2 enemies intersect with only 1 of these tiles
tiles = [{ x: 0, y: 0, w: 32, h: 32}, { x: 32, y: 0, w: 32, h: 32}]
collide(enemies, tiles) do |enemy, tile|
counter += 1
assert.equal!(enemy.type, :e)
end
assert.equal!(counter, 2)
end
it "has access to args in the block" do
args.state.detect = false
enemies = [{ x: 0, y: 0, w: 8, h: 8}]
tiles = [{ x: 0, y: 0, w: 32, h: 32}]
collide(enemies, tiles) do |enemy, tile|
args.state.detect = true
end
assert.true!(args.state.detect)
end
it "wraps non-arrays in an array" do
counter = 0
tiles = [{ x: 0, y: 0, w: 32, h: 32}, { x: 32, y: 0, w: 32, h: 32}]
# player only intersects with 1 tile
player = { x: 0, y: 0, w: 8, h: 8, type: :player}
collide(tiles, player) do |tile, player|
counter += 1
assert.equal!(player.type, :player)
end
assert.equal!(counter, 1)
end
end
test :mobile do |args, assert|
it "supports simulation setting" do
$gtk.args.state.simulate_mobile = true
assert.true!(mobile?)
$gtk.args.state.simulate_mobile = false
assert.false!(mobile?)
end
end
test :center_of do |args, assert|
it "returns a hash with the x and y coord of the center of the rectangle-ish object" do
assert.equal!(center_of({ x: 100, y: 100, w: 200, h: 250 }), { x: 200.0, y: 225.0 })
end
it "errors when the object isn't rectangle-ish" do
assert.exception!(StandardError, "entity does not have needed properties to find center; must have x, y, w, and h properties") do
center_of({ x: 100, h: 250 })
end
end
end
# add your tests here
run_tests