Files
mtgodot-poc/project/ui/minimap.gd
T
shenandClaude Sonnet 5 47baf6c0c6 Metin2 game client (P0–P11) + mobile asset pipeline
Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
  phases, EntityStore world model, ~all GC/CG headers. char create/delete,
  private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
  quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
  char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
  token), system-option + game-option + ESC system menu, private-shop 39-grid,
  party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
  dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.

Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.

Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).

ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
2026-08-31 20:02:12 +09:00

158 lines
6.2 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Minimap (P9) —— 右上角圆形小地图。北朝上。
#
# var mm := preload("res://ui/minimap.gd").new()
# add_child(mm)
# mm.setup(m2client, canvas_parent, func() -> Node3D: return player)
#
# 画:底圆 + 外环 + 玩家箭头 + 实体蓝点 + 怪红点 + NPC 黄点 + 传送青点 + 任务标记橙星 +
# 观战者紫色菱形 + 公会领地边框;支持 SetScale / 鼠标滚轮缩放。
# 世界→图:Δ = blip.xz - player.xz,按 scale 转像素,(+x 右, -z 上),超出半径夹到边缘。
extends Node
const RADIUS := 70.0
const SCALE := 0.25 # 像素/米(越大越放大)
const NPC_WARP_TYPE := 2 # NPCMark.type: 2 = 传送
const MIN_SCALE := 0.10
const MAX_SCALE := 1.25
var client: Node
var player_getter: Callable
var _root: Control
var _view: Control
var _ch_label: Label
var _scale := SCALE
func setup(m2client: Node, parent: Node, get_player: Callable) -> void:
client = m2client
player_getter = get_player
_build(parent)
if client and client.has_signal("channel_changed"):
client.channel_changed.connect(func(c): _ch_label.text = "CH %d" % c)
func _build(parent: Node) -> void:
_root = Control.new()
_root.set_anchors_preset(Control.PRESET_TOP_RIGHT)
_root.position = Vector2(-2 * RADIUS - 16, 12)
_root.size = Vector2(2 * RADIUS, 2 * RADIUS + 16)
parent.add_child(_root)
_view = Control.new()
_view.size = Vector2(2 * RADIUS, 2 * RADIUS)
_view.mouse_filter = Control.MOUSE_FILTER_IGNORE
_view.draw.connect(_draw_map)
_root.add_child(_view)
_root.gui_input.connect(_on_gui_input)
_root.mouse_filter = Control.MOUSE_FILTER_STOP
_ch_label = Label.new()
_ch_label.position = Vector2(0, 2 * RADIUS)
_ch_label.add_theme_font_size_override("font_size", 11)
_ch_label.text = "CH -"
_root.add_child(_ch_label)
func _process(_dt: float) -> void:
if _view:
_view.queue_redraw()
# Matches CPythonMiniMap::SetScale: callers may provide pixels per world metre.
# The wheel gesture is intentionally local to the minimap so world controls do
# not change when the pointer is elsewhere.
func set_scale(pixels_per_meter: float) -> void:
_scale = clampf(pixels_per_meter, MIN_SCALE, MAX_SCALE)
if _view:
_view.queue_redraw()
func get_scale() -> float:
return _scale
func _on_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed:
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
set_scale(_scale * 1.15)
_root.accept_event()
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
set_scale(_scale / 1.15)
_root.accept_event()
func _player() -> Node3D:
return player_getter.call() if player_getter.is_valid() else null
func _to_map(world: Vector3, origin: Vector3) -> Vector2:
var d := world - origin
# screen: +x right, +y down. world z = north -> screen up (y). world +x = east -> right.
var p := Vector2(d.x, d.z) * _scale
if p.length() > RADIUS - 4.0:
p = p.normalized() * (RADIUS - 4.0)
return Vector2(RADIUS, RADIUS) + p
func _draw_map() -> void:
var c := Vector2(RADIUS, RADIUS)
_view.draw_circle(c, RADIUS, Color(0.05, 0.06, 0.08, 0.82))
_view.draw_arc(c, RADIUS - 1.5, 0, TAU, 48, Color(0.6, 0.6, 0.7, 0.7), 2.0)
var pl := _player()
if pl == null or client == null:
return
var origin: Vector3 = pl.global_position
var party_vids := {}
if client.has_method("get_party"):
for member in client.get_party():
var pvid := int(member.get("vid", 0))
if pvid != 0:
party_vids[pvid] = bool(member.get("leader", false))
# 实体:怪红、其它蓝
if client.has_method("get_entities"):
for e in client.get_entities():
if bool(e.get("is_main", false)):
continue
var pos: Vector3 = e.get("pos", Vector3.ZERO)
var vid := int(e.get("vid", 0))
var map_pos := _to_map(MapCoord.to_world(pos), origin)
var col := Color(0.35, 1.0, 0.55) if party_vids.has(vid) \
else Color(0.4, 0.6, 1.0) if int(e.get("ch_type", 0)) != 2 else Color(1.0, 0.35, 0.3)
_view.draw_circle(map_pos, 2.5, col)
if bool(party_vids.get(vid, false)):
_view.draw_arc(map_pos, 4.5, 0.0, TAU, 16, Color(1.0, 0.9, 0.25, 0.9), 1.0)
# NPC 标记:传送青、其它黄
if client.has_method("get_npc_marks"):
for m in client.get_npc_marks():
var col := Color(0.3, 0.9, 0.9) if int(m.get("type", 0)) == NPC_WARP_TYPE else Color(1.0, 0.85, 0.2)
_view.draw_circle(_to_map(MapCoord.to_world(m.get("pos", Vector3.ZERO)), origin), 3.0, col)
# 观战者:原版在小地图上显示独立的观察点(紫色菱形)。
if client.has_method("get_observers"):
for o in client.get_observers():
var op := _to_map(MapCoord.to_world(o.get("pos", Vector3.ZERO)), origin)
var diamond := PackedVector2Array([op + Vector2(0, -4), op + Vector2(4, 0),
op + Vector2(0, 4), op + Vector2(-4, 0)])
_view.draw_colored_polygon(diamond, Color(0.8, 0.35, 1.0, 0.95))
# 公会领地:GC_LAND_LIST 的全局厘米矩形,在小地图上画细边框;
# guild_id=0 使用中性青色,其余领地使用金色。
if client.has_method("get_land_areas"):
for a in client.get_land_areas():
var x := float(a.get("x", 0))
var y := float(a.get("y", 0))
var w := float(a.get("width", 0))
var h := float(a.get("height", 0))
var p0 := _to_map(MapCoord.to_world(Vector3(x * 0.01, 0, -y * 0.01)), origin)
var p1 := _to_map(MapCoord.to_world(Vector3((x + w) * 0.01, 0, -(y + h) * 0.01)), origin)
var rect := Rect2(Vector2(min(p0.x, p1.x), min(p0.y, p1.y)),
Vector2(abs(p1.x - p0.x), abs(p1.y - p0.y)))
var col := Color(0.3, 0.9, 0.9, 0.75) if int(a.get("guild_id", 0)) == 0 \
else Color(1.0, 0.75, 0.2, 0.85)
_view.draw_rect(rect, col, false, 1.0)
# 任务标记:橙星(小方块代替)
if client.has_method("get_world_markers"):
for m in client.get_world_markers():
var mp := _to_map(MapCoord.to_world(m.get("pos", Vector3.ZERO)), origin)
_view.draw_rect(Rect2(mp - Vector2(3, 3), Vector2(6, 6)), Color(1.0, 0.6, 0.1))
# 玩家箭头(朝向)
var yaw: float = pl.rotation.y
var fwd := Vector2(sin(yaw), cos(yaw)) # world +? -> map;与 _to_map 的 (x,-z) 对齐
var tip := c + fwd * 8.0
var left := c + fwd.rotated(deg_to_rad(140)) * 6.0
var right := c + fwd.rotated(deg_to_rad(-140)) * 6.0
_view.draw_colored_polygon(PackedVector2Array([tip, left, right]), Color(1, 1, 1))