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 var _key: Dictionary = {} ## mark kind -> its line in the key 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) _key[k] = 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)) # The key lists this land's places only (the Cove has no barrow...). var here := {"den": world.den != null and world.den.found, "barrow": world.barrow != null, "stones": world.stones != null, "seam": world.tunnel != null, "pier": world.pier != null, "cave": world.sea_cave != null, "wreck": world.wreck != null, "treasure": world.wreck != null and player.inventory.count("treasure_map") > 0} for k in _key: _key[k].visible = bool(here.get(k, 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)