Merge branch 'plant-builder'

This commit is contained in:
Altaezio
2026-02-24 14:42:24 +01:00
138 changed files with 2553 additions and 1131 deletions

View File

@@ -1,7 +1,7 @@
extends Node
const IMAGE_WIDTH := 1000
const IMAGE_HEIGHT := 2000
const IMAGE_WIDTH := 2048
const IMAGE_HEIGHT := 2048
const SEED_TEXTURE_SIZE = 150
@@ -16,27 +16,73 @@ const COLOR_PALETTE : Array[Color] = [
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 parts_mutation_associations: Dictionary[String, PartMutationAssociation]
@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]
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 build_seed_texture(random_seed: int) -> Texture:
rng.seed = random_seed
var texture_set : SeedTextureSet = pick_random(seed_texture_sets)
var image := Image.create(SEED_TEXTURE_SIZE,SEED_TEXTURE_SIZE, false, Image.FORMAT_RGBA8)
var sedd_image := Image.create(SEED_TEXTURE_SIZE,SEED_TEXTURE_SIZE, false, Image.FORMAT_RGBA8)
for color_texture in texture_set.color_textures:
var color_image = color_texture.get_image().duplicate()
color_image.resize(SEED_TEXTURE_SIZE,SEED_TEXTURE_SIZE)
modulate_image(color_image, pick_random(COLOR_PALETTE))
image.blend_rect(
sedd_image.blend_rect(
color_image,
Rect2i(0,0,SEED_TEXTURE_SIZE,SEED_TEXTURE_SIZE),
Vector2i.ZERO
@@ -44,150 +90,117 @@ func build_seed_texture(random_seed: int) -> Texture:
if texture_set.outline_texture:
var outline_image = texture_set.outline_texture.get_image().duplicate()
outline_image.resize(SEED_TEXTURE_SIZE,SEED_TEXTURE_SIZE)
image.blend_rect(outline_image, Rect2i(0,0,SEED_TEXTURE_SIZE,SEED_TEXTURE_SIZE),Vector2i.ZERO)
sedd_image.blend_rect(outline_image, Rect2i(0,0,SEED_TEXTURE_SIZE,SEED_TEXTURE_SIZE),Vector2i.ZERO)
if rng.randi()%2 == 0:
image.flip_x()
sedd_image.flip_x()
return ImageTexture.create_from_image(image)
func build_plant_texture(plant_data: PlantData) -> Texture:
rng.seed = plant_data.random_seed
var mature_texture: Texture = PLACEHOLDER_MATURE_TEXTURE
var mature_image: Image = Image.create_empty(IMAGE_WIDTH, IMAGE_HEIGHT, false, Image.FORMAT_RGBA8)
var mature_image_center: Vector2i = 0.5 * mature_image.get_size()
var branch_parts: Array[PlantPart]
var texture: Texture
var base_part: PlantPart
var available_base_attaches: Array[Vector2]
var available_base_bottom_attach: Array[Vector2]
var branch_attaches: Array[Vector2]
var branch_root: Array[Vector2]
var branch_parent_attach: Array[Vector2]
var parts_to_place: Array[PlantPart]
match plant_data.get_state():
PlantData.State.MATURE:
print("Build mature texture")
# var plant_archetype := plant_data.archetype
if bases.size() == 0:
printerr("No base in archetype")
return mature_texture
# var base_part: PlantPart = pick_random(parts_archetype_associations[plant_archetype].bases)
texture = PLACEHOLDER_MATURE_TEXTURE
base_part = pick_random(bases)
var base_image = base_part.texture.get_image()
var base_image_center: Vector2i = 0.5 * base_image.get_size()
mature_image.blend_rect(base_image, Rect2i(Vector2i.ZERO, base_image.get_size()), mature_image_center - base_image_center - Vector2i(base_part.root))
if branches.size() == 0:
printerr("No branches in archetype")
# var branch_parts: Array[PlantPart] = parts_archetype_associations[plant_archetype].branches
for i in n_branches:
branch_parts.append(pick_random(branches))
for m in plant_data.mutations:
print("mutations: ", m.id)
var association: PartMutationAssociation = parts_mutation_associations[m.id]
var mutation_possible_parts := association.parts
for p in association.part_amount:
parts_to_place.append(pick_random(mutation_possible_parts))
PlantData.State.GROWING:
print("Build growing texture")
# var plant_archetype := plant_data.archetype
if baby_bases.size() == 0:
printerr("No baby base in archetype")
return mature_texture
# var base_part: PlantPart = pick_random(parts_archetype_associations[plant_archetype].baby_bases)
texture = PLACEHOLDER_GROWING_TEXTURE
base_part = pick_random(baby_bases)
var base_image = base_part.texture.get_image()
var base_image_center: Vector2i = 0.5 * base_image.get_size()
mature_image.blend_rect(base_image, Rect2i(Vector2i.ZERO, base_image.get_size()), mature_image_center - base_image_center - Vector2i(base_part.root))
for m in plant_data.mutations:
print("mutations: ", m.id)
var association: PartMutationAssociation = parts_mutation_associations[m.id]
var mutation_possible_parts := association.parts
for p in ceil(0.5 * association.part_amount):
parts_to_place.append(pick_random(mutation_possible_parts))
_:
print("Not handled state")
return null
available_base_attaches = base_part.attaches.duplicate()
available_base_bottom_attach = base_part.bottom_attaches.duplicate()
var weight_per_origin_type: Array[int] = origin_weights_base.values().duplicate()
assert(branch_parts.size() <= base_part.attaches.size(),
str("More branches (", branch_parts.size(), ") than base attaches (", base_part.attaches.size(), ")"))
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
parts_to_place[OriginType.MUTATION_ORIGIN] = []
parts_to_place[OriginType.BASE_LEAF_ORIGIN] = base_leaves
var mutation_weights: Array[int] = []
for mutation in plant_data.mutations:
parts_to_place[OriginType.MUTATION_ORIGIN].append(parts_mutation_associations[mutation.id].parts)
mutation_weights.append(mutation_weight_base)
for branch in branch_parts:
if available_base_attaches.size() == 0:
break
var ind: int = rng.randi_range(0, available_base_attaches.size() - 1)
var attach: Vector2 = available_base_attaches.pop_at(ind)
var base_image_coord = blend_part(image_center, Vector2.ZERO, base_part)
populate_part(parts_to_place, weight_per_origin_type, mutation_weights, base_part, base_image_coord)
var branch_image: Image = branch.texture.get_image()
var branch_image_center: Vector2i = 0.5 * branch_image.get_size()
mature_image.blend_rect(branch_image, Rect2i(Vector2i.ZERO, branch.texture.get_size()), mature_image_center - branch_image_center + Vector2i(attach - branch.root - base_part.root))
texture = ImageTexture.create_from_image(image)
image.fill(Color.TRANSPARENT)
return texture
for branch_attach in branch.attaches:
branch_attaches.append(branch_attach)
branch_root.append(branch.root)
branch_parent_attach.append(attach)
## 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
if parts_to_place.size() > branch_attaches.size() + base_part.attaches.size() - branch_parts.size():
printerr("ERROR generating : Parts to place : ", parts_to_place.size(),
"; Branch Attaches : ", branch_attaches.size(),
"; Base attaches : ", base_part.attaches.size(),
"; Branch parts : ", branch_parts.size())
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):
var part_placed: Array[PlantPart] # same ind as their corresponding attach
var part_image_coords: Array[Vector2i] # idem
for part: PlantPart in parts_to_place:
print("create part")
var attach: Vector2
var parent_root: Vector2
var chosen_attach_type: int = 0
var attachables: Array[int]
if part.base_attachable && available_base_attaches.size() > 0:
attachables.append(1)
if part.bottom_attachable && available_base_bottom_attach.size() > 0:
attachables.append(2)
if part.branch_attachable && branch_attaches.size() > 0:
attachables.append(3)
# assert(attachables.size() > 0)
if attachables.size() == 0:
print("No attach available")
continue
chosen_attach_type = pick_random(attachables)
# 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)
part_placed.append(part_to_place)
if chosen_attach_type == 1: # base attach
var ind := rng.randi_range(0, available_base_attaches.size() - 1)
attach = available_base_attaches.pop_at(ind)
parent_root = base_part.root
elif chosen_attach_type == 2: # bottom
var ind := rng.randi_range(0, available_base_bottom_attach.size() - 1)
attach = available_base_bottom_attach.pop_at(ind)
parent_root = base_part.root
elif chosen_attach_type == 3: # branch
var ind := rng.randi_range(0, branch_attaches.size() - 1)
attach = branch_attaches.pop_at(ind) - branch_root.pop_at(ind) + branch_parent_attach.pop_at(ind)
parent_root = base_part.root
# blend part
if part_to_place:
var part_image_coord := blend_part(parent_image_coord, attach.position, part_to_place)
part_image_coords.append(part_image_coord)
else:
part_image_coords.append(Vector2i.ZERO)
var part_image: Image = part.texture.get_image()
var part_image_center: Vector2i = 0.5 * part_image.get_size()
var relative_root_centered_pos: Vector2 = Vector2(mature_image_center - part_image_center) - part.root
mature_image.blend_rect(part_image, Rect2i(Vector2i.ZERO, part.texture.get_size()), relative_root_centered_pos + attach - parent_root)
# then populate them
for i in range(part_placed.size()):
if part_placed[i] != null:
populate_part(all_parts, weight_per_origin_type, mutation_weights, part_placed[i], part_image_coords[i])
if rng.randi() % 2 == 0:
mature_image.flip_x()
mature_texture = ImageTexture.create_from_image(mature_image)
return mature_texture
func pick_random(array: Array):
return array[rng.randi_range(0, array.size() - 1)]
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
weight_per_origin_type[((origin + 1) % 3) as OriginType] += origin_weight_gain
weight_per_origin_type[((origin + 2) % 3) as OriginType] += origin_weight_gain
return all_parts[origin][ind]
else: # find a mutation part to place
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
weight_per_origin_type[(origin + 1) % 3] += origin_weight_gain
weight_per_origin_type[(origin + 2) % 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) -> Vector2i:
var part_image: Image = part_to_blend.image
var part_image_center: Vector2i = 0.5 * part_image.get_size()
var part_image_coord: Vector2i = parent_image_coord + Vector2i(attach_position - part_to_blend.root.position)
image.blend_rect(part_image, Rect2i(Vector2i.ZERO, part_to_blend.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: