ajhahn.de
← Theria
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