220 lines
6.1 KiB
GDScript
220 lines
6.1 KiB
GDScript
class_name TerrainBiomeAssigner
|
|
extends RefCounted
|
|
|
|
const BIOME_SEED_SALT := 0xB10E5
|
|
const SCORE_JITTER := 0.2
|
|
|
|
|
|
static func assign(
|
|
catalog: TerrainBiomeCatalog,
|
|
records: Array[Dictionary],
|
|
generation_seed: int,
|
|
) -> Dictionary[Vector2i, StringName]:
|
|
var assignments: Dictionary[Vector2i, StringName] = {}
|
|
if catalog == null:
|
|
return assignments
|
|
var records_by_signature: Dictionary[String, Array] = {}
|
|
var definitions_by_signature: Dictionary[String, Array] = {}
|
|
for record: Dictionary in records:
|
|
var tags: PackedStringArray = record.get("tags", PackedStringArray())
|
|
var definitions := catalog.definitions_for_tags(tags)
|
|
if definitions.is_empty():
|
|
continue
|
|
var signature := _definition_signature(definitions)
|
|
var grouped_records: Array = records_by_signature.get(signature, [])
|
|
grouped_records.append(record)
|
|
records_by_signature[signature] = grouped_records
|
|
definitions_by_signature[signature] = definitions
|
|
|
|
var signatures := PackedStringArray()
|
|
for signature_value: Variant in records_by_signature:
|
|
signatures.append(str(signature_value))
|
|
signatures.sort()
|
|
for signature: String in signatures:
|
|
var grouped_records: Array[Dictionary] = []
|
|
grouped_records.assign(records_by_signature[signature])
|
|
grouped_records.sort_custom(_record_less_than)
|
|
var definitions: Array[TerrainBiomeDefinition] = []
|
|
definitions.assign(definitions_by_signature[signature])
|
|
if definitions.size() == 1:
|
|
for record: Dictionary in grouped_records:
|
|
assignments[record.get("coordinate", Vector2i.ZERO)] = (
|
|
definitions[0].stable_id
|
|
)
|
|
continue
|
|
_assign_clustered_group(
|
|
assignments,
|
|
grouped_records,
|
|
definitions,
|
|
generation_seed,
|
|
signature,
|
|
)
|
|
return assignments
|
|
|
|
|
|
static func counts(
|
|
assignments: Dictionary[Vector2i, StringName],
|
|
) -> Dictionary[StringName, int]:
|
|
var result: Dictionary[StringName, int] = {}
|
|
for biome_id: StringName in assignments.values():
|
|
result[biome_id] = result.get(biome_id, 0) + 1
|
|
return result
|
|
|
|
|
|
static func fingerprint(
|
|
assignments: Dictionary[Vector2i, StringName],
|
|
) -> String:
|
|
var coordinates: Array[Vector2i] = []
|
|
coordinates.assign(assignments.keys())
|
|
coordinates.sort_custom(_coordinate_less_than)
|
|
var tokens := PackedStringArray()
|
|
for coordinate: Vector2i in coordinates:
|
|
tokens.append(
|
|
"%d,%d:%s"
|
|
% [coordinate.x, coordinate.y, assignments[coordinate]]
|
|
)
|
|
return "\n".join(tokens).sha256_text()
|
|
|
|
|
|
static func _assign_clustered_group(
|
|
assignments: Dictionary[Vector2i, StringName],
|
|
records: Array[Dictionary],
|
|
definitions: Array[TerrainBiomeDefinition],
|
|
generation_seed: int,
|
|
signature: String,
|
|
) -> void:
|
|
var anchors := _build_anchors(
|
|
records,
|
|
definitions,
|
|
generation_seed,
|
|
signature,
|
|
)
|
|
for record: Dictionary in records:
|
|
var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO)
|
|
var best_definition: TerrainBiomeDefinition
|
|
var best_score := INF
|
|
for definition: TerrainBiomeDefinition in definitions:
|
|
var anchor: Vector2i = anchors.get(
|
|
definition.stable_id,
|
|
coordinate,
|
|
)
|
|
var delta := coordinate - anchor
|
|
var score := (
|
|
float(delta.length_squared())
|
|
/ maxf(definition.region_weight, 0.1)
|
|
+ _score_jitter(
|
|
generation_seed,
|
|
coordinate,
|
|
definition.stable_id,
|
|
)
|
|
)
|
|
if score < best_score:
|
|
best_score = score
|
|
best_definition = definition
|
|
if best_definition != null:
|
|
assignments[coordinate] = best_definition.stable_id
|
|
|
|
|
|
static func _build_anchors(
|
|
records: Array[Dictionary],
|
|
definitions: Array[TerrainBiomeDefinition],
|
|
generation_seed: int,
|
|
signature: String,
|
|
) -> Dictionary[StringName, Vector2i]:
|
|
var anchors: Dictionary[StringName, Vector2i] = {}
|
|
var available_coordinates: Array[Vector2i] = []
|
|
for record: Dictionary in records:
|
|
available_coordinates.append(
|
|
record.get("coordinate", Vector2i.ZERO)
|
|
)
|
|
var random := RandomNumberGenerator.new()
|
|
random.seed = (
|
|
generation_seed
|
|
^ int(signature.hash())
|
|
^ BIOME_SEED_SALT
|
|
)
|
|
for definition: TerrainBiomeDefinition in definitions:
|
|
if available_coordinates.is_empty():
|
|
for record: Dictionary in records:
|
|
available_coordinates.append(
|
|
record.get("coordinate", Vector2i.ZERO)
|
|
)
|
|
var selected_index := 0
|
|
if anchors.is_empty():
|
|
selected_index = random.randi_range(
|
|
0,
|
|
available_coordinates.size() - 1,
|
|
)
|
|
else:
|
|
selected_index = _most_separated_coordinate_index(
|
|
available_coordinates,
|
|
anchors.values(),
|
|
random,
|
|
)
|
|
anchors[definition.stable_id] = available_coordinates[selected_index]
|
|
available_coordinates.remove_at(selected_index)
|
|
return anchors
|
|
|
|
|
|
static func _most_separated_coordinate_index(
|
|
coordinates: Array[Vector2i],
|
|
anchor_values: Array,
|
|
random: RandomNumberGenerator,
|
|
) -> int:
|
|
var best_distance := -1
|
|
var best_indices: Array[int] = []
|
|
for index: int in coordinates.size():
|
|
var coordinate := coordinates[index]
|
|
var nearest_distance := 1 << 30
|
|
for anchor_value: Variant in anchor_values:
|
|
var anchor: Vector2i = anchor_value
|
|
nearest_distance = mini(
|
|
nearest_distance,
|
|
(coordinate - anchor).length_squared(),
|
|
)
|
|
if nearest_distance > best_distance:
|
|
best_distance = nearest_distance
|
|
best_indices = [index]
|
|
elif nearest_distance == best_distance:
|
|
best_indices.append(index)
|
|
return best_indices[random.randi_range(0, best_indices.size() - 1)]
|
|
|
|
|
|
static func _score_jitter(
|
|
generation_seed: int,
|
|
coordinate: Vector2i,
|
|
biome_id: StringName,
|
|
) -> float:
|
|
var token := "%d:%d:%d:%s" % [
|
|
generation_seed,
|
|
coordinate.x,
|
|
coordinate.y,
|
|
biome_id,
|
|
]
|
|
return (
|
|
float(posmod(token.hash(), 1000))
|
|
/ 1000.0
|
|
* SCORE_JITTER
|
|
)
|
|
|
|
|
|
static func _definition_signature(
|
|
definitions: Array[TerrainBiomeDefinition],
|
|
) -> String:
|
|
var ids := PackedStringArray()
|
|
for definition: TerrainBiomeDefinition in definitions:
|
|
ids.append(String(definition.stable_id))
|
|
return "|".join(ids)
|
|
|
|
|
|
static func _record_less_than(first: Dictionary, second: Dictionary) -> bool:
|
|
return _coordinate_less_than(
|
|
first.get("coordinate", Vector2i.ZERO),
|
|
second.get("coordinate", Vector2i.ZERO),
|
|
)
|
|
|
|
|
|
static func _coordinate_less_than(first: Vector2i, second: Vector2i) -> bool:
|
|
if first.y == second.y:
|
|
return first.x < second.x
|
|
return first.y < second.y
|