From 950d177936806a478a9f6e5644c5a34e1b1d8a31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torjus=20H=C3=A5kestad?= Date: Fri, 22 Aug 2025 07:49:02 +0200 Subject: [PATCH] game: add enemy mods --- global_const.gd | 23 ++++++++--- scenes/enemies/enemy.gd | 6 +-- scenes/enemies/enemy_base.gd | 43 ++++++++++++++++++-- scenes/enemies/enemy_base.tscn | 16 ++++++++ scenes/enemies/enemy_bat.gd | 5 ++- scenes/enemies/enemy_mod.gd | 19 ++++++--- scenes/enemies/enemy_mod_pool.gd | 58 +++++++++++++++++++++++++++ scenes/enemies/enemy_mod_pool.gd.uid | 1 + scenes/enemies/enemy_rat.gd | 13 +++--- scenes/enemies/enemy_slime_small.gd | 8 +++- scenes/managers/enemy_manager.gd | 4 ++ scenes/managers/ui/level_up_choice.gd | 8 ++-- scenes/pickups/pickup_magnet.gd | 2 +- scenes/player_stats_modifier.gd | 2 +- 14 files changed, 173 insertions(+), 35 deletions(-) create mode 100644 scenes/enemies/enemy_mod_pool.gd create mode 100644 scenes/enemies/enemy_mod_pool.gd.uid diff --git a/global_const.gd b/global_const.gd index 8e09a29..ac81ac4 100644 --- a/global_const.gd +++ b/global_const.gd @@ -24,7 +24,7 @@ const GROUP_XP_ORB = "xp_orb" const GROUP_PICKUP = "pickup" const GROUP_PROJ_MANAGER = "proj_manager" -enum ModRarity { LEGENDARY, EPIC, RARE, NORMAL } +enum Rarity { LEGENDARY, EPIC, RARE, NORMAL } const placeholder_tex = preload("res://assets/sprites/64x64_placeholder.tres") @@ -32,7 +32,7 @@ var MOD_CHOICES = [ { "internal_name": "flat_health_small", "name": "+10 Health", - "rarity": ModRarity.NORMAL, + "rarity": Rarity.NORMAL, "tex": placeholder_tex, "description": "Adds 10 flat health.", "weight": 100, @@ -43,7 +43,7 @@ var MOD_CHOICES = [ { "internal_name": "flat_health_med", "name": "+25 Health", - "rarity": ModRarity.RARE, + "rarity": Rarity.RARE, "tex": placeholder_tex, "description": "Adds 25 flat health.", "weight": 50, @@ -54,7 +54,7 @@ var MOD_CHOICES = [ { "internal_name": "flat_health_large", "name": "+50 Health", - "rarity": ModRarity.EPIC, + "rarity": Rarity.EPIC, "tex": placeholder_tex, "description": "Adds 50 flat health.", "weight": 10, @@ -65,7 +65,7 @@ var MOD_CHOICES = [ { "internal_name": "flat_crit_small", "name": "2.5% More Critical Chance", - "rarity": ModRarity.RARE, + "rarity": Rarity.RARE, "tex": placeholder_tex, "description": "Gives 2.5% more base critical hit chance", "weight": 50, @@ -76,7 +76,7 @@ var MOD_CHOICES = [ { "internal_name": "flat_crit_large", "name": "5% More Critical Chance", - "rarity": ModRarity.EPIC, + "rarity": Rarity.EPIC, "tex": placeholder_tex, "description": "Gives 5% more base critical hit chance", "weight": 10, @@ -87,6 +87,17 @@ var MOD_CHOICES = [ ] +func rarity_to_color(rarity: Rarity) -> Color: + match rarity: + Rarity.RARE: + return Color.YELLOW + Rarity.EPIC: + return Color.BLUE_VIOLET + Rarity.LEGENDARY: + return Color.DARK_ORANGE + return Color.WHITE + + func _draw_random_choice(fortune: float = 1.0) -> Dictionary: var total_weight: int = 0 for choice in MOD_CHOICES: diff --git a/scenes/enemies/enemy.gd b/scenes/enemies/enemy.gd index 69776b6..e8218cc 100644 --- a/scenes/enemies/enemy.gd +++ b/scenes/enemies/enemy.gd @@ -9,9 +9,9 @@ extends CharacterBody2D @onready var contact_damage_cd: Timer = $ContactDamageCD @onready var animation_player: AnimationPlayer = $AnimationPlayer -var move_speed: float -var health: float -var max_health: float +var move_speed: float = default_move_speed +var health: float = default_max_health +var max_health: float = default_max_health var god_mode: bool = false var is_dead: bool = false diff --git a/scenes/enemies/enemy_base.gd b/scenes/enemies/enemy_base.gd index d10e942..2387a46 100644 --- a/scenes/enemies/enemy_base.gd +++ b/scenes/enemies/enemy_base.gd @@ -7,6 +7,7 @@ extends CharacterBody2D @export var target_distance: float = 6.0 @export var path_update_interval: float = 1.5 @export var xp_dropped: float = 5.0 +@export var enemy_rarity: GlobalConst.Rarity = GlobalConst.Rarity.NORMAL var modifiers: Array[EnemyMod] = [] @onready var target_cast: RayCast2D = $TargetCast @@ -16,6 +17,7 @@ var modifiers: Array[EnemyMod] = [] @onready var collision_shape_2d: CollisionShape2D = $CollisionShape2D @onready var shape_cast_2d: ShapeCast2D = $ShapeCast2D @onready var sprite_2d: Sprite2D = $Sprite2D +@onready var label: Label = $Label var player: Player var enemy_name: String @@ -28,8 +30,23 @@ var _path_update_timer: float = 0.0 func _ready() -> void: - health = max_health enemy_name = _gen_name() + match enemy_rarity: + GlobalConst.Rarity.NORMAL: + label.visible = false + GlobalConst.Rarity.RARE: + var mods = EnemyModPool.get_random_mods(2) + label.visible = true + modifiers = mods + GlobalConst.Rarity.EPIC: + var mods = EnemyModPool.get_random_mods(4) + label.visible = true + modifiers = mods + if modifiers.size() > 0: + enemy_name += " the %s" % modifiers.pick_random().adjective + label.add_theme_color_override("font_color", GlobalConst.rarity_to_color(enemy_rarity)) + label.text = enemy_name + health = get_calculated("max_health") shape_cast_2d.shape.radius = collision_shape_2d.shape.radius shape_cast_2d.enabled = false sprite_2d.material = sprite_2d.material.duplicate() @@ -80,7 +97,7 @@ func _do_simple_movement(): var direction = global_position.direction_to(target.global_position) var distance = global_position.distance_to(target.global_position) if distance > 4: - velocity = direction * move_speed + velocity = direction * get_calculated("move_speed") move_and_slide() @@ -98,7 +115,7 @@ func _do_nav_agent_movement(): if shape_cast_2d.is_colliding(): direction = direction.bounce(shape_cast_2d.get_collision_normal(0)).normalized() - velocity = direction * move_speed + velocity = direction * get_calculated("move_speed") move_and_slide() @@ -148,7 +165,7 @@ func die(): func drop_xp_orb() -> void: var orb: XPOrb = preload("res://scenes/xp_orb.tscn").instantiate() - orb.value = xp_dropped + orb.value = xp_dropped * (1 + (modifiers.size() * 3)) orb.position = position get_parent().call_deferred("add_child", orb) @@ -156,3 +173,21 @@ func drop_xp_orb() -> void: func _on_animation_player_animation_finished(anim_name: StringName) -> void: if is_dead: queue_free() + + +func get_calculated(key: String) -> Variant: + # set max move speed to players move speed + if key == "move_speed": + return clampf( + EnemyMod.get_calculated(self, key), + 0, + player.player_stats.get_final("move_speed", player.modifiers) + ) + return EnemyMod.get_calculated(self, key) + + +func has_property(key: String) -> bool: + for prop in get_property_list(): + if prop.name == key: + return true + return false diff --git a/scenes/enemies/enemy_base.tscn b/scenes/enemies/enemy_base.tscn index c2c8ff5..fa66c2d 100644 --- a/scenes/enemies/enemy_base.tscn +++ b/scenes/enemies/enemy_base.tscn @@ -90,4 +90,20 @@ wait_time = 0.5 shape = SubResource("CircleShape2D_pkqou") max_results = 2 +[node name="Label" type="Label" parent="."] +anchors_preset = 7 +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +offset_left = -85.5 +offset_top = 5.0 +offset_right = 85.5 +offset_bottom = 28.0 +grow_horizontal = 2 +grow_vertical = 0 +theme_override_font_sizes/font_size = 9 +text = "Unnamed the Adjective" +horizontal_alignment = 1 + [connection signal="animation_finished" from="AnimationPlayer" to="." method="_on_animation_player_animation_finished"] diff --git a/scenes/enemies/enemy_bat.gd b/scenes/enemies/enemy_bat.gd index ac583b5..dfc0661 100644 --- a/scenes/enemies/enemy_bat.gd +++ b/scenes/enemies/enemy_bat.gd @@ -26,4 +26,7 @@ const NAME_SUFFIXES: Array[String] = [ func _gen_name() -> String: - return "%s%s%s" % [NAME_PREFIXES.pick_random(), NAME_ROOTS.pick_random(), NAME_SUFFIXES.pick_random()] + return ( + "%s%s%s" + % [NAME_PREFIXES.pick_random(), NAME_ROOTS.pick_random(), NAME_SUFFIXES.pick_random()] + ) diff --git a/scenes/enemies/enemy_mod.gd b/scenes/enemies/enemy_mod.gd index c2121f4..9b7cf1b 100644 --- a/scenes/enemies/enemy_mod.gd +++ b/scenes/enemies/enemy_mod.gd @@ -3,13 +3,20 @@ extends Resource enum ModType { ADDITIVE, MULTIPLICATIVE, ABSOLUTE, BOOL } +@export var mod_name: String @export var mod_property: String @export var mod_value: float @export var mod_value_bool: bool = false @export var mod_type: ModType = ModType.MULTIPLICATIVE -func get_calculated(enemy: EnemyBase, key: String) -> Variant: - assert(enemy.has_property(key), "tried to calculate property '%s' where base value does not exist on %s" % [key, enemy]) +var adjective: String + + +static func get_calculated(enemy: EnemyBase, key: String) -> Variant: + assert( + enemy.has_property(key), + "tried to calculate property '%s' where base value does not exist on %s" % [key, enemy] + ) var base_value = enemy.get(key) var add = 0.0 var mul = 1.0 @@ -17,11 +24,11 @@ func get_calculated(enemy: EnemyBase, key: String) -> Variant: if mod.mod_property == key: match mod.mod_type: ModType.ADDITIVE: - add += mod.mod_value + add += mod.mod_value ModType.MULTIPLICATIVE: - mul *= mod.mod_value + mul *= mod.mod_value ModType.ABSOLUTE: - return mod.mod_value + return mod.mod_value ModType.BOOL: - return mod.mod_value_bool + return mod.mod_value_bool return (base_value + add) * mul diff --git a/scenes/enemies/enemy_mod_pool.gd b/scenes/enemies/enemy_mod_pool.gd new file mode 100644 index 0000000..669b750 --- /dev/null +++ b/scenes/enemies/enemy_mod_pool.gd @@ -0,0 +1,58 @@ +class_name EnemyModPool +extends Resource + +const MAX_HEALTH_MOD: Dictionary = { + "mod_name": "Extra health", + "mod_property": "max_health", + "mod_value": 1.0, + "mod_value_bool": false, + "mod_type": EnemyMod.ModType.MULTIPLICATIVE, + "mod_adjectives": + [ + "Sturdy", + "Bulky", + "Ironclad", + "Unyielding", + "Stonehide", + "Thicc", + "Chunky", + "Pudgy", + "Chonky", + "Double-stuffed" + ], +} + +const MOD_POOL = [ + { + "mod_name": "Extra movement speed", + "mod_property": "move_speed", + "mod_value": 1.25, + "mod_value_bool": false, + "mod_type": EnemyMod.ModType.MULTIPLICATIVE, + "mod_adjectives": ["Swift", "Nimble", "Fleet", "Spry", "Skittering", "Zoomy", "Zippy"] + } +] + + +static func get_random_mods(count: int) -> Array[EnemyMod]: + var mods: Array[EnemyMod] = [] + # always include extra health mod + var hp_mod = _dict_to_mod(MAX_HEALTH_MOD) + hp_mod.mod_value *= 5 * count + mods.append(hp_mod) + for i in count: + var mod_data = MOD_POOL.pick_random() + var mod = _dict_to_mod(mod_data) + mods.append(mod) + return mods + + +static func _dict_to_mod(d: Dictionary) -> EnemyMod: + var mod: EnemyMod = EnemyMod.new() + mod.mod_name = d["mod_name"] + mod.mod_property = d["mod_property"] + mod.mod_value = d["mod_value"] + mod.mod_value_bool = d["mod_value_bool"] + mod.mod_type = d["mod_type"] + mod.adjective = d["mod_adjectives"].pick_random() + return mod diff --git a/scenes/enemies/enemy_mod_pool.gd.uid b/scenes/enemies/enemy_mod_pool.gd.uid new file mode 100644 index 0000000..a18f454 --- /dev/null +++ b/scenes/enemies/enemy_mod_pool.gd.uid @@ -0,0 +1 @@ +uid://bl11yfav37ecs diff --git a/scenes/enemies/enemy_rat.gd b/scenes/enemies/enemy_rat.gd index 40a214d..a35a5ac 100644 --- a/scenes/enemies/enemy_rat.gd +++ b/scenes/enemies/enemy_rat.gd @@ -19,14 +19,11 @@ const NAME_ROOTS: Array[String] = [ "uzzle", ] -const NAME_SUFFIXES: Array[String] = [ - "y", - "er", - "o", - "ok", - "in" -] +const NAME_SUFFIXES: Array[String] = ["y", "er", "o", "ok", "in"] func _gen_name() -> String: - return "%s%s%s" % [NAME_PREFIXES.pick_random(), NAME_ROOTS.pick_random(), NAME_SUFFIXES.pick_random()] + return ( + "%s%s%s" + % [NAME_PREFIXES.pick_random(), NAME_ROOTS.pick_random(), NAME_SUFFIXES.pick_random()] + ) diff --git a/scenes/enemies/enemy_slime_small.gd b/scenes/enemies/enemy_slime_small.gd index ed29dfa..f181000 100644 --- a/scenes/enemies/enemy_slime_small.gd +++ b/scenes/enemies/enemy_slime_small.gd @@ -39,16 +39,22 @@ const NAME_SUFFIXES: Array[String] = [ "oo", ] + func _ready() -> void: shader_material = ShaderMaterial.new() disabled_sprite.visible = false set_color(color) shader_material.shader = shader base_sprite.material = shader_material + super._ready() func set_color(new_color: Color) -> void: shader_material.set_shader_parameter("base_color", new_color) + func _gen_name() -> String: - return "%s%s%s" % [NAME_PREFIXES.pick_random(), NAME_ROOTS.pick_random(), NAME_SUFFIXES.pick_random()] + return ( + "%s%s%s" + % [NAME_PREFIXES.pick_random(), NAME_ROOTS.pick_random(), NAME_SUFFIXES.pick_random()] + ) diff --git a/scenes/managers/enemy_manager.gd b/scenes/managers/enemy_manager.gd index fa7c35b..dc60930 100644 --- a/scenes/managers/enemy_manager.gd +++ b/scenes/managers/enemy_manager.gd @@ -37,6 +37,10 @@ func _on_timer_timeout() -> void: var new_enemy = next_enemy.instantiate() new_enemy.position = _get_spawn_pos() new_enemy.target = target + if randf() < 0.1: + new_enemy.enemy_rarity = GlobalConst.Rarity.RARE + if randf() < 0.1: + new_enemy.enemy_rarity = GlobalConst.Rarity.EPIC if is_instance_of(new_enemy, EnemySlimeSmall): var slime_color: Color = SLIME_COLOR_VARIATIONS.pick_random() new_enemy.color = slime_color diff --git a/scenes/managers/ui/level_up_choice.gd b/scenes/managers/ui/level_up_choice.gd index 4978097..763d1cb 100644 --- a/scenes/managers/ui/level_up_choice.gd +++ b/scenes/managers/ui/level_up_choice.gd @@ -13,13 +13,13 @@ signal lvlup_picked(mod: PlayerStatsModifier) func _ready() -> void: match mod.rarity: - GlobalConst.ModRarity.NORMAL: + GlobalConst.Rarity.NORMAL: upgrade_name.add_theme_color_override("font_color", Color.WHITE) - GlobalConst.ModRarity.RARE: + GlobalConst.Rarity.RARE: upgrade_name.add_theme_color_override("font_color", Color.DODGER_BLUE) - GlobalConst.ModRarity.EPIC: + GlobalConst.Rarity.EPIC: upgrade_name.add_theme_color_override("font_color", Color.DARK_ORCHID) - GlobalConst.ModRarity.LEGENDARY: + GlobalConst.Rarity.LEGENDARY: upgrade_name.add_theme_color_override("font_color", Color.DARK_ORANGE) upgrade_name.text = mod.title diff --git a/scenes/pickups/pickup_magnet.gd b/scenes/pickups/pickup_magnet.gd index 79272ce..b9aba3a 100644 --- a/scenes/pickups/pickup_magnet.gd +++ b/scenes/pickups/pickup_magnet.gd @@ -16,7 +16,7 @@ func pickup() -> void: mod.title = "Magnet" mod.tex = GlobalConst.placeholder_tex mod.internal_name = "magnet" - mod.rarity = GlobalConst.ModRarity.RARE + mod.rarity = GlobalConst.Rarity.RARE mod.stat_name = "pickup_radius" mod.value = 5000.0 mod.type = PlayerStatsModifier.ModifierType.ABSOLUTE diff --git a/scenes/player_stats_modifier.gd b/scenes/player_stats_modifier.gd index 12aab6b..0d8e8b1 100644 --- a/scenes/player_stats_modifier.gd +++ b/scenes/player_stats_modifier.gd @@ -8,6 +8,6 @@ var value: Variant var type: ModifierType = ModifierType.ADDITIVE var internal_name: String var title: String -var rarity: GlobalConst.ModRarity +var rarity: GlobalConst.Rarity var tex: Texture2D var description: String