GDScript 116 lines
extends SceneTree
## Top-down map preview: renders the map straight overhead and saves it as a PNG, so the
## map can be reshaped by editing MapData coordinates and seeing the result without playing.
##
## Run it:
## godot --path . -s tools/map_shot.gd
## then open the written file (printed at the end). Edit src/sim/map_data.gd, run again.
const IMG := 1200 # output is a square IMG x IMG png
const VIEW := 10080.0 # world units the camera frames (a touch past the 9600-wide bounds)
const OUT := "res://map_preview.png"
var _frames := 0
func _initialize() -> void:
DisplayServer.window_set_size(Vector2i(IMG, IMG))
var world := Node3D.new()
get_root().add_child(world)
# A flat overhead orthographic camera: x stays x, the sim's y reads as screen-down,
# so the picture matches a top-down map drawn on paper.
var cam := Camera3D.new()
cam.projection = Camera3D.PROJECTION_ORTHOGONAL
cam.size = VIEW
cam.position = Vector3(0.0, 6000.0, 0.0)
cam.rotation_degrees = Vector3(-90.0, 0.0, 0.0)
cam.far = 20000.0
world.add_child(cam)
cam.make_current()
var light := DirectionalLight3D.new()
light.rotation_degrees = Vector3(-90.0, 0.0, 0.0)
world.add_child(light)
var ground := MeshInstance3D.new()
var plane := PlaneMesh.new()
plane.size = MapData.BOUNDS.size
ground.mesh = plane
ground.material_override = _mat(Color(0.12, 0.13, 0.15))
world.add_child(ground)
# A coordinate grid under everything: a faint line every 500 world units and a brighter
# pair through the origin, so a spot in the picture maps back to the numbers in MapData.
_grid(world)
# The real map decor (lanes, river, camps) drawn by the same code the game uses,
# plus a marker on each nexus so the bases read.
MapView.build(world)
for team in MapData.NEXUS_POSITIONS.size():
var color := Color(0.30, 0.60, 1.0) if team == 0 else Color(1.0, 0.42, 0.38)
_disc(world, MapData.nexus_for_team(team), 360.0, color)
for tower in MapData.tower_positions(team):
_disc(world, tower, 168.0, color.darkened(0.3))
## Waits a few frames so the scene is drawn, then grabs the framebuffer and saves it.
func _process(_delta: float) -> bool:
_frames += 1
if _frames < 4:
return false
var image := get_root().get_texture().get_image()
image.save_png(OUT)
print("map preview saved -> ", ProjectSettings.globalize_path(OUT))
return true
## Draws the coordinate grid: a faint strip every 500 units along both axes, the origin pair
## brighter, plus a label at each axis end so the numbers are readable off the picture.
func _grid(parent: Node3D) -> void:
for k in range(-4800, 4801, 1200):
var major := k == 0
var color := Color(0.55, 0.55, 0.62) if major else Color(0.22, 0.23, 0.27)
var width := 29.0 if major else 10.0
_strip(parent, Vector3(float(k), 1.0, 0.0), Vector3(width, 1.0, 9600.0), color)
_strip(parent, Vector3(0.0, 1.0, float(k)), Vector3(9600.0, 1.0, width), color)
_label(parent, Vector3(4440.0, 8.0, 216.0), "+x")
_label(parent, Vector3(216.0, 8.0, 4440.0), "+y")
func _strip(parent: Node3D, pos: Vector3, size: Vector3, color: Color) -> void:
var strip := MeshInstance3D.new()
var box := BoxMesh.new()
box.size = size
strip.mesh = box
strip.material_override = _mat(color)
strip.position = pos
parent.add_child(strip)
func _label(parent: Node3D, pos: Vector3, text: String) -> void:
var label := Label3D.new()
label.text = text
label.font_size = 312
label.position = pos
label.rotation_degrees = Vector3(-90.0, 0.0, 0.0)
parent.add_child(label)
func _disc(parent: Node3D, pos: Vector2, radius: float, color: Color) -> void:
var disc := MeshInstance3D.new()
var cyl := CylinderMesh.new()
cyl.top_radius = radius
cyl.bottom_radius = radius
cyl.height = 1.0
disc.mesh = cyl
disc.material_override = _mat(color)
disc.position = Vector3(pos.x, 5.0, pos.y)
parent.add_child(disc)
func _mat(color: Color) -> StandardMaterial3D:
var mat := StandardMaterial3D.new()
mat.albedo_color = color
return mat