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
158 lines
6.2 KiB
GDScript
158 lines
6.2 KiB
GDScript
# 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))
|