218 lines
8.8 KiB
GDScript
218 lines
8.8 KiB
GDScript
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"],
|
|
"wreck": [Color(0.55, 0.38, 0.22), "The wreck"],
|
|
"treasure": [Color(0.9, 0.12, 0.1), "X: your treasure"],
|
|
}
|
|
|
|
var world: Node3D
|
|
var player: Node3D
|
|
var _tex: TextureRect
|
|
var _marks: Control
|
|
var _title: Label
|
|
var _facing: Label
|
|
var _others_key: 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)
|
|
_others_key = UITheme.label("● Other players (co-op)", 14)
|
|
_others_key.add_theme_color_override("font_color", Color(0.6, 1.0, 0.65))
|
|
legend.add_child(_others_key)
|
|
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())
|
|
_others_key.visible = world.is_online() and not bool(Net.server_info.get("pvp", true))
|
|
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]
|
|
if m.kind == "treasure":
|
|
# An inked X, as on the old chart.
|
|
for d in [Vector2(1, 1), Vector2(1, -1)]:
|
|
_marks.draw_line(at - d * 8.0, at + d * 8.0, Color(0, 0, 0, 0.8), 5.0)
|
|
_marks.draw_line(at - d * 8.0, at + d * 8.0, col, 3.0)
|
|
continue
|
|
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", "wreck"]:
|
|
_marks.draw_string(font, at + Vector2(9, 5), String(m.label), HORIZONTAL_ALIGNMENT_LEFT, -1, 13, col)
|
|
# Other players in this land: only on a co-op server (with PvP on, the
|
|
# map never gives anyone away). Green dots with their names.
|
|
for o in others(world):
|
|
var at2 := _to_map(o.pos)
|
|
_marks.draw_circle(at2, 5.5, Color(0, 0, 0, 0.8))
|
|
_marks.draw_circle(at2, 4.0, Color(0.45, 0.95, 0.5))
|
|
_marks.draw_string(font, at2 + Vector2(8, 5), String(o.name), HORIZONTAL_ALIGNMENT_LEFT, -1, 13, Color(0.6, 1.0, 0.65))
|
|
# 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 other players to show: [{name, pos}], everyone in this land on a
|
|
## server with PvP off; nobody single-player or with PvP on (rivals and
|
|
## adversarial), so the map can't be used to hunt anyone down.
|
|
static func others(w: Node3D) -> Array:
|
|
var out: Array = []
|
|
if not w.is_online() or bool(Net.server_info.get("pvp", true)) or w.net_sync == null:
|
|
return out
|
|
for peer in w.net_sync.avatars:
|
|
var a: Node3D = w.net_sync.avatars[peer]
|
|
if is_instance_valid(a) and a.visible:
|
|
out.append({"name": String(a.info.get("name", "?")), "pos": a.global_position})
|
|
return out
|
|
|
|
## 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)
|
|
if t.near_stream(x, z, 0.3):
|
|
return Color(0.32, 0.56, 0.66) # (the stream, a little wider than it is so it shows)
|
|
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)
|