From 78a615665642e6b6f47699f28de1ce4385fc566a Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Wed, 4 Feb 2026 00:52:16 -0500 Subject: [PATCH] Oh wow, I thought I already pushed some of this stuff. Let's see if I can remember it all. Undead residents added. Whights added, Vampyrs added. Bandit Hideout adventure added. --- README.md | 4 +- draugnorak.nvgt | 5 +- sounds/enemies/undead_resident1.ogg | 3 + sounds/enemies/undead_resident_dies.ogg | 3 + sounds/enemies/vampyr1.ogg | 3 + sounds/enemies/vampyr2.ogg | 3 + sounds/enemies/vampyr3.ogg | 3 + sounds/enemies/vampyr4.ogg | 3 + sounds/enemies/vampyr_dies.ogg | 3 + sounds/enemies/wight1.ogg | 3 + sounds/enemies/wight_dies.ogg | 3 + src/bosses/adventure_system.nvgt | 10 +- src/bosses/bandit_hideout.nvgt | 669 ++++++++++++++++++++++++ src/constants.nvgt | 19 + src/enemies/undead.nvgt | 294 ++++++++++- src/save_system.nvgt | 20 + src/time_system.nvgt | 6 + src/world/barricade.nvgt | 2 + 18 files changed, 1029 insertions(+), 27 deletions(-) create mode 100644 sounds/enemies/undead_resident1.ogg create mode 100644 sounds/enemies/undead_resident_dies.ogg create mode 100644 sounds/enemies/vampyr1.ogg create mode 100644 sounds/enemies/vampyr2.ogg create mode 100644 sounds/enemies/vampyr3.ogg create mode 100644 sounds/enemies/vampyr4.ogg create mode 100644 sounds/enemies/vampyr_dies.ogg create mode 100644 sounds/enemies/wight1.ogg create mode 100644 sounds/enemies/wight_dies.ogg create mode 100644 src/bosses/bandit_hideout.nvgt diff --git a/README.md b/README.md index 6f6aebe..59ae10e 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ chmod +x draugnorak ## Quick Start - Move with Left/Right, jump with Up. -- Hold Shift for 1 second to search the current area. +- Hold Shift, /, or Z for 1 second to search the current area. - Craft basic tools in the base with C. - Set snares and build fires to survive. @@ -20,7 +20,7 @@ Some of the first things you will want are a stone knife, spear, stone axe, and - **Left/Right**: Move. - **Up**: Jump, climb trees, start rope climbs when prompted. - **Down**: Climb down trees or descend rope climbs when prompted. -- **Shift (hold)**: Search area (1-second hold, 1-second delay). +- **Shift, /, or Z (hold)**: Search area (1-second hold, 1-second delay). - **Control (hold/release)**: Attack with equipped weapon. Sling uses a charge window. - **A**: Action menu (place snare, feed fire, burn incense). - **C**: Crafting menu (base only). diff --git a/draugnorak.nvgt b/draugnorak.nvgt index 8ae8b1e..036610c 100755 --- a/draugnorak.nvgt +++ b/draugnorak.nvgt @@ -394,12 +394,13 @@ void run_game() // Searching Logic bool shift_down = (key_down(KEY_LSHIFT) || key_down(KEY_RSHIFT)); - if (!shift_down && !searching) { + bool search_key_down = shift_down || key_down(KEY_SLASH) || key_down(KEY_Z); + if (!search_key_down && !searching) { search_timer.restart(); } // Apply rune gathering bonus to search time int search_time = apply_rune_gather_bonus(2000); - if (shift_down && search_timer.elapsed > search_time && !searching) + if (search_key_down && search_timer.elapsed > search_time && !searching) { searching = true; search_delay_timer.restart(); diff --git a/sounds/enemies/undead_resident1.ogg b/sounds/enemies/undead_resident1.ogg new file mode 100644 index 0000000..d6760c8 --- /dev/null +++ b/sounds/enemies/undead_resident1.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ac847f7adc8ce3a4b74e0bfa820bf440322b9bb684fb5a15900933b8e21c93c +size 23457 diff --git a/sounds/enemies/undead_resident_dies.ogg b/sounds/enemies/undead_resident_dies.ogg new file mode 100644 index 0000000..ac00404 --- /dev/null +++ b/sounds/enemies/undead_resident_dies.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e3d84681dc990be008f34cae66095d548f2d0263661f8075b97eaa604683060 +size 13986 diff --git a/sounds/enemies/vampyr1.ogg b/sounds/enemies/vampyr1.ogg new file mode 100644 index 0000000..3b75e78 --- /dev/null +++ b/sounds/enemies/vampyr1.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09e18ff032fe26f58fe1241d1e6883c4defa7bdaae772b4c507472fed5b39286 +size 17360 diff --git a/sounds/enemies/vampyr2.ogg b/sounds/enemies/vampyr2.ogg new file mode 100644 index 0000000..05534ec --- /dev/null +++ b/sounds/enemies/vampyr2.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0569819aa7d6b9d7357a3af8791b0eb17912964faf2b98174610befd52b40d1b +size 19055 diff --git a/sounds/enemies/vampyr3.ogg b/sounds/enemies/vampyr3.ogg new file mode 100644 index 0000000..cf84e79 --- /dev/null +++ b/sounds/enemies/vampyr3.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cc005c357e4ae9e60fdc1290920e3e64819e6bdc597d0de16e2214f56c7e0b4 +size 21079 diff --git a/sounds/enemies/vampyr4.ogg b/sounds/enemies/vampyr4.ogg new file mode 100644 index 0000000..4661c6f --- /dev/null +++ b/sounds/enemies/vampyr4.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58a3e979a22c50962b760c6196f47c788fcc60d1bf4d336119790dda4322ace6 +size 31128 diff --git a/sounds/enemies/vampyr_dies.ogg b/sounds/enemies/vampyr_dies.ogg new file mode 100644 index 0000000..880d568 --- /dev/null +++ b/sounds/enemies/vampyr_dies.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e92b33c3bf02d01b6d993c01356c88d57c3e54635ae70319d5422015942954c +size 26885 diff --git a/sounds/enemies/wight1.ogg b/sounds/enemies/wight1.ogg new file mode 100644 index 0000000..43df492 --- /dev/null +++ b/sounds/enemies/wight1.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ef5923dd9d6c726f2598aa7f4c6ab28ab00d9a4788c24190496dd7c743a6d25 +size 70284 diff --git a/sounds/enemies/wight_dies.ogg b/sounds/enemies/wight_dies.ogg new file mode 100644 index 0000000..880d568 --- /dev/null +++ b/sounds/enemies/wight_dies.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e92b33c3bf02d01b6d993c01356c88d57c3e54635ae70319d5422015942954c +size 26885 diff --git a/src/bosses/adventure_system.nvgt b/src/bosses/adventure_system.nvgt index c557292..d4fa2a8 100644 --- a/src/bosses/adventure_system.nvgt +++ b/src/bosses/adventure_system.nvgt @@ -3,6 +3,7 @@ #include "src/bosses/adventure_combat.nvgt" #include "src/bosses/unicorn/unicorn_boss.nvgt" +#include "src/bosses/bandit_hideout.nvgt" void check_adventure_menu(int player_x) { if (key_pressed(KEY_TAB)) { @@ -26,13 +27,18 @@ void run_adventure_menu(int player_x) { // Check available adventures based on terrain string[] options; - int[] adventure_ids; // 1 = Unicorn + int[] adventure_ids; // 1 = Unicorn, 2 = Bandit's Hideout if (mountain !is null) { // Mountain terrain options.insert_last("Unicorn Hunt (Mountain Boss)"); adventure_ids.insert_last(1); } + + if (mountain is null && (terrain == "forest" || terrain == "deep_forest")) { + options.insert_last("Bandit's Hideout"); + adventure_ids.insert_last(ADVENTURE_BANDIT_HIDEOUT); + } if (options.length() == 0) { speak_with_history("No adventures found in this area.", true); @@ -78,5 +84,7 @@ void start_adventure(int adventure_id) { last_adventure_day = current_day; if (adventure_id == 1) { run_unicorn_adventure(); + } else if (adventure_id == ADVENTURE_BANDIT_HIDEOUT) { + run_bandit_hideout_adventure(); } } diff --git a/src/bosses/bandit_hideout.nvgt b/src/bosses/bandit_hideout.nvgt new file mode 100644 index 0000000..462adca --- /dev/null +++ b/src/bosses/bandit_hideout.nvgt @@ -0,0 +1,669 @@ +// Bandit's Hideout Adventure logic +// Terrain: Forest / Deep Forest +// Objective: Break the barricade at the enemy base. + +const int ADVENTURE_BANDIT_HIDEOUT = 2; +const int BANDIT_HIDEOUT_MAP_SIZE = 100; +const int BANDIT_HIDEOUT_SEGMENT_MIN = 5; +const int BANDIT_HIDEOUT_SEGMENT_MAX = 10; +const int BANDIT_HIDEOUT_BANDIT_COUNT = 5; +const int BANDIT_HIDEOUT_BASE_SPAWN_RANGE = 10; +const int BANDIT_HIDEOUT_START_SPAWN_RANGE = 10; +const int BANDIT_HIDEOUT_BARRICADE_HP_PER_DAY = 34; +const int BANDIT_HIDEOUT_BARRICADE_HP_MAX = 500; +const double BANDIT_HIDEOUT_FAVOR_PER_KILL = 0.2; +const double BANDIT_HIDEOUT_BASE_FAVOR = 3.0; +const double BANDIT_HIDEOUT_FAVOR_MAX = 10.0; + +class HideoutBandit { + int position; + int health; + string alertSound; + string weaponType; // "spear" or "axe" + int soundHandle; + bool inWeaponRange; + timer moveTimer; + timer attackTimer; + int moveInterval; + + HideoutBandit(int pos, const string&in alert, const string&in weapon, int interval) { + position = pos; + health = BANDIT_HEALTH; + alertSound = alert; + weaponType = weapon; + moveInterval = interval; + soundHandle = -1; + inWeaponRange = false; + moveTimer.restart(); + attackTimer.restart(); + } +} + +HideoutBandit@[] hideoutBandits; +string[] hideoutTerrain; +int hideoutPlayerX = 0; +int hideoutPlayerFacing = 1; // 0 = west, 1 = east +int hideoutBaseX = BANDIT_HIDEOUT_MAP_SIZE - 1; +int hideoutBarricadeHealth = 0; +int hideoutBarricadeMax = 0; +int hideoutBanditsKilled = 0; +timer hideoutWalkTimer; +timer hideoutAttackTimer; +timer hideoutSearchTimer; +timer hideoutSearchDelayTimer; +bool hideoutSearching = false; + +string pick_hideout_terrain() { + int roll = random(0, 2); + if (roll == 0) return "grass"; + if (roll == 1) return "gravel"; + return "stone"; +} + +void build_hideout_terrain() { + hideoutTerrain.resize(BANDIT_HIDEOUT_MAP_SIZE); + int index = 0; + while (index < BANDIT_HIDEOUT_MAP_SIZE) { + int segmentLength = random(BANDIT_HIDEOUT_SEGMENT_MIN, BANDIT_HIDEOUT_SEGMENT_MAX); + string terrain = pick_hideout_terrain(); + for (int i = 0; i < segmentLength && index < BANDIT_HIDEOUT_MAP_SIZE; i++) { + hideoutTerrain[index] = terrain; + index++; + } + } + if (hideoutBaseX >= 0 && hideoutBaseX < int(hideoutTerrain.length())) { + hideoutTerrain[hideoutBaseX] = "stone"; + } +} + +string get_hideout_terrain_at(int pos) { + if (pos < 0 || pos >= int(hideoutTerrain.length())) return "grass"; + string terrain = hideoutTerrain[pos]; + if (terrain == "") return "grass"; + return terrain; +} + +string get_hideout_footstep_sound(int pos) { + string terrain = get_hideout_terrain_at(pos); + if (terrain == "stone") return "sounds/terrain/stone.ogg"; + if (terrain == "gravel") return "sounds/terrain/gravel.ogg"; + return "sounds/terrain/grass.ogg"; +} + +void play_hideout_player_footstep() { + string soundFile = get_hideout_footstep_sound(hideoutPlayerX); + if (file_exists(soundFile)) { + p.play_stationary(soundFile, false); + } +} + +void play_hideout_positional_footstep(int listenerX, int stepX, int maxDistance, float volumeStep) { + if (abs(stepX - listenerX) > maxDistance) return; + string soundFile = get_hideout_footstep_sound(stepX); + if (file_exists(soundFile)) { + play_1d_with_volume_step(soundFile, listenerX, stepX, false, volumeStep); + } +} + +void clear_hideout_bandits() { + for (uint i = 0; i < hideoutBandits.length(); i++) { + if (hideoutBandits[i].soundHandle != -1) { + p.destroy_sound(hideoutBandits[i].soundHandle); + hideoutBandits[i].soundHandle = -1; + } + hideoutBandits[i].inWeaponRange = false; + } + hideoutBandits.resize(0); +} + +HideoutBandit@ get_hideout_bandit_at(int pos) { + for (uint i = 0; i < hideoutBandits.length(); i++) { + if (hideoutBandits[i].position == pos) { + return @hideoutBandits[i]; + } + } + return null; +} + +int clamp_hideout_spawn_start(int startX) { + if (startX < 0) return 0; + if (startX >= BANDIT_HIDEOUT_MAP_SIZE) return BANDIT_HIDEOUT_MAP_SIZE - 1; + return startX; +} + +int clamp_hideout_spawn_end(int endX) { + if (endX < 0) return 0; + if (endX >= BANDIT_HIDEOUT_MAP_SIZE) return BANDIT_HIDEOUT_MAP_SIZE - 1; + return endX; +} + +int pick_hideout_spawn_position(int startX, int endX) { + int startClamp = clamp_hideout_spawn_start(startX); + int endClamp = clamp_hideout_spawn_end(endX); + if (startClamp > endClamp) { + int swapTemp = startClamp; + startClamp = endClamp; + endClamp = swapTemp; + } + + int spawnX = -1; + for (int attempt = 0; attempt < 20; attempt++) { + int candidate = random(startClamp, endClamp); + if (candidate == hideoutPlayerX) continue; + if (get_hideout_bandit_at(candidate) != null) continue; + spawnX = candidate; + break; + } + if (spawnX == -1) { + spawnX = random(startClamp, endClamp); + } + return spawnX; +} + +void spawn_hideout_bandit_in_range(int startX, int endX) { + int spawnX = pick_hideout_spawn_position(startX, endX); + string alertSound = pick_invader_alert_sound("bandit"); + if (alertSound == "") alertSound = "sounds/enemies/bandit1.ogg"; + string weaponType = (random(0, 1) == 0) ? "spear" : "axe"; + int moveInterval = random(BANDIT_MOVE_INTERVAL_MIN, BANDIT_MOVE_INTERVAL_MAX); + + HideoutBandit@ bandit = HideoutBandit(spawnX, alertSound, weaponType, moveInterval); + hideoutBandits.insert_last(bandit); + bandit.soundHandle = play_1d_with_volume_step(bandit.alertSound, hideoutPlayerX, bandit.position, true, BANDIT_SOUND_VOLUME_STEP); +} + +void spawn_hideout_bandits_initial() { + int baseSpawnStart = hideoutBaseX - (BANDIT_HIDEOUT_BASE_SPAWN_RANGE - 1); + int baseSpawnEnd = hideoutBaseX; + for (int i = 0; i < BANDIT_HIDEOUT_BANDIT_COUNT; i++) { + spawn_hideout_bandit_in_range(baseSpawnStart, baseSpawnEnd); + } +} + +void respawn_hideout_bandit() { + int startSpawnStart = 0; + int startSpawnEnd = BANDIT_HIDEOUT_START_SPAWN_RANGE - 1; + if (startSpawnEnd > hideoutBaseX) startSpawnEnd = hideoutBaseX; + + int baseSpawnStart = hideoutBaseX - (BANDIT_HIDEOUT_BASE_SPAWN_RANGE - 1); + int baseSpawnEnd = hideoutBaseX; + + int roll = random(0, 1); + if (roll == 0) { + spawn_hideout_bandit_in_range(startSpawnStart, startSpawnEnd); + } else { + spawn_hideout_bandit_in_range(baseSpawnStart, baseSpawnEnd); + } +} + +void init_bandit_hideout_adventure() { + reset_adventure_combat_state(); + hideoutPlayerX = 0; + hideoutPlayerFacing = 1; + hideoutBaseX = BANDIT_HIDEOUT_MAP_SIZE - 1; + hideoutBanditsKilled = 0; + + int barricadeBase = current_day * BANDIT_HIDEOUT_BARRICADE_HP_PER_DAY; + if (barricadeBase < BANDIT_HIDEOUT_BARRICADE_HP_PER_DAY) barricadeBase = BANDIT_HIDEOUT_BARRICADE_HP_PER_DAY; + if (barricadeBase > BANDIT_HIDEOUT_BARRICADE_HP_MAX) barricadeBase = BANDIT_HIDEOUT_BARRICADE_HP_MAX; + hideoutBarricadeMax = barricadeBase; + hideoutBarricadeHealth = barricadeBase; + + build_hideout_terrain(); + clear_hideout_bandits(); + spawn_hideout_bandits_initial(); +} + +void cleanup_bandit_hideout_adventure() { + clear_hideout_bandits(); + reset_adventure_combat_state(); + p.destroy_all(); +} + +void run_bandit_hideout_adventure() { + // Stop main game sounds + p.destroy_all(); + + init_bandit_hideout_adventure(); + + string[] intro; + intro.insert_last("=== Bandit's Hideout ==="); + intro.insert_last(""); + intro.insert_last("You find a hidden bandit base deep in the forest."); + intro.insert_last("The base lies far to the east, guarded by a barricade."); + intro.insert_last(""); + intro.insert_last("Objective:"); + intro.insert_last(" - Reach the base and destroy the barricade"); + intro.insert_last(""); + intro.insert_last("Bandits will, of course, not take this lying down."); + text_reader_lines(intro, "Adventure", true); + + while (true) { + wait(5); + + if (key_pressed(KEY_ESCAPE)) { + cleanup_bandit_hideout_adventure(); + speak_with_history("You flee the hideout.", true); + return; + } + + // Standard game keys + check_quick_slot_keys(); + check_notification_keys(); + check_speech_history_keys(); + + if (key_pressed(KEY_H)) { + speak_with_history(player_health + " health of " + max_health, true); + } + + if (key_pressed(KEY_X)) { + int distanceToBase = hideoutBaseX - hideoutPlayerX; + if (distanceToBase < 0) distanceToBase = 0; + string terrain = get_hideout_terrain_at(hideoutPlayerX); + speak_with_history("x " + hideoutPlayerX + ", terrain " + terrain + ". Base " + distanceToBase + " tiles east. Barricade " + hideoutBarricadeHealth + " of " + hideoutBarricadeMax + ".", true); + } + + handle_hideout_player_movement(); + handle_hideout_player_actions(); + update_hideout_search(); + + update_hideout_bandits(); + adventure_update_bow_shot(hideoutPlayerX); + + if (hideoutBarricadeHealth <= 0) { + cleanup_bandit_hideout_adventure(); + p.play_stationary("sounds/actions/break_snare.ogg", false); + give_bandit_hideout_rewards(); + return; + } + + if (player_health <= 0) { + cleanup_bandit_hideout_adventure(); + speak_with_history("The bandits cut you down.", true); + return; + } + + p.update_listener_1d(hideoutPlayerX); + } +} + +void handle_hideout_player_movement() { + if (key_pressed(KEY_LEFT) && hideoutPlayerFacing != 0) { + hideoutPlayerFacing = 0; + speak_with_history("west", true); + hideoutWalkTimer.restart(); + } + if (key_pressed(KEY_RIGHT) && hideoutPlayerFacing != 1) { + hideoutPlayerFacing = 1; + speak_with_history("east", true); + hideoutWalkTimer.restart(); + } + + if (hideoutWalkTimer.elapsed > walk_speed) { + if (key_down(KEY_LEFT) && hideoutPlayerX > 0) { + hideoutPlayerFacing = 0; + hideoutPlayerX--; + hideoutWalkTimer.restart(); + if (player_health > 0) play_hideout_player_footstep(); + } else if (key_down(KEY_RIGHT) && hideoutPlayerX < hideoutBaseX) { + hideoutPlayerFacing = 1; + hideoutPlayerX++; + hideoutWalkTimer.restart(); + if (player_health > 0) play_hideout_player_footstep(); + } + } +} + +void handle_hideout_player_actions() { + bool ctrlDown = (key_down(KEY_LCTRL) || key_down(KEY_RCTRL)); + + // Bow draw detection + if (bow_equipped) { + if (ctrlDown && !bow_drawing) { + if (get_personal_count(ITEM_ARROWS) > 0) { + bow_drawing = true; + bow_draw_timer.restart(); + p.play_stationary("sounds/weapons/bow_draw.ogg", false); + } else { + speak_ammo_blocked("No arrows."); + } + } + + if (bow_drawing && !ctrlDown) { + adventure_release_bow_attack(hideoutPlayerX, hideoutPlayerFacing, @bandit_hideout_ranged_attack); + bow_drawing = false; + } + } + if (!bow_equipped && bow_drawing) { + bow_drawing = false; + } + + // Sling charge detection + if (!bow_equipped && sling_equipped && ctrlDown && !sling_charging) { + if (get_personal_count(ITEM_STONES) > 0) { + sling_charging = true; + sling_charge_timer.restart(); + sling_sound_handle = p.play_stationary("sounds/weapons/sling_swing.ogg", true); + last_sling_stage = -1; + } else { + speak_ammo_blocked("No stones."); + } + } + + if (sling_charging && ctrlDown) { + update_sling_charge(); + } + + if (sling_charging && !ctrlDown) { + adventure_release_sling_attack(hideoutPlayerX, hideoutPlayerFacing, @bandit_hideout_ranged_attack); + sling_charging = false; + safe_destroy_sound(sling_sound_handle); + } + + if (!bow_equipped && !bow_drawing && !sling_equipped && !sling_charging) { + if (fishing_pole_equipped) return; + int weaponType = get_hideout_melee_weapon_type(); + if (weaponType == -1) return; + + int attackCooldown = 1000; + if (weaponType == ADVENTURE_WEAPON_SPEAR) attackCooldown = 800; + if (weaponType == ADVENTURE_WEAPON_AXE) attackCooldown = 1600; + + if (ctrlDown && hideoutAttackTimer.elapsed > attackCooldown) { + hideoutAttackTimer.restart(); + play_hideout_melee_swing(weaponType); + if (hideout_melee_hit(weaponType)) { + play_hideout_melee_hit(weaponType); + } + } + } +} + +void update_hideout_search() { + bool shiftDown = (key_down(KEY_LSHIFT) || key_down(KEY_RSHIFT)); + if (shiftDown) { + if (key_pressed(KEY_COMMA) || key_pressed(KEY_PERIOD)) { + hideoutSearching = false; + hideoutSearchDelayTimer.restart(); + } + } + + bool searchKeyDown = shiftDown || key_down(KEY_SLASH) || key_down(KEY_Z); + if (!searchKeyDown && !hideoutSearching) { + hideoutSearchTimer.restart(); + } + + int searchTime = apply_rune_gather_bonus(2000); + if (searchKeyDown && hideoutSearchTimer.elapsed > searchTime && !hideoutSearching) { + hideoutSearching = true; + hideoutSearchDelayTimer.restart(); + } + + if (hideoutSearching && hideoutSearchDelayTimer.elapsed >= 1000) { + hideoutSearching = false; + hideoutSearchTimer.restart(); + perform_hideout_search(); + } +} + +void perform_hideout_search() { + if (random(1, 100) <= 10) { + speak_with_history("Found nothing.", true); + return; + } + + string terrain = get_hideout_terrain_at(hideoutPlayerX); + if (terrain == "stone" || terrain == "gravel") { + if (try_search_for_terrain(terrain)) { + return; + } + } + + speak_with_history("Found nothing.", true); +} + +int get_hideout_melee_weapon_type() { + if (spear_equipped) return ADVENTURE_WEAPON_SPEAR; + if (axe_equipped) return ADVENTURE_WEAPON_AXE; + return -1; +} + +void play_hideout_melee_swing(int weaponType) { + if (weaponType == ADVENTURE_WEAPON_SPEAR) { + p.play_stationary("sounds/weapons/spear_swing.ogg", false); + } else if (weaponType == ADVENTURE_WEAPON_AXE) { + p.play_stationary("sounds/weapons/axe_swing.ogg", false); + } +} + +void play_hideout_melee_hit(int weaponType) { + if (weaponType == ADVENTURE_WEAPON_SPEAR) { + p.play_stationary("sounds/weapons/spear_hit.ogg", false); + } else if (weaponType == ADVENTURE_WEAPON_AXE) { + p.play_stationary("sounds/weapons/axe_hit.ogg", false); + } +} + +bool hideout_melee_hit(int weaponType) { + int range = (weaponType == ADVENTURE_WEAPON_SPEAR) ? 1 : 0; + int damage = (weaponType == ADVENTURE_WEAPON_SPEAR) ? SPEAR_DAMAGE : AXE_DAMAGE; + + for (int offset = -range; offset <= range; offset++) { + int targetX = hideoutPlayerX + offset; + if (damage_hideout_bandit_at(targetX, damage)) { + play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", hideoutPlayerX, targetX, BANDIT_SOUND_VOLUME_STEP); + return true; + } + } + + if (abs(hideoutPlayerX - hideoutBaseX) <= range) { + if (apply_hideout_barricade_damage(damage)) { + return true; + } + } + + return false; +} + +bool apply_hideout_barricade_damage(int damage) { + if (damage <= 0) return false; + if (hideoutBarricadeHealth <= 0) return false; + hideoutBarricadeHealth -= damage; + if (hideoutBarricadeHealth < 0) hideoutBarricadeHealth = 0; + play_1d_with_volume_step("sounds/weapons/axe_hit.ogg", hideoutPlayerX, hideoutBaseX, false, BANDIT_SOUND_VOLUME_STEP); + return true; +} + +int find_hideout_ranged_target(int playerX, int direction, int range) { + for (int dist = 1; dist <= range; dist++) { + int checkX = playerX + (dist * direction); + if (checkX < 0 || checkX >= BANDIT_HIDEOUT_MAP_SIZE) break; + + if (get_hideout_bandit_at(checkX) != null) { + return checkX; + } + + if (hideoutBarricadeHealth > 0 && checkX == hideoutBaseX) { + return checkX; + } + } + return -1; +} + +int bandit_hideout_ranged_attack(int playerX, int direction, int range, int weaponType, int damage) { + int targetX = find_hideout_ranged_target(playerX, direction, range); + if (targetX == -1) return -1; + + if (targetX == hideoutBaseX) { + apply_hideout_barricade_damage(damage); + return targetX; + } + + if (damage_hideout_bandit_at(targetX, damage)) { + return targetX; + } + + return -1; +} + +bool damage_hideout_bandit_at(int pos, int damage) { + for (uint i = 0; i < hideoutBandits.length(); i++) { + if (hideoutBandits[i].position == pos) { + hideoutBandits[i].health -= damage; + if (hideoutBandits[i].health <= 0) { + if (hideoutBandits[i].soundHandle != -1) { + p.destroy_sound(hideoutBandits[i].soundHandle); + hideoutBandits[i].soundHandle = -1; + } + if (hideoutBandits[i].inWeaponRange) { + play_weapon_range_sound("sounds/enemies/exit_range.ogg", hideoutBandits[i].position); + } + play_creature_death_sounds("sounds/enemies/enemy_falls.ogg", hideoutBandits[i].alertSound, hideoutPlayerX, pos, BANDIT_SOUND_VOLUME_STEP); + hideoutBandits.remove_at(i); + hideoutBanditsKilled++; + respawn_hideout_bandit(); + } + return true; + } + } + return false; +} + +bool try_hideout_bandit_attack_player(HideoutBandit@ bandit) { + if (player_health <= 0) return false; + if (abs(bandit.position - hideoutPlayerX) > 1) return false; + if (bandit.attackTimer.elapsed < BANDIT_ATTACK_INTERVAL) return false; + + bandit.attackTimer.restart(); + + if (bandit.weaponType == "spear") { + play_creature_attack_sound("sounds/weapons/spear_swing.ogg", hideoutPlayerX, bandit.position, BANDIT_SOUND_VOLUME_STEP); + } else { + play_creature_attack_sound("sounds/weapons/axe_swing.ogg", hideoutPlayerX, bandit.position, BANDIT_SOUND_VOLUME_STEP); + } + + int damage = random(BANDIT_DAMAGE_MIN, BANDIT_DAMAGE_MAX); + player_health -= damage; + if (player_health < 0) player_health = 0; + + if (bandit.weaponType == "spear") { + play_creature_attack_sound("sounds/weapons/spear_hit.ogg", hideoutPlayerX, bandit.position, BANDIT_SOUND_VOLUME_STEP); + } else { + play_creature_attack_sound("sounds/weapons/axe_hit.ogg", hideoutPlayerX, bandit.position, BANDIT_SOUND_VOLUME_STEP); + } + play_player_damage_sound(); + return true; +} + +void update_hideout_bandit_audio(HideoutBandit@ bandit) { + if (bandit.soundHandle != -1 && p.sound_is_active(bandit.soundHandle)) { + p.update_sound_1d(bandit.soundHandle, bandit.position); + return; + } + if (bandit.soundHandle != -1) { + p.destroy_sound(bandit.soundHandle); + } + bandit.soundHandle = play_1d_with_volume_step(bandit.alertSound, hideoutPlayerX, bandit.position, true, BANDIT_SOUND_VOLUME_STEP); +} + +void update_hideout_bandit(HideoutBandit@ bandit) { + update_weapon_range_audio_with_listener(hideoutPlayerX, bandit.position, bandit.inWeaponRange); + + if (try_hideout_bandit_attack_player(bandit)) { + update_hideout_bandit_audio(bandit); + return; + } + + if (bandit.moveTimer.elapsed < bandit.moveInterval) { + update_hideout_bandit_audio(bandit); + return; + } + bandit.moveTimer.restart(); + + int direction = 0; + if (hideoutPlayerX > bandit.position) direction = 1; + else if (hideoutPlayerX < bandit.position) direction = -1; + + if (direction != 0) { + int targetX = bandit.position + direction; + if (targetX >= 0 && targetX < BANDIT_HIDEOUT_MAP_SIZE) { + if (get_hideout_bandit_at(targetX) == null) { + bandit.position = targetX; + play_hideout_positional_footstep(hideoutPlayerX, bandit.position, BANDIT_FOOTSTEP_MAX_DISTANCE, BANDIT_SOUND_VOLUME_STEP); + } + } + } + + update_hideout_bandit_audio(bandit); +} + +void update_hideout_bandits() { + for (uint i = 0; i < hideoutBandits.length(); i++) { + update_hideout_bandit(hideoutBandits[i]); + } +} + +int add_hideout_storage_item(int itemType, int amount) { + if (amount <= 0) return 0; + int capacity = BASE_STORAGE_MAX - get_storage_count(itemType); + if (capacity <= 0) return 0; + int addedAmount = amount; + if (addedAmount > capacity) addedAmount = capacity; + if (addedAmount <= 0) return 0; + + add_storage_count(itemType, addedAmount); + + if (itemType == ITEM_SMALL_GAME) { + for (int i = 0; i < addedAmount; i++) { + storage_small_game_types.insert_last("small game"); + } + } + if (itemType == ITEM_FISH) { + for (int i = 0; i < addedAmount; i++) { + add_storage_fish_weight(get_default_fish_weight()); + } + } + + return addedAmount; +} + +void give_bandit_hideout_rewards() { + speak_with_history("Victory!", true); + + string[] rewards; + rewards.insert_last("=== Victory Rewards ==="); + rewards.insert_last(""); + rewards.insert_last("Bandits defeated: " + hideoutBanditsKilled + "."); + + if (world_altars.length() > 0) { + double favorReward = BANDIT_HIDEOUT_BASE_FAVOR + (hideoutBanditsKilled * BANDIT_HIDEOUT_FAVOR_PER_KILL); + if (favorReward > BANDIT_HIDEOUT_FAVOR_MAX) favorReward = BANDIT_HIDEOUT_FAVOR_MAX; + if (favorReward < BANDIT_HIDEOUT_BASE_FAVOR) favorReward = BANDIT_HIDEOUT_BASE_FAVOR; + favor += favorReward; + rewards.insert_last("Favor awarded: " + format_favor(favorReward) + "."); + } else { + rewards.insert_last("Altar not built. No favor awarded."); + } + + rewards.insert_last(""); + + if (world_storages.length() == 0) { + rewards.insert_last("Storage not built. No item rewards."); + } else { + rewards.insert_last("Storage rewards:"); + bool anyItems = false; + for (int itemType = 0; itemType < ITEM_COUNT; itemType++) { + int roll = random(0, 9); + if (roll <= 0) continue; + int addedAmount = add_hideout_storage_item(itemType, roll); + if (addedAmount <= 0) continue; + rewards.insert_last(get_item_display_name(itemType) + ": +" + addedAmount + "."); + anyItems = true; + } + if (!anyItems) { + rewards.insert_last("No items were recovered."); + } + } + + text_reader_lines(rewards, "Bandit's Hideout", true); +} diff --git a/src/constants.nvgt b/src/constants.nvgt index 7e2482d..d82a50c 100644 --- a/src/constants.nvgt +++ b/src/constants.nvgt @@ -63,6 +63,25 @@ const int ZOMBIE_FOOTSTEP_MAX_DISTANCE = 5; const float ZOMBIE_SOUND_VOLUME_STEP = 3.0; const int ZOMBIE_ATTACK_MAX_HEIGHT = 6; +// Undead resident settings (stronger than zombies) +const int UNDEAD_RESIDENT_HEALTH = 16; +const int UNDEAD_RESIDENT_DAMAGE_MIN = 5; +const int UNDEAD_RESIDENT_DAMAGE_MAX = 7; + +// Wight settings (undead elite) +const int WIGHT_HEALTH = 40; +const int WIGHT_DAMAGE_MIN = 6; +const int WIGHT_DAMAGE_MAX = 8; +const int WIGHT_SPAWN_CHANCE_START = 5; +const int WIGHT_SPAWN_CHANCE_STEP = 5; + +// Vampyr settings (undead abductor) +const int VAMPYR_HEALTH = 40; +const int VAMPYR_SPAWN_CHANCE_START = 5; +const int VAMPYR_SPAWN_CHANCE_STEP = 5; +const int VAMPYR_CAPTURE_CHANCE = 50; +const int VAMPYR_CAPTURE_INTERVAL = 1000; + // Boar settings const int BOAR_HEALTH = 4; const int BOAR_MAX_COUNT = 1; diff --git a/src/enemies/undead.nvgt b/src/enemies/undead.nvgt index 141130c..48d8695 100644 --- a/src/enemies/undead.nvgt +++ b/src/enemies/undead.nvgt @@ -1,32 +1,113 @@ -// Undead creatures (zombies, vampires, ghosts, etc.) -// Currently only zombies are implemented +// Undead creatures (zombies, wights, vampyrs, ghosts, etc.) +// Currently zombies, wights, vampyrs, and undead residents are implemented string[] undead_zombie_sounds = {"sounds/enemies/zombie1.ogg"}; +string[] undead_wight_sounds = {"sounds/enemies/wight1.ogg"}; +string[] undead_vampyr_sounds = { + "sounds/enemies/vampyr1.ogg", + "sounds/enemies/vampyr2.ogg", + "sounds/enemies/vampyr3.ogg", + "sounds/enemies/vampyr4.ogg" +}; +string[] undead_resident_sounds = {"sounds/enemies/undead_resident1.ogg"}; + +int wight_spawn_chance = WIGHT_SPAWN_CHANCE_START; +bool wight_spawned_this_night = false; +int vampyr_spawn_chance = VAMPYR_SPAWN_CHANCE_START; +bool vampyr_spawned_this_night = false; + +int get_undead_base_health(const string &in undead_type) { + if (undead_type == "wight") return WIGHT_HEALTH; + if (undead_type == "vampyr") return VAMPYR_HEALTH; + if (undead_type == "undead_resident") return UNDEAD_RESIDENT_HEALTH; + return ZOMBIE_HEALTH; +} + +int get_undead_damage_min(const string &in undead_type) { + if (undead_type == "wight") return WIGHT_DAMAGE_MIN; + if (undead_type == "vampyr") return WIGHT_DAMAGE_MIN; + if (undead_type == "undead_resident") return UNDEAD_RESIDENT_DAMAGE_MIN; + return ZOMBIE_DAMAGE_MIN; +} + +int get_undead_damage_max(const string &in undead_type) { + if (undead_type == "wight") return WIGHT_DAMAGE_MAX; + if (undead_type == "vampyr") return WIGHT_DAMAGE_MAX; + if (undead_type == "undead_resident") return UNDEAD_RESIDENT_DAMAGE_MAX; + return ZOMBIE_DAMAGE_MAX; +} + +string pick_undead_voice_sound(const string &in undead_type) { + if (undead_type == "wight") { + int sound_index = random(0, undead_wight_sounds.length() - 1); + return undead_wight_sounds[sound_index]; + } + if (undead_type == "vampyr") { + int sound_index = random(0, undead_vampyr_sounds.length() - 1); + return undead_vampyr_sounds[sound_index]; + } + if (undead_type == "undead_resident") { + int sound_index = random(0, undead_resident_sounds.length() - 1); + return undead_resident_sounds[sound_index]; + } + int sound_index = random(0, undead_zombie_sounds.length() - 1); + return undead_zombie_sounds[sound_index]; +} + +string get_undead_label(const string &in undead_type) { + if (undead_type == "wight") return "wight"; + if (undead_type == "vampyr") return "vampyr"; + if (undead_type == "undead_resident") return "undead resident"; + return "zombie"; +} class Undead { int position; int health; - string undead_type; // "zombie", future: "vampire", "ghost", etc. + string undead_type; // "zombie", "wight", "vampyr", "undead_resident", future: "ghost", etc. string voice_sound; int sound_handle; bool in_weapon_range; + bool retreating; + bool suppress_voice; + bool should_despawn; timer move_timer; timer attack_timer; Undead(int pos, string type = "zombie") { position = pos; undead_type = type; - health = ZOMBIE_HEALTH; - int sound_index = random(0, undead_zombie_sounds.length() - 1); - voice_sound = undead_zombie_sounds[sound_index]; + health = get_undead_base_health(undead_type); + voice_sound = pick_undead_voice_sound(undead_type); sound_handle = -1; in_weapon_range = false; + retreating = false; + suppress_voice = false; + should_despawn = false; move_timer.restart(); attack_timer.restart(); } } Undead@[] undeads; +bool has_wight() { + for (uint i = 0; i < undeads.length(); i++) { + if (undeads[i].undead_type == "wight") { + return true; + } + } + return false; +} + +bool has_vampyr() { + for (uint i = 0; i < undeads.length(); i++) { + if (undeads[i].undead_type == "vampyr") { + return true; + } + } + return false; +} + void update_undead_weapon_range_audio() { for (uint i = 0; i < undeads.length(); i++) { update_weapon_range_audio(undeads[i].position, undeads[i].in_weapon_range); @@ -61,7 +142,7 @@ Undead@ get_undead_at(int pos) { return null; } -void spawn_undead() { +void spawn_undead(const string &in undead_type = "zombie") { int spawn_x = -1; for (int attempts = 0; attempts < 20; attempts++) { int candidate = random(BASE_END + 1, MAP_SIZE - 1); @@ -74,14 +155,18 @@ void spawn_undead() { spawn_x = random(BASE_END + 1, MAP_SIZE - 1); } - Undead@ z = Undead(spawn_x, "zombie"); - undeads.insert_last(z); - // Play looping sound that follows the zombie + Undead@ undead = Undead(spawn_x, undead_type); + undeads.insert_last(undead); + // Play looping sound that follows the undead int[] areaStarts; int[] areaEnds; get_active_audio_areas(areaStarts, areaEnds); if (areaStarts.length() == 0 || range_overlaps_active_areas(spawn_x, spawn_x, areaStarts, areaEnds)) { - z.sound_handle = play_1d_with_volume_step(z.voice_sound, x, spawn_x, true, ZOMBIE_SOUND_VOLUME_STEP); + bool loop_voice = (undead_type != "vampyr"); + if (undead_type == "vampyr") { + undead.voice_sound = pick_undead_voice_sound(undead_type); + } + undead.sound_handle = play_1d_with_volume_step(undead.voice_sound, x, spawn_x, loop_voice, ZOMBIE_SOUND_VOLUME_STEP); } } @@ -90,7 +175,7 @@ void try_attack_barricade_undead(Undead@ undead) { if (undead.attack_timer.elapsed < ZOMBIE_ATTACK_INTERVAL) return; undead.attack_timer.restart(); - int damage = random(ZOMBIE_DAMAGE_MIN, ZOMBIE_DAMAGE_MAX); + int damage = random(get_undead_damage_min(undead.undead_type), get_undead_damage_max(undead.undead_type)); barricade_health -= damage; if (barricade_health < 0) barricade_health = 0; @@ -103,7 +188,7 @@ void try_attack_barricade_undead(Undead@ undead) { int before_health = undead.health; damage_undead_at(undead.position, counterDamage); if (before_health - counterDamage <= 0 && x <= BASE_END) { - speak_with_history("Residents killed an attacking zombie.", true); + speak_with_history("Residents killed an attacking " + get_undead_label(undead.undead_type) + ".", true); } } } @@ -118,6 +203,10 @@ bool can_undead_attack_player(Undead@ undead) { return false; } + if (undead.undead_type == "vampyr") { + return false; + } + if (barricade_health > 0 && x <= BASE_END) { return false; } @@ -142,7 +231,7 @@ bool try_attack_player_undead(Undead@ undead) { } undead.attack_timer.restart(); - int damage = random(ZOMBIE_DAMAGE_MIN, ZOMBIE_DAMAGE_MAX); + int damage = random(get_undead_damage_min(undead.undead_type), get_undead_damage_max(undead.undead_type)); player_health -= damage; if (player_health < 0) { player_health = 0; @@ -152,9 +241,42 @@ bool try_attack_player_undead(Undead@ undead) { return true; } +void start_vampyr_retreat(Undead@ undead) { + undead.retreating = true; + undead.suppress_voice = true; + if (undead.sound_handle != -1) { + p.destroy_sound(undead.sound_handle); + undead.sound_handle = -1; + } + undead.move_timer.restart(); +} + +void try_capture_resident_vampyr(Undead@ undead) { + if (undead.attack_timer.elapsed < VAMPYR_CAPTURE_INTERVAL) { + return; + } + undead.attack_timer.restart(); + + if (residents_count <= 0) { + start_vampyr_retreat(undead); + return; + } + + if (random(1, 100) <= VAMPYR_CAPTURE_CHANCE) { + residents_count--; + if (residents_count < 0) residents_count = 0; + undead_residents_pending++; + speak_with_history("A resident has been taken.", true); + start_vampyr_retreat(undead); + } +} + void update_undead(Undead@ undead, bool audio_active) { + bool is_vampyr = (undead.undead_type == "vampyr"); + bool loop_voice = !is_vampyr; + // Update looping sound position - if (!audio_active) { + if (!audio_active || undead.suppress_voice) { if (undead.sound_handle != -1) { p.destroy_sound(undead.sound_handle); undead.sound_handle = -1; @@ -166,23 +288,49 @@ void update_undead(Undead@ undead, bool audio_active) { if (undead.sound_handle != -1) { p.destroy_sound(undead.sound_handle); } - undead.sound_handle = play_1d_with_volume_step(undead.voice_sound, x, undead.position, true, ZOMBIE_SOUND_VOLUME_STEP); + if (is_vampyr) { + undead.voice_sound = pick_undead_voice_sound(undead.undead_type); + } + undead.sound_handle = play_1d_with_volume_step(undead.voice_sound, x, undead.position, loop_voice, ZOMBIE_SOUND_VOLUME_STEP); } if (try_attack_player_undead(undead)) { return; } + if (undead.undead_type == "vampyr" && !undead.retreating && barricade_health > 0 && undead.position == BASE_END + 1) { + try_capture_resident_vampyr(undead); + return; + } + if (undead.move_timer.elapsed < ZOMBIE_MOVE_INTERVAL) return; undead.move_timer.restart(); - if (barricade_health > 0 && undead.position == BASE_END + 1) { + if (undead.undead_type != "vampyr" && barricade_health > 0 && undead.position == BASE_END + 1) { try_attack_barricade_undead(undead); return; } int direction = 0; - if (x > BASE_END) { + if (undead.undead_type == "vampyr") { + if (undead.retreating) { + if (undead.position >= MAP_SIZE - 1) { + undead.should_despawn = true; + return; + } + direction = 1; + } else if (undead.position > 0) { + direction = -1; + } else { + return; + } + } else if (undead.undead_type == "wight") { + if (undead.position > 0) { + direction = -1; + } else { + return; + } + } else if (x > BASE_END) { if (x > undead.position) { direction = 1; } else if (x < undead.position) { @@ -198,7 +346,7 @@ void update_undead(Undead@ undead, bool audio_active) { int target_x = undead.position + direction; if (target_x < 0 || target_x >= MAP_SIZE) return; - if (target_x <= BASE_END && barricade_health > 0) { + if (undead.undead_type != "vampyr" && target_x <= BASE_END && barricade_health > 0) { try_attack_barricade_undead(undead); return; } @@ -223,8 +371,24 @@ void update_undeads() { int maxCount = ZOMBIE_MAX_COUNT + extra; if (maxCount > ZOMBIE_MAX_COUNT_CAP) maxCount = ZOMBIE_MAX_COUNT_CAP; - while (undeads.length() < maxCount) { - spawn_undead(); + int zombie_count = 0; + int undead_resident_count = 0; + for (uint i = 0; i < undeads.length(); i++) { + if (undeads[i].undead_type == "zombie") { + zombie_count++; + } else if (undeads[i].undead_type == "undead_resident") { + undead_resident_count++; + } + } + + while (zombie_count < maxCount) { + spawn_undead("zombie"); + zombie_count++; + } + + while (undead_resident_count < undead_residents_count) { + spawn_undead("undead_resident"); + undead_resident_count++; } int[] areaStarts; @@ -236,6 +400,88 @@ void update_undeads() { bool audio_active = !limit_audio || range_overlaps_active_areas(undeads[i].position, undeads[i].position, areaStarts, areaEnds); update_undead(undeads[i], audio_active); } + + for (int i = int(undeads.length()) - 1; i >= 0; i--) { + if (undeads[i].should_despawn) { + if (undeads[i].sound_handle != -1) { + p.destroy_sound(undeads[i].sound_handle); + undeads[i].sound_handle = -1; + } + undeads.remove_at(i); + } + } +} + +void attempt_hourly_wight_spawn() { + if (current_hour == 19) { + wight_spawned_this_night = false; + } + + if (is_daytime) { + return; + } + + if (has_vampyr()) { + return; + } + + if (wight_spawned_this_night) { + return; + } + + if (has_wight()) { + wight_spawned_this_night = true; + return; + } + + int roll = random(1, 100); + if (roll <= wight_spawn_chance) { + spawn_undead("wight"); + wight_spawned_this_night = true; + wight_spawn_chance = WIGHT_SPAWN_CHANCE_START; + return; + } + + wight_spawn_chance += WIGHT_SPAWN_CHANCE_STEP; + if (wight_spawn_chance > 100) wight_spawn_chance = 100; +} + +void attempt_hourly_vampyr_spawn() { + if (current_hour == 19) { + vampyr_spawned_this_night = false; + } + + if (is_daytime) { + return; + } + + if (residents_count <= 0) { + return; + } + + if (has_wight()) { + return; + } + + if (vampyr_spawned_this_night) { + return; + } + + if (has_vampyr()) { + vampyr_spawned_this_night = true; + return; + } + + int roll = random(1, 100); + if (roll <= vampyr_spawn_chance) { + spawn_undead("vampyr"); + vampyr_spawned_this_night = true; + vampyr_spawn_chance = VAMPYR_SPAWN_CHANCE_START; + return; + } + + vampyr_spawn_chance += VAMPYR_SPAWN_CHANCE_STEP; + if (vampyr_spawn_chance > 100) vampyr_spawn_chance = 100; } bool damage_undead_at(int pos, int damage) { @@ -246,6 +492,10 @@ bool damage_undead_at(int pos, int damage) { if (world_altars.length() > 0) { favor += 0.2; } + if (undeads[i].undead_type == "undead_resident") { + undead_residents_count--; + if (undead_residents_count < 0) undead_residents_count = 0; + } if (undeads[i].sound_handle != -1) { p.destroy_sound(undeads[i].sound_handle); undeads[i].sound_handle = -1; @@ -265,4 +515,4 @@ Undead@ get_zombie_at(int pos) { return get_undead_at(pos); } bool damage_zombie_at(int pos, int damage) { return damage_undead_at(pos, damage); } void update_zombies() { update_undeads(); } void clear_zombies() { clear_undeads(); } -void spawn_zombie() { spawn_undead(); } +void spawn_zombie() { spawn_undead("zombie"); } diff --git a/src/save_system.nvgt b/src/save_system.nvgt index e01a6a5..f5d780d 100644 --- a/src/save_system.nvgt +++ b/src/save_system.nvgt @@ -606,6 +606,8 @@ void reset_game_state() { barricade_health = 0; barricade_initialized = false; residents_count = 0; + undead_residents_count = 0; + undead_residents_pending = 0; current_hour = 8; current_day = 1; @@ -868,6 +870,10 @@ bool save_game_state() { saveData.set("time_invasion_scheduled_hour", invasion_scheduled_hour); saveData.set("time_invasion_enemy_type", invasion_enemy_type); saveData.set("quest_roll_done_today", quest_roll_done_today); + saveData.set("wight_spawn_chance", wight_spawn_chance); + saveData.set("wight_spawned_this_night", wight_spawned_this_night); + saveData.set("vampyr_spawn_chance", vampyr_spawn_chance); + saveData.set("vampyr_spawned_this_night", vampyr_spawned_this_night); string[] questData; for (uint i = 0; i < quest_queue.length(); i++) { questData.insert_last("" + quest_queue[i]); @@ -882,6 +888,8 @@ bool save_game_state() { saveData.set("world_barricade_initialized", barricade_initialized); saveData.set("world_barricade_health", barricade_health); saveData.set("world_residents_count", residents_count); + saveData.set("world_undead_residents_count", undead_residents_count); + saveData.set("world_undead_residents_pending", undead_residents_pending); saveData.set("world_expanded_terrain_types", join_string_array(expanded_terrain_types)); string[] treeData; @@ -1019,6 +1027,10 @@ bool load_game_state_from_file(const string&in filename) { barricade_health = int(get_number(saveData, "world_barricade_health", BARRICADE_BASE_HEALTH)); residents_count = int(get_number(saveData, "world_residents_count", 0)); if (residents_count < 0) residents_count = 0; + undead_residents_count = int(get_number(saveData, "world_undead_residents_count", 0)); + if (undead_residents_count < 0) undead_residents_count = 0; + undead_residents_pending = int(get_number(saveData, "world_undead_residents_pending", 0)); + if (undead_residents_pending < 0) undead_residents_pending = 0; if (!barricade_initialized) { init_barricade(); } else { @@ -1276,6 +1288,14 @@ bool load_game_state_from_file(const string&in filename) { if (invasion_scheduled_hour < -1) invasion_scheduled_hour = -1; if (invasion_scheduled_hour > 23) invasion_scheduled_hour = -1; quest_roll_done_today = get_bool(saveData, "quest_roll_done_today", false); + wight_spawn_chance = int(get_number(saveData, "wight_spawn_chance", WIGHT_SPAWN_CHANCE_START)); + wight_spawned_this_night = get_bool(saveData, "wight_spawned_this_night", false); + if (wight_spawn_chance < WIGHT_SPAWN_CHANCE_START) wight_spawn_chance = WIGHT_SPAWN_CHANCE_START; + if (wight_spawn_chance > 100) wight_spawn_chance = 100; + vampyr_spawn_chance = int(get_number(saveData, "vampyr_spawn_chance", VAMPYR_SPAWN_CHANCE_START)); + vampyr_spawned_this_night = get_bool(saveData, "vampyr_spawned_this_night", false); + if (vampyr_spawn_chance < VAMPYR_SPAWN_CHANCE_START) vampyr_spawn_chance = VAMPYR_SPAWN_CHANCE_START; + if (vampyr_spawn_chance > 100) vampyr_spawn_chance = 100; quest_queue.resize(0); string[] loadedQuests = get_string_list_or_split(saveData, "quest_queue"); diff --git a/src/time_system.nvgt b/src/time_system.nvgt index 7dd17da..c7b05a2 100644 --- a/src/time_system.nvgt +++ b/src/time_system.nvgt @@ -517,6 +517,10 @@ void update_time() { } if (current_hour == 6) { + if (undead_residents_pending > 0) { + undead_residents_count += undead_residents_pending; + undead_residents_pending = 0; + } process_daily_weapon_breakage(); attempt_daily_quest(); attempt_resident_butchering(); @@ -529,6 +533,8 @@ void update_time() { update_incense_burning(); attempt_hourly_flying_creature_spawn(); attempt_hourly_boar_spawn(); + attempt_hourly_wight_spawn(); + attempt_hourly_vampyr_spawn(); check_scheduled_invasion(); attempt_blessing(); check_weather_transition(); diff --git a/src/world/barricade.nvgt b/src/world/barricade.nvgt index 6ee91c4..2d702bc 100644 --- a/src/world/barricade.nvgt +++ b/src/world/barricade.nvgt @@ -4,6 +4,8 @@ int barricade_health = 0; bool barricade_initialized = false; int residents_count = 0; +int undead_residents_count = 0; +int undead_residents_pending = 0; void init_barricade() { if (barricade_initialized) {