ravensight-playtest 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +319 -14
  2. package/addons/ravensight_driver/driver.gd +406 -8
  3. package/addons/ravensight_simulate/simulate.gd +360 -0
  4. package/docs/godot-driver.md +70 -8
  5. package/package.json +4 -3
  6. package/src/api/README.md +4 -1
  7. package/src/api/client.js +120 -3
  8. package/src/api/errors.js +17 -1
  9. package/src/api/http.js +43 -17
  10. package/src/assets/formats.js +267 -0
  11. package/src/assets/index.js +260 -0
  12. package/src/assets/integration.js +429 -0
  13. package/src/assets/inventory.js +337 -0
  14. package/src/assets/measure.js +392 -0
  15. package/src/cli.js +233 -0
  16. package/src/commands/assets.js +62 -0
  17. package/src/commands/check.js +4 -1
  18. package/src/commands/create.js +734 -0
  19. package/src/commands/fakeCore.js +16 -1
  20. package/src/commands/feature.js +845 -0
  21. package/src/commands/fix.js +647 -70
  22. package/src/commands/init.js +19 -3
  23. package/src/commands/lists.js +206 -0
  24. package/src/commands/profile.js +25 -6
  25. package/src/commands/punch.js +116 -0
  26. package/src/commands/replay.js +204 -0
  27. package/src/commands/resume.js +7 -0
  28. package/src/commands/run.js +200 -24
  29. package/src/commands/tape.js +74 -0
  30. package/src/commands/template.js +70 -0
  31. package/src/commands/validate.js +414 -0
  32. package/src/commands/validateHosted.js +292 -0
  33. package/src/create/scaffold.js +192 -0
  34. package/src/run/acceptance.js +212 -0
  35. package/src/run/args.js +18 -1
  36. package/src/run/checks.js +705 -0
  37. package/src/run/dimension.js +95 -0
  38. package/src/run/drivers/godot-observation.js +59 -11
  39. package/src/run/drivers/godot-project.js +201 -31
  40. package/src/run/drivers/godot.js +532 -45
  41. package/src/run/drivers/web.js +18 -0
  42. package/src/run/evidence.js +170 -0
  43. package/src/run/featureLoop.js +288 -0
  44. package/src/run/fixLoop.js +82 -12
  45. package/src/run/git.js +110 -20
  46. package/src/run/json.js +65 -9
  47. package/src/run/labsOutcome.js +136 -0
  48. package/src/run/liveWork.js +103 -0
  49. package/src/run/model.js +105 -5
  50. package/src/run/openPr.js +107 -0
  51. package/src/run/paths.js +4 -1
  52. package/src/run/pendingRegister.js +81 -0
  53. package/src/run/personaLoop.js +111 -4
  54. package/src/run/profile.js +68 -7
  55. package/src/run/replay.js +236 -0
  56. package/src/run/repoTools.js +227 -27
  57. package/src/run/report.js +7 -1
  58. package/src/run/resume.js +19 -1
  59. package/src/run/secretScan.js +35 -0
  60. package/src/run/settle.js +41 -0
  61. package/src/run/tools.js +33 -4
  62. package/src/run/trace.js +124 -0
  63. package/src/simulation/godotPack.js +112 -0
  64. package/src/ui/index.js +61 -9
  65. package/src/upload/allowlist.js +6 -2
  66. package/src/upload/index.js +5 -4
  67. package/templates/godot-2d/README.md +36 -0
  68. package/templates/godot-2d/addons/ravensight/LICENSE +21 -0
  69. package/templates/godot-2d/addons/ravensight/Ravensight.gd +1823 -0
  70. package/templates/godot-2d/export_presets.cfg +27 -0
  71. package/templates/godot-2d/gitignore +10 -0
  72. package/templates/godot-2d/project.godot +58 -0
  73. package/templates/godot-2d/scenes/main.tscn +67 -0
  74. package/templates/godot-2d/scripts/autoload/game_config.gd +83 -0
  75. package/templates/godot-2d/scripts/autoload/game_state.gd +44 -0
  76. package/templates/godot-2d/scripts/autoload/telemetry.gd +37 -0
  77. package/templates/godot-2d/scripts/main.gd +32 -0
  78. package/templates/godot-2d/scripts/player.gd +10 -0
  79. package/templates/godot-2d/scripts/state_hook.gd +16 -0
  80. package/templates/godot-2d/template.json +14 -0
  81. package/templates/godot-2d/tools/inject_release_key.sh +19 -0
  82. package/templates/godot-2d/tools/selftest.gd +70 -0
  83. package/templates/godot-3d/README.md +49 -0
  84. package/templates/godot-3d/addons/ravensight/LICENSE +21 -0
  85. package/templates/godot-3d/addons/ravensight/Ravensight.gd +1823 -0
  86. package/templates/godot-3d/export_presets.cfg +27 -0
  87. package/templates/godot-3d/gitignore +10 -0
  88. package/templates/godot-3d/project.godot +56 -0
  89. package/templates/godot-3d/scenes/main.tscn +154 -0
  90. package/templates/godot-3d/scripts/autoload/game_config.gd +83 -0
  91. package/templates/godot-3d/scripts/autoload/game_state.gd +44 -0
  92. package/templates/godot-3d/scripts/autoload/telemetry.gd +37 -0
  93. package/templates/godot-3d/scripts/beacon.gd +20 -0
  94. package/templates/godot-3d/scripts/main.gd +49 -0
  95. package/templates/godot-3d/scripts/player.gd +19 -0
  96. package/templates/godot-3d/scripts/state_hook.gd +25 -0
  97. package/templates/godot-3d/template.json +16 -0
  98. package/templates/godot-3d/tools/inject_release_key.sh +19 -0
  99. 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/*"
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/*"
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,56 @@
1
+ ; Engine configuration file.
2
+ config_version=5
3
+
4
+ [application]
5
+
6
+ config/name="__GAME_NAME__"
7
+ config/description="A placeholder 3D template from ravensight-playtest (Alpha)."
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=1280
23
+ window/size/viewport_height=720
24
+ window/stretch/mode="canvas_items"
25
+ window/stretch/aspect="keep"
26
+
27
+ [input]
28
+
29
+ move_left={
30
+ "deadzone": 0.25,
31
+ "events": [Object(InputEventKey,"physical_keycode":65), Object(InputEventKey,"physical_keycode":4194319), Object(InputEventJoypadMotion,"device":-1,"axis":0,"axis_value":-1.0)]
32
+ }
33
+ move_right={
34
+ "deadzone": 0.25,
35
+ "events": [Object(InputEventKey,"physical_keycode":68), Object(InputEventKey,"physical_keycode":4194321), Object(InputEventJoypadMotion,"device":-1,"axis":0,"axis_value":1.0)]
36
+ }
37
+ move_up={
38
+ "deadzone": 0.25,
39
+ "events": [Object(InputEventKey,"physical_keycode":87), Object(InputEventKey,"physical_keycode":4194320), Object(InputEventJoypadMotion,"device":-1,"axis":1,"axis_value":-1.0)]
40
+ }
41
+ move_down={
42
+ "deadzone": 0.25,
43
+ "events": [Object(InputEventKey,"physical_keycode":83), Object(InputEventKey,"physical_keycode":4194322), Object(InputEventJoypadMotion,"device":-1,"axis":1,"axis_value":1.0)]
44
+ }
45
+ action={
46
+ "deadzone": 0.5,
47
+ "events": [Object(InputEventKey,"physical_keycode":32), Object(InputEventKey,"physical_keycode":69), Object(InputEventJoypadButton,"device":-1,"button_index":0)]
48
+ }
49
+ pause={
50
+ "deadzone": 0.5,
51
+ "events": [Object(InputEventKey,"physical_keycode":4194305), Object(InputEventJoypadButton,"device":-1,"button_index":6)]
52
+ }
53
+
54
+ [rendering]
55
+
56
+ renderer/rendering_method="gl_compatibility"
@@ -0,0 +1,154 @@
1
+ [gd_scene load_steps=19 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
+ [ext_resource type="Script" path="res://scripts/beacon.gd" id="4"]
7
+
8
+ [sub_resource type="Environment" id="sky"]
9
+ background_mode = 1
10
+ background_color = Color(0.1, 0.12, 0.16, 1)
11
+ ambient_light_source = 2
12
+ ambient_light_color = Color(0.6, 0.65, 0.75, 1)
13
+ ambient_light_energy = 0.6
14
+
15
+ [sub_resource type="StandardMaterial3D" id="floor_material"]
16
+ albedo_color = Color(0.25, 0.3, 0.36, 1)
17
+
18
+ [sub_resource type="BoxMesh" id="floor_mesh"]
19
+ material = SubResource("floor_material")
20
+ size = Vector3(20, 1, 20)
21
+
22
+ [sub_resource type="BoxShape3D" id="floor_shape"]
23
+ size = Vector3(20, 1, 20)
24
+
25
+ [sub_resource type="StandardMaterial3D" id="player_material"]
26
+ albedo_color = Color(0.95, 0.75, 0.3, 1)
27
+
28
+ [sub_resource type="CapsuleMesh" id="player_mesh"]
29
+ material = SubResource("player_material")
30
+ radius = 0.4
31
+ height = 1.8
32
+
33
+ [sub_resource type="CapsuleShape3D" id="player_shape"]
34
+ radius = 0.4
35
+ height = 1.8
36
+
37
+ [sub_resource type="StandardMaterial3D" id="beacon_material"]
38
+ albedo_color = Color(0.35, 0.85, 0.7, 1)
39
+ emission_enabled = true
40
+ emission = Color(0.2, 0.6, 0.5, 1)
41
+
42
+ [sub_resource type="SphereMesh" id="beacon_mesh"]
43
+ material = SubResource("beacon_material")
44
+ radius = 0.5
45
+ height = 1.0
46
+
47
+ [sub_resource type="SphereShape3D" id="beacon_shape"]
48
+ radius = 0.8
49
+
50
+ [sub_resource type="StandardMaterial3D" id="crate_material"]
51
+ albedo_color = Color(0.7, 0.45, 0.3, 1)
52
+
53
+ [sub_resource type="BoxMesh" id="crate_mesh"]
54
+ material = SubResource("crate_material")
55
+
56
+ [sub_resource type="BoxShape3D" id="crate_shape"]
57
+
58
+ [node name="Main" type="Node3D"]
59
+ script = ExtResource("1")
60
+
61
+ [node name="Sky" type="WorldEnvironment" parent="."]
62
+ environment = SubResource("sky")
63
+
64
+ [node name="Sun" type="DirectionalLight3D" parent="."]
65
+ transform = Transform3D(0.866025, 0.25, -0.433013, 0, 0.866025, 0.5, 0.5, -0.433013, 0.75, 0, 10, 0)
66
+
67
+ [node name="Camera" type="Camera3D" parent="."]
68
+ transform = Transform3D(1, 0, 0, 0, 0.707107, 0.707107, 0, -0.707107, 0.707107, 0, 14, 14)
69
+ current = true
70
+
71
+ [node name="Floor" type="StaticBody3D" parent="."]
72
+ position = Vector3(0, -0.5, 0)
73
+
74
+ [node name="Mesh" type="MeshInstance3D" parent="Floor"]
75
+ mesh = SubResource("floor_mesh")
76
+
77
+ [node name="Shape" type="CollisionShape3D" parent="Floor"]
78
+ shape = SubResource("floor_shape")
79
+
80
+ [node name="Player" type="CharacterBody3D" parent="."]
81
+ position = Vector3(0, 0.95, 0)
82
+ script = ExtResource("2")
83
+
84
+ [node name="Mesh" type="MeshInstance3D" parent="Player"]
85
+ mesh = SubResource("player_mesh")
86
+
87
+ [node name="Shape" type="CollisionShape3D" parent="Player"]
88
+ shape = SubResource("player_shape")
89
+
90
+ [node name="Beacon" type="Area3D" parent="."]
91
+ position = Vector3(4, 0.9, 0)
92
+ script = ExtResource("4")
93
+
94
+ [node name="Mesh" type="MeshInstance3D" parent="Beacon"]
95
+ mesh = SubResource("beacon_mesh")
96
+
97
+ [node name="Shape" type="CollisionShape3D" parent="Beacon"]
98
+ shape = SubResource("beacon_shape")
99
+
100
+ [node name="Crate" type="RigidBody3D" parent="." groups=["crate"]]
101
+ position = Vector3(-3, 3, -2)
102
+
103
+ [node name="Mesh" type="MeshInstance3D" parent="Crate"]
104
+ mesh = SubResource("crate_mesh")
105
+
106
+ [node name="Shape" type="CollisionShape3D" parent="Crate"]
107
+ shape = SubResource("crate_shape")
108
+
109
+ [node name="StateHook" type="Node" parent="."]
110
+ script = ExtResource("3")
111
+ player_path = NodePath("../Player")
112
+ beacon_path = NodePath("../Beacon")
113
+
114
+ [node name="Interface" type="CanvasLayer" parent="."]
115
+
116
+ [node name="Hud" type="Label" parent="Interface"]
117
+ offset_left = 8.0
118
+ offset_top = 6.0
119
+ offset_right = 632.0
120
+ offset_bottom = 30.0
121
+ text = ""
122
+
123
+ [node name="Settings" type="Control" parent="Interface"]
124
+ layout_mode = 3
125
+ anchors_preset = 15
126
+ anchor_right = 1.0
127
+ anchor_bottom = 1.0
128
+
129
+ [node name="Panel" type="PanelContainer" parent="Interface/Settings"]
130
+ layout_mode = 1
131
+ anchors_preset = 8
132
+ anchor_left = 0.5
133
+ anchor_top = 0.5
134
+ anchor_right = 0.5
135
+ anchor_bottom = 0.5
136
+ offset_left = -160.0
137
+ offset_top = -60.0
138
+ offset_right = 160.0
139
+ offset_bottom = 60.0
140
+
141
+ [node name="Rows" type="VBoxContainer" parent="Interface/Settings/Panel"]
142
+ layout_mode = 2
143
+
144
+ [node name="Title" type="Label" parent="Interface/Settings/Panel/Rows"]
145
+ layout_mode = 2
146
+ text = "Settings"
147
+
148
+ [node name="Analytics" type="CheckBox" parent="Interface/Settings/Panel/Rows"]
149
+ layout_mode = 2
150
+ text = "Share anonymous analytics"
151
+
152
+ [node name="Hint" type="Label" parent="Interface/Settings/Panel/Rows"]
153
+ layout_mode = 2
154
+ 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,20 @@
1
+ extends Area3D
2
+ ## What the player walks to. It moves somewhere else on the floor each time. The first
3
+ ## spot is in the scene; later ones come from the global random numbers, which a
4
+ ## playtest seeds, so a replay sees them land in the same places.
5
+
6
+ signal collected
7
+
8
+ const REACH := 8.0
9
+
10
+ func _ready() -> void:
11
+ body_entered.connect(_on_body_entered)
12
+
13
+ func _on_body_entered(body: Node3D) -> void:
14
+ if body is CharacterBody3D:
15
+ collect()
16
+
17
+ ## One pass of the core loop.
18
+ func collect() -> void:
19
+ position = Vector3(randf_range(-REACH, REACH), position.y, randf_range(-REACH, REACH))
20
+ collected.emit()
@@ -0,0 +1,49 @@
1
+ extends Node3D
2
+ ## The one scene the template starts on: walk to the beacon, it moves, and that is one
3
+ ## pass of the core loop. Fall off the floor and you start again from the middle.
4
+
5
+ const FALL_LIMIT := -10.0
6
+
7
+ @onready var hud: Label = $Interface/Hud
8
+ @onready var settings: Control = $Interface/Settings
9
+ @onready var analytics: CheckBox = $Interface/Settings/Panel/Rows/Analytics
10
+ @onready var player: CharacterBody3D = $Player
11
+ @onready var start_position: Vector3 = $Player.position
12
+
13
+ func _ready() -> void:
14
+ process_mode = Node.PROCESS_MODE_ALWAYS
15
+ for node in [$Player, $Beacon, $Crate]:
16
+ node.process_mode = Node.PROCESS_MODE_PAUSABLE
17
+ settings.visible = false
18
+ analytics.button_pressed = Telemetry.analytics_enabled()
19
+ analytics.toggled.connect(Telemetry.set_analytics_enabled)
20
+ $Beacon.collected.connect(_on_beacon_collected)
21
+ GameState.start_game()
22
+ _refresh()
23
+
24
+ func _unhandled_input(event: InputEvent) -> void:
25
+ if event.is_action_pressed("pause"):
26
+ _toggle_settings()
27
+ get_viewport().set_input_as_handled()
28
+
29
+ func _physics_process(_delta: float) -> void:
30
+ if player.position.y < FALL_LIMIT:
31
+ GameState.record_death()
32
+ player.position = start_position
33
+ player.velocity = Vector3.ZERO
34
+
35
+ func _process(_delta: float) -> void:
36
+ _refresh()
37
+
38
+ func _on_beacon_collected() -> void:
39
+ GameState.add_score(1)
40
+ GameState.complete_core_loop()
41
+
42
+ func _toggle_settings() -> void:
43
+ settings.visible = not settings.visible
44
+ get_tree().paused = settings.visible
45
+ if settings.visible:
46
+ analytics.grab_focus()
47
+
48
+ func _refresh() -> void:
49
+ hud.text = "Score %d Loops %d Esc for settings" % [GameState.score, GameState.core_loops_completed]
@@ -0,0 +1,19 @@
1
+ extends CharacterBody3D
2
+ ## A placeholder player: a capsule that walks on the floor and jumps. Swap the mesh for
3
+ ## your own model. Left and right move along x, up and down along z.
4
+
5
+ const SPEED := 6.0
6
+ const JUMP_SPEED := 5.0
7
+
8
+ var gravity := float(ProjectSettings.get_setting("physics/3d/default_gravity", 9.8))
9
+
10
+ func _physics_process(delta: float) -> void:
11
+ var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")
12
+ velocity.x = direction.x * SPEED
13
+ velocity.z = direction.y * SPEED
14
+ if is_on_floor():
15
+ if Input.is_action_just_pressed("action"):
16
+ velocity.y = JUMP_SPEED
17
+ else:
18
+ velocity.y -= gravity * delta
19
+ move_and_slide()
@@ -0,0 +1,25 @@
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. Positions are in world
5
+ ## units, x and z along the floor and y up.
6
+
7
+ @export var player_path: NodePath
8
+ @export var beacon_path: NodePath
9
+
10
+ func _ready() -> void:
11
+ add_to_group("ravensight_state")
12
+
13
+ func _ravensight_state() -> Dictionary:
14
+ var state := GameState.snapshot()
15
+ var player := get_node_or_null(player_path) as CharacterBody3D
16
+ if player != null:
17
+ state["player_position"] = _rounded(player.position)
18
+ state["player_on_floor"] = player.is_on_floor()
19
+ var beacon := get_node_or_null(beacon_path) as Node3D
20
+ if beacon != null:
21
+ state["beacon_position"] = _rounded(beacon.position)
22
+ return state
23
+
24
+ func _rounded(at: Vector3) -> Array:
25
+ return [snappedf(at.x, 0.01), snappedf(at.y, 0.01), snappedf(at.z, 0.01)]
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "godot-3d",
3
+ "version": "0.1.0",
4
+ "engine": "godot",
5
+ "dimension": "3d",
6
+ "stage": "alpha",
7
+ "min_engine": "4.3",
8
+ "sdk": {
9
+ "version": "0.3.1",
10
+ "source": "https://github.com/Reality-Software-Entertainment/ravensight-godot",
11
+ "tag": "v0.3.1",
12
+ "commit": "997d53edfd40641e88f40fd712df711dbb116f78",
13
+ "path": "addons/ravensight/Ravensight.gd",
14
+ "sha256": "e7a5dc6f5f0edb62c3265867da2e8da74f5ff3ed2145a8b162d77f2dbb69733e"
15
+ }
16
+ }
@@ -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,77 @@
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 root is 3D", scene is Node3D)
66
+ _check("the state says where the player is, in three axes", state.has("player_position") and (state["player_position"] as Array).size() == 3)
67
+ _check("the state says where the beacon is", state.has("beacon_position"))
68
+ var loops: int = game_state.core_loops_completed
69
+ var spot: Vector3 = scene.get_node("Beacon").position
70
+ scene.get_node("Beacon").collect()
71
+ _check("collecting the beacon is one pass of the loop", game_state.core_loops_completed == loops + 1)
72
+ _check("the beacon moves on", scene.get_node("Beacon").position != spot)
73
+ _check("the SDK is the public one", String(sdk.VERSION).length() > 0)
74
+ scene.queue_free()
75
+
76
+ print("selftest %s" % ("failed" if failed > 0 else "passed"))
77
+ quit(1 if failed > 0 else 0)