ravensight-playtest 0.5.2 → 0.6.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.
- package/README.md +249 -14
- package/addons/ravensight_driver/driver.gd +406 -8
- package/addons/ravensight_simulate/simulate.gd +360 -0
- package/docs/godot-driver.md +70 -8
- package/package.json +4 -3
- package/src/api/README.md +4 -1
- package/src/api/client.js +92 -3
- package/src/api/errors.js +17 -1
- package/src/api/http.js +43 -17
- package/src/assets/formats.js +267 -0
- package/src/assets/index.js +260 -0
- package/src/assets/integration.js +429 -0
- package/src/assets/inventory.js +337 -0
- package/src/assets/measure.js +392 -0
- package/src/cli.js +165 -0
- package/src/commands/assets.js +62 -0
- package/src/commands/check.js +4 -1
- package/src/commands/create.js +734 -0
- package/src/commands/fakeCore.js +16 -1
- package/src/commands/feature.js +845 -0
- package/src/commands/fix.js +578 -68
- package/src/commands/init.js +19 -3
- package/src/commands/profile.js +25 -6
- package/src/commands/replay.js +204 -0
- package/src/commands/resume.js +7 -0
- package/src/commands/run.js +200 -24
- package/src/commands/template.js +70 -0
- package/src/commands/validate.js +414 -0
- package/src/commands/validateHosted.js +292 -0
- package/src/create/scaffold.js +192 -0
- package/src/run/acceptance.js +212 -0
- package/src/run/args.js +18 -1
- package/src/run/checks.js +705 -0
- package/src/run/dimension.js +95 -0
- package/src/run/drivers/godot-observation.js +59 -11
- package/src/run/drivers/godot-project.js +201 -31
- package/src/run/drivers/godot.js +532 -45
- package/src/run/drivers/web.js +18 -0
- package/src/run/evidence.js +170 -0
- package/src/run/featureLoop.js +288 -0
- package/src/run/fixLoop.js +82 -12
- package/src/run/git.js +110 -20
- package/src/run/json.js +65 -9
- package/src/run/labsOutcome.js +136 -0
- package/src/run/liveWork.js +103 -0
- package/src/run/model.js +105 -5
- package/src/run/openPr.js +107 -0
- package/src/run/paths.js +4 -1
- package/src/run/pendingRegister.js +81 -0
- package/src/run/personaLoop.js +111 -4
- package/src/run/profile.js +68 -7
- package/src/run/replay.js +236 -0
- package/src/run/repoTools.js +227 -27
- package/src/run/report.js +7 -1
- package/src/run/resume.js +19 -1
- package/src/run/secretScan.js +35 -0
- package/src/run/settle.js +41 -0
- package/src/run/tools.js +33 -4
- package/src/run/trace.js +124 -0
- package/src/simulation/godotPack.js +112 -0
- package/src/ui/index.js +61 -9
- package/src/upload/allowlist.js +6 -2
- package/src/upload/index.js +5 -4
- package/templates/godot-2d/README.md +36 -0
- package/templates/godot-2d/addons/ravensight/LICENSE +21 -0
- package/templates/godot-2d/addons/ravensight/Ravensight.gd +1823 -0
- package/templates/godot-2d/export_presets.cfg +27 -0
- package/templates/godot-2d/gitignore +10 -0
- package/templates/godot-2d/project.godot +58 -0
- package/templates/godot-2d/scenes/main.tscn +67 -0
- package/templates/godot-2d/scripts/autoload/game_config.gd +83 -0
- package/templates/godot-2d/scripts/autoload/game_state.gd +44 -0
- package/templates/godot-2d/scripts/autoload/telemetry.gd +37 -0
- package/templates/godot-2d/scripts/main.gd +32 -0
- package/templates/godot-2d/scripts/player.gd +10 -0
- package/templates/godot-2d/scripts/state_hook.gd +16 -0
- package/templates/godot-2d/template.json +14 -0
- package/templates/godot-2d/tools/inject_release_key.sh +19 -0
- package/templates/godot-2d/tools/selftest.gd +70 -0
- package/templates/godot-3d/README.md +49 -0
- package/templates/godot-3d/addons/ravensight/LICENSE +21 -0
- package/templates/godot-3d/addons/ravensight/Ravensight.gd +1823 -0
- package/templates/godot-3d/export_presets.cfg +27 -0
- package/templates/godot-3d/gitignore +10 -0
- package/templates/godot-3d/project.godot +56 -0
- package/templates/godot-3d/scenes/main.tscn +154 -0
- package/templates/godot-3d/scripts/autoload/game_config.gd +83 -0
- package/templates/godot-3d/scripts/autoload/game_state.gd +44 -0
- package/templates/godot-3d/scripts/autoload/telemetry.gd +37 -0
- package/templates/godot-3d/scripts/beacon.gd +20 -0
- package/templates/godot-3d/scripts/main.gd +49 -0
- package/templates/godot-3d/scripts/player.gd +19 -0
- package/templates/godot-3d/scripts/state_hook.gd +25 -0
- package/templates/godot-3d/template.json +16 -0
- package/templates/godot-3d/tools/inject_release_key.sh +19 -0
- package/templates/godot-3d/tools/selftest.gd +77 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[preset.0]
|
|
2
|
+
|
|
3
|
+
name="Web"
|
|
4
|
+
platform="Web"
|
|
5
|
+
runnable=true
|
|
6
|
+
export_filter="all_resources"
|
|
7
|
+
include_filter="ravensight_key.release.cfg"
|
|
8
|
+
exclude_filter="ravensight_key.cfg,tools/*,DESIGN.md"
|
|
9
|
+
export_path="build/web/index.html"
|
|
10
|
+
|
|
11
|
+
[preset.0.options]
|
|
12
|
+
|
|
13
|
+
html/export_icon=true
|
|
14
|
+
|
|
15
|
+
[preset.1]
|
|
16
|
+
|
|
17
|
+
name="Desktop"
|
|
18
|
+
platform="Linux"
|
|
19
|
+
runnable=false
|
|
20
|
+
export_filter="all_resources"
|
|
21
|
+
include_filter="ravensight_key.release.cfg"
|
|
22
|
+
exclude_filter="ravensight_key.cfg,tools/*,DESIGN.md"
|
|
23
|
+
export_path="build/desktop/game.x86_64"
|
|
24
|
+
|
|
25
|
+
[preset.1.options]
|
|
26
|
+
|
|
27
|
+
binary_format/embed_pck=true
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
.godot/
|
|
2
|
+
.ravensight/brief.local.json
|
|
3
|
+
.ravensight/jobs/
|
|
4
|
+
.ravensight/create.json
|
|
5
|
+
.DS_Store
|
|
6
|
+
/build/
|
|
7
|
+
export_credentials.cfg
|
|
8
|
+
# The development key stays on this machine, and the release key is written at build time.
|
|
9
|
+
ravensight_key.cfg
|
|
10
|
+
ravensight_key.release.cfg
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
; Engine configuration file.
|
|
2
|
+
config_version=5
|
|
3
|
+
|
|
4
|
+
[application]
|
|
5
|
+
|
|
6
|
+
config/name="__GAME_NAME__"
|
|
7
|
+
config/description="A placeholder prototype made with ravensight-playtest create."
|
|
8
|
+
config/version="0.1.0"
|
|
9
|
+
run/main_scene="res://scenes/main.tscn"
|
|
10
|
+
config/features=PackedStringArray("4.3", "GL Compatibility")
|
|
11
|
+
boot_splash/show_image=false
|
|
12
|
+
|
|
13
|
+
[autoload]
|
|
14
|
+
|
|
15
|
+
GameConfig="*res://scripts/autoload/game_config.gd"
|
|
16
|
+
Ravensight="*res://addons/ravensight/Ravensight.gd"
|
|
17
|
+
Telemetry="*res://scripts/autoload/telemetry.gd"
|
|
18
|
+
GameState="*res://scripts/autoload/game_state.gd"
|
|
19
|
+
|
|
20
|
+
[display]
|
|
21
|
+
|
|
22
|
+
window/size/viewport_width=640
|
|
23
|
+
window/size/viewport_height=384
|
|
24
|
+
window/size/window_width_override=1280
|
|
25
|
+
window/size/window_height_override=768
|
|
26
|
+
window/stretch/mode="canvas_items"
|
|
27
|
+
window/stretch/aspect="keep"
|
|
28
|
+
|
|
29
|
+
[input]
|
|
30
|
+
|
|
31
|
+
move_left={
|
|
32
|
+
"deadzone": 0.25,
|
|
33
|
+
"events": [Object(InputEventKey,"physical_keycode":65), Object(InputEventKey,"physical_keycode":4194319), Object(InputEventJoypadMotion,"device":-1,"axis":0,"axis_value":-1.0)]
|
|
34
|
+
}
|
|
35
|
+
move_right={
|
|
36
|
+
"deadzone": 0.25,
|
|
37
|
+
"events": [Object(InputEventKey,"physical_keycode":68), Object(InputEventKey,"physical_keycode":4194321), Object(InputEventJoypadMotion,"device":-1,"axis":0,"axis_value":1.0)]
|
|
38
|
+
}
|
|
39
|
+
move_up={
|
|
40
|
+
"deadzone": 0.25,
|
|
41
|
+
"events": [Object(InputEventKey,"physical_keycode":87), Object(InputEventKey,"physical_keycode":4194320), Object(InputEventJoypadMotion,"device":-1,"axis":1,"axis_value":-1.0)]
|
|
42
|
+
}
|
|
43
|
+
move_down={
|
|
44
|
+
"deadzone": 0.25,
|
|
45
|
+
"events": [Object(InputEventKey,"physical_keycode":83), Object(InputEventKey,"physical_keycode":4194322), Object(InputEventJoypadMotion,"device":-1,"axis":1,"axis_value":1.0)]
|
|
46
|
+
}
|
|
47
|
+
action={
|
|
48
|
+
"deadzone": 0.5,
|
|
49
|
+
"events": [Object(InputEventKey,"physical_keycode":32), Object(InputEventKey,"physical_keycode":69), Object(InputEventJoypadButton,"device":-1,"button_index":0)]
|
|
50
|
+
}
|
|
51
|
+
pause={
|
|
52
|
+
"deadzone": 0.5,
|
|
53
|
+
"events": [Object(InputEventKey,"physical_keycode":4194305), Object(InputEventJoypadButton,"device":-1,"button_index":6)]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
[rendering]
|
|
57
|
+
|
|
58
|
+
renderer/rendering_method="gl_compatibility"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[gd_scene load_steps=4 format=3]
|
|
2
|
+
|
|
3
|
+
[ext_resource type="Script" path="res://scripts/main.gd" id="1"]
|
|
4
|
+
[ext_resource type="Script" path="res://scripts/player.gd" id="2"]
|
|
5
|
+
[ext_resource type="Script" path="res://scripts/state_hook.gd" id="3"]
|
|
6
|
+
|
|
7
|
+
[node name="Main" type="Node2D"]
|
|
8
|
+
script = ExtResource("1")
|
|
9
|
+
|
|
10
|
+
[node name="Ground" type="ColorRect" parent="."]
|
|
11
|
+
offset_right = 640.0
|
|
12
|
+
offset_bottom = 384.0
|
|
13
|
+
color = Color(0.1, 0.12, 0.16, 1)
|
|
14
|
+
|
|
15
|
+
[node name="Player" type="Node2D" parent="."]
|
|
16
|
+
position = Vector2(320, 192)
|
|
17
|
+
script = ExtResource("2")
|
|
18
|
+
|
|
19
|
+
[node name="Shape" type="Polygon2D" parent="Player"]
|
|
20
|
+
color = Color(0.95, 0.75, 0.3, 1)
|
|
21
|
+
polygon = PackedVector2Array(-10, -10, 10, -10, 10, 10, -10, 10)
|
|
22
|
+
|
|
23
|
+
[node name="StateHook" type="Node" parent="."]
|
|
24
|
+
script = ExtResource("3")
|
|
25
|
+
player_path = NodePath("../Player")
|
|
26
|
+
|
|
27
|
+
[node name="Interface" type="CanvasLayer" parent="."]
|
|
28
|
+
|
|
29
|
+
[node name="Hud" type="Label" parent="Interface"]
|
|
30
|
+
offset_left = 8.0
|
|
31
|
+
offset_top = 6.0
|
|
32
|
+
offset_right = 632.0
|
|
33
|
+
offset_bottom = 30.0
|
|
34
|
+
text = ""
|
|
35
|
+
|
|
36
|
+
[node name="Settings" type="Control" parent="Interface"]
|
|
37
|
+
layout_mode = 3
|
|
38
|
+
anchors_preset = 15
|
|
39
|
+
anchor_right = 1.0
|
|
40
|
+
anchor_bottom = 1.0
|
|
41
|
+
|
|
42
|
+
[node name="Panel" type="PanelContainer" parent="Interface/Settings"]
|
|
43
|
+
layout_mode = 1
|
|
44
|
+
anchors_preset = 8
|
|
45
|
+
anchor_left = 0.5
|
|
46
|
+
anchor_top = 0.5
|
|
47
|
+
anchor_right = 0.5
|
|
48
|
+
anchor_bottom = 0.5
|
|
49
|
+
offset_left = -160.0
|
|
50
|
+
offset_top = -60.0
|
|
51
|
+
offset_right = 160.0
|
|
52
|
+
offset_bottom = 60.0
|
|
53
|
+
|
|
54
|
+
[node name="Rows" type="VBoxContainer" parent="Interface/Settings/Panel"]
|
|
55
|
+
layout_mode = 2
|
|
56
|
+
|
|
57
|
+
[node name="Title" type="Label" parent="Interface/Settings/Panel/Rows"]
|
|
58
|
+
layout_mode = 2
|
|
59
|
+
text = "Settings"
|
|
60
|
+
|
|
61
|
+
[node name="Analytics" type="CheckBox" parent="Interface/Settings/Panel/Rows"]
|
|
62
|
+
layout_mode = 2
|
|
63
|
+
text = "Share anonymous analytics"
|
|
64
|
+
|
|
65
|
+
[node name="Hint" type="Label" parent="Interface/Settings/Panel/Rows"]
|
|
66
|
+
layout_mode = 2
|
|
67
|
+
text = "Esc to go back"
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
extends Node
|
|
2
|
+
## Runs before the Ravensight autoload and hands it this build's key, so the key is
|
|
3
|
+
## never typed into a script or committed.
|
|
4
|
+
##
|
|
5
|
+
## Where the key comes from, first match wins:
|
|
6
|
+
## 1. --ravensight-key-file=<path> after -- on the command line
|
|
7
|
+
## 2. the RAVENSIGHT_INGEST_KEY environment variable
|
|
8
|
+
## 3. res://ravensight_key.release.cfg, in an exported build only
|
|
9
|
+
## 4. res://ravensight_key.cfg, the development key on this machine
|
|
10
|
+
## --no-key skips all four. Both files are ignored by git. With no key the SDK is
|
|
11
|
+
## inert and the game plays exactly the same.
|
|
12
|
+
|
|
13
|
+
const GAME_VERSION := "0.1.0"
|
|
14
|
+
const DEVELOPMENT_KEY := "res://ravensight_key.cfg"
|
|
15
|
+
const RELEASE_KEY := "res://ravensight_key.release.cfg"
|
|
16
|
+
|
|
17
|
+
## Which of the four answered: "argument", "environment", "release", "development" or "none".
|
|
18
|
+
var key_source := "none"
|
|
19
|
+
|
|
20
|
+
func _ready() -> void:
|
|
21
|
+
var found := find_key(_user_args(), OS.has_feature("editor"))
|
|
22
|
+
key_source = String(found["source"])
|
|
23
|
+
if not String(found["key"]).is_empty():
|
|
24
|
+
Ravensight.ingest_key = String(found["key"])
|
|
25
|
+
if not String(found["api_url"]).is_empty():
|
|
26
|
+
Ravensight.api_url = String(found["api_url"])
|
|
27
|
+
Ravensight.optional = true
|
|
28
|
+
Ravensight.game_version = GAME_VERSION
|
|
29
|
+
Ravensight.distribution = _build_label()
|
|
30
|
+
|
|
31
|
+
## The key this run should use, and what it came from. No side effects, so a test
|
|
32
|
+
## can call it.
|
|
33
|
+
func find_key(args: Dictionary, in_editor: bool) -> Dictionary:
|
|
34
|
+
var none := {"key": "", "api_url": "", "source": "none"}
|
|
35
|
+
if args.has("no-key"):
|
|
36
|
+
return none
|
|
37
|
+
if args.has("ravensight-key-file"):
|
|
38
|
+
var from_file := _read_key(String(args["ravensight-key-file"]))
|
|
39
|
+
if not String(from_file["key"]).is_empty():
|
|
40
|
+
from_file["source"] = "argument"
|
|
41
|
+
return from_file
|
|
42
|
+
var from_environment := OS.get_environment("RAVENSIGHT_INGEST_KEY").strip_edges()
|
|
43
|
+
if not from_environment.is_empty():
|
|
44
|
+
return {"key": from_environment, "api_url": OS.get_environment("RAVENSIGHT_API_URL").strip_edges(), "source": "environment"}
|
|
45
|
+
if not in_editor:
|
|
46
|
+
var release := _read_key(RELEASE_KEY)
|
|
47
|
+
if not String(release["key"]).is_empty():
|
|
48
|
+
release["source"] = "release"
|
|
49
|
+
return release
|
|
50
|
+
var development := _read_key(DEVELOPMENT_KEY)
|
|
51
|
+
if not String(development["key"]).is_empty():
|
|
52
|
+
development["source"] = "development"
|
|
53
|
+
return development
|
|
54
|
+
return none
|
|
55
|
+
|
|
56
|
+
func _read_key(path: String) -> Dictionary:
|
|
57
|
+
var answer := {"key": "", "api_url": "", "source": "none"}
|
|
58
|
+
if not FileAccess.file_exists(path):
|
|
59
|
+
return answer
|
|
60
|
+
var file := ConfigFile.new()
|
|
61
|
+
if file.load(path) != OK:
|
|
62
|
+
return answer
|
|
63
|
+
answer["key"] = String(file.get_value("ravensight", "ingest_key", "")).strip_edges()
|
|
64
|
+
answer["api_url"] = String(file.get_value("ravensight", "api_url", "")).strip_edges()
|
|
65
|
+
return answer
|
|
66
|
+
|
|
67
|
+
func _user_args() -> Dictionary:
|
|
68
|
+
var out := {}
|
|
69
|
+
for raw in OS.get_cmdline_user_args():
|
|
70
|
+
var text := String(raw).trim_prefix("--")
|
|
71
|
+
var split := text.find("=")
|
|
72
|
+
if split >= 0:
|
|
73
|
+
out[text.substr(0, split)] = text.substr(split + 1)
|
|
74
|
+
else:
|
|
75
|
+
out[text] = "true"
|
|
76
|
+
return out
|
|
77
|
+
|
|
78
|
+
func _build_label() -> String:
|
|
79
|
+
if OS.has_feature("web"):
|
|
80
|
+
return "web"
|
|
81
|
+
if OS.has_feature("editor"):
|
|
82
|
+
return "editor"
|
|
83
|
+
return "native-" + OS.get_name().to_lower()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
extends Node
|
|
2
|
+
## What the game is, in numbers. Nothing here draws anything. A persona reads this
|
|
3
|
+
## through scripts/state_hook.gd, and the playable check reads core_loops_completed.
|
|
4
|
+
|
|
5
|
+
## A milestone calls complete_core_loop() once for each pass of the game's core loop.
|
|
6
|
+
signal core_loop_completed(total: int)
|
|
7
|
+
|
|
8
|
+
var score := 0
|
|
9
|
+
var level := 1
|
|
10
|
+
var deaths := 0
|
|
11
|
+
var core_loops_completed := 0
|
|
12
|
+
var started := false
|
|
13
|
+
|
|
14
|
+
## The game started playing. Reported once a run.
|
|
15
|
+
func start_game() -> void:
|
|
16
|
+
if started:
|
|
17
|
+
return
|
|
18
|
+
started = true
|
|
19
|
+
Telemetry.track("game_play_started", {"level": "level_%d" % level}, "level_start")
|
|
20
|
+
|
|
21
|
+
## One whole pass of the core loop, which is what "playable" means here.
|
|
22
|
+
func complete_core_loop() -> void:
|
|
23
|
+
core_loops_completed += 1
|
|
24
|
+
Telemetry.track("core_loop_completed", {"level": "level_%d" % level, "total": core_loops_completed}, "level_complete")
|
|
25
|
+
# The moment worth not losing if the player closes the game now.
|
|
26
|
+
Ravensight.flush()
|
|
27
|
+
core_loop_completed.emit(core_loops_completed)
|
|
28
|
+
|
|
29
|
+
func add_score(amount: int) -> void:
|
|
30
|
+
score += amount
|
|
31
|
+
|
|
32
|
+
func record_death() -> void:
|
|
33
|
+
deaths += 1
|
|
34
|
+
Telemetry.track("player_died", {"level": "level_%d" % level, "deaths": deaths}, "death")
|
|
35
|
+
|
|
36
|
+
## Plain values only, so it can cross to a playtest as JSON.
|
|
37
|
+
func snapshot() -> Dictionary:
|
|
38
|
+
return {
|
|
39
|
+
"started": started,
|
|
40
|
+
"score": score,
|
|
41
|
+
"level": level,
|
|
42
|
+
"deaths": deaths,
|
|
43
|
+
"core_loops_completed": core_loops_completed,
|
|
44
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
extends Node
|
|
2
|
+
## Every gameplay event goes through track(), which always says what the event
|
|
3
|
+
## means. Ravensight reads a level start, a level completion or a death from the
|
|
4
|
+
## reserved rs_kind property first, and only guesses from the event's name when it
|
|
5
|
+
## is missing. A natural name such as "room_restarted" or "run_finished" would
|
|
6
|
+
## otherwise be read as something it is not, so nothing here leaves that to chance.
|
|
7
|
+
|
|
8
|
+
const KINDS := ["level_start", "level_complete", "death", "none"]
|
|
9
|
+
|
|
10
|
+
## The last event this run asked to send, name and data, for a test to read. Set
|
|
11
|
+
## whether or not the SDK has a key.
|
|
12
|
+
var last_event := {}
|
|
13
|
+
|
|
14
|
+
## Queue an event. `kind` is one of KINDS. Anything else is sent as "none", with a
|
|
15
|
+
## warning, because a wrong kind would move the game's funnel.
|
|
16
|
+
func track(event_name: String, data: Dictionary = {}, kind: String = "none") -> void:
|
|
17
|
+
var prepared := prepare(data, kind)
|
|
18
|
+
last_event = {"name": event_name, "data": prepared}
|
|
19
|
+
Ravensight.track_event(event_name, prepared)
|
|
20
|
+
|
|
21
|
+
## The event's data as it will be sent.
|
|
22
|
+
func prepare(data: Dictionary, kind: String) -> Dictionary:
|
|
23
|
+
var out := data.duplicate()
|
|
24
|
+
if KINDS.has(kind):
|
|
25
|
+
out["rs_kind"] = kind
|
|
26
|
+
else:
|
|
27
|
+
push_warning("Telemetry: unknown kind %s, sent as none" % kind)
|
|
28
|
+
out["rs_kind"] = "none"
|
|
29
|
+
return out
|
|
30
|
+
|
|
31
|
+
## The player's own choice: the settings overlay calls this. The SDK saves it and
|
|
32
|
+
## it holds on every later run. False stops all analytics, including what is queued.
|
|
33
|
+
func set_analytics_enabled(value: bool) -> void:
|
|
34
|
+
Ravensight.set_tracking_enabled(value)
|
|
35
|
+
|
|
36
|
+
func analytics_enabled() -> bool:
|
|
37
|
+
return not FileAccess.file_exists(Ravensight.OPTOUT_PATH)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
extends Node2D
|
|
2
|
+
## The one scene the scaffold starts on. Milestones build the game here.
|
|
3
|
+
|
|
4
|
+
@onready var hud: Label = $Interface/Hud
|
|
5
|
+
@onready var settings: Control = $Interface/Settings
|
|
6
|
+
@onready var analytics: CheckBox = $Interface/Settings/Panel/Rows/Analytics
|
|
7
|
+
|
|
8
|
+
func _ready() -> void:
|
|
9
|
+
process_mode = Node.PROCESS_MODE_ALWAYS
|
|
10
|
+
$Player.process_mode = Node.PROCESS_MODE_PAUSABLE
|
|
11
|
+
settings.visible = false
|
|
12
|
+
analytics.button_pressed = Telemetry.analytics_enabled()
|
|
13
|
+
analytics.toggled.connect(Telemetry.set_analytics_enabled)
|
|
14
|
+
GameState.start_game()
|
|
15
|
+
_refresh()
|
|
16
|
+
|
|
17
|
+
func _unhandled_input(event: InputEvent) -> void:
|
|
18
|
+
if event.is_action_pressed("pause"):
|
|
19
|
+
_toggle_settings()
|
|
20
|
+
get_viewport().set_input_as_handled()
|
|
21
|
+
|
|
22
|
+
func _process(_delta: float) -> void:
|
|
23
|
+
_refresh()
|
|
24
|
+
|
|
25
|
+
func _toggle_settings() -> void:
|
|
26
|
+
settings.visible = not settings.visible
|
|
27
|
+
get_tree().paused = settings.visible
|
|
28
|
+
if settings.visible:
|
|
29
|
+
analytics.grab_focus()
|
|
30
|
+
|
|
31
|
+
func _refresh() -> void:
|
|
32
|
+
hud.text = "Score %d Loops %d Esc for settings" % [GameState.score, GameState.core_loops_completed]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
extends Node2D
|
|
2
|
+
## A placeholder player: a shape that moves. Swap the Polygon2D for your own art.
|
|
3
|
+
|
|
4
|
+
const SPEED := 180.0
|
|
5
|
+
|
|
6
|
+
var bounds := Rect2(16.0, 16.0, 608.0, 352.0)
|
|
7
|
+
|
|
8
|
+
func _physics_process(delta: float) -> void:
|
|
9
|
+
var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")
|
|
10
|
+
position = (position + direction * SPEED * delta).clamp(bounds.position, bounds.end)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
extends Node
|
|
2
|
+
## Lets a playtest read the game. The driver calls _ravensight_state() on every node
|
|
3
|
+
## in the ravensight_state group. Add what a player can see to this dictionary as the
|
|
4
|
+
## game grows: a persona reads it, and a replay compares it.
|
|
5
|
+
|
|
6
|
+
@export var player_path: NodePath
|
|
7
|
+
|
|
8
|
+
func _ready() -> void:
|
|
9
|
+
add_to_group("ravensight_state")
|
|
10
|
+
|
|
11
|
+
func _ravensight_state() -> Dictionary:
|
|
12
|
+
var state := GameState.snapshot()
|
|
13
|
+
var player := get_node_or_null(player_path) as Node2D
|
|
14
|
+
if player != null:
|
|
15
|
+
state["player_position"] = [round(player.position.x), round(player.position.y)]
|
|
16
|
+
return state
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "godot-2d",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"engine": "godot",
|
|
5
|
+
"min_engine": "4.3",
|
|
6
|
+
"sdk": {
|
|
7
|
+
"version": "0.3.1",
|
|
8
|
+
"source": "https://github.com/Reality-Software-Entertainment/ravensight-godot",
|
|
9
|
+
"tag": "v0.3.1",
|
|
10
|
+
"commit": "997d53edfd40641e88f40fd712df711dbb116f78",
|
|
11
|
+
"path": "addons/ravensight/Ravensight.gd",
|
|
12
|
+
"sha256": "e7a5dc6f5f0edb62c3265867da2e8da74f5ff3ed2145a8b162d77f2dbb69733e"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Writes the public release key into the project just before an export, and removes
|
|
3
|
+
# it after, so it is in the exported build and never in git:
|
|
4
|
+
#
|
|
5
|
+
# RAVENSIGHT_RELEASE_KEY=gt_pub_... sh tools/inject_release_key.sh godot --headless --export-release Web build/index.html
|
|
6
|
+
#
|
|
7
|
+
# Everything after the script's name is the export command. The development key is
|
|
8
|
+
# never exported: export_presets.cfg leaves it out by name.
|
|
9
|
+
set -eu
|
|
10
|
+
: "${RAVENSIGHT_RELEASE_KEY:?Set RAVENSIGHT_RELEASE_KEY to this game public key, the one that starts gt_pub_.}"
|
|
11
|
+
case "$RAVENSIGHT_RELEASE_KEY" in
|
|
12
|
+
gt_pub_*) ;;
|
|
13
|
+
*) echo "A build anyone can download needs a public key, the one that starts gt_pub_." >&2; exit 2 ;;
|
|
14
|
+
esac
|
|
15
|
+
file="ravensight_key.release.cfg"
|
|
16
|
+
trap 'rm -f "$file"' EXIT
|
|
17
|
+
umask 077
|
|
18
|
+
printf '[ravensight]\ningest_key="%s"\n' "$RAVENSIGHT_RELEASE_KEY" > "$file"
|
|
19
|
+
"$@"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
extends SceneTree
|
|
2
|
+
## Proves the parts of this project a game depends on, headless, with no key:
|
|
3
|
+
## godot --headless --path . --script tools/selftest.gd
|
|
4
|
+
## Prints one line per check and exits non-zero if any failed.
|
|
5
|
+
|
|
6
|
+
var failed := 0
|
|
7
|
+
|
|
8
|
+
func _initialize() -> void:
|
|
9
|
+
# Autoloads and the scene have to be ready first.
|
|
10
|
+
process_frame.connect(_run, CONNECT_ONE_SHOT)
|
|
11
|
+
|
|
12
|
+
func _check(name: String, passed: bool) -> void:
|
|
13
|
+
print("%s %s" % ["ok " if passed else "FAIL", name])
|
|
14
|
+
if not passed:
|
|
15
|
+
failed += 1
|
|
16
|
+
|
|
17
|
+
func _run() -> void:
|
|
18
|
+
var game_config: Node = root.get_node("GameConfig")
|
|
19
|
+
var telemetry: Node = root.get_node("Telemetry")
|
|
20
|
+
var game_state: Node = root.get_node("GameState")
|
|
21
|
+
var sdk: Node = root.get_node("Ravensight")
|
|
22
|
+
|
|
23
|
+
# Every kind reaches the event as rs_kind, and a bad one reads as none.
|
|
24
|
+
for kind in ["level_start", "level_complete", "death", "none"]:
|
|
25
|
+
_check("rs_kind %s" % kind, telemetry.prepare({}, kind)["rs_kind"] == kind)
|
|
26
|
+
_check("an unknown kind is sent as none", telemetry.prepare({}, "nonsense")["rs_kind"] == "none")
|
|
27
|
+
var original := {"a": 1}
|
|
28
|
+
telemetry.prepare(original, "none")
|
|
29
|
+
_check("prepare leaves the caller's data alone", not original.has("rs_kind"))
|
|
30
|
+
|
|
31
|
+
# The key loader's order, without a key on this machine being read.
|
|
32
|
+
var dir := OS.get_user_data_dir().path_join("selftest")
|
|
33
|
+
DirAccess.make_dir_recursive_absolute(dir)
|
|
34
|
+
var path := dir.path_join("key.cfg")
|
|
35
|
+
var file := ConfigFile.new()
|
|
36
|
+
file.set_value("ravensight", "ingest_key", "gt_live_selftest")
|
|
37
|
+
file.set_value("ravensight", "api_url", "http://127.0.0.1:9")
|
|
38
|
+
file.save(path)
|
|
39
|
+
var found: Dictionary = game_config.find_key({"ravensight-key-file": path}, true)
|
|
40
|
+
_check("a key file argument wins", found["source"] == "argument" and found["key"] == "gt_live_selftest" and found["api_url"] == "http://127.0.0.1:9")
|
|
41
|
+
_check("--no-key answers none", game_config.find_key({"no-key": "true", "ravensight-key-file": path}, true)["source"] == "none")
|
|
42
|
+
DirAccess.remove_absolute(path)
|
|
43
|
+
|
|
44
|
+
# The opt-out is saved where the SDK reads it, and a later run would honour it.
|
|
45
|
+
var was_on: bool = telemetry.analytics_enabled()
|
|
46
|
+
telemetry.set_analytics_enabled(false)
|
|
47
|
+
_check("opting out is saved", not telemetry.analytics_enabled())
|
|
48
|
+
telemetry.set_analytics_enabled(true)
|
|
49
|
+
_check("opting back in clears it", telemetry.analytics_enabled())
|
|
50
|
+
telemetry.set_analytics_enabled(was_on)
|
|
51
|
+
|
|
52
|
+
# What the playable check reads.
|
|
53
|
+
var before: int = game_state.core_loops_completed
|
|
54
|
+
game_state.complete_core_loop()
|
|
55
|
+
_check("a completed loop is counted", game_state.core_loops_completed == before + 1)
|
|
56
|
+
_check("it is reported as a level completion", telemetry.last_event["data"]["rs_kind"] == "level_complete")
|
|
57
|
+
|
|
58
|
+
# The hook a playtest reads, on the real scene.
|
|
59
|
+
var scene: Node = load("res://scenes/main.tscn").instantiate()
|
|
60
|
+
root.add_child(scene)
|
|
61
|
+
var hook: Node = scene.get_node("StateHook")
|
|
62
|
+
_check("the state node is in the group", hook.is_in_group("ravensight_state"))
|
|
63
|
+
var state: Dictionary = hook._ravensight_state()
|
|
64
|
+
_check("the state says how many loops", state.has("core_loops_completed"))
|
|
65
|
+
_check("the state says where the player is", state.has("player_position"))
|
|
66
|
+
_check("the SDK is the public one", String(sdk.VERSION).length() > 0)
|
|
67
|
+
scene.queue_free()
|
|
68
|
+
|
|
69
|
+
print("selftest %s" % ("failed" if failed > 0 else "passed"))
|
|
70
|
+
quit(1 if failed > 0 else 0)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# __GAME_NAME__
|
|
2
|
+
|
|
3
|
+
A small placeholder 3D project from the Ravensight `godot-3d` template (Alpha, a proof of
|
|
4
|
+
concept). Every visual is a primitive mesh, and your own models, art and audio replace
|
|
5
|
+
them. Ravensight does not make a 3D game from a brief: this template, Build a feature
|
|
6
|
+
and Validate changes are the whole of the 3D Alpha.
|
|
7
|
+
|
|
8
|
+
## Run it
|
|
9
|
+
|
|
10
|
+
Open this folder in Godot 4, or run `godot --path .`. Move with WASD or the arrow keys
|
|
11
|
+
along the floor, jump with Space or E, and Esc for settings. Walk into the beacon: it
|
|
12
|
+
moves somewhere else, and that is one pass of the core loop. Fall off the floor and you
|
|
13
|
+
start again from the middle.
|
|
14
|
+
|
|
15
|
+
`godot --headless --path . --script tools/selftest.gd` checks the parts a game
|
|
16
|
+
depends on.
|
|
17
|
+
|
|
18
|
+
## Analytics
|
|
19
|
+
|
|
20
|
+
The Ravensight SDK (`addons/ravensight/Ravensight.gd`, MIT, unmodified) is an autoload.
|
|
21
|
+
Report events with `Telemetry.track(name, data, kind)`, where `kind` is `level_start`,
|
|
22
|
+
`level_complete`, `death` or `none`. It always sets `rs_kind`, so an event's name is
|
|
23
|
+
never used to guess what it means.
|
|
24
|
+
|
|
25
|
+
- **The key.** With no key the SDK does nothing and the game plays the same. To send
|
|
26
|
+
events, put this game's key in `ravensight_key.cfg`, which git ignores, under
|
|
27
|
+
`[ravensight]` as `ingest_key="..."`. `scripts/autoload/game_config.gd` says where else
|
|
28
|
+
a key can come from.
|
|
29
|
+
- **The opt-out.** Esc opens settings, and "Share anonymous analytics" is the player's
|
|
30
|
+
own choice. The SDK saves it and honours it on every later run.
|
|
31
|
+
- **A build anyone can download.** Use a public key, the one that starts `gt_pub_`, so
|
|
32
|
+
it is written only into the build:
|
|
33
|
+
|
|
34
|
+
RAVENSIGHT_RELEASE_KEY=gt_pub_... sh tools/inject_release_key.sh godot --headless --export-release Web build/web/index.html
|
|
35
|
+
|
|
36
|
+
## How a playtest reads the game
|
|
37
|
+
|
|
38
|
+
`scripts/state_hook.gd` is in the `ravensight_state` group, and its
|
|
39
|
+
`_ravensight_state()` answer is what a persona sees and a replay compares. Add what a
|
|
40
|
+
player can see to it as the game grows. Call `GameState.complete_core_loop()` once for
|
|
41
|
+
each pass of the core loop.
|
|
42
|
+
|
|
43
|
+
A persona plays with keys and input actions, and it can click a node by its path. It
|
|
44
|
+
cannot steer a camera with the mouse, so a feature that needs mouse look cannot be
|
|
45
|
+
played by a persona yet.
|
|
46
|
+
|
|
47
|
+
Random numbers from the global `randf()` and `randi()` are seeded by a playtest, so a
|
|
48
|
+
replay sees them come out the same. See the replay notes in the CLI's `docs/REPLAY.md`
|
|
49
|
+
for what else holds and what does not.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Reality Software Entertainment
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|