summaryrefslogtreecommitdiffhomepage
path: root/docs/API_SPEC.md
blob: 680152d806133fff3f9c285a1346850e2a0bfa98 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# raylib-jamstack — Ruby API Specification

A stack for building raylib gamejam games in Ruby (mruby).

**Core parts**

1. **Raylib** — graphics / audio / input, exposed to Ruby as `Rl::`
2. **MRuby** — the embedded Ruby interpreter that runs the game code
3. **RmlUi** — HTML/CSS-based UI layer, exposed to Ruby as `Rml::`, with Ruby data binding

This document specifies *how the bindings look in Ruby*. It is the contract the C
binding layer must satisfy. Implementation uses **modern raylib + modern mruby**;
the `orc-arena-of-time` project is referenced only for *how the API feels*, not for
how its (old) bindings were implemented.

---

## 0. Design principles

These are distilled from the reference game and extended. Every binding decision
should be checkable against these rules.

1. **snake_case** everything. `InitWindow` -> `Rl.init_window`.
2. **Predicates end in `?`** and return `true`/`false`. `IsKeyDown` -> `Rl.key_down?`.
3. **Setters use `=`.** `SetTargetFPS(60)` -> `Rl.target_fps = 60`.
4. **Begin/End pairs are blocks.** Anything that comes as `BeginX`/`EndX` in C is a
   method that takes a block and guarantees the `EndX` runs (even on exception/return).
5. **Many-arg functions take keyword args with sensible defaults.** 2–3 obvious
   positional args may stay positional (`init_window(w, h, title)`).
6. **C structs become classes** under `Rl::`. Constructors that load a resource
   (`LoadTexture`) are `ClassName.new(path)`. Plain data structs (`Color`,
   `Vector2`, `Rectangle`) are `ClassName.new(fields...)` with mutable accessors.
7. **Struct-first methods become instance methods.** `DrawRectangleRec(rec, c)` ->
   `rec.draw(color: c)`; `CheckCollisionRecs(a, b)` -> `a.collide_with_rec?(b)`.
8. **Resources free themselves** via mruby GC finalizers, with an explicit
   `#unload` escape hatch (see §6).

---

## 1. Module-level: `Rl` (raylib core)

### 1.1 Window & lifecycle

```ruby
Rl.init_window(900, 675, "Orc: Arena of Time")   # InitWindow
Rl.close_window                                   # CloseWindow (auto on exit)
Rl.window_should_close?                           # WindowShouldClose
Rl.window_open?                                    # !WindowShouldClose (convenience)

# The main loop. Runs the block each frame until the window should close.
# On web (emscripten) this is wired to emscripten_set_main_loop instead of a
# real `while`, transparently. THIS IS WHY IT IS A BLOCK, NOT A RAW LOOP.
Rl.while_window_open do
  # update + draw
end
```

> **Improvement over reference:** the reference used a `while` loop that does not
> map cleanly to emscripten's callback-driven loop. Keeping `while_window_open` as
> the *only* sanctioned loop lets the binding swap in `emscripten_set_main_loop`
> for web with zero game-code changes.

### 1.2 Timing

```ruby
Rl.target_fps = 60     # SetTargetFPS
Rl.frame_time          # GetFrameTime  (delta, seconds)  -> Float
Rl.time                # GetTime       (since init)      -> Float
Rl.fps                 # GetFPS                          -> Integer
```

### 1.3 Drawing scope (block-based Begin/End)

```ruby
Rl.draw(clear_color: Rl::BLACK) do      # BeginDrawing + ClearBackground + EndDrawing
  ...
end

Rl.scissor_mode(x:, y:, width:, height:) do   # Begin/EndScissorMode
  ...
end

Rl.mode_2d(camera) do ... end           # Begin/EndMode2D
Rl.texture_mode(render_texture) do ... end  # Begin/EndTextureMode
Rl.blend_mode(Rl::BLEND_ADDITIVE) do ... end # Begin/EndBlendMode
```

> `draw`, `scissor_mode`, `mode_2d`, `texture_mode`, `blend_mode` all follow the
> same block rule (#4). `clear_color:` defaults to `Rl::RAYWHITE`.

### 1.4 Text

```ruby
Rl.draw_text(text: "fps: #{Rl.fps}", x: 10, y: 10, font_size: 30, color: Rl::WHITE)
Rl.draw_text(text:, x:, y:, font_size:, color:, font: Rl::Font.default, spacing: 1.0)
Rl.measure_text(text:, font_size:, font: Rl::Font.default) # -> Rl::Vector2
```

### 1.5 Textures / images

```ruby
Rl.draw_texture_pro(
  texture:,
  source: src_rec,                 # NOTE: renamed from reference's source_rec
  dest:   dst_rec,                 # NOTE: renamed from reference's dest_rec
  origin: Rl::Vector2.new(0, 0),
  rotation: 0,
  tint: Rl::WHITE
)
Rl.draw_texture(texture:, x:, y:, tint: Rl::WHITE)
Rl.draw_texture_v(texture:, position:, tint: Rl::WHITE)
```

> **Naming decision needed (see §9):** the reference is inconsistent — it uses
> `source_rec:`/`dest_rec:` in calls but `source:`/`dest:` in a commented
> signature, and `Rl::Vector` vs `Rl::Vector2`. This spec standardizes on
> raylib's own names `source:`/`dest:` and the class name `Rl::Vector2`.

### 1.6 Input — keyboard

```ruby
Rl.key_down?(Rl::KEY_W)        # IsKeyDown
Rl.key_down?(:w)               # symbol alias
Rl.key_pressed?(Rl::KEY_R)     # IsKeyPressed
Rl.key_released?(key)          # IsKeyReleased
Rl.key_up?(key)                # IsKeyUp
```

> **Improvement over reference:** the reference used magic numbers
> (`Rl.key_down? 87` for W). This spec uses `Rl::KEY_*` constants and also accepts
> symbols: `Rl.key_down?(:w)`. (Clean break — raw magic numbers are not a
> supported style.)

### 1.7 Input — mouse

```ruby
Rl.mouse_button_pressed?(Rl::MOUSE_BUTTON_LEFT)
Rl.mouse_button_down?(Rl::MOUSE_BUTTON_LEFT)
Rl.mouse_button_up?(Rl::MOUSE_BUTTON_LEFT)   # 0/1/2 ok
Rl.mouse_button_released?(button)
Rl.mouse_position     # GetMousePosition -> Rl::Vector2
Rl.mouse_x            # GetMouseX        -> Integer
Rl.mouse_y            # GetMouseY        -> Integer
Rl.mouse_wheel        # GetMouseWheelMove -> Float
```

### 1.8 Audio

```ruby
Rl.init_audio_device          # InitAudioDevice
Rl.audio_device_ready?        # IsAudioDeviceReady
Rl.set_master_volume(0.5)     # SetMasterVolume   (also: Rl.master_volume = 0.5)
```

### 1.9 Platform helper (custom, not in raylib)

```ruby
Rl.platform   # => :web | :desktop   (reference used the strings 'web'/'desktop')
Rl.web?       # convenience
Rl.desktop?   # convenience
```

> **Improvement:** return symbols and add predicates; the reference compared
> against the string `'web'` everywhere, which is error-prone.

---

## 2. Data structs as classes

### 2.1 `Rl::Color`

```ruby
c = Rl::Color.new(255, 255, 255, 255)   # r, g, b, a
c.r; c.g; c.b; c.a                       # readers
c.a = 150                                # writers (mutable — reference relies on this)
```

Built-in constants (no more hand-defining WHITE/BLACK): `Rl::WHITE`, `Rl::BLACK`,
`Rl::BLANK`, `Rl::RAYWHITE`, `Rl::RED`, `Rl::GREEN`, `Rl::BLUE`, `Rl::YELLOW`,
`Rl::GRAY`, `Rl::DARKGRAY`, ... (full raylib palette).

### 2.2 `Rl::Vector2`

```ruby
v = Rl::Vector2.new(x, y)
v.x; v.y; v.x = ...; v.y = ...
```

> **Improvement (raymath):** operators + helpers so games stop hand-rolling
> `Math.sqrt(x**2 + y**2)` (the reference does this dozens of times):
> ```ruby
> a + b   a - b   a * scalar   a / scalar
> v.length        v.length_sqr      v.normalize
> v.dot(other)    v.distance(other) v.lerp(other, t)
> ```

### 2.3 `Rl::Rectangle`

```ruby
r = Rl::Rectangle.new(x, y, width, height)
r.x; r.y; r.width; r.height    # all mutable

r.draw(color:)                              # DrawRectangleRec
r.draw_lines(line_thick:, color:)           # DrawRectangleLinesEx
r.collide_with_point?(vec2)                 # CheckCollisionPointRec
r.collide_with_rec?(other)                  # CheckCollisionRecs
r.collision_rec(other)        -> Rectangle  # GetCollisionRec
# additions:
r.center                      -> Vector2
r.contains?(vec2)             # alias of collide_with_point?
```

### 2.4 `Rl::Texture`

```ruby
tex = Rl::Texture.new("./assets/orc.png")   # LoadTexture
tex.width; tex.height
tex.unload                                   # UnloadTexture (also GC-finalized)
```

> **Improvement:** add `Rl::Texture.load("path")` that **caches** by path. The
> reference loads the same file repeatedly (e.g. on each level construct), leaking
> GPU memory. `Texture.new` = always fresh; `Texture.load` = cached.

### 2.5 `Rl::Image` (CPU-side) vs `Rl::Texture` (GPU-side)

```ruby
img = Rl::Image.new("./assets/orc.png")   # LoadImage (stays in RAM, CPU editable)
tex = img.to_texture                       # LoadTextureFromImage
img.unload
```

### 2.6 `Rl::Sound` / `Rl::Music`

```ruby
snd = Rl::Sound.new("./assets/hurt.wav")   # LoadSound
snd.play           # PlaySound
snd.stop
snd.playing?       # IsSoundPlaying
snd.volume = 0.45  # SetSoundVolume

mus = Rl::Music.new("./assets/music.ogg")  # LoadMusicStream
mus.play; mus.playing?; mus.volume = 0.09
mus.update         # UpdateMusicStream (call each frame for streaming)
```

> **Note:** the reference loaded `music.ogg` as a `Sound`; long tracks should be
> `Music` (streamed). Both are provided.

### 2.7 `Rl::Camera2D`, `Rl::RenderTexture`, `Rl::Font`

```ruby
cam = Rl::Camera2D.new(target: Rl::Vector2.new(0,0),
                       offset: Rl::Vector2.new(0,0),
                       rotation: 0, zoom: 1.0)

rt = Rl::RenderTexture.new(width, height)   # LoadRenderTexture
font = Rl::Font.new("./assets/font.ttf", size: 32)
```

---

## 3. The canonical game shape

Putting the idioms together (this should read like the reference game, cleaned up):

```ruby
Rl.init_window(900, 675, "Orc: Arena of Time")
Rl.target_fps = 60

player = Rl::Texture.load("./assets/orc.png")
src    = Rl::Rectangle.new(0, 0, 24, 24)
dest   = Rl::Rectangle.new(100, 100, 48, 48)

Rl.while_window_open do
  dest.x += 100 * Rl.frame_time if Rl.key_down?(:d)

  Rl.draw(clear_color: Rl::BLACK) do
    Rl.draw_texture_pro(texture: player, source: src, dest: dest)
    Rl.draw_text(text: "fps: #{Rl.fps}", x: 10, y: 10, font_size: 20, color: Rl::WHITE)
  end
end
```

---

## 4. RmlUi: `Rml::` (specified in detail in API_SPEC_RMLUI.md)

Summary of the surface (full spec to follow as the next deliverable):

```ruby
Rml.init(width: 900, height: 675)        # backend wired to raylib's GL context
ctx = Rml::Context.new("main", 900, 675)

# Data binding (MVC) — bind a Ruby object to a named data model
model = ctx.data_model("hud") do |m|
  m.bind(:hp, -> { player.hp })          # one-way view
  m.bind(:score, score)                  # two-way for plain values
  m.event(:reset) { reset_game }         # rml: data-event-click="reset()"
end

doc = ctx.load_document("ui/hud.rml")
doc.show

Rl.while_window_open do
  Rl.draw(clear_color: Rl::BLACK) do
    # game render ...
    ctx.update      # process data model changes
    ctx.render      # draw UI on top via raylib
  end
  ctx.process_input # feed raylib mouse/keyboard into RmlUi
end

model.dirty(:hp)   # notify UI that a bound variable changed (DataModelHandle::DirtyVariable)
```

This mirrors RmlUi's `DataModelHandle` MVC model: Ruby objects are the model,
`.rml`/`.rcss` files are the view, and `m.event` callbacks are controllers.

---

## 5. Block-safety contract

Every block-form method (`while_window_open`, `draw`, `scissor_mode`, `mode_2d`,
`texture_mode`, `blend_mode`, `data_model`) MUST run its matching `EndX` even if the
block raises or returns early. In C-binding terms: wrap the `mrb_yield` and always
emit the `EndX` call, re-raising any pending exception afterward.

---

## 6. Resource lifetime

- Resource classes (`Texture`, `Image`, `Sound`, `Music`, `Font`, `RenderTexture`,
  `Camera`-no) hold a native handle and register an mruby **finalizer** that calls
  the corresponding `UnloadX` when garbage-collected.
- All expose an explicit `#unload` for deterministic freeing (important on web,
  where GC timing is unpredictable). Double-unload is a no-op.
- `Texture.load(path)` / `Image.load(path)` use a per-path cache; cached resources
  are unloaded at `Rl.close_window` or via `Rl::Texture.clear_cache`.

---

## 7. Constants

- Keys: `Rl::KEY_A` .. `Rl::KEY_Z`, `Rl::KEY_SPACE`, `Rl::KEY_ENTER`, arrows, etc.
  Symbol aliases accepted by all key predicates (`:w`, `:space`, `:enter`).
- Mouse: `Rl::MOUSE_BUTTON_LEFT/RIGHT/MIDDLE` (integers `0/1/2` still accepted).
- Colors: full raylib palette (§2.1).
- Blend modes, config flags, etc. as `Rl::*` integer constants.

---

## 8. What changed vs. the orc-arena-of-time reference (summary)

| Area | Reference | This spec |
|------|-----------|-----------|
| Loop | `Rl.while_window_open` (raw while) | same, but defined as the emscripten-safe seam |
| Keys | magic numbers (`87`) | `Rl::KEY_W` + symbols (`:w`), numbers still ok |
| Mouse btn | magic numbers (`0`) | `Rl::MOUSE_BUTTON_LEFT` (int `0/1/2` ok) |
| Colors | hand-defined WHITE/BLACK | built-in `Rl::WHITE`/`Rl::BLACK`/full palette |
| Vector math | manual `Math.sqrt(...)` | `Vector2` operators + raymath helpers |
| Texture args | `source_rec:` / `dest_rec:` | `source:` / `dest:` (matches raylib) |
| Vector class | mixed `Vector`/`Vector2` | always `Rl::Vector2` |
| Long audio | `Sound` for music | `Music` (streamed) + `Sound` (one-shot) |
| Texture reuse | reloaded each level (leak) | `Texture.load` path cache + `#unload` |
| Platform | string `'web'` | `Rl.platform` symbol + `Rl.web?`/`desktop?` |
| UI | (none / hand-drawn) | RmlUi `Rml::` with Ruby data binding |

> The orc-arena-of-time project is a **style reference only**. This stack makes a
> clean break — there is no goal of running existing orc game code unmodified.

---

## 9. Settled decisions

- **Clean break** — no backward-compat with orc game code.
- **Symbol keys enabled** — `Rl.key_down?(:w)` alongside `Rl::KEY_W`.
- **ECS via flecs** — the stack ships optional `Flecs::` bindings (ECS) modeled on
  flecs' Lua binding; see [API_SPEC_FLECS.md](API_SPEC_FLECS.md). Using it is
  optional — game architecture is still up to the author.
  (Originally the stack shipped no ECS; this was reversed when flecs was added.)
- **No raygui** — RmlUi is the sole UI layer.