0.4.0: map (N) that fills in as you explore and a compass (closes #1); Old Tam's seam and Cove tips (voiced); the journey and sea fish in the guide; trailer with the tunnel and the Cove; protocol 16

This commit is contained in:
Paul Bailey
2026-11-06 18:36:20 +00:00
parent 980ee53579
commit 2b7e52cd82
28 changed files with 678 additions and 43 deletions

53
scripts/ui/compass.gd Normal file
View File

@@ -0,0 +1,53 @@
extends Control
## The compass (Gitea issue #1): a strip across the top of the screen that
## shows which way you're facing, N/E/S/W and the points between, with a tick
## at the centre. North is the map's top (-Z).
const UITheme = preload("res://scripts/ui/theme.gd")
const WIDTH := 420.0
const SPAN := 180.0 ## degrees shown across the strip
const POINTS := [[0, "N"], [45, "NE"], [90, "E"], [135, "SE"], [180, "S"], [225, "SW"], [270, "W"], [315, "NW"]]
var player: Node3D
func setup(p: Node3D) -> void:
player = p
mouse_filter = Control.MOUSE_FILTER_IGNORE
set_anchors_preset(Control.PRESET_CENTER_TOP)
offset_left = -WIDTH * 0.5
offset_right = WIDTH * 0.5
offset_top = 44.0
offset_bottom = 74.0
## Which way `p` faces, in degrees clockwise from north (0 = N, 90 = E).
static func heading(p: Node3D) -> float:
var f: Vector3 = -p.global_basis.z
return fposmod(rad_to_deg(atan2(f.x, -f.z)), 360.0)
func _process(_d: float) -> void:
visible = player != null and is_instance_valid(player) and not bool(player.get("ui_open"))
queue_redraw()
func _draw() -> void:
if player == null or not is_instance_valid(player):
return
var h := heading(player)
var font := get_theme_default_font()
draw_rect(Rect2(Vector2.ZERO, size), Color(0, 0, 0, 0.35))
for deg in range(0, 360, 15):
var off := wrapf(deg - h, -180.0, 180.0)
if absf(off) > SPAN * 0.5:
continue
var x := WIDTH * 0.5 + off / SPAN * WIDTH
var big := deg % 45 == 0
draw_line(Vector2(x, 0), Vector2(x, 6 if not big else 9), Color(1, 1, 1, 0.5 if not big else 0.8), 1.0)
for pt in POINTS:
var off := wrapf(float(pt[0]) - h, -180.0, 180.0)
if absf(off) > SPAN * 0.5 - 6.0:
continue
var x := WIDTH * 0.5 + off / SPAN * WIDTH
var s := 17 if String(pt[1]).length() == 1 else 13
var col := Color(1.0, 0.55, 0.35) if pt[1] == "N" else Color(1, 1, 1, 0.9)
var w := font.get_string_size(pt[1], HORIZONTAL_ALIGNMENT_CENTER, -1, s).x
draw_string(font, Vector2(x - w * 0.5, 25), pt[1], HORIZONTAL_ALIGNMENT_LEFT, -1, s, col)
draw_line(Vector2(WIDTH * 0.5, 0), Vector2(WIDTH * 0.5, 30), Color(1.0, 0.85, 0.4, 0.9), 2.0)

View File

@@ -0,0 +1 @@
uid://b86dwl7mmrk2y

View File

@@ -21,6 +21,8 @@ var _diet_bars: Dictionary = {}
var _status: Label
var _clock: Label
var _hurt: ColorRect
var compass: Control
var map: Control
## A red flash at the screen edge when something hits you.
func _on_hurt(amount: float, _from: Vector3) -> void:
@@ -196,6 +198,15 @@ func _build_stats(root: Control) -> void:
tips.name = "HintCard"
root.add_child(tips)
tips.setup()
# The compass along the top, and the map on N (Gitea issue #1).
compass = preload("res://scripts/ui/compass.gd").new()
compass.name = "Compass"
root.add_child(compass)
compass.setup(player)
map = preload("res://scripts/ui/map_screen.gd").new()
map.name = "Map"
root.add_child(map)
map.setup(player.get_parent(), player)
func _process(_d: float) -> void:
_fade_chat()
@@ -387,7 +398,9 @@ func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_ESCAPE:
if is_dead_screen():
return # (respawn first)
if _screen.is_open():
if map != null and map.is_open():
map.close()
elif _screen.is_open():
_screen.close()
elif _menu.is_open():
_menu.back()
@@ -398,6 +411,13 @@ func _input(event: InputEvent) -> void:
func _unhandled_input(event: InputEvent) -> void:
if _menu.is_open():
return
if event.is_action_pressed("map") and not _screen.is_open() and not is_dead_screen() \
and (map.is_open() or not player.ui_open):
map.toggle()
get_viewport().set_input_as_handled()
return
if map.is_open():
return
if event.is_action_pressed("inventory") and not _screen.is_open():
_screen.open(null)
get_viewport().set_input_as_handled()

182
scripts/ui/map_screen.gd Normal file
View File

@@ -0,0 +1,182 @@
extends Control
## The map (Gitea issue #1), on N. The worldlet you're in, drawn from its
## terrain (water, sand, grass, rock, snow, shaded by slope), but only where
## you've been: the rest is blank until you explore it (world.explored).
## Marked where you've seen them: the den, the barrow, the standing stones,
## the tunnel's seam, the pier and sea cave, your beds and buildings, your
## camp (fires, chests, benches) and your grave. You're a dot; which way
## you're facing is on the compass (and under the map). North is up.
const UITheme = preload("res://scripts/ui/theme.gd")
const Compass = preload("res://scripts/ui/compass.gd")
const RES := 200 ## map image pixels per side
const SIDE := 500.0 ## map size on screen
const MARKS := {
"den": [Color(0.85, 0.30, 0.25), "Wolf den"],
"barrow": [Color(0.75, 0.70, 0.55), "Barrow"],
"stones": [Color(0.80, 0.80, 0.85), "Standing stones"],
"seam": [Color(0.95, 0.60, 0.25), "Cliff tunnel"],
"pier": [Color(0.70, 0.50, 0.30), "Pier"],
"cave": [Color(0.95, 0.60, 0.25), "Sea cave"],
"bed": [Color(0.40, 0.75, 1.00), "Bed"],
"build": [Color(0.62, 0.45, 0.28), "Your buildings"],
"camp": [Color(1.00, 0.80, 0.35), "Camp"],
"grave": [Color(0.85, 0.85, 0.85), "Your things"],
}
var world: Node3D
var player: Node3D
var _tex: TextureRect
var _marks: Control
var _title: Label
var _facing: Label
func setup(w: Node3D, p: Node3D) -> void:
world = w
player = p
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
mouse_filter = Control.MOUSE_FILTER_STOP
visible = false
var dim := ColorRect.new()
dim.color = Color(0, 0, 0, 0.6)
dim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(dim)
var centre := CenterContainer.new()
centre.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
centre.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(centre)
var box := VBoxContainer.new()
box.add_theme_constant_override("separation", 6)
centre.add_child(box)
_title = UITheme.label("", 22)
_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
box.add_child(_title)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 14)
box.add_child(row)
var frame := Control.new()
frame.custom_minimum_size = Vector2(SIDE, SIDE)
row.add_child(frame)
_tex = TextureRect.new()
_tex.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_tex.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
_tex.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
frame.add_child(_tex)
_marks = Control.new()
_marks.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_marks.mouse_filter = Control.MOUSE_FILTER_IGNORE
_marks.draw.connect(_draw_marks)
frame.add_child(_marks)
var legend := VBoxContainer.new()
legend.add_theme_constant_override("separation", 4)
row.add_child(legend)
legend.add_child(UITheme.label("Key", 16, UITheme.TEXT_DIM))
var you := UITheme.label("● You", 14)
you.add_theme_color_override("font_color", Color(1, 1, 1))
legend.add_child(you)
for k in MARKS:
var l := UITheme.label("■ %s" % MARKS[k][1], 14)
l.add_theme_color_override("font_color", MARKS[k][0])
legend.add_child(l)
_facing = UITheme.label("", 15, UITheme.TEXT_DIM)
_facing.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
box.add_child(_facing)
var help := UITheme.label("N or Esc to close · north is up · the map fills in as you explore", 13, UITheme.TEXT_DIM)
help.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
box.add_child(help)
func is_open() -> bool:
return visible
func open() -> void:
_tex.texture = ImageTexture.create_from_image(render(world))
_title.text = "The %s" % preload("res://scripts/world/terrain.gd").worldlet_name(world.worldlet_id())
visible = true
player.ui_open = true
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
func close() -> void:
visible = false
player.ui_open = false
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func toggle() -> void:
if visible:
close()
else:
open()
func _process(_d: float) -> void:
if not visible:
return
var h: float = Compass.heading(player)
var names := ["north", "north-east", "east", "south-east", "south", "south-west", "west", "north-west"]
_facing.text = "Facing %s" % names[int(round(h / 45.0)) % 8]
if world.underground_at(player.global_position):
_facing.text += " · in the tunnel, under the cliff"
_marks.queue_redraw()
## Where (x, z) in the world falls on the map, in its frame's pixels.
func _to_map(p: Vector3) -> Vector2:
var s: float = world.terrain.size
return Vector2((p.x + s * 0.5) / s, (p.z + s * 0.5) / s) * SIDE
func _draw_marks() -> void:
var font := get_theme_default_font()
for m in world.map_marks():
var at := _to_map(m.pos)
var col: Color = MARKS.get(m.kind, [Color.WHITE])[0]
var r := 3.0 if m.kind == "build" else 6.0
_marks.draw_rect(Rect2(at - Vector2(r, r), Vector2(r, r) * 2.0), col)
_marks.draw_rect(Rect2(at - Vector2(r, r), Vector2(r, r) * 2.0), Color(0, 0, 0, 0.7), false, 1.0)
if m.kind in ["den", "barrow", "stones", "seam", "pier", "cave"]:
_marks.draw_string(font, at + Vector2(9, 5), String(m.label), HORIZONTAL_ALIGNMENT_LEFT, -1, 13, col)
# You: a dot (the way you're facing is on the compass).
var p := player.global_position
if world.underground_at(p) and world.tunnel != null:
p = world.tunnel.seam_at # (down the tunnel: shown at its top)
var me := _to_map(p)
_marks.draw_circle(me, 6.5, Color(0, 0, 0, 0.8))
_marks.draw_circle(me, 5.0, Color(1, 1, 1))
# North.
_marks.draw_string(font, Vector2(SIDE - 26, 24), "N", HORIZONTAL_ALIGNMENT_LEFT, -1, 18, Color(1.0, 0.55, 0.35))
_marks.draw_line(Vector2(SIDE - 20, 30), Vector2(SIDE - 20, 48), Color(1.0, 0.55, 0.35), 2.0)
## The map image: the terrain, coloured and shaded, blank where unexplored.
static func render(w: Node3D) -> Image:
var t: Node3D = w.terrain
var img := Image.create(RES, RES, false, Image.FORMAT_RGB8)
var s: float = t.size
var step := s / RES
var blank := Color(0.16, 0.14, 0.12)
for iy in range(RES):
for ix in range(RES):
var x := -s * 0.5 + (ix + 0.5) * step
var z := -s * 0.5 + (iy + 0.5) * step
if not w.seen(x, z):
# (a faint paper grain, so blank reads as "not yet", not "nothing")
img.set_pixel(ix, iy, blank.lightened(0.04 * float((ix * 7 + iy * 13) % 3) / 2.0))
continue
img.set_pixel(ix, iy, ground_colour(t, x, z))
return img
static func ground_colour(t: Node3D, x: float, z: float) -> Color:
var h: float = t.sample_height(x, z)
var surface: float = t.water_level if t.inside_boundary(x, z, -2.0) else t.sea_level
if h < surface:
var deep := clampf((surface - h) / 6.0, 0.0, 1.0)
return Color(0.30, 0.52, 0.62).lerp(Color(0.10, 0.22, 0.36), deep)
var n: Vector3 = t.sample_normal(x, z)
var c: Color
if h > t.snow_height:
c = Color(0.92, 0.93, 0.95)
elif n.y < 0.75:
c = Color(0.48, 0.46, 0.43) # rock
elif h < surface + 0.9:
c = Color(0.80, 0.74, 0.55) # shore
else:
c = Color(0.36, 0.52, 0.27).lerp(Color(0.46, 0.50, 0.32), clampf(h / 20.0, 0.0, 1.0))
# Shading from the north-west, so hills and cliffs stand out.
var light := clampf(0.75 + (-n.x - n.z) * 0.9, 0.45, 1.25)
return Color(c.r * light, c.g * light, c.b * light)

View File

@@ -0,0 +1 @@
uid://b80va7jn1pyxo

View File

@@ -15,6 +15,7 @@ const CONTROLS := [
["E on a bed", "Set spawn point / sleep (night, under a roof)"],
["1 - 8, wheel", "Hotbar (the bag row you're on)"], ["K / M", "Hotbar shows the bag row above / below"],
["Q (shift+Q)", "Drop what you're holding (just one)"], ["Tab", "Inventory & crafting (shift-click Craft for 5)"],
["N", "Map (fills in as you explore; the compass is along the top)"],
["Shift + click", "Move stack to/from a chest"], ["Right click", "Split stack"],
["Esc", "This menu (game autosaves every 2 min)"],
]