239 lines
10 KiB
GDScript
239 lines
10 KiB
GDScript
extends Node
|
|
class_name PlantTextureBuilderInstance
|
|
|
|
const IMAGE_WIDTH := 2048
|
|
const IMAGE_HEIGHT := 2048
|
|
|
|
const SEED_TEXTURE_SIZE = 150
|
|
|
|
const COLOR_PALETTE: Array[Color] = [
|
|
Color("#78AEBA"),
|
|
Color("#A7B35B"),
|
|
Color("#DB6B75"),
|
|
Color("#EC8E49"),
|
|
Color("#F9FFCE"),
|
|
]
|
|
|
|
const PLACEHOLDER_MATURE_TEXTURE: Texture = preload("res://entities/plants/assets/sprites/default/mature.png")
|
|
const PLACEHOLDER_GROWING_TEXTURE: Texture = preload("res://entities/plants/assets/sprites/default/growing.png")
|
|
|
|
enum OriginType {BRANCH_ORIGIN, MUTATION_ORIGIN, BASE_LEAF_ORIGIN}
|
|
|
|
# @export var parts_archetype_associations: Dictionary[PlantArchetype, PartArchetypeAssociation] TODO:: have the archetypes
|
|
@export var bases: Array[PlantPart]
|
|
@export var baby_bases: Array[PlantPart]
|
|
@export var branches: Array[PlantPart]
|
|
@export var n_branches: int = 2
|
|
@export var base_leaves: Array[PlantPart]
|
|
@export var parts_mutation_associations: Dictionary[String, PartMutationAssociation] # Array[PlantPart]
|
|
|
|
@export var chance_to_have_part := 0.75;
|
|
|
|
@export var origin_weights_base: Dictionary[OriginType, int] = {OriginType.BRANCH_ORIGIN: 10, OriginType.MUTATION_ORIGIN: 10, OriginType.BASE_LEAF_ORIGIN: 10}
|
|
@export var origin_weight_loss := 3
|
|
@export var origin_weight_gain := 1
|
|
|
|
@export var mutation_weight_base := 10
|
|
@export var mutation_weight_loss := 3
|
|
@export var mutation_weight_gain := 1
|
|
|
|
@export var seed_texture_sets: Array[SeedTextureSet]
|
|
|
|
@export var base_y_offset := -50.0
|
|
|
|
var rng := RandomNumberGenerator.new()
|
|
var image: Image = Image.create_empty(IMAGE_WIDTH, IMAGE_HEIGHT, false, Image.FORMAT_RGBA8)
|
|
var image_center: Vector2i = Vector2(0.5, 1) * Vector2(image.get_size())
|
|
|
|
func random_ind(array: Array) -> int:
|
|
return rng.randi_range(0, array.size() - 1)
|
|
|
|
func pick_random(array: Array) -> Variant:
|
|
return array[random_ind(array)]
|
|
|
|
func shuffle(array: Array):
|
|
var available_ind := 0
|
|
for i in range(array.size()):
|
|
var temp = array[available_ind]
|
|
var picked_ind := rng.randi_range(available_ind, array.size() - 1)
|
|
array[available_ind] = array[picked_ind]
|
|
array[picked_ind] = temp
|
|
|
|
func shuffle_weighted(array: Array, weights: Array[int]):
|
|
assert(array.size() == weights.size(), "Suffle with weights not same size")
|
|
|
|
var indices := range(array.size())
|
|
var random_values: Array[int] = []
|
|
for i in range(array.size()):
|
|
random_values.append(rng.randi_range(0, weights[i]))
|
|
|
|
indices.sort_custom(func(a, b): return random_values[a] > random_values[b])
|
|
|
|
var originalArray := array.duplicate()
|
|
var originalWeights := weights.duplicate()
|
|
|
|
for i in range(array.size()):
|
|
array[i] = originalArray[indices[i]]
|
|
weights[i] = originalWeights[indices[i]]
|
|
|
|
func get_mutation_weight(level: int) -> int:
|
|
return 2 * level
|
|
|
|
func build_seed_texture(random_seed: int) -> Texture:
|
|
rng.seed = random_seed
|
|
|
|
|
|
var texture_set: SeedTextureSet = pick_random(seed_texture_sets)
|
|
var seed_image := Image.create(SEED_TEXTURE_SIZE, SEED_TEXTURE_SIZE, false, Image.FORMAT_RGBA8)
|
|
|
|
for i in texture_set.color_textures.size():
|
|
var color_image = texture_set.get_color_image(i).duplicate()
|
|
color_image.resize(SEED_TEXTURE_SIZE, SEED_TEXTURE_SIZE)
|
|
modulate_image(color_image, pick_random(COLOR_PALETTE))
|
|
seed_image.blend_rect(
|
|
color_image,
|
|
Rect2i(0, 0, SEED_TEXTURE_SIZE, SEED_TEXTURE_SIZE),
|
|
Vector2i.ZERO
|
|
)
|
|
if texture_set.outline_texture:
|
|
var outline_image = texture_set.outline_image
|
|
outline_image.resize(SEED_TEXTURE_SIZE, SEED_TEXTURE_SIZE)
|
|
seed_image.blend_rect(outline_image, Rect2i(0, 0, SEED_TEXTURE_SIZE, SEED_TEXTURE_SIZE), Vector2i.ZERO)
|
|
|
|
if rng.randi() % 2 == 0:
|
|
seed_image.flip_x()
|
|
|
|
return ImageTexture.create_from_image(seed_image)
|
|
|
|
|
|
func build_plant_texture(plant_data: PlantData) -> Texture:
|
|
rng.seed = plant_data.random_seed
|
|
|
|
var texture: Texture
|
|
var base_part: PlantPart
|
|
|
|
match plant_data.get_state():
|
|
PlantData.State.MATURE:
|
|
texture = PLACEHOLDER_MATURE_TEXTURE
|
|
base_part = pick_random(bases)
|
|
PlantData.State.GROWING:
|
|
texture = PLACEHOLDER_GROWING_TEXTURE
|
|
base_part = pick_random(baby_bases)
|
|
_:
|
|
print("{state} not handled".format({"state": PlantData.State.keys()[plant_data.get_state()]}))
|
|
return null
|
|
|
|
var weight_per_origin_type: Array[int] = origin_weights_base.values().duplicate()
|
|
|
|
var parts_to_place: Dictionary[OriginType, Array] # BRANCH_ORIGIN : Array[PlantPart], MUTATION_ORIGIN : Array[Array[PlantPart]], BASE_LEAF_ORIGIN : Array[PlantPart]
|
|
parts_to_place[OriginType.BRANCH_ORIGIN] = branches.duplicate()
|
|
parts_to_place[OriginType.MUTATION_ORIGIN] = []
|
|
parts_to_place[OriginType.BASE_LEAF_ORIGIN] = base_leaves.duplicate()
|
|
var mutation_weights: Array[int] = []
|
|
for mutation in plant_data.mutations:
|
|
if mutation.id in parts_mutation_associations:
|
|
var mutation_parts := parts_mutation_associations[mutation.id].parts
|
|
parts_to_place[OriginType.MUTATION_ORIGIN].append(mutation_parts)
|
|
var mutation_weight := get_mutation_weight(mutation.level)
|
|
mutation_weights.append(mutation_weight)
|
|
weight_per_origin_type[OriginType.MUTATION_ORIGIN] += mutation_weight
|
|
if len(parts_to_place[OriginType.BASE_LEAF_ORIGIN]) > 0 and mutation_parts[0].type == PlantPart.PartType.LEAF_PART:
|
|
parts_to_place[OriginType.BASE_LEAF_ORIGIN].clear()
|
|
|
|
var flipped: bool = rng.randi() % 2 == 0
|
|
|
|
var base_image_coord = blend_part(image_center, Vector2(0.0, base_y_offset), base_part, flipped)
|
|
populate_part(parts_to_place, weight_per_origin_type, mutation_weights, base_part, base_image_coord, flipped)
|
|
|
|
texture = ImageTexture.create_from_image(image)
|
|
image.fill(Color.TRANSPARENT)
|
|
return texture
|
|
|
|
## returns -1 if not found
|
|
func find_random_matching_attach_ind(attach_to_match: PlantAttach, array: Array[PlantPart]) -> int:
|
|
var indices := range(array.size())
|
|
shuffle(indices)
|
|
for i in indices:
|
|
if array[i].root.attach_types.any(func(type): return attach_to_match.attach_types.has(type)):
|
|
return i
|
|
return -1
|
|
|
|
func populate_part(all_parts: Dictionary[OriginType, Array], weight_per_origin_type: Array[int], mutation_weights: Array[int], parent_part: PlantPart, parent_image_coord: Vector2i, parent_is_flipped: bool):
|
|
var placed_parts: Array[PlantPart] # same ind as their corresponding attach
|
|
var part_image_coords: Array[Vector2i] # idem
|
|
var part_is_flipped: Array[bool]
|
|
|
|
# first find and blend parts per attach
|
|
for attach in parent_part.attaches:
|
|
# get part to place
|
|
var part_to_place := get_part(all_parts, weight_per_origin_type, mutation_weights, attach)
|
|
placed_parts.append(part_to_place)
|
|
part_is_flipped.append(rng.randi() % 2 == 0)
|
|
|
|
var attach_position := attach.position
|
|
if parent_is_flipped:
|
|
attach_position *= Vector2(-1, 1)
|
|
|
|
# blend part
|
|
if part_to_place:
|
|
var part_image_coord := blend_part(parent_image_coord, attach_position, part_to_place, part_is_flipped.back())
|
|
part_image_coords.append(part_image_coord)
|
|
else:
|
|
part_image_coords.append(Vector2i.ZERO)
|
|
|
|
# then populate them
|
|
for i in range(placed_parts.size()):
|
|
if placed_parts[i] != null:
|
|
populate_part(all_parts, weight_per_origin_type, mutation_weights, placed_parts[i], part_image_coords[i], part_is_flipped[i])
|
|
|
|
|
|
func get_part(all_parts: Dictionary[OriginType, Array], weight_per_origin_type: Array[int], mutation_weights: Array[int], attach: PlantAttach) -> PlantPart:
|
|
var rand := rng.randf()
|
|
if rand <= chance_to_have_part:
|
|
var origins: Array[int] = all_parts.keys().duplicate()
|
|
var weight_copy: Array[int] = weight_per_origin_type.duplicate()
|
|
shuffle_weighted(origins, weight_copy)
|
|
for originIndInd: int in range(origins.size()):
|
|
var originInd: int = origins[originIndInd]
|
|
var origin := originInd as OriginType
|
|
if origin == OriginType.BRANCH_ORIGIN || origin == OriginType.BASE_LEAF_ORIGIN:
|
|
var ind := find_random_matching_attach_ind(attach, all_parts[origin])
|
|
if ind >= 0:
|
|
weight_per_origin_type[origin] -= origin_weight_loss
|
|
for i in range(len(weight_per_origin_type) - 1):
|
|
weight_per_origin_type[((origin + i + 1) % 3)] += origin_weight_gain
|
|
return all_parts[origin][ind]
|
|
else: # find a mutation part to place
|
|
if weight_copy[originIndInd] < 0:
|
|
break
|
|
|
|
var parts_per_mutations: Array = all_parts[origin]
|
|
shuffle_weighted(parts_per_mutations, mutation_weights)
|
|
for mutation_parts_ind in range(parts_per_mutations.size()):
|
|
var ind := find_random_matching_attach_ind(attach, parts_per_mutations[mutation_parts_ind])
|
|
if ind >= 0:
|
|
for i in range(mutation_weights.size()):
|
|
mutation_weights[i] += mutation_weight_gain
|
|
mutation_weights[mutation_parts_ind] -= mutation_weight_gain + mutation_weight_loss
|
|
weight_per_origin_type[origin] -= origin_weight_loss
|
|
for i in range(len(weight_per_origin_type) - 1):
|
|
weight_per_origin_type[((origin + i + 1) % 3)] += origin_weight_gain
|
|
return parts_per_mutations[mutation_parts_ind][ind]
|
|
return null
|
|
|
|
func blend_part(parent_image_coord: Vector2i, attach_position: Vector2, part_to_blend: PlantPart, is_flipped: bool) -> Vector2i:
|
|
var part_image := Image.create_from_data(part_to_blend.image.get_width(), part_to_blend.image.get_height(), false, Image.FORMAT_RGBA8, part_to_blend.image.get_data())
|
|
var part_root_position := part_to_blend.root.position
|
|
if is_flipped:
|
|
part_image.flip_x()
|
|
part_root_position *= Vector2(-1, 1)
|
|
var part_image_center: Vector2i = 0.5 * part_image.get_size()
|
|
var part_image_coord: Vector2i = parent_image_coord + Vector2i(attach_position - part_root_position)
|
|
image.blend_rect(part_image, Rect2i(Vector2i.ZERO, part_image.get_size()), part_image_coord - part_image_center)
|
|
return part_image_coord
|
|
|
|
func modulate_image(i: Image, color: Color):
|
|
for x in i.get_size().x:
|
|
for y in i.get_size().y:
|
|
i.set_pixel(x, y, i.get_pixel(x, y) * color)
|