388 lines
11 KiB
GDScript3
388 lines
11 KiB
GDScript3
|
extends Node2D
|
||
|
|
||
|
signal dropoff_score_changed
|
||
|
signal game_ended
|
||
|
# Declare member variables here. Examples:
|
||
|
# var a = 2
|
||
|
# var b = "text"
|
||
|
|
||
|
const Dropoff = preload("DropOff.tscn")
|
||
|
const Pickup = preload("Pickup.tscn")
|
||
|
const Start = preload("Start.tscn")
|
||
|
const Empty = preload("Empty.tscn")
|
||
|
const Link = preload("Link.tscn")
|
||
|
const Character = preload("Character.tscn")
|
||
|
|
||
|
# Configuration parameters
|
||
|
const default_params = {
|
||
|
'seed': 'ld53',
|
||
|
'node_count': 12,
|
||
|
'cargo_availability': 2.0,
|
||
|
'pickup_count_range': Vector2(1, 2),
|
||
|
'empty_node_chance': 15,
|
||
|
'dropoff_value_range': Vector2(1, 3),
|
||
|
'dropoff_capacity_range': Vector2(1, 3),
|
||
|
'link_cost_range': Vector2(1, 2),
|
||
|
}
|
||
|
|
||
|
static func easy_params():
|
||
|
var p = default_params.duplicate()
|
||
|
p.cargo_availability = INF;
|
||
|
p.pickup_count_range = Vector2(1, 1)
|
||
|
p.node_count = 6
|
||
|
p.dropoff_capacity_range = Vector2(1, 2)
|
||
|
p.link_cost_range = Vector2(1, 1)
|
||
|
return p
|
||
|
|
||
|
static func medium_params():
|
||
|
var p = default_params.duplicate()
|
||
|
return p
|
||
|
|
||
|
static func hard_params():
|
||
|
var p = default_params.duplicate()
|
||
|
p.pickup_count_range = Vector2(1, 3)
|
||
|
p.cargo_availability = 1.0
|
||
|
p.node_count = 24
|
||
|
p.dropoff_capacity_range = Vector2(1, 5)
|
||
|
p.link_cost_range = Vector2(1, 3)
|
||
|
return p
|
||
|
|
||
|
var params = null
|
||
|
var random = RandomNumberGenerator.new()
|
||
|
|
||
|
func get_levelcode():
|
||
|
return Marshalls.variant_to_base64(self.params)
|
||
|
|
||
|
static func parse_levelcode(code):
|
||
|
var p = Marshalls.base64_to_variant(code)
|
||
|
# Make sure we have the keys present in the default params
|
||
|
# Extraneous keys are ignored
|
||
|
if p != null and !p.has_all(default_params.keys()):
|
||
|
return null
|
||
|
return p
|
||
|
|
||
|
func restart(new_params = null):
|
||
|
if new_params:
|
||
|
self.params = new_params
|
||
|
for n in self.get_children():
|
||
|
n.free()
|
||
|
self._init(self.params['seed'])
|
||
|
self.params = new_params
|
||
|
self._ready()
|
||
|
|
||
|
func _init(random_seed: String = "ld53"):
|
||
|
self.random = RandomNumberGenerator.new()
|
||
|
print("Seed: %s" % random_seed)
|
||
|
self.random.set_seed(hash(random_seed))
|
||
|
|
||
|
func random_position():
|
||
|
var bottom = self.random.randi_range(0, 1)
|
||
|
var right = self.random.randi_range(0, 1)
|
||
|
var x_range = Vector2(
|
||
|
32 + (right * 512),
|
||
|
1024 + ((right - 1) * 512)
|
||
|
)
|
||
|
var y_range = Vector2(
|
||
|
32 + (bottom * 300),
|
||
|
600 + ((bottom - 1) * 300)
|
||
|
)
|
||
|
return Vector2(
|
||
|
self.random.randi_range(x_range.x, x_range.y),
|
||
|
self.random.randi_range(y_range.x, y_range.y)
|
||
|
)
|
||
|
|
||
|
func indexed_position(index):
|
||
|
# Positions are on a grid
|
||
|
var row_count = 3
|
||
|
var col_count = 4
|
||
|
if self.params.node_count == 24:
|
||
|
col_count = 6
|
||
|
row_count = 4
|
||
|
if self.params.node_count == 36:
|
||
|
col_count = 6
|
||
|
row_count = 6
|
||
|
var row = index % row_count
|
||
|
var col = index / row_count
|
||
|
var position = Vector2(
|
||
|
(1024 / col_count) * col,
|
||
|
(600 / row_count) * row
|
||
|
)
|
||
|
var jitter_value = 64
|
||
|
var jitter = Vector2(
|
||
|
self.random.randi_range(-jitter_value, jitter_value),
|
||
|
self.random.randi_range(-jitter_value, jitter_value)
|
||
|
)
|
||
|
return position + Vector2(64, 64) + jitter
|
||
|
|
||
|
func distance_to_closest_node(position, node):
|
||
|
var closest = 65000
|
||
|
for n in get_tree().get_nodes_in_group("nodes"):
|
||
|
if n == node:
|
||
|
continue
|
||
|
var distance = n.get_position().distance_to(position)
|
||
|
if distance < closest:
|
||
|
closest = distance
|
||
|
return closest
|
||
|
|
||
|
func link_nodes(n1, n2):
|
||
|
assert(n1 != null)
|
||
|
assert(n2 != null)
|
||
|
var n = Link.instance()
|
||
|
n.set_ends(n1, n2)
|
||
|
n1.peers.append(n2)
|
||
|
n2.peers.append(n1)
|
||
|
n1.remove_from_group("unlinked")
|
||
|
n2.remove_from_group("unlinked")
|
||
|
add_child(n)
|
||
|
# print("Linked ", n1, " to ", n2)
|
||
|
# print("\tN1 peers: ", n1.peers)
|
||
|
# print("\tN2 peers: ", n2.peers)
|
||
|
return n
|
||
|
|
||
|
func reconstruct_path(cameFrom, current):
|
||
|
var total_path = []
|
||
|
while cameFrom.has(current):
|
||
|
current = cameFrom[current]
|
||
|
total_path.push_front(current)
|
||
|
return total_path
|
||
|
|
||
|
func find_path(start, destination):
|
||
|
var openSet = [start]
|
||
|
var from = {}
|
||
|
var gScore = {}
|
||
|
var fScore = {}
|
||
|
fScore[start] = 1
|
||
|
while openSet.size() > 0:
|
||
|
print(openSet)
|
||
|
var current = null
|
||
|
for n in openSet:
|
||
|
if !fScore.has(n):
|
||
|
fScore[n] = INF
|
||
|
if current == null:
|
||
|
current = n
|
||
|
else:
|
||
|
if fScore[n] < fScore[current]:
|
||
|
current = n
|
||
|
if current == destination:
|
||
|
return reconstruct_path(from, current)
|
||
|
openSet.remove(openSet.find(current))
|
||
|
|
||
|
for n in current.peers:
|
||
|
if !gScore.has(current):
|
||
|
gScore[current] = INF
|
||
|
var tentative_score = gScore[current] + 1
|
||
|
if gScore.has(n) && tentative_score < gScore[n]:
|
||
|
from[n] = current
|
||
|
fScore[n] = tentative_score + 100
|
||
|
gScore[n] = tentative_score
|
||
|
if openSet.find(n) == -1:
|
||
|
openSet.append(n)
|
||
|
return null
|
||
|
|
||
|
func get_reachable_nodes(start):
|
||
|
var nodes_in_starting_graph = [start] + start.peers
|
||
|
var nodes_to_check = start.peers
|
||
|
var nodes_checked = [start]
|
||
|
while nodes_to_check.size() > 0:
|
||
|
var checking_node = nodes_to_check.pop_front()
|
||
|
#print("Checking: ", checking_node)
|
||
|
#print("\tPeers", checking_node.peers)
|
||
|
if nodes_checked.find(checking_node) != -1:
|
||
|
continue
|
||
|
for nx in checking_node.peers:
|
||
|
if nodes_checked.find(nx) == -1:
|
||
|
nodes_to_check.append(nx)
|
||
|
if nodes_in_starting_graph.find(checking_node) == -1:
|
||
|
nodes_in_starting_graph.append(checking_node)
|
||
|
nodes_checked.append(checking_node)
|
||
|
return nodes_in_starting_graph
|
||
|
|
||
|
# Called when the node enters the scene tree for the first time.
|
||
|
func _ready():
|
||
|
if self.params == null:
|
||
|
print("Resetting params")
|
||
|
self.params = easy_params()
|
||
|
var nodes = 0
|
||
|
var n = null
|
||
|
# Add start
|
||
|
n = Start.instance()
|
||
|
n.set_position(Vector2(512 - 32, 300-32))
|
||
|
n.add_to_group("unlinked")
|
||
|
n.name_from_index(nodes)
|
||
|
self.add_child(n)
|
||
|
var start = n
|
||
|
nodes += 1
|
||
|
|
||
|
# Add pickup(s)
|
||
|
var count = self.random.randi_range(self.params.pickup_count_range.x, self.params.pickup_count_range.y)
|
||
|
while count > 0:
|
||
|
n = Pickup.instance()
|
||
|
n.add_to_group("unlinked")
|
||
|
n.name_from_index(nodes)
|
||
|
self.add_child(n)
|
||
|
count -= 1
|
||
|
nodes += 1
|
||
|
|
||
|
# Add other node(s)
|
||
|
var total_to_dropoff = 0
|
||
|
while nodes < self.params.node_count + 1:
|
||
|
if self.random.randi_range(0, 99) < self.params.empty_node_chance:
|
||
|
n = Empty.instance()
|
||
|
else:
|
||
|
n = Dropoff.instance()
|
||
|
n.capacity = self.random.randi_range(self.params.dropoff_capacity_range.x, self.params.dropoff_capacity_range.y)
|
||
|
n.value = self.random.randi_range(self.params.dropoff_value_range.x, self.params.dropoff_value_range.y)
|
||
|
total_to_dropoff += n.capacity
|
||
|
n.add_to_group("unlinked")
|
||
|
n.name_from_index(nodes)
|
||
|
self.add_child(n)
|
||
|
print("Added node: ", n)
|
||
|
nodes += 1
|
||
|
|
||
|
# Distribute available cargo between dropoff points randomly when finite
|
||
|
# amount available.
|
||
|
if !is_inf(self.params.cargo_availability):
|
||
|
var cargo_available = total_to_dropoff * self.params.cargo_availability
|
||
|
for cargo_node in get_tree().get_nodes_in_group("pickup"):
|
||
|
var cargo_count = self.random.randi_range(1, cargo_available)
|
||
|
cargo_available -= cargo_count
|
||
|
cargo_node.set_stored(cargo_count)
|
||
|
if cargo_available > 0:
|
||
|
get_tree().get_nodes_in_group("pickup")[-1].add_stored(cargo_available)
|
||
|
|
||
|
# Each node should have at least one link
|
||
|
var unlinked_nodes = get_tree().get_nodes_in_group("unlinked")
|
||
|
while unlinked_nodes.size() > 0:
|
||
|
var source = unlinked_nodes.pop_front()
|
||
|
var targets = get_tree().get_nodes_in_group("nodes")
|
||
|
var target = targets[self.random.randi_range(0, targets.size()-1)]
|
||
|
while target == source:
|
||
|
target = targets[self.random.randi_range(0, targets.size()-1)]
|
||
|
var link = link_nodes(source, target)
|
||
|
link.set_weight(self.random.randi_range(
|
||
|
self.params.link_cost_range.x,
|
||
|
self.params.link_cost_range.y
|
||
|
))
|
||
|
|
||
|
unlinked_nodes = get_tree().get_nodes_in_group("unlinked")
|
||
|
|
||
|
# For each node, make sure we can reach the start node, merging
|
||
|
# any disjointed graphs
|
||
|
var nodes_in_starting_graph = get_reachable_nodes(start)
|
||
|
|
||
|
print("Starting graph nodes: ", nodes_in_starting_graph)
|
||
|
for node in get_tree().get_nodes_in_group("nodes"):
|
||
|
if nodes_in_starting_graph.find(node) == -1:
|
||
|
print(node, " can't be reached from start")
|
||
|
var target = nodes_in_starting_graph[self.random.randi_range(0, nodes_in_starting_graph.size()-1)]
|
||
|
link_nodes(node, target)
|
||
|
nodes_in_starting_graph = get_reachable_nodes(start)
|
||
|
|
||
|
# Set new node positions
|
||
|
# @TODO: Better (?) layout algorithm, eg. https://en.wikipedia.org/wiki/Graph_drawing
|
||
|
var x_range = Vector2(32, 1024-32)
|
||
|
var y_range = Vector2(32, 600-32)
|
||
|
var distance_threshold = 32
|
||
|
var idx = 0
|
||
|
for node in get_tree().get_nodes_in_group("nodes"):
|
||
|
if node.is_in_group("start"):
|
||
|
continue
|
||
|
var position = self.indexed_position(idx)
|
||
|
var tries = 0
|
||
|
var closest = distance_to_closest_node(position, node)
|
||
|
# While the position is within a distance of another node, rechoose
|
||
|
while closest <= distance_threshold:
|
||
|
if closest == 0:
|
||
|
closest = 65000
|
||
|
for n2 in get_tree().get_nodes_in_group("nodes"):
|
||
|
if n2 == node:
|
||
|
continue
|
||
|
var distance = n2.get_position().distance_to(position)
|
||
|
if distance < closest:
|
||
|
closest = distance
|
||
|
# print("Closest point to ", position, " ", closest)
|
||
|
tries += 1
|
||
|
if tries % 10 == 0:
|
||
|
distance_threshold /= 2
|
||
|
print("Reducing threshold for point ", node, " to ", distance_threshold)
|
||
|
position = self.random_position()
|
||
|
node.set_position(position)
|
||
|
# print("Placed node ", node, " after ", tries, " tries")
|
||
|
idx += 1
|
||
|
|
||
|
|
||
|
|
||
|
# Update link linepositions based on the node positions
|
||
|
for link in get_tree().get_nodes_in_group("links"):
|
||
|
link.redraw_line()
|
||
|
# @BUG It shouldn't be necessary to recalculate the peers
|
||
|
# For some reason, we lose the peer on the start node
|
||
|
if link.start == start:
|
||
|
start.peers.append(link.end)
|
||
|
if link.end == start:
|
||
|
start.peers.append(link.start)
|
||
|
|
||
|
# Add player
|
||
|
n = Character.instance()
|
||
|
add_child(n)
|
||
|
n.add_to_group("character")
|
||
|
n.set_location(start, true)
|
||
|
n.connect("arrived_at", self, "on_character_arrival")
|
||
|
|
||
|
|
||
|
func on_character_arrival(node):
|
||
|
if node.is_in_group("dropoff"):
|
||
|
var max_delivery = node.capacity - node.stored
|
||
|
var max_avail = get_node("Character").stored
|
||
|
var delta = min(max_avail, max_delivery)
|
||
|
print("Dropoff %d to %s" % [delta, node])
|
||
|
get_node("Character").dropoff(delta)
|
||
|
node.receive(delta)
|
||
|
# Recalculate dropoff score
|
||
|
var score = self.get_supplied()
|
||
|
var all_supplied = score[0] >= score[1]
|
||
|
emit_signal("dropoff_score_changed", [score[2], score[3]])
|
||
|
if all_supplied:
|
||
|
emit_signal("game_ended")
|
||
|
elif node.is_in_group("pickup"):
|
||
|
var max_pickup = get_node("Character").capacity - get_node("Character").stored
|
||
|
var min_avail = node.stored
|
||
|
var delta = min(min_avail, max_pickup)
|
||
|
print("Pickup up %d from %s" % [delta, node])
|
||
|
get_node("Character").pickup(delta)
|
||
|
node.drain(delta)
|
||
|
elif node.is_in_group("start"):
|
||
|
print("@TODO: Start")
|
||
|
|
||
|
func get_moves():
|
||
|
return get_node("Character").movement_cost
|
||
|
|
||
|
func get_score():
|
||
|
var data = get_supplied()
|
||
|
# Delivered + Supplied bonus - Movement
|
||
|
return data[2] + data[3] - get_moves()
|
||
|
|
||
|
func get_supplied():
|
||
|
# [nodes_done, nodes_total, earned_value, cargo_done, cargo_total]
|
||
|
var done = 0
|
||
|
var dropoff_score = 0
|
||
|
var cargo_done = 0
|
||
|
var cargo_total = 0
|
||
|
for n in get_tree().get_nodes_in_group("dropoff"):
|
||
|
cargo_total += n.capacity
|
||
|
cargo_done += n.stored
|
||
|
if n.stored >= n.capacity:
|
||
|
dropoff_score += n.value
|
||
|
done += 1
|
||
|
return [
|
||
|
done,
|
||
|
get_tree().get_nodes_in_group("dropoff").size(),
|
||
|
dropoff_score,
|
||
|
cargo_done,
|
||
|
cargo_total
|
||
|
]
|
||
|
|
||
|
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||
|
#func _process(delta):
|
||
|
# pass
|