diff --git a/README.md b/README.md index 3232d11..09205e1 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,12 @@ https://emberwild-95k.pages.dev, builds on the Gitea releases). Built so far: with a save file per worldlet; on a server, a server per land (the Valley's starts the Cove's on the next UDP port), with chat, the player list, admin commands and sleep across lands, and one clock everywhere. -Next: J2 (each tunnel's own gate and supplies), then the Highlands (by sail). +- 0.6.0, the Cove's own (`docs/WORLDLETS_DESIGN.md` ยง5): the wreck, a + treasure map and a dig for each player, the drowned crew, crabs and gulls, + and a rowing boat to build and moor at the pier (sailing comes with the + Highlands). Report a bug in the pause menu; `docs/PLAYTEST.md` for testers. +Next: after the playtest, J2 (each tunnel's own gate and supplies), then the +Highlands (by sail). Running from the source, `godot --path . -- --dev-worldlets` lets New World start in any worldlet, to try one directly. diff --git a/docs/PLAYTEST.md b/docs/PLAYTEST.md new file mode 100644 index 0000000..b9f2026 --- /dev/null +++ b/docs/PLAYTEST.md @@ -0,0 +1,80 @@ +# Emberwild 0.6.0 playtest + +Thanks for playing. This build is the **Valley** and the **Cove**, and +we'd like to know what breaks, what's confusing, and what's no fun. The +game's player guide is on the download page: https://emberwild-95k.pages.dev + +## Reporting a bug + +Press **Esc** in the game, then **Report a bug**: + +- **Open the bug report page** opens a new issue on the Emberwild Gitea page + in your browser, already filled in with your version, computer, mode, + land, the in-game time and where you are. Say what happened, what you + expected, and how to make it happen again, then press **Create Issue**. + Posting needs a (free) account on the Gitea server, and you need to be + signed in. Nothing is sent from the game itself. +- **Copy the details** puts the same details on your clipboard, if you'd + rather send them by message. +- **Show the log file** opens the folder with `godot.log`. For a crash or an + error message, attach it to the issue (drag it into the comment box). + `godot.log` is the latest run; the last few runs before it are kept + beside it, named with the date and time. + +Where the log is: + +| | | +|---|---| +| Mac | `~/Library/Application Support/Godot/app_userdata/Emberwild/logs/godot.log` | +| Windows | `%APPDATA%\Godot\app_userdata\Emberwild\logs\godot.log` | +| Linux | `~/.local/share/godot/app_userdata/Emberwild/logs/godot.log` | + +A screenshot helps for anything that looks wrong (Cmd+Shift+4 on a Mac, +Win+Shift+S on Windows). + +One problem per issue, please; a second issue is better than a long one. + +## What to try + +Play however you like; these are the parts that are new or that we'd most +like tried. + +**The Valley** +- The tutorial with Old Tam (New World, Tutorial), or start a normal world + and listen for his tips. +- Make a camp, survive a few nights, a winter, a wolf moon. +- Iron, stone building, the barrow, the standing stones and the Old Stag. +- Out on the clifftop, the cracked seam: dig the cliff tunnel down to the + Cove (an antler pickaxe, wood to prop it, lights). + +**The Cove (new in 0.6.0)** +- The wreck at the far end of the beach: the captain's sea chest has a + treasure map for each player. Right-click the map, find your X (N for the + map), and dig it up with a pickaxe. Don't dig at night, or do. +- The pier: mackerel close in, cod from the far end. +- Gulls, after food on the sand and fish on a smoking rack. +- Crabs on the beach from dusk till dawn. +- Build a rowing boat (seagrass rope, pitch from pine resin and salt, + driftwood and nails, at a workbench) and tie it up at the end of the pier. + +**Online** +- Host Game from the title screen, and have a friend join. Over the + internet, forward UDP ports **24242** and **24243** (the Cove has its own + server on the next port). +- Go between the lands together and apart, chat, sleep (everyone in every + land has to be in bed), and leave and come back. + +## Not in this build (these aren't bugs) + +- **Setting sail.** The boat ties up but can't leave yet: the Highlands are + the next part of the journey. +- **Pirate gold** has no use yet beyond being a trophy: there's no trader. +- **The ship's compass** does nothing yet; it's what the boat will need. + +## Things we'd especially like to hear + +- Anywhere you got stuck, or didn't know what to do next. +- Anything that felt too hard, too easy, or too slow (night, hunger, cold, + the drowned crew, the crabs). +- Frame-rate drops, and on what computer. +- Anything online that went out of step between players. diff --git a/docs/WORLDLETS_DESIGN.md b/docs/WORLDLETS_DESIGN.md index 55b02f9..9cb8698 100644 --- a/docs/WORLDLETS_DESIGN.md +++ b/docs/WORLDLETS_DESIGN.md @@ -323,6 +323,28 @@ own set pieces instead of copies of the Valley's: - Order: set pieces per land, the wreck and map, the dig and the drowned crew, crabs and gulls, the boat, report a bug; then 0.6.0 (protocol 18). +**Built (Nov 2026), and where it differs from the plan above:** +- Places: `"places"` in `data/worldlets/*.json` (`world.has_place()`); the + Cove's extra wildlife in `"wildlife"` (`world.has_wildlife()`). +- The wreck (`scripts/world/wreck.gd`): the sea chest gives a map, 4 nails + and 2 salt (no rope or coins: gold comes from the dig). Each X is one of + 12 hiding spots picked from the seed (`world.treasure_spots()`), chosen + per player by their id. +- The dig (`treasure.gd`, `treasure_mound.gd`, `crew_site.gd`): 18 HP, any + pickaxe; three drowned rise round the mound itself (not from the + shallows), at the first blow by night or as the chest comes up by day. +- Crabs (`crab.gd`, `shore.gd`): up to 5 per player on the sand from 17:30 + to 06:30, pinch for 5 within about 2 m, let you go past 6 m; crab meat. + Gulls (`gull.gd`, `thieves.gd`): food on the sand, and fish on a smoking + rack; they take one and fly off (only food, not tools left on the sand). +- The boat (`moorings.gd`, `boat.gd`, `mooring_post.gd`): a rowing boat + item (12 driftwood, 4 rope, 3 pitch, 6 nails), tied up at the post at the + end of the pier rather than placed freely: 6 berths, one boat each, saved + with the Cove. Pine resin drops from felled pines in every land. +- Report a bug (`scripts/core/bug_report.gd`): opens the repo's new-issue + page filled in (the tester posts it, signed in); no token in the game. + Testers' notes: `docs/PLAYTEST.md`. + ## 6. Done already (groundwork) - `world.water_at(x, z)` returns `{type, surface, depth}` for the lake and the sea, and fishing uses it instead of `terrain.water_level`. diff --git a/scripts/core/bug_report.gd b/scripts/core/bug_report.gd new file mode 100644 index 0000000..5b858ce --- /dev/null +++ b/scripts/core/bug_report.gd @@ -0,0 +1,88 @@ +extends RefCounted +## Report a bug (0.6.0, the playtest; pause menu): opens a new issue on the +## Emberwild repo in your browser, already filled in with a title to edit, +## a few questions to answer, and the details we'd ask for anyway (version, +## computer, single-player or online, which land, the day and time, where you +## are). You read it, add what happened, and press Create Issue: nothing is +## sent from the game itself. Posting needs an account on the Gitea server. +## +## The game's log (Godot writes it to user://logs/godot.log) helps with a +## crash or an error: the pause menu shows where it is and opens its folder, +## to attach the file to the issue. + +const REPO := "https://git.psbailey.uk/Admin/Emberwild" + +## An autoload (Net, Updates), looked up at run time: this script is also +## loaded by tests that start before the autoloads are compiled in. +static func _al(n: String) -> Node: + return (Engine.get_main_loop() as SceneTree).root.get_node_or_null(n) + +## Where this game's log is, on disk. +static func log_path() -> String: + return ProjectSettings.globalize_path("user://logs/godot.log") + +## The same, with your home folder as "~" (so your user name isn't in an +## issue anyone can read). +static func log_path_short() -> String: + var home := OS.get_environment("HOME") if OS.get_name() != "Windows" else OS.get_environment("USERPROFILE") + var p := log_path() + if not home.is_empty() and p.begins_with(home): + p = "~" + p.substr(home.length()) + return p + +static func log_dir() -> String: + return ProjectSettings.globalize_path("user://logs") + +## How you're playing: "single-player", or online with the server's rules. +static func mode(world: Node) -> String: + if world == null or not world.has_method("is_online") or not world.is_online(): + return "single-player" + var net := _al("Net") + var rules := "PvP" if bool(net.server_info.get("pvp", false)) else "co-op" + if bool(net.server_info.get("raiding", false)): + rules += ", raiding" + return "online (%s%s)" % [rules, ", hosting" if net.hosting else ""] + +## The details, one per line (what's in the issue, and what "Copy" copies). +static func details(world: Node) -> Dictionary: + var d := { + "Version": "Emberwild %s (protocol %d)" % [String(ProjectSettings.get_setting("application/config/version", "?")), int(_al("Net").PROTOCOL)], + "Computer": "%s %s" % [OS.get_name(), OS.get_version()], + "Graphics": "%s (%s)" % [RenderingServer.get_video_adapter_name(), RenderingServer.get_current_rendering_method()], + "Mode": mode(world), + } + if world != null and is_instance_valid(world) and world.has_method("worldlet_id"): + d["Land"] = preload("res://scripts/world/terrain.gd").worldlet_name(String(world.worldlet_id())) + if world.get("day_night") != null: + var h: float = world.day_night.hours() + d["Time"] = "day %d, %02d:%02d" % [int(world.day_night.day), int(h), int(fmod(h, 1.0) * 60.0)] + if world.get("player") != null and is_instance_valid(world.player): + var p: Vector3 = world.player.global_position + d["Where"] = "%.0f, %.0f, %.0f" % [p.x, p.y, p.z] + if world.get("terrain") != null: + d["Seed"] = str(world.terrain.noise_seed) + return d + +static func details_text(world: Node) -> String: + var d := details(world) + var lines: Array[String] = [] + for k in d: + lines.append("- %s: %s" % [k, d[k]]) + return "\n".join(lines) + +static func body(world: Node) -> String: + return "\n".join([ + "**What happened?**", "", "", + "**What did you expect to happen?**", "", "", + "**How can we make it happen again?** (what you did just before)", "1. ", "", + "**Screenshot or log** (optional: drag a screenshot here, or the log file from %s)" % log_path_short(), "", + "---", details_text(world)]) + +## The new-issue page, filled in. +static func issue_url(world: Node, title: String = "") -> String: + var t := title if not title.is_empty() else "Bug: " + return "%s/issues/new?title=%s&body=%s" % [REPO, t.uri_encode(), body(world).uri_encode()] + +## Opens it in the browser. Returns false when the browser couldn't be opened. +static func open(world: Node) -> bool: + return OS.shell_open(issue_url(world)) == OK diff --git a/scripts/core/bug_report.gd.uid b/scripts/core/bug_report.gd.uid new file mode 100644 index 0000000..66aeca2 --- /dev/null +++ b/scripts/core/bug_report.gd.uid @@ -0,0 +1 @@ +uid://tnlssmningwq diff --git a/scripts/ui/pause_menu.gd b/scripts/ui/pause_menu.gd index e313b66..c41fa80 100644 --- a/scripts/ui/pause_menu.gd +++ b/scripts/ui/pause_menu.gd @@ -1,9 +1,11 @@ extends Control -## Pause menu (ESC): resume, controls, fullscreen, mouse sensitivity, quit. +## Pause menu (ESC): resume, controls, report a bug, fullscreen, mouse +## sensitivity, volumes, quit. ## Pauses the scene tree while open; runs with PROCESS_MODE_ALWAYS so it still ## gets input. const UITheme = preload("res://scripts/ui/theme.gd") +const BugReport = preload("res://scripts/core/bug_report.gd") const CONTROLS := [ ["W A S D", "Move"], ["Shift", "Sprint"], ["Space", "Jump / swim up"], @@ -23,6 +25,8 @@ const CONTROLS := [ var player: CharacterBody3D var _main: VBoxContainer var _controls: VBoxContainer +var _report: VBoxContainer +var _report_note: Label var _quit_btn: Button var _quit_armed := false var _online := false ## on a server: no pausing, no saving here @@ -60,6 +64,7 @@ func bind(p: CharacterBody3D) -> void: _main.add_child(title) _main.add_child(_button("Resume", close)) _main.add_child(_button("Controls", _show_controls.bind(true))) + _main.add_child(_button("Report a bug", _show_report.bind(true))) var fs := CheckButton.new() fs.text = "Fullscreen" @@ -174,6 +179,31 @@ func bind(p: CharacterBody3D) -> void: _controls.add_child(grid) _controls.add_child(_button("Back", _show_controls.bind(false))) + # Report a bug page (0.6.0, the playtest: bug_report.gd). + _report = VBoxContainer.new() + _report.add_theme_constant_override("separation", 10) + _report.visible = false + stack.add_child(_report) + var rt := UITheme.label("Report a bug", 26) + rt.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _report.add_child(rt) + var how := UITheme.label("This opens a new issue on Emberwild's Gitea page in your browser, filled in with your version, computer and where you are in the game. Say what happened, then press Create Issue. (You'll need to be signed in there.) Nothing is sent from the game itself.", 15) + how.autowrap_mode = TextServer.AUTOWRAP_WORD + how.custom_minimum_size = Vector2(380, 0) + _report.add_child(how) + _report.add_child(_button("Open the bug report page", _on_report_open)) + _report.add_child(_button("Copy the details", _on_report_copy)) + _report.add_child(_button("Show the log file", _on_report_log)) + var lp := UITheme.label("For a crash or an error, attach the log:\n%s" % BugReport.log_path(), 13, UITheme.TEXT_DIM) + lp.autowrap_mode = TextServer.AUTOWRAP_ARBITRARY + lp.custom_minimum_size = Vector2(380, 0) + _report.add_child(lp) + _report_note = UITheme.label("", 14, UITheme.SELECTED) + _report_note.autowrap_mode = TextServer.AUTOWRAP_WORD + _report_note.custom_minimum_size = Vector2(380, 0) + _report.add_child(_report_note) + _report.add_child(_button("Back", _show_report.bind(false))) + func _button(text: String, cb: Callable) -> Button: var b := Button.new() b.text = text @@ -189,6 +219,7 @@ func is_open() -> bool: func open() -> void: visible = true _show_controls(false) + _show_report(false) _disarm_quit() player.ui_open = true get_tree().paused = not _online # a shared world can't be paused by one player @@ -215,10 +246,12 @@ func close() -> void: get_tree().paused = false Input.mouse_mode = Input.MOUSE_MODE_CAPTURED -## ESC on the controls page goes back; on the main page it resumes. +## ESC on the controls or bug report page goes back; on the main page it resumes. func back() -> void: if _controls.visible: _show_controls(false) + elif _report.visible: + _show_report(false) else: close() @@ -226,6 +259,31 @@ func _show_controls(on: bool) -> void: _controls.visible = on _main.visible = not on +func _show_report(on: bool) -> void: + _report.visible = on + _main.visible = not on + _report_note.text = "" + +func _world() -> Node: + return player.get_parent() if player != null else null + +func _on_report_open() -> void: + if DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_FULLSCREEN: + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) # (so the browser isn't hidden behind the game) + if BugReport.open(_world()): + _report_note.text = "Opened in your browser. (Can't post there? Copy the details and send them to whoever gave you the game.)" + else: + DisplayServer.clipboard_set(BugReport.issue_url(_world())) + _report_note.text = "Couldn't open a browser: the link's on your clipboard to paste into one." + +func _on_report_copy() -> void: + DisplayServer.clipboard_set(BugReport.details_text(_world())) + _report_note.text = "Copied: paste them into your message." + +func _on_report_log() -> void: + OS.shell_open(BugReport.log_dir()) + _report_note.text = "The log is godot.log in that folder." + func _on_fullscreen(on: bool) -> void: DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN if on else DisplayServer.WINDOW_MODE_WINDOWED) diff --git a/tests/run_all.sh b/tests/run_all.sh index eb08934..c920f41 100755 --- a/tests/run_all.sh +++ b/tests/run_all.sh @@ -9,7 +9,7 @@ if [ ! -f .godot/global_script_class_cache.cfg ]; then echo "Importing the project first (fresh copy)..." godot --headless --path . --import >/dev/null 2>&1 fi -for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_alpha test_bear test_thieves test_barrow test_boss test_den_sites test_fishing_diet test_food test_hazards test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_boss test_net_rules test_net_lands test_host test_tutorial test_tutorial_full test_hints test_updates test_audio test_items_more test_worldlets test_worldlet_pick test_water_bodies test_cove test_wreck test_shore test_boat test_tunnel test_map test_terrain_edges test_boundary; do +for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_alpha test_bear test_thieves test_barrow test_boss test_den_sites test_fishing_diet test_food test_hazards test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_boss test_net_rules test_net_lands test_host test_tutorial test_tutorial_full test_hints test_updates test_audio test_items_more test_worldlets test_worldlet_pick test_water_bodies test_cove test_wreck test_shore test_boat test_bug_report test_tunnel test_map test_terrain_edges test_boundary; do extra="" [ "$t" = "test_boundary" ] && extra="--fixed-fps 30" # A suite that waits on a world that failed to load would hang forever, diff --git a/tests/test_bug_report.gd b/tests/test_bug_report.gd new file mode 100644 index 0000000..8acbab1 --- /dev/null +++ b/tests/test_bug_report.gd @@ -0,0 +1,76 @@ +extends SceneTree +## 0.6.0, the playtest: Report a bug. The pause menu has a "Report a bug" +## page; its link is a new issue on the Emberwild repo, filled in with +## questions to answer and the details (version, computer, mode, land, time, +## where you are), short enough for a browser; the log's path is shown. +## Run: godot --headless --path . --script res://tests/test_bug_report.gd + +const BugReport = preload("res://scripts/core/bug_report.gd") + +var _fails := 0 +var _finished := false +var S: Node + +func check(cond: bool, what: String) -> void: + print((" ok " if cond else " FAIL ") + what) + if not cond: + _fails += 1 + +func _initialize() -> void: + _run.call_deferred() + +func _run() -> void: + S = root.get_node("Saves") + S.root_dir = "user://test_bug_worlds" + for x in S.list_worlds(): + S.delete_world(x.id) + await _body() + check(_finished, "test ran to the end (no script errors)") + for x in S.list_worlds(): + S.delete_world(x.id) + DirAccess.remove_absolute(S.root_dir) + print("RESULT: %s (%d failures)" % ["PASS" if _fails == 0 else "FAIL", _fails]) + quit(1 if _fails > 0 else 0) + +func _body() -> void: + S.current_id = S.create_world("Cove", 1337, {"worldlet": "cove"}) + var w: Node3D = load("res://scenes/world.tscn").instantiate() + root.add_child(w) + for _i in range(8): + await physics_frame + w.creatures.enabled = false + w.player.set_physics_process(false) + + print("the link") + var url: String = BugReport.issue_url(w) + check(url.begins_with("https://git.psbailey.uk/Admin/Emberwild/issues/new?title=Bug%3A%20&body="), "a new issue on the Emberwild repo") + check(url.length() < 4000, "short enough for any browser (%d characters)" % url.length()) + var body := url.get_slice("&body=", 1).uri_decode() + var ver: String = String(ProjectSettings.get_setting("application/config/version")) + for want in ["**What happened?**", "**How can we make it happen again?**", "- Version: Emberwild %s" % ver, + "- Computer: ", "- Mode: single-player", "- Land: Cove", "- Time: day ", "- Where: ", "- Seed: 1337", "godot.log"]: + check(body.contains(want), "it says %s" % want) + check(BugReport.log_path().ends_with("logs/godot.log") and BugReport.log_path().begins_with("/"), "the log's full path (%s)" % BugReport.log_path()) + check(not BugReport.details_text(w).contains("**"), "Copy gives just the details") + check(not body.contains(OS.get_environment("HOME")) and body.contains("~/"), "and not your user name (the home folder is ~)") + + print("the pause menu") + var menu: Control = load("res://scripts/ui/pause_menu.gd").new() # (headless there's no HUD: build the menu as it does) + w.add_child(menu) + menu.bind(w.player) + var btn: Button = null + for c in menu._main.get_children(): + if c is Button and c.text == "Report a bug": + btn = c + check(btn != null, "a Report a bug button") + if btn != null: + menu.open() + btn.pressed.emit() + check(menu._report.visible and not menu._main.visible, "it opens the Report a bug page") + var labels: Array = menu._report.get_children().filter(func(c): return c is Button).map(func(c): return c.text) + check("Open the bug report page" in labels and "Copy the details" in labels and "Show the log file" in labels, "with %s" % [labels]) + menu.back() + check(menu._main.visible and not menu._report.visible, "Esc goes back") + menu.close() + menu.queue_free() + _finished = true diff --git a/tests/test_bug_report.gd.uid b/tests/test_bug_report.gd.uid new file mode 100644 index 0000000..5502354 --- /dev/null +++ b/tests/test_bug_report.gd.uid @@ -0,0 +1 @@ +uid://bqvh4okrloluq diff --git a/tools/site/guide_sections.md b/tools/site/guide_sections.md index 817d18c..a36a8f0 100644 --- a/tools/site/guide_sections.md +++ b/tools/site/guide_sections.md @@ -349,6 +349,13 @@ Low hills run down to a sheltered sandy bay, with mountains behind, a small pond - **The pier** runs out over the bay. Fish off its end for **mackerel**, the usual catch. **Cod** are bigger but only bite where the water's at least 2.2 m deep, so cast well out. Both bite best at dawn and dusk, and grill at a campfire, salt or smoke like lake fish. - **The sea cave** at the end of the beach is the way back: `E` goes into it and up the same tunnel, arriving at the bottom. +- **The beach** has its own pickings: **driftwood** (it burns, and it's what a boat is built from), **salt** crusted above the waterline, and **seagrass** at the tide line. +- **The wreck** lies at the far end of the bay. Inside is the captain's **sea chest**: a torn **treasure map**, 4 nails and 2 salt, once for each player. +- **The treasure:** right-click the map to read it, and an **X** goes on your map (`N`). Every player's map has its own X. There, only you can see a low mound by a marker stone; dig it with any pickaxe (18 blows with a stone one). Inside the pirate chest are the **ship's compass**, a **spyglass** (hold right-click to look through it), a **cutlass** (quick, and harder-hitting than the iron spear) and **pirate gold** (nothing takes coin yet, but you can set a heap of it in your hut). The map's used up. +- **The drowned crew:** three of the ship's crew guard it. Dig at night and they rise at the first blow; by day, when the chest comes up. They're slow but hit hard; bone, so blunt weapons do more. +- **Gulls** are the Cove's crows: by day they come for food left on the sand, and for fish on a smoking rack that nobody's watching. A gull snatches one and flies off with it; shoot it down and it drops it. +- **Crabs** come up the beach from dusk till dawn and keep to the sand. Walk within a couple of metres and one comes at you and pinches; back off and it lets you go. A dead one gives **crab meat**: two cook into a **cooked crab** at a campfire. At dawn they go back into the sea. +- **The rowing boat:** twist 3 seagrass into a **rope** by hand; boil 2 **pine resin** (from felled pines, in any land) and a salt into **pitch** at a campfire. A **rowing boat** is 12 driftwood, 4 rope, 3 pitch and 6 nails at a workbench. Take it to the **mooring post** at the end of the pier and `E` ties it up at a berth of its own, one boat each. Its **Set sail** needs the ship's compass, and even then the way to the Highlands isn't charted yet: that's the next part of the journey. Each land keeps its own state, and there's one clock for them all. What you build in the Valley waits for you while you're away, and when you come back the time you were gone catches up, as after a night's sleep: kilns and smelters will have done their work, fires burned down and food gone off. Nothing roams a land while you're away, so your base won't have been raided. @@ -411,7 +418,7 @@ Each extra player at the stones when it's called gives the Old Stag 60% more hea ### The pause menu -`Esc` opens it (and pauses your own world). It has **Resume**, **Controls**, **Fullscreen**, **Mouse sensitivity**, volumes for **Music**, **Sounds**, **Ambience** and **Old Tam's voice**, **Old Tam's tips** on or off with **Show again**, **Check for updates at start**, and **Save & Quit** to the title or desktop. The game autosaves every 2 minutes and when you sleep. In the tutorial it also offers **Skip this tutorial step** and **Leave the tutorial**. +`Esc` opens it (and pauses your own world). It has **Resume**, **Controls**, **Report a bug**, **Fullscreen**, **Mouse sensitivity**, volumes for **Music**, **Sounds**, **Ambience** and **Old Tam's voice**, **Old Tam's tips** on or off with **Show again**, **Check for updates at start**, and **Save & Quit** to the title or desktop. The game autosaves every 2 minutes and when you sleep. In the tutorial it also offers **Skip this tutorial step** and **Leave the tutorial**. ### Old Tam's tips @@ -421,6 +428,10 @@ In your own worlds Old Tam offers a one-off tip the first time something comes u The music follows what's happening: calm by day, a night theme after dark, a storm theme in storms, blizzards and wolf moons, danger music when something is coming for you, and a boss theme for the Old Stag. Between pieces there's ambience: birds by day, crickets and owls at night. Indoors everything is quieter, and the music dips while Old Tam speaks. +### Reporting a bug + +**Report a bug** in the pause menu opens a new issue on Emberwild's [Gitea page](https://git.psbailey.uk/Admin/Emberwild/issues) in your browser, already filled in with your version, computer, mode, land, the in-game time and where you are. Say what happened and how to make it happen again, then press **Create Issue** (you'll need to be signed in there). Nothing is sent from the game itself. **Copy the details** puts the same details on your clipboard instead, and **Show the log file** opens the folder with `godot.log`: attach it for a crash or an error. + ### Updates With the update check on, the title screen tells you when a newer Emberwild is out and offers a **Download** button or the release page. diff --git a/tools/site/make_guide.py b/tools/site/make_guide.py index 807ea83..ee84305 100644 --- a/tools/site/make_guide.py +++ b/tools/site/make_guide.py @@ -21,6 +21,8 @@ def inline(s): s = esc(s) s = re.sub(r"`([^`]+)`", r"\1", s) s = re.sub(r"\*\*([^*]+)\*\*", r"\1", s) + # [text](https://...) links (after esc: the URL has no quotes or brackets). + s = re.sub(r"\[([^\]]+)\]\((https://[^)\s\"<>]+)\)", r'\1', s) return s