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 all_bases: PartGroup @export var baby_bases_start_ind: int @export var branches: PartGroup @export var base_leaves: PartGroup @export var part_group: Dictionary[String, PartGroup] @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()) var bases: Array[PlantPart] var baby_bases: Array[PlantPart] func _ready() -> void: bases.resize(baby_bases_start_ind) baby_bases.resize(all_bases.parts.size() - baby_bases_start_ind) for i in range(all_bases.parts.size()): if i < baby_bases_start_ind: bases[i] = all_bases.parts[i] else: baby_bases[i - baby_bases_start_ind] = all_bases.parts[i] 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.parts.duplicate() parts_to_place[OriginType.MUTATION_ORIGIN] = [] parts_to_place[OriginType.BASE_LEAF_ORIGIN] = base_leaves.parts.duplicate() var mutation_weights: Array[int] = [] for mutation in plant_data.mutations: if mutation.id in part_group: var mutation_parts := part_group[mutation.id].parts.duplicate() 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)