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.

This commit is contained in:
Storm Dragon
2026-02-04 00:52:16 -05:00
parent e1928a1039
commit 78a6156656
18 changed files with 1029 additions and 27 deletions
+2 -2
View File
@@ -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).
+3 -2
View File
@@ -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();
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+9 -1
View File
@@ -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();
}
}
+669
View File
@@ -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);
}
+19
View File
@@ -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;
+272 -22
View File
@@ -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"); }
+20
View File
@@ -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");
+6
View File
@@ -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();
+2
View File
@@ -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) {