Weather system added, mostly decoration. Some tweaks to residents. Moved altar to its own menu, s for sacrifice. You can no longer burn incense outside the base.

This commit is contained in:
Storm Dragon
2026-01-20 12:30:26 -05:00
parent 9b7fbc8266
commit 4caa5caefb
28 changed files with 1973 additions and 72 deletions

BIN
sounds/game/game_falls.ogg LFS Normal file

Binary file not shown.

BIN
sounds/game/goose.ogg LFS Normal file

Binary file not shown.

BIN
sounds/items/item.ogg LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
sounds/nature/rain.ogg LFS Normal file

Binary file not shown.

BIN
sounds/nature/thunder_high.ogg LFS Normal file

Binary file not shown.

BIN
sounds/nature/thunder_low.ogg LFS Normal file

Binary file not shown.

BIN
sounds/nature/thunder_medium.ogg LFS Normal file

Binary file not shown.

BIN
sounds/nature/wind_high.ogg LFS Normal file

Binary file not shown.

BIN
sounds/nature/wind_low.ogg LFS Normal file

Binary file not shown.

BIN
sounds/nature/wind_medium.ogg LFS Normal file

Binary file not shown.

View File

@@ -76,16 +76,78 @@ int perform_resident_defense() {
int damage = 0; int damage = 0;
if (useSpear && storage_spears > 0) { if (useSpear && storage_spears > 0) {
damage = RESIDENT_SPEAR_DAMAGE; damage = RESIDENT_SPEAR_DAMAGE;
play_1d_with_volume_step("sounds/weapons/spear_swing.ogg", x, BASE_END + 1, false, 3.0); // Weapons don't get consumed on use - they break via daily breakage check
// Just play the sound
play_1d_with_volume_step("sounds/weapons/spear_swing.ogg", x, BASE_END + 1, false, RESIDENT_DEFENSE_VOLUME_STEP);
} else if (storage_slings > 0 && storage_stones > 0) { } else if (storage_slings > 0 && storage_stones > 0) {
damage = random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX); damage = random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX);
// Slings use stones as ammo, so consume a stone
storage_stones--; storage_stones--;
play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", x, BASE_END + 1, false, 3.0); play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", x, BASE_END + 1, false, RESIDENT_DEFENSE_VOLUME_STEP);
} }
return damage; return damage;
} }
// Proactive resident sling defense
timer resident_sling_timer;
const int RESIDENT_SLING_COOLDOWN = 4000; // 4 seconds between shots
void attempt_resident_sling_defense() {
// Only if residents exist and have slings with stones
if (residents_count <= 0) return;
if (storage_slings <= 0 || storage_stones <= 0) return;
// Cooldown between shots
if (resident_sling_timer.elapsed < RESIDENT_SLING_COOLDOWN) return;
// Find nearest enemy within sling range
int nearestDistance = SLING_RANGE + 1;
int targetPos = -1;
bool targetIsBandit = false;
int sling_origin = BASE_END;
// Check zombies
for (uint i = 0; i < zombies.length(); i++) {
int dist = abs(zombies[i].position - sling_origin);
if (dist > 0 && dist <= SLING_RANGE && dist < nearestDistance) {
nearestDistance = dist;
targetPos = zombies[i].position;
targetIsBandit = false;
}
}
// Check bandits
for (uint i = 0; i < bandits.length(); i++) {
int dist = abs(bandits[i].position - sling_origin);
if (dist > 0 && dist <= SLING_RANGE && dist < nearestDistance) {
nearestDistance = dist;
targetPos = bandits[i].position;
targetIsBandit = true;
}
}
// No targets in range
if (targetPos == -1) return;
// Shoot!
resident_sling_timer.restart();
storage_stones--;
int damage = random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX);
play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", x, targetPos, false, RESIDENT_DEFENSE_VOLUME_STEP);
if (targetIsBandit) {
damage_bandit_at(targetPos, damage);
} else {
damage_zombie_at(targetPos, damage);
}
// Play hit sound on enemy
play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", x, targetPos, ZOMBIE_SOUND_VOLUME_STEP);
}
void process_daily_weapon_breakage() { void process_daily_weapon_breakage() {
if (residents_count <= 0) return; if (residents_count <= 0) return;
@@ -149,3 +211,53 @@ void process_daily_weapon_breakage() {
notify(msg); notify(msg);
} }
} }
// Resident resource collection
const int RESIDENT_COLLECTION_CHANCE = 10; // 10% chance per basket per hour
void attempt_resident_collection() {
// Only during daytime
if (!is_daytime) return;
// Need residents
if (residents_count <= 0) return;
// Need baskets in storage to enable collection
if (storage_reed_baskets <= 0) return;
// Number of residents who can collect = min(residents, baskets)
int active_collectors = (residents_count < storage_reed_baskets) ? residents_count : storage_reed_baskets;
// Each active collector has a 10% chance to collect something
for (int i = 0; i < active_collectors; i++) {
if (random(1, 100) > RESIDENT_COLLECTION_CHANCE) continue;
// Determine what to collect (weighted random)
// Sticks and vines more common, logs and stones less common
int roll = random(1, 100);
string item_name = "";
if (roll <= 40) {
// 40% chance - stick
storage_sticks++;
item_name = "stick";
} else if (roll <= 70) {
// 30% chance - vine
storage_vines++;
item_name = "vine";
} else if (roll <= 85) {
// 15% chance - stone
storage_stones++;
item_name = "stone";
} else {
// 15% chance - log
storage_logs++;
item_name = "log";
}
// Announce only if player is in base
if (x <= BASE_END) {
screen_reader_speak("Resident added " + item_name + " to storage.", true);
}
}
}

View File

@@ -130,6 +130,7 @@ void release_sling_attack(int player_x) {
int search_direction = (facing == 1) ? 1 : -1; int search_direction = (facing == 1) ? 1 : -1;
int target_x = -1; int target_x = -1;
bool hit_bandit = false; bool hit_bandit = false;
bool hit_flying_creature = false;
// Priority: Find nearest enemy (bandit or zombie) first // Priority: Find nearest enemy (bandit or zombie) first
for (int dist = 1; dist <= SLING_RANGE; dist++) { for (int dist = 1; dist <= SLING_RANGE; dist++) {
@@ -151,6 +152,14 @@ void release_sling_attack(int player_x) {
hit_bandit = false; hit_bandit = false;
break; break;
} }
// Then check for flying creature (only if flying)
FlyingCreature@ creature = get_flying_creature_at(check_x);
if (creature != null && creature.state == "flying") {
target_x = check_x;
hit_flying_creature = true;
break;
}
} }
// If no enemy found, check for trees (but don't damage them) // If no enemy found, check for trees (but don't damage them)
@@ -161,7 +170,7 @@ void release_sling_attack(int player_x) {
Tree@ tree = get_tree_at(check_x); Tree@ tree = get_tree_at(check_x);
if (tree != null && !tree.is_chopped) { if (tree != null && !tree.is_chopped) {
// Stone hits tree but doesn't damage it // Stone hits tree but doesn't damage it
p.play_1d("sounds/weapons/sling_hit.ogg", player_x, check_x, false); play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, check_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP);
screen_reader_speak("Stone hit tree at " + check_x + ".", true); screen_reader_speak("Stone hit tree at " + check_x + ".", true);
return; return;
} }
@@ -179,11 +188,15 @@ void release_sling_attack(int player_x) {
// Damage the correct enemy type // Damage the correct enemy type
if (hit_bandit) { if (hit_bandit) {
damage_bandit_at(target_x, damage); damage_bandit_at(target_x, damage);
p.play_1d("sounds/weapons/sling_hit.ogg", player_x, target_x, false); play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP);
play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", player_x, target_x, BANDIT_SOUND_VOLUME_STEP); play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", player_x, target_x, BANDIT_SOUND_VOLUME_STEP);
} else if (hit_flying_creature) {
damage_flying_creature_at(target_x, damage);
play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP);
// Falling sound handled by damage_flying_creature_at
} else { } else {
damage_zombie_at(target_x, damage); damage_zombie_at(target_x, damage);
p.play_1d("sounds/weapons/sling_hit.ogg", player_x, target_x, false); play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP);
play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", player_x, target_x, ZOMBIE_SOUND_VOLUME_STEP); play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", player_x, target_x, ZOMBIE_SOUND_VOLUME_STEP);
} }
} }

View File

@@ -30,7 +30,22 @@ const int SPEAR_DAMAGE = 3;
const int AXE_DAMAGE = 4; const int AXE_DAMAGE = 4;
const int SLING_DAMAGE_MIN = 5; const int SLING_DAMAGE_MIN = 5;
const int SLING_DAMAGE_MAX = 8; const int SLING_DAMAGE_MAX = 8;
const int SLING_RANGE = 7; const int SLING_RANGE = 8;
// Bow settings (for future implementation)
// Option 1: Longer range, similar damage
// const int BOW_DAMAGE_MIN = 6;
// const int BOW_DAMAGE_MAX = 9;
// const int BOW_RANGE = 12; // 50% more range than sling
//
// Option 2: Much longer range, slightly more damage
// const int BOW_DAMAGE_MIN = 7;
// const int BOW_DAMAGE_MAX = 10;
// const int BOW_RANGE = 15; // Nearly double sling range
//
// Recommendation: Bows should have BOTH more range AND more damage than slings
// to justify the likely higher resource cost and complexity to craft.
// Suggested balance: BOW_RANGE = 12, damage 6-9 (average 7.5 vs sling's 6.5)
// Zombie settings // Zombie settings
const int ZOMBIE_HEALTH = 12; const int ZOMBIE_HEALTH = 12;
@@ -68,6 +83,11 @@ const int STABLE_STONE_COST = 15;
const int STABLE_VINE_COST = 10; const int STABLE_VINE_COST = 10;
const int ALTAR_STONE_COST = 9; const int ALTAR_STONE_COST = 9;
const int ALTAR_STICK_COST = 3; const int ALTAR_STICK_COST = 3;
const int INCENSE_STICK_COST = 6;
const int INCENSE_VINE_COST = 2;
const int INCENSE_REED_COST = 1;
const int INCENSE_HOURS_PER_STICK = 4;
const double INCENSE_FAVOR_PER_HOUR = 0.3;
// Bandit settings // Bandit settings
const int BANDIT_HEALTH = 4; const int BANDIT_HEALTH = 4;
@@ -99,7 +119,7 @@ const float SNARE_SOUND_PAN_STEP = 4.0; // Stronger pan for direction
const int SNARE_COLLECT_RANGE = 1; const int SNARE_COLLECT_RANGE = 1;
const int FIRE_SOUND_RANGE = 3; const int FIRE_SOUND_RANGE = 3;
const float FIRE_SOUND_VOLUME_STEP = 5.0; // 30 dB over 6 tiles const float FIRE_SOUND_VOLUME_STEP = 5.0; // 15 dB over 3 tiles (FIRE_SOUND_RANGE)
const int FIREPIT_SOUND_RANGE = 5; const int FIREPIT_SOUND_RANGE = 5;
const float FIREPIT_SOUND_VOLUME_STEP = 6.0; // 30 dB over 5 tiles const float FIREPIT_SOUND_VOLUME_STEP = 6.0; // 30 dB over 5 tiles
@@ -108,6 +128,12 @@ const int STREAM_SOUND_RANGE = 7;
const float STREAM_SOUND_VOLUME_STEP = 4.3; // 30 dB over 7 tiles const float STREAM_SOUND_VOLUME_STEP = 4.3; // 30 dB over 7 tiles
const float TREE_SOUND_VOLUME_STEP = 4.0; // Similar to snares for good audibility const float TREE_SOUND_VOLUME_STEP = 4.0; // Similar to snares for good audibility
const int TREE_SOUND_RANGE = 4;
const float RESIDENT_DEFENSE_VOLUME_STEP = 3.0; // Default volume for resident counter-attacks
const float PLAYER_WEAPON_SOUND_VOLUME_STEP = 3.0;
const int FLYING_CREATURE_FADE_OUT_DURATION = 1500; // ms
const float FLYING_CREATURE_FADE_OUT_MIN_VOLUME = -40.0; // dB
// Mountain configuration // Mountain configuration
const int MOUNTAIN_SIZE = 60; const int MOUNTAIN_SIZE = 60;
@@ -126,8 +152,25 @@ const int QUEST_STONE_SCORE = 6;
const int QUEST_LOG_SCORE = 10; const int QUEST_LOG_SCORE = 10;
const int QUEST_SKIN_SCORE = 14; const int QUEST_SKIN_SCORE = 14;
// Resident defense settings // Resident settings
const int MAX_RESIDENTS = 4; // Max residents per base (+ player = 5 total)
const int RESIDENT_WEAPON_BREAK_CHANCE = 10; const int RESIDENT_WEAPON_BREAK_CHANCE = 10;
const int RESIDENT_SPEAR_DAMAGE = 2; const int RESIDENT_SPEAR_DAMAGE = 2;
const int RESIDENT_SLING_DAMAGE_MIN = 3; const int RESIDENT_SLING_DAMAGE_MIN = 3;
const int RESIDENT_SLING_DAMAGE_MAX = 5; const int RESIDENT_SLING_DAMAGE_MAX = 5;
// Goose settings
const int GOOSE_HEALTH = 1;
const int GOOSE_MOVE_INTERVAL_MIN = 800; // Faster movement
const int GOOSE_MOVE_INTERVAL_MAX = 2000;
const int GOOSE_FLYING_HEIGHT_MIN = 10;
const int GOOSE_FLYING_HEIGHT_MAX = 30;
const float GOOSE_SOUND_VOLUME_STEP = 3.0;
const int GOOSE_FLIGHT_SOUND_DELAY_MIN = 2000; // Honk more often
const int GOOSE_FLIGHT_SOUND_DELAY_MAX = 5000;
const int GOOSE_FALL_SPEED = 100; // ms per foot
const int GOOSE_FLY_AWAY_CHANCE = 0; // Chance out of 1000 per tick to fly away
const int GOOSE_MAX_DIST_FROM_WATER = 4; // How far they can wander from water
const int GOOSE_MAX_COUNT = 3;
const int GOOSE_HOURLY_SPAWN_CHANCE = 35; // Percent chance per hour to spawn a goose
const int GOOSE_SIGHT_RANGE = 0;

View File

@@ -11,12 +11,8 @@ void run_crafting_menu() {
screen_reader_speak("Crafting menu.", true); screen_reader_speak("Crafting menu.", true);
int selection = 0; int selection = 0;
string[] categories = {"Weapons", "Tools", "Clothing", "Buildings", "Barricade"}; string[] categories = {"Weapons", "Tools", "Materials", "Clothing", "Buildings", "Barricade"};
int[] category_types = {0, 1, 2, 3, 4}; int[] category_types = {0, 1, 2, 3, 4, 5};
if (world_altars.length() > 0) {
categories.insert_last("Altar");
category_types.insert_last(5);
}
while(true) { while(true) {
wait(5); wait(5);
@@ -42,10 +38,10 @@ void run_crafting_menu() {
int category = category_types[selection]; int category = category_types[selection];
if (category == 0) run_weapons_menu(); if (category == 0) run_weapons_menu();
else if (category == 1) run_tools_menu(); else if (category == 1) run_tools_menu();
else if (category == 2) run_clothing_menu(); else if (category == 2) run_materials_menu();
else if (category == 3) run_buildings_menu(); else if (category == 3) run_clothing_menu();
else if (category == 4) run_barricade_menu(); else if (category == 4) run_buildings_menu();
else if (category == 5) run_altar_menu(); else if (category == 5) run_barricade_menu();
break; break;
} }
} }
@@ -99,8 +95,7 @@ void run_tools_menu() {
"Fishing Pole (1 Stick, 2 Vines)", "Fishing Pole (1 Stick, 2 Vines)",
"Rope (3 Vines)", "Rope (3 Vines)",
"Reed Basket (3 Reeds)", "Reed Basket (3 Reeds)",
"Clay Pot (3 Clay)", "Clay Pot (3 Clay)"
"Butcher Small Game (1 Small Game) [Requires Knife and Fire nearby]"
}; };
while(true) { while(true) {
@@ -131,12 +126,47 @@ void run_tools_menu() {
else if (selection == 4) craft_rope(); else if (selection == 4) craft_rope();
else if (selection == 5) craft_reed_basket(); else if (selection == 5) craft_reed_basket();
else if (selection == 6) craft_clay_pot(); else if (selection == 6) craft_clay_pot();
else if (selection == 7) butcher_small_game();
break; break;
} }
} }
} }
void run_materials_menu() {
screen_reader_speak("Materials.", true);
int selection = 0;
string[] options = {
"Butcher Small Game (1 Small Game) [Requires Knife and Fire nearby]",
"Incense (6 Sticks, 2 Vines, 1 Reed) [Requires Altar]"
};
while(true) {
wait(5);
menu_background_tick();
if (key_pressed(KEY_ESCAPE)) {
screen_reader_speak("Closed.", true);
break;
}
if (key_pressed(KEY_DOWN)) {
selection++;
if (selection >= options.length()) selection = 0;
screen_reader_speak(options[selection], true);
}
if (key_pressed(KEY_UP)) {
selection--;
if (selection < 0) selection = options.length() - 1;
screen_reader_speak(options[selection], true);
}
if (key_pressed(KEY_RETURN)) {
if (selection == 0) butcher_small_game();
else if (selection == 1) craft_incense();
break;
}
}
}
void run_clothing_menu() { void run_clothing_menu() {
screen_reader_speak("Clothing.", true); screen_reader_speak("Clothing.", true);
@@ -856,6 +886,33 @@ void craft_clay_pot() {
} }
} }
void craft_incense() {
if (world_altars.length() == 0) {
screen_reader_speak("You need an altar to craft incense.", true);
return;
}
string missing = "";
if (inv_sticks < INCENSE_STICK_COST) missing += INCENSE_STICK_COST + " sticks ";
if (inv_vines < INCENSE_VINE_COST) missing += INCENSE_VINE_COST + " vines ";
if (inv_reeds < INCENSE_REED_COST) missing += INCENSE_REED_COST + " reed ";
if (missing == "") {
if (inv_incense >= get_personal_stack_limit()) {
screen_reader_speak("You can't carry any more incense.", true);
return;
}
simulate_crafting(INCENSE_STICK_COST + INCENSE_VINE_COST + INCENSE_REED_COST);
inv_sticks -= INCENSE_STICK_COST;
inv_vines -= INCENSE_VINE_COST;
inv_reeds -= INCENSE_REED_COST;
inv_incense++;
screen_reader_speak("Crafted incense.", true);
} else {
screen_reader_speak("Missing: " + missing, true);
}
}
void butcher_small_game() { void butcher_small_game() {
string missing = ""; string missing = "";
@@ -883,15 +940,23 @@ void butcher_small_game() {
} }
simulate_crafting(1); simulate_crafting(1);
// Get the type of game we're butchering (first in the list)
string game_type = inv_small_game_types[0]; string game_type = inv_small_game_types[0];
inv_small_game_types.remove_at(0); inv_small_game_types.remove_at(0);
inv_small_game--; inv_small_game--;
inv_meat++;
inv_skins++;
screen_reader_speak("Butchered " + game_type + ". Got 1 meat and 1 skin.", true); if (game_type == "goose") {
inv_meat++;
inv_feathers += random(3, 6);
inv_down += random(1, 3);
screen_reader_speak("Butchered goose. Got 1 meat, feathers, and down.", true);
} else {
inv_meat++;
inv_skins++;
screen_reader_speak("Butchered " + game_type + ". Got 1 meat and 1 skin.", true);
}
// Play sound
p.play_stationary("sounds/items/miscellaneous.ogg", false);
} else { } else {
screen_reader_speak("Missing: " + missing, true); screen_reader_speak("Missing: " + missing, true);
} }

View File

@@ -1,11 +1,14 @@
// Unified creature/enemy audio system // Unified creature/enemy audio system
// Ensures consistent panning and distance behavior for all animated entities // Ensures consistent panning and distance behavior for all animated entities
// //
// USAGE GUIDE: // *** IMPORTANT: See src/creature_template.nvgt for complete step-by-step guide ***
// The template file has code examples, a checklist, and enforces consistency.
//
// QUICK USAGE GUIDE:
// This system provides standardized audio functions for all creatures (zombies, bandits, animals). // This system provides standardized audio functions for all creatures (zombies, bandits, animals).
// Using these functions ensures all creatures sound consistent to the player. // Using these functions ensures all creatures sound consistent to the player.
// //
// When adding new creatures (sheep, cattle, horses, etc): // When adding new creatures (goblins, sheep, cattle, horses, etc):
// 1. Define creature-specific constants in constants.nvgt: // 1. Define creature-specific constants in constants.nvgt:
// const float SHEEP_SOUND_VOLUME_STEP = 3.0; // const float SHEEP_SOUND_VOLUME_STEP = 3.0;
// const int SHEEP_FOOTSTEP_MAX_DISTANCE = 5; // const int SHEEP_FOOTSTEP_MAX_DISTANCE = 5;

327
src/creature_template.nvgt Normal file
View File

@@ -0,0 +1,327 @@
// CREATURE CREATION TEMPLATE
// Use this as a guide when creating new creatures (goblins, animals, etc.)
//
// This template ensures all creatures have consistent audio behavior and required features.
// Copy the sections you need and fill in creature-specific details.
/* ============================================================================
STEP 1: Add constants to src/constants.nvgt
============================================================================ */
// Example for "Goblin" creature:
/*
// Goblin Configuration
const int GOBLIN_HEALTH = 15;
const int GOBLIN_DAMAGE_MIN = 2;
const int GOBLIN_DAMAGE_MAX = 4;
const int GOBLIN_MOVE_INTERVAL = 1500;
const int GOBLIN_ATTACK_INTERVAL = 3000;
const int GOBLIN_ALERT_MIN_DELAY = 4000;
const int GOBLIN_ALERT_MAX_DELAY = 8000;
const int GOBLIN_FOOTSTEP_MAX_DISTANCE = 6;
const float GOBLIN_SOUND_VOLUME_STEP = 3.0; // Default creature volume
*/
/* ============================================================================
STEP 2: Define sound arrays in src/world_state.nvgt (near top with zombies/bandits)
============================================================================ */
// Example:
/*
string[] goblin_sounds = {
"sounds/enemies/goblin_cackle1.ogg",
"sounds/enemies/goblin_cackle2.ogg",
"sounds/enemies/goblin_cackle3.ogg"
};
*/
/* ============================================================================
STEP 3: Create creature class in src/world_state.nvgt
============================================================================
REQUIRED MEMBERS:
- int position (where the creature is on the map)
- int health (creature's HP)
- int sound_handle (CRITICAL: for tracking active voice/alert sounds)
- timer move_timer (for movement intervals)
- timer attack_timer (for attack cooldown)
- timer voice_timer (for periodic sounds - groans/alerts/cackles)
- int next_voice_delay (randomized delay between voice sounds)
- string voice_sound (which sound file to play for voice)
OPTIONAL MEMBERS (creature-specific):
- string weapon_type
- string behavior_state
- int wander_direction
- etc.
*/
// Example:
/*
class Goblin {
int position;
int health;
int sound_handle; // REQUIRED: Must track for death cleanup
string voice_sound;
timer move_timer;
timer voice_timer; // Renamed from alert_timer/groan_timer for clarity
timer attack_timer;
int next_voice_delay;
// Creature-specific behavior
string weapon_type; // Example: "dagger" or "club"
Goblin(int pos) {
position = pos;
health = GOBLIN_HEALTH;
sound_handle = -1; // CRITICAL: Always initialize to -1
// Choose random voice sound
int sound_index = random(0, goblin_sounds.length() - 1);
voice_sound = goblin_sounds[sound_index];
// Choose random weapon
weapon_type = (random(0, 1) == 0) ? "dagger" : "club";
// Initialize timers
move_timer.restart();
voice_timer.restart();
attack_timer.restart();
next_voice_delay = random(GOBLIN_ALERT_MIN_DELAY, GOBLIN_ALERT_MAX_DELAY);
}
}
Goblin@[] goblins; // Global array to store all active goblins
*/
/* ============================================================================
STEP 4: Spawn function
============================================================================
REQUIRED AUDIO: Use play_creature_voice() and store the handle
*/
// Example:
/*
void spawn_goblin() {
// Find spawn location (avoid duplicates)
int spawn_x = -1;
for (int attempts = 0; attempts < 20; attempts++) {
int candidate = random(BASE_END + 1, MAP_SIZE - 1);
if (get_goblin_at(candidate) == null) {
spawn_x = candidate;
break;
}
}
if (spawn_x == -1) {
spawn_x = random(BASE_END + 1, MAP_SIZE - 1);
}
Goblin@ g = Goblin(spawn_x);
goblins.insert_last(g);
// REQUIRED: Use creature_audio system and store handle
g.sound_handle = play_creature_voice(g.voice_sound, x, spawn_x, GOBLIN_SOUND_VOLUME_STEP);
}
*/
/* ============================================================================
STEP 5: Update function
============================================================================
REQUIRED AUDIO:
- Periodic voice: play_creature_voice() and store handle
- Movement: play_creature_footstep()
- Attacks: play_creature_attack_sound()
*/
// Example:
/*
void update_goblin(Goblin@ goblin) {
// Play periodic voice sound
if (goblin.voice_timer.elapsed > goblin.next_voice_delay) {
goblin.voice_timer.restart();
goblin.next_voice_delay = random(GOBLIN_ALERT_MIN_DELAY, GOBLIN_ALERT_MAX_DELAY);
// REQUIRED: Store handle for cleanup on death
goblin.sound_handle = play_creature_voice(goblin.voice_sound, x, goblin.position, GOBLIN_SOUND_VOLUME_STEP);
}
// Try to attack player
if (try_attack_player_goblin(goblin)) {
return;
}
// Movement logic
if (goblin.move_timer.elapsed < GOBLIN_MOVE_INTERVAL) return;
goblin.move_timer.restart();
// [Your movement AI here - pathfinding toward player, wandering, etc.]
// When goblin moves:
goblin.position = target_x;
// REQUIRED: Use creature_audio for footsteps
play_creature_footstep(x, goblin.position, BASE_END, GRASS_END, GOBLIN_FOOTSTEP_MAX_DISTANCE, GOBLIN_SOUND_VOLUME_STEP);
}
*/
/* ============================================================================
STEP 6: Attack functions
============================================================================
REQUIRED AUDIO: Use play_creature_attack_sound() for weapon sounds
*/
// Example:
/*
bool try_attack_player_goblin(Goblin@ goblin) {
if (player_health <= 0) return false;
if (abs(goblin.position - x) > 1) return false;
if (goblin.attack_timer.elapsed < GOBLIN_ATTACK_INTERVAL) return false;
goblin.attack_timer.restart();
// REQUIRED: Positional weapon sounds using creature_audio
if (goblin.weapon_type == "dagger") {
play_creature_attack_sound("sounds/weapons/dagger_swing.ogg", x, goblin.position, GOBLIN_SOUND_VOLUME_STEP);
} else {
play_creature_attack_sound("sounds/weapons/club_swing.ogg", x, goblin.position, GOBLIN_SOUND_VOLUME_STEP);
}
int damage = random(GOBLIN_DAMAGE_MIN, GOBLIN_DAMAGE_MAX);
player_health -= damage;
if (player_health < 0) player_health = 0;
// REQUIRED: Hit sound using creature_audio
play_creature_attack_sound("sounds/enemies/player_hit.ogg", x, goblin.position, GOBLIN_SOUND_VOLUME_STEP);
return true;
}
*/
/* ============================================================================
STEP 7: Damage/Death function
============================================================================
CRITICAL: MUST stop creature's sound before playing death sound
*/
// Example:
/*
bool damage_goblin_at(int pos, int damage) {
for (uint i = 0; i < goblins.length(); i++) {
if (goblins[i].position == pos) {
goblins[i].health -= damage;
if (goblins[i].health <= 0) {
// CRITICAL: Stop active sounds before death sound
if (goblins[i].sound_handle != -1) {
p.destroy_sound(goblins[i].sound_handle);
goblins[i].sound_handle = -1;
}
// REQUIRED: Use creature_audio for death sound
play_creature_death_sound("sounds/enemies/enemy_falls.ogg", x, pos, GOBLIN_SOUND_VOLUME_STEP);
goblins.remove_at(i);
}
return true;
}
}
return false;
}
*/
/* ============================================================================
STEP 8: Helper functions
============================================================================ */
// Example:
/*
Goblin@ get_goblin_at(int pos) {
for (uint i = 0; i < goblins.length(); i++) {
if (goblins[i].position == pos) {
return @goblins[i];
}
}
return null;
}
void update_goblins() {
for (uint i = 0; i < goblins.length(); i++) {
update_goblin(goblins[i]);
}
}
void clear_goblins() {
for (uint i = 0; i < goblins.length(); i++) {
if (goblins[i].sound_handle != -1) {
p.destroy_sound(goblins[i].sound_handle);
goblins[i].sound_handle = -1;
}
}
goblins.resize(0);
}
*/
/* ============================================================================
STEP 9: Update src/combat.nvgt damage functions
============================================================================ */
// Add to relevant damage functions in src/combat.nvgt:
/*
// In attack_enemy_ranged():
if (damage_goblin_at(check_x, damage)) {
return check_x;
}
// In attack_enemy():
if (damage_goblin_at(target_x, damage)) {
return true;
}
// When player damages goblin, play hit sound:
play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", player_x, target_x, GOBLIN_SOUND_VOLUME_STEP);
*/
/* ============================================================================
CHECKLIST: Required for Every Creature
============================================================================
Constants in constants.nvgt:
[ ] CREATURE_HEALTH
[ ] CREATURE_DAMAGE_MIN / CREATURE_DAMAGE_MAX
[ ] CREATURE_MOVE_INTERVAL
[ ] CREATURE_ATTACK_INTERVAL
[ ] CREATURE_ALERT_MIN_DELAY / CREATURE_ALERT_MAX_DELAY
[ ] CREATURE_FOOTSTEP_MAX_DISTANCE
[ ] CREATURE_SOUND_VOLUME_STEP
Class members:
[ ] int position
[ ] int health
[ ] int sound_handle (initialized to -1)
[ ] timer move_timer
[ ] timer attack_timer
[ ] timer voice_timer
[ ] int next_voice_delay
[ ] string voice_sound
Audio usage:
[ ] Spawn: play_creature_voice() with stored handle
[ ] Update: play_creature_voice() with stored handle
[ ] Movement: play_creature_footstep()
[ ] Attacks: play_creature_attack_sound() for weapons
[ ] Death: Stop sound_handle BEFORE play_creature_death_sound()
[ ] Player damages creature: play_creature_hit_sound()
Functions:
[ ] spawn_creature()
[ ] update_creature()
[ ] try_attack_player_creature()
[ ] damage_creature_at()
[ ] get_creature_at()
[ ] update_creatures()
[ ] clear_creatures()
Integration:
[ ] Add creature to combat.nvgt damage checks
[ ] Call update_creatures() in main game loop
[ ] Handle cleanup (clear_creatures on game over, etc.)
============================================================================ */

View File

@@ -1,3 +1,34 @@
// Centralized falling damage system
// Safe fall height is 10 feet or less
// Each foot above 10 has a chance to deal 0-4 damage
// This means falling from great heights is VERY dangerous but not guaranteed fatal
const int SAFE_FALL_HEIGHT = 10;
const int FALL_DAMAGE_MIN = 0;
const int FALL_DAMAGE_MAX = 4;
void apply_falling_damage(int fall_height) {
// Always play the hit ground sound
p.play_stationary("sounds/actions/hit_ground.ogg", false);
if (fall_height <= SAFE_FALL_HEIGHT) {
screen_reader_speak("Landed safely.", true);
return;
}
// Calculate damage: roll 0-4 for each foot above 10
int damage = 0;
for (int i = SAFE_FALL_HEIGHT; i < fall_height; i++) {
damage += random(FALL_DAMAGE_MIN, FALL_DAMAGE_MAX);
}
// Apply damage
player_health -= damage;
if (player_health < 0) player_health = 0;
// Feedback
screen_reader_speak("Fell " + fall_height + " feet! Took " + damage + " damage. " + player_health + " health remaining.", true);
}
// Tree Object // Tree Object
class Tree { class Tree {
int position; int position;
@@ -45,7 +76,7 @@ class Tree {
int tree_distance = x - position; int tree_distance = x - position;
if (tree_distance < 0) tree_distance = -tree_distance; if (tree_distance < 0) tree_distance = -tree_distance;
if (tree_distance <= 4) { if (tree_distance <= TREE_SOUND_RANGE) {
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) { if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
sound_handle = p.play_1d("sounds/environment/tree.ogg", x, position, true); sound_handle = p.play_1d("sounds/environment/tree.ogg", x, position, true);
if (sound_handle != -1) { if (sound_handle != -1) {
@@ -140,10 +171,16 @@ class Tree {
Tree@[] trees; Tree@[] trees;
bool tree_too_close(int pos) { bool tree_too_close(int pos) {
// Check distance from base (must be at least 5 tiles away)
if (pos <= BASE_END + 5) {
return true;
}
// Check distance from other trees (must be at least 10 tiles apart)
for (uint i = 0; i < trees.length(); i++) { for (uint i = 0; i < trees.length(); i++) {
int distance = trees[i].position - pos; int distance = trees[i].position - pos;
if (distance < 0) distance = -distance; if (distance < 0) distance = -distance;
if (distance <= 5) { if (distance < 10) {
return true; return true;
} }
} }
@@ -235,6 +272,19 @@ void damage_tree(int target_x, int damage) {
void perform_search(int current_x) void perform_search(int current_x)
{ {
// First priority: Check for world drops on this tile or adjacent
for (int check_x = current_x - 1; check_x <= current_x + 1; check_x++) {
WorldDrop@ drop = get_drop_at(check_x);
if (drop != null) {
if (!try_pickup_world_drop(drop)) {
return;
}
p.play_stationary("sounds/items/miscellaneous.ogg", false);
remove_drop_at(check_x);
return;
}
}
// Check for snares nearby (adjacent within range) // Check for snares nearby (adjacent within range)
for (int check_x = current_x - SNARE_COLLECT_RANGE; check_x <= current_x + SNARE_COLLECT_RANGE; check_x++) { for (int check_x = current_x - SNARE_COLLECT_RANGE; check_x <= current_x + SNARE_COLLECT_RANGE; check_x++) {
// Skip current x? User said "beside". If on top, it breaks. // Skip current x? User said "beside". If on top, it breaks.
@@ -543,23 +593,10 @@ void land_on_ground(int ground_level) {
fall_sound_handle = -1; fall_sound_handle = -1;
} }
p.play_stationary("sounds/actions/hit_ground.ogg", false); // Calculate fall damage using centralized function (also plays hit_ground sound)
// Calculate fall damage
int fall_height = fall_start_y - ground_level; int fall_height = fall_start_y - ground_level;
y = ground_level; y = ground_level;
apply_falling_damage(fall_height);
if (fall_height > 10) {
int damage = 0;
for (int i = 10; i < fall_height; i++) {
damage += random(1, 3);
}
player_health -= damage;
screen_reader_speak("Fell " + fall_height + " feet! Took " + damage + " damage. " + player_health + " health remaining.", true);
} else {
screen_reader_speak("Landed safely.", true);
}
fall_start_y = 0; fall_start_y = 0;
} }

View File

@@ -0,0 +1,71 @@
// FLYING CREATURE TEMPLATE
// Use this as a guide when adding new flying small game (ducks, geese, etc.)
//
// This template ensures all flying creatures have consistent audio, spawning, and drops.
/* ============================================================================
STEP 1: Add creature sound(s) near the top of src/world_state.nvgt
============================================================================ */
// Example:
// string[] duck_sounds = {"sounds/game/duck.ogg"};
/* ============================================================================
STEP 2: Add constants to src/constants.nvgt
============================================================================ */
// Example for "duck":
// const int DUCK_HEALTH = 1;
// const int DUCK_MOVE_INTERVAL_MIN = 900;
// const int DUCK_MOVE_INTERVAL_MAX = 2200;
// const int DUCK_FLYING_HEIGHT_MIN = 8;
// const int DUCK_FLYING_HEIGHT_MAX = 25;
// const float DUCK_SOUND_VOLUME_STEP = 3.0;
// const int DUCK_FLIGHT_SOUND_DELAY_MIN = 2500;
// const int DUCK_FLIGHT_SOUND_DELAY_MAX = 6000;
// const int DUCK_FALL_SPEED = 100;
// const int DUCK_FLY_AWAY_CHANCE = 2;
// const int DUCK_MAX_DIST_FROM_WATER = 4;
// const int DUCK_MAX_COUNT = 3;
// const int DUCK_HOURLY_SPAWN_CHANCE = 30;
// const int DUCK_SIGHT_RANGE = 6;
/* ============================================================================
STEP 3: Register config in init_flying_creature_configs() (src/world_state.nvgt)
============================================================================ */
// Example:
// FlyingCreatureConfig@ duck_cfg = FlyingCreatureConfig();
// duck_cfg.id = "duck";
// duck_cfg.drop_type = "duck";
// duck_cfg.sounds = duck_sounds;
// duck_cfg.fall_sound = "sounds/actions/falling.ogg";
// duck_cfg.impact_sound = "sounds/game/game_falls.ogg";
// duck_cfg.health = DUCK_HEALTH;
// duck_cfg.move_interval_min = DUCK_MOVE_INTERVAL_MIN;
// duck_cfg.move_interval_max = DUCK_MOVE_INTERVAL_MAX;
// duck_cfg.min_height = DUCK_FLYING_HEIGHT_MIN;
// duck_cfg.max_height = DUCK_FLYING_HEIGHT_MAX;
// duck_cfg.sound_volume_step = DUCK_SOUND_VOLUME_STEP;
// duck_cfg.sound_delay_min = DUCK_FLIGHT_SOUND_DELAY_MIN;
// duck_cfg.sound_delay_max = DUCK_FLIGHT_SOUND_DELAY_MAX;
// duck_cfg.fall_speed = DUCK_FALL_SPEED;
// duck_cfg.fly_away_chance = DUCK_FLY_AWAY_CHANCE;
// duck_cfg.max_dist_from_water = DUCK_MAX_DIST_FROM_WATER;
// duck_cfg.hourly_spawn_chance = DUCK_HOURLY_SPAWN_CHANCE;
// duck_cfg.max_count = DUCK_MAX_COUNT;
// duck_cfg.sight_range = DUCK_SIGHT_RANGE;
// duck_cfg.flee_on_sight = true;
// flying_creature_configs.insert_last(duck_cfg);
/* ============================================================================
STEP 4: Add butcher behavior (src/crafting.nvgt) if yields differ
============================================================================ */
// Example:
// if (game_type == "duck") {
// inv_meat++;
// inv_feathers += random(2, 4);
// inv_down += random(1, 2);
// screen_reader_speak("Butchered duck. Got 1 meat, feathers, and down.", true);
// }

View File

@@ -10,6 +10,9 @@ string[] inv_small_game_types; // Array to track what types of small game we hav
int inv_meat = 0; int inv_meat = 0;
int inv_skins = 0; int inv_skins = 0;
int inv_feathers = 0;
int inv_down = 0;
int inv_incense = 0;
int inv_spears = 0; int inv_spears = 0;
int inv_snares = 0; int inv_snares = 0;
@@ -37,6 +40,10 @@ int storage_small_game = 0;
string[] storage_small_game_types; string[] storage_small_game_types;
int storage_meat = 0; int storage_meat = 0;
int storage_skins = 0; int storage_skins = 0;
int storage_feathers = 0;
int storage_down = 0;
int storage_incense = 0;
int storage_spears = 0; int storage_spears = 0;
int storage_snares = 0; int storage_snares = 0;
int storage_axes = 0; int storage_axes = 0;
@@ -91,6 +98,9 @@ const int ITEM_SKIN_POUCHES = 20;
const int ITEM_ROPES = 21; const int ITEM_ROPES = 21;
const int ITEM_REED_BASKETS = 22; const int ITEM_REED_BASKETS = 22;
const int ITEM_CLAY_POTS = 23; const int ITEM_CLAY_POTS = 23;
const int ITEM_FEATHERS = 24;
const int ITEM_DOWN = 25;
const int ITEM_INCENSE = 26;
const int HAT_MAX_HEALTH_BONUS = 1; const int HAT_MAX_HEALTH_BONUS = 1;
const int GLOVES_MAX_HEALTH_BONUS = 1; const int GLOVES_MAX_HEALTH_BONUS = 1;
const int PANTS_MAX_HEALTH_BONUS = 3; const int PANTS_MAX_HEALTH_BONUS = 3;
@@ -273,6 +283,9 @@ int get_personal_count(int item_type) {
if (item_type == ITEM_SMALL_GAME) return inv_small_game; if (item_type == ITEM_SMALL_GAME) return inv_small_game;
if (item_type == ITEM_MEAT) return inv_meat; if (item_type == ITEM_MEAT) return inv_meat;
if (item_type == ITEM_SKINS) return inv_skins; if (item_type == ITEM_SKINS) return inv_skins;
if (item_type == ITEM_FEATHERS) return inv_feathers;
if (item_type == ITEM_DOWN) return inv_down;
if (item_type == ITEM_INCENSE) return inv_incense;
if (item_type == ITEM_SPEARS) return inv_spears; if (item_type == ITEM_SPEARS) return inv_spears;
if (item_type == ITEM_SLINGS) return inv_slings; if (item_type == ITEM_SLINGS) return inv_slings;
if (item_type == ITEM_AXES) return inv_axes; if (item_type == ITEM_AXES) return inv_axes;
@@ -301,6 +314,9 @@ int get_storage_count(int item_type) {
if (item_type == ITEM_SMALL_GAME) return storage_small_game; if (item_type == ITEM_SMALL_GAME) return storage_small_game;
if (item_type == ITEM_MEAT) return storage_meat; if (item_type == ITEM_MEAT) return storage_meat;
if (item_type == ITEM_SKINS) return storage_skins; if (item_type == ITEM_SKINS) return storage_skins;
if (item_type == ITEM_FEATHERS) return storage_feathers;
if (item_type == ITEM_DOWN) return storage_down;
if (item_type == ITEM_INCENSE) return storage_incense;
if (item_type == ITEM_SPEARS) return storage_spears; if (item_type == ITEM_SPEARS) return storage_spears;
if (item_type == ITEM_SLINGS) return storage_slings; if (item_type == ITEM_SLINGS) return storage_slings;
if (item_type == ITEM_AXES) return storage_axes; if (item_type == ITEM_AXES) return storage_axes;
@@ -329,6 +345,9 @@ string get_item_label(int item_type) {
if (item_type == ITEM_SMALL_GAME) return "small game"; if (item_type == ITEM_SMALL_GAME) return "small game";
if (item_type == ITEM_MEAT) return "meat"; if (item_type == ITEM_MEAT) return "meat";
if (item_type == ITEM_SKINS) return "skins"; if (item_type == ITEM_SKINS) return "skins";
if (item_type == ITEM_FEATHERS) return "feathers";
if (item_type == ITEM_DOWN) return "down";
if (item_type == ITEM_INCENSE) return "incense";
if (item_type == ITEM_SPEARS) return "spears"; if (item_type == ITEM_SPEARS) return "spears";
if (item_type == ITEM_SLINGS) return "slings"; if (item_type == ITEM_SLINGS) return "slings";
if (item_type == ITEM_AXES) return "axes"; if (item_type == ITEM_AXES) return "axes";
@@ -366,6 +385,9 @@ string get_item_label_singular(int item_type) {
if (item_type == ITEM_SMALL_GAME) return "small game"; if (item_type == ITEM_SMALL_GAME) return "small game";
if (item_type == ITEM_MEAT) return "meat"; if (item_type == ITEM_MEAT) return "meat";
if (item_type == ITEM_SKINS) return "skin"; if (item_type == ITEM_SKINS) return "skin";
if (item_type == ITEM_FEATHERS) return "feather";
if (item_type == ITEM_DOWN) return "down";
if (item_type == ITEM_INCENSE) return "incense stick";
if (item_type == ITEM_SPEARS) return "spear"; if (item_type == ITEM_SPEARS) return "spear";
if (item_type == ITEM_SLINGS) return "sling"; if (item_type == ITEM_SLINGS) return "sling";
if (item_type == ITEM_AXES) return "axe"; if (item_type == ITEM_AXES) return "axe";
@@ -394,6 +416,9 @@ double get_item_favor_value(int item_type) {
if (item_type == ITEM_SMALL_GAME) return 0.20; if (item_type == ITEM_SMALL_GAME) return 0.20;
if (item_type == ITEM_MEAT) return 0.15; if (item_type == ITEM_MEAT) return 0.15;
if (item_type == ITEM_SKINS) return 0.15; if (item_type == ITEM_SKINS) return 0.15;
if (item_type == ITEM_FEATHERS) return 0.05;
if (item_type == ITEM_DOWN) return 0.05;
if (item_type == ITEM_INCENSE) return 0.10;
if (item_type == ITEM_SPEARS) return 1.00; if (item_type == ITEM_SPEARS) return 1.00;
if (item_type == ITEM_SLINGS) return 2.00; if (item_type == ITEM_SLINGS) return 2.00;
if (item_type == ITEM_AXES) return 1.50; if (item_type == ITEM_AXES) return 1.50;

View File

@@ -257,6 +257,9 @@ void deposit_item(int item_type) {
else if (item_type == ITEM_SMALL_GAME) { inv_small_game -= amount; storage_small_game += amount; move_small_game_to_storage(amount); } else if (item_type == ITEM_SMALL_GAME) { inv_small_game -= amount; storage_small_game += amount; move_small_game_to_storage(amount); }
else if (item_type == ITEM_MEAT) { inv_meat -= amount; storage_meat += amount; } else if (item_type == ITEM_MEAT) { inv_meat -= amount; storage_meat += amount; }
else if (item_type == ITEM_SKINS) { inv_skins -= amount; storage_skins += amount; } else if (item_type == ITEM_SKINS) { inv_skins -= amount; storage_skins += amount; }
else if (item_type == ITEM_FEATHERS) { inv_feathers -= amount; storage_feathers += amount; }
else if (item_type == ITEM_DOWN) { inv_down -= amount; storage_down += amount; }
else if (item_type == ITEM_INCENSE) { inv_incense -= amount; storage_incense += amount; }
else if (item_type == ITEM_SPEARS) { inv_spears -= amount; storage_spears += amount; } else if (item_type == ITEM_SPEARS) { inv_spears -= amount; storage_spears += amount; }
else if (item_type == ITEM_SLINGS) { inv_slings -= amount; storage_slings += amount; } else if (item_type == ITEM_SLINGS) { inv_slings -= amount; storage_slings += amount; }
else if (item_type == ITEM_AXES) { inv_axes -= amount; storage_axes += amount; } else if (item_type == ITEM_AXES) { inv_axes -= amount; storage_axes += amount; }
@@ -301,6 +304,9 @@ void withdraw_item(int item_type) {
else if (item_type == ITEM_SMALL_GAME) { storage_small_game -= amount; inv_small_game += amount; move_small_game_to_personal(amount); } else if (item_type == ITEM_SMALL_GAME) { storage_small_game -= amount; inv_small_game += amount; move_small_game_to_personal(amount); }
else if (item_type == ITEM_MEAT) { storage_meat -= amount; inv_meat += amount; } else if (item_type == ITEM_MEAT) { storage_meat -= amount; inv_meat += amount; }
else if (item_type == ITEM_SKINS) { storage_skins -= amount; inv_skins += amount; } else if (item_type == ITEM_SKINS) { storage_skins -= amount; inv_skins += amount; }
else if (item_type == ITEM_FEATHERS) { storage_feathers -= amount; inv_feathers += amount; }
else if (item_type == ITEM_DOWN) { storage_down -= amount; inv_down += amount; }
else if (item_type == ITEM_INCENSE) { storage_incense -= amount; inv_incense += amount; }
else if (item_type == ITEM_SPEARS) { storage_spears -= amount; inv_spears += amount; } else if (item_type == ITEM_SPEARS) { storage_spears -= amount; inv_spears += amount; }
else if (item_type == ITEM_SLINGS) { storage_slings -= amount; inv_slings += amount; } else if (item_type == ITEM_SLINGS) { storage_slings -= amount; inv_slings += amount; }
else if (item_type == ITEM_AXES) { storage_axes -= amount; inv_axes += amount; } else if (item_type == ITEM_AXES) { storage_axes -= amount; inv_axes += amount; }
@@ -341,6 +347,9 @@ void sacrifice_item(int item_type) {
} }
else if (item_type == ITEM_MEAT) inv_meat--; else if (item_type == ITEM_MEAT) inv_meat--;
else if (item_type == ITEM_SKINS) inv_skins--; else if (item_type == ITEM_SKINS) inv_skins--;
else if (item_type == ITEM_FEATHERS) inv_feathers--;
else if (item_type == ITEM_DOWN) inv_down--;
else if (item_type == ITEM_INCENSE) inv_incense--;
else if (item_type == ITEM_SPEARS) inv_spears--; else if (item_type == ITEM_SPEARS) inv_spears--;
else if (item_type == ITEM_SLINGS) inv_slings--; else if (item_type == ITEM_SLINGS) inv_slings--;
else if (item_type == ITEM_AXES) inv_axes--; else if (item_type == ITEM_AXES) inv_axes--;
@@ -375,6 +384,9 @@ void build_personal_inventory_options(string[]@ options, int[]@ item_types) {
options.insert_last("Small Game: " + inv_small_game); item_types.insert_last(ITEM_SMALL_GAME); options.insert_last("Small Game: " + inv_small_game); item_types.insert_last(ITEM_SMALL_GAME);
options.insert_last("Meat: " + inv_meat); item_types.insert_last(ITEM_MEAT); options.insert_last("Meat: " + inv_meat); item_types.insert_last(ITEM_MEAT);
options.insert_last("Skins: " + inv_skins); item_types.insert_last(ITEM_SKINS); options.insert_last("Skins: " + inv_skins); item_types.insert_last(ITEM_SKINS);
options.insert_last("Feathers: " + inv_feathers); item_types.insert_last(ITEM_FEATHERS);
options.insert_last("Down: " + inv_down); item_types.insert_last(ITEM_DOWN);
options.insert_last("Incense: " + inv_incense); item_types.insert_last(ITEM_INCENSE);
options.insert_last("Spears: " + inv_spears); item_types.insert_last(ITEM_SPEARS); options.insert_last("Spears: " + inv_spears); item_types.insert_last(ITEM_SPEARS);
options.insert_last("Slings: " + inv_slings); item_types.insert_last(ITEM_SLINGS); options.insert_last("Slings: " + inv_slings); item_types.insert_last(ITEM_SLINGS);
options.insert_last("Axes: " + inv_axes); item_types.insert_last(ITEM_AXES); options.insert_last("Axes: " + inv_axes); item_types.insert_last(ITEM_AXES);
@@ -404,6 +416,9 @@ void build_storage_inventory_options(string[]@ options, int[]@ item_types) {
options.insert_last("Small Game: " + storage_small_game); item_types.insert_last(ITEM_SMALL_GAME); options.insert_last("Small Game: " + storage_small_game); item_types.insert_last(ITEM_SMALL_GAME);
options.insert_last("Meat: " + storage_meat); item_types.insert_last(ITEM_MEAT); options.insert_last("Meat: " + storage_meat); item_types.insert_last(ITEM_MEAT);
options.insert_last("Skins: " + storage_skins); item_types.insert_last(ITEM_SKINS); options.insert_last("Skins: " + storage_skins); item_types.insert_last(ITEM_SKINS);
options.insert_last("Feathers: " + storage_feathers); item_types.insert_last(ITEM_FEATHERS);
options.insert_last("Down: " + storage_down); item_types.insert_last(ITEM_DOWN);
options.insert_last("Incense: " + storage_incense); item_types.insert_last(ITEM_INCENSE);
options.insert_last("Spears: " + storage_spears); item_types.insert_last(ITEM_SPEARS); options.insert_last("Spears: " + storage_spears); item_types.insert_last(ITEM_SPEARS);
options.insert_last("Slings: " + storage_slings); item_types.insert_last(ITEM_SLINGS); options.insert_last("Slings: " + storage_slings); item_types.insert_last(ITEM_SLINGS);
options.insert_last("Axes: " + storage_axes); item_types.insert_last(ITEM_AXES); options.insert_last("Axes: " + storage_axes); item_types.insert_last(ITEM_AXES);
@@ -431,7 +446,10 @@ void show_inventory() {
info += inv_clay + " clay, "; info += inv_clay + " clay, ";
info += inv_small_game + " small game, "; info += inv_small_game + " small game, ";
info += inv_meat + " meat, "; info += inv_meat + " meat, ";
info += inv_skins + " skins. "; info += inv_skins + " skins, ";
info += inv_feathers + " feathers, ";
info += inv_down + " down, ";
info += inv_incense + " incense. ";
info += "Tools: " + inv_spears + " spears, " + inv_slings + " slings, " + inv_axes + " axes, " + inv_snares + " snares, " + inv_knives + " knives, " + inv_fishing_poles + " fishing poles, " + inv_ropes + " ropes, " + inv_reed_baskets + " reed baskets, " + inv_clay_pots + " clay pots. "; info += "Tools: " + inv_spears + " spears, " + inv_slings + " slings, " + inv_axes + " axes, " + inv_snares + " snares, " + inv_knives + " knives, " + inv_fishing_poles + " fishing poles, " + inv_ropes + " ropes, " + inv_reed_baskets + " reed baskets, " + inv_clay_pots + " clay pots. ";
info += "Clothing: " + inv_skin_hats + " hats, " + inv_skin_gloves + " gloves, " + inv_skin_pants + " pants, " + inv_skin_tunics + " tunics, " + inv_moccasins + " moccasins, " + inv_skin_pouches + " skin pouches."; info += "Clothing: " + inv_skin_hats + " hats, " + inv_skin_gloves + " gloves, " + inv_skin_pants + " pants, " + inv_skin_tunics + " tunics, " + inv_moccasins + " moccasins, " + inv_skin_pouches + " skin pouches.";
screen_reader_speak(info, true); screen_reader_speak(info, true);
@@ -550,12 +568,25 @@ void run_storage_menu() {
} }
} }
void run_altar_menu() { void check_altar_menu(int player_x) {
if (!key_pressed(KEY_S)) return;
// Must be in base
if (player_x > BASE_END) {
screen_reader_speak("Must be in base to use altar.", true);
return;
}
// Must have altar
if (world_altars.length() == 0) { if (world_altars.length() == 0) {
screen_reader_speak("No altar built.", true); screen_reader_speak("No altar built.", true);
return; return;
} }
run_altar_menu();
}
void run_altar_menu() {
screen_reader_speak("Altar. Favor " + format_favor(favor) + ".", true); screen_reader_speak("Altar. Favor " + format_favor(favor) + ".", true);
int selection = 0; int selection = 0;
@@ -635,6 +666,26 @@ void try_feed_fire_log(WorldFire@ fire) {
} }
} }
void try_burn_incense() {
if (world_altars.length() == 0) {
screen_reader_speak("No altar built.", true);
return;
}
if (inv_clay_pots <= 0) {
screen_reader_speak("You need a clay pot to burn incense.", true);
return;
}
if (inv_incense <= 0) {
screen_reader_speak("No incense to burn.", true);
return;
}
inv_incense--;
incense_hours_remaining += INCENSE_HOURS_PER_STICK;
incense_burning = true;
screen_reader_speak("Incense burning. " + incense_hours_remaining + " hours remaining.", true);
}
void check_equipment_menu() { void check_equipment_menu() {
if (key_pressed(KEY_E)) { if (key_pressed(KEY_E)) {
// Check if player has any equipment // Check if player has any equipment
@@ -678,6 +729,11 @@ void run_action_menu(int x) {
} }
} }
if (x <= BASE_END && world_altars.length() > 0 && inv_incense > 0) {
options.insert_last("Burn incense");
action_types.insert_last(4);
}
while(true) { while(true) {
wait(5); wait(5);
menu_background_tick(); menu_background_tick();
@@ -708,6 +764,8 @@ void run_action_menu(int x) {
try_feed_fire_vine(nearby_fire); try_feed_fire_vine(nearby_fire);
} else if (action == 3) { } else if (action == 3) {
try_feed_fire_log(nearby_fire); try_feed_fire_log(nearby_fire);
} else if (action == 4) {
try_burn_incense();
} }
break; break;
} }

View File

@@ -1,17 +1,18 @@
// Notification System // Notification System
string[] notification_history; string[] notification_history;
const int MAX_NOTIFICATIONS = 10; const int MAX_NOTIFICATIONS = 10;
const int NOTIFICATION_DELAY = 3000; // 3 seconds between notifications
int current_notification_index = -1; int current_notification_index = -1;
string[] notification_queue; string[] notification_queue;
int[] notification_sound_handles; timer notification_timer;
bool notification_active = false;
int notification_sound_handle = -1;
void notify(string message) { void notify(string message) {
// Play notification sound // Add to queue (don't play yet)
int sound_handle = p.play_stationary("sounds/notify.ogg", false);
notification_queue.insert_last(message); notification_queue.insert_last(message);
notification_sound_handles.insert_last(sound_handle);
// Add to history // Add to history immediately so it appears in history even if queued
notification_history.insert_last(message); notification_history.insert_last(message);
// Keep only last 10 notifications // Keep only last 10 notifications
@@ -25,17 +26,35 @@ void notify(string message) {
void update_notifications() { void update_notifications() {
if (notification_queue.length() == 0) { if (notification_queue.length() == 0) {
notification_active = false;
notification_sound_handle = -1;
return; return;
} }
int sound_handle = notification_sound_handles[0]; // If a notification is currently active, wait for delay
if (sound_handle != -1 && p.sound_is_playing(sound_handle)) { if (notification_active && notification_timer.elapsed < NOTIFICATION_DELAY) {
return; return;
} }
screen_reader_speak(notification_queue[0], true); // If we're waiting for the notification sound to finish playing
notification_queue.remove_at(0); if (notification_sound_handle != -1) {
notification_sound_handles.remove_at(0); // Check if sound is still playing
if (p.sound_is_active(notification_sound_handle)) {
return; // Still playing, wait
}
// Sound finished, now speak
screen_reader_speak(notification_queue[0], true);
notification_queue.remove_at(0);
notification_sound_handle = -1;
// Start timer for next notification
notification_timer.restart();
notification_active = true;
return;
}
// Play next notification sound (don't speak yet)
notification_sound_handle = p.play_stationary("sounds/notify.ogg", false);
} }
void check_notification_keys() { void check_notification_keys() {

View File

@@ -36,6 +36,8 @@ int last_sling_stage = -1; // Track which stage we're in to avoid duplicate soun
// Favor system // Favor system
double favor = 0.0; double favor = 0.0;
int incense_hours_remaining = 0;
bool incense_burning = false;
bool blessing_speed_active = false; bool blessing_speed_active = false;
timer blessing_speed_timer; timer blessing_speed_timer;

View File

@@ -145,7 +145,9 @@ void clear_world_objects() {
clear_zombies(); clear_zombies();
clear_bandits(); clear_bandits();
clear_flying_creatures();
clear_mountains(); clear_mountains();
clear_world_drops();
} }
void reset_game_state() { void reset_game_state() {
@@ -174,6 +176,8 @@ void reset_game_state() {
base_max_health = 10; base_max_health = 10;
max_health = 10; max_health = 10;
favor = 0.0; favor = 0.0;
incense_hours_remaining = 0;
incense_burning = false;
blessing_speed_active = false; blessing_speed_active = false;
inv_stones = 0; inv_stones = 0;
@@ -186,6 +190,9 @@ void reset_game_state() {
inv_small_game_types.resize(0); inv_small_game_types.resize(0);
inv_meat = 0; inv_meat = 0;
inv_skins = 0; inv_skins = 0;
inv_feathers = 0;
inv_down = 0;
inv_incense = 0;
inv_spears = 0; inv_spears = 0;
inv_snares = 0; inv_snares = 0;
inv_axes = 0; inv_axes = 0;
@@ -211,6 +218,9 @@ void reset_game_state() {
storage_small_game_types.resize(0); storage_small_game_types.resize(0);
storage_meat = 0; storage_meat = 0;
storage_skins = 0; storage_skins = 0;
storage_feathers = 0;
storage_down = 0;
storage_incense = 0;
storage_spears = 0; storage_spears = 0;
storage_snares = 0; storage_snares = 0;
storage_axes = 0; storage_axes = 0;
@@ -283,6 +293,7 @@ void start_new_game() {
spawn_trees(5, 19); spawn_trees(5, 19);
init_barricade(); init_barricade();
init_time(); init_time();
init_weather();
save_game_state(); save_game_state();
} }
@@ -430,6 +441,8 @@ bool load_game_state_from_raw(const string&in rawData) {
if (get_raw_number(rawData, "player_base_health", value)) base_max_health = value; if (get_raw_number(rawData, "player_base_health", value)) base_max_health = value;
if (get_raw_number(rawData, "player_max_health", value)) max_health = value; if (get_raw_number(rawData, "player_max_health", value)) max_health = value;
if (get_raw_number(rawData, "player_favor", value)) favor = value; if (get_raw_number(rawData, "player_favor", value)) favor = value;
if (get_raw_number(rawData, "incense_hours_remaining", value)) incense_hours_remaining = value;
if (get_raw_bool(rawData, "incense_burning", bool_value)) incense_burning = bool_value;
if (get_raw_number(rawData, "time_current_hour", value)) current_hour = value; if (get_raw_number(rawData, "time_current_hour", value)) current_hour = value;
if (get_raw_number(rawData, "world_map_size", value)) MAP_SIZE = value; if (get_raw_number(rawData, "world_map_size", value)) MAP_SIZE = value;
if (get_raw_number(rawData, "world_expanded_area_start", value)) expanded_area_start = value; if (get_raw_number(rawData, "world_expanded_area_start", value)) expanded_area_start = value;
@@ -459,6 +472,9 @@ bool load_game_state_from_raw(const string&in rawData) {
if (get_raw_number(rawData, "inventory_small_game", value)) inv_small_game = value; if (get_raw_number(rawData, "inventory_small_game", value)) inv_small_game = value;
if (get_raw_number(rawData, "inventory_meat", value)) inv_meat = value; if (get_raw_number(rawData, "inventory_meat", value)) inv_meat = value;
if (get_raw_number(rawData, "inventory_skins", value)) inv_skins = value; if (get_raw_number(rawData, "inventory_skins", value)) inv_skins = value;
if (get_raw_number(rawData, "inventory_feathers", value)) inv_feathers = value;
if (get_raw_number(rawData, "inventory_down", value)) inv_down = value;
if (get_raw_number(rawData, "inventory_incense", value)) inv_incense = value;
if (get_raw_number(rawData, "inventory_spears", value)) inv_spears = value; if (get_raw_number(rawData, "inventory_spears", value)) inv_spears = value;
if (get_raw_number(rawData, "inventory_snares", value)) inv_snares = value; if (get_raw_number(rawData, "inventory_snares", value)) inv_snares = value;
if (get_raw_number(rawData, "inventory_axes", value)) inv_axes = value; if (get_raw_number(rawData, "inventory_axes", value)) inv_axes = value;
@@ -487,6 +503,7 @@ bool load_game_state_from_raw(const string&in rawData) {
if (equipped_arms != EQUIP_POUCH) equipped_arms = EQUIP_NONE; if (equipped_arms != EQUIP_POUCH) equipped_arms = EQUIP_NONE;
if (equipped_arms == EQUIP_POUCH && inv_skin_pouches <= 0) equipped_arms = EQUIP_NONE; if (equipped_arms == EQUIP_POUCH && inv_skin_pouches <= 0) equipped_arms = EQUIP_NONE;
if (incense_hours_remaining > 0) incense_burning = true;
if (inv_small_game_types.length() == 0 && inv_small_game > 0) { if (inv_small_game_types.length() == 0 && inv_small_game > 0) {
for (int i = 0; i < inv_small_game; i++) { for (int i = 0; i < inv_small_game; i++) {
@@ -518,6 +535,8 @@ bool save_game_state() {
saveData.set("player_base_health", base_max_health); saveData.set("player_base_health", base_max_health);
saveData.set("player_max_health", max_health); saveData.set("player_max_health", max_health);
saveData.set("player_favor", favor); saveData.set("player_favor", favor);
saveData.set("incense_hours_remaining", incense_hours_remaining);
saveData.set("incense_burning", incense_burning);
saveData.set("inventory_stones", inv_stones); saveData.set("inventory_stones", inv_stones);
saveData.set("inventory_sticks", inv_sticks); saveData.set("inventory_sticks", inv_sticks);
@@ -528,6 +547,9 @@ bool save_game_state() {
saveData.set("inventory_small_game", inv_small_game); saveData.set("inventory_small_game", inv_small_game);
saveData.set("inventory_meat", inv_meat); saveData.set("inventory_meat", inv_meat);
saveData.set("inventory_skins", inv_skins); saveData.set("inventory_skins", inv_skins);
saveData.set("inventory_feathers", inv_feathers);
saveData.set("inventory_down", inv_down);
saveData.set("inventory_incense", inv_incense);
saveData.set("inventory_spears", inv_spears); saveData.set("inventory_spears", inv_spears);
saveData.set("inventory_snares", inv_snares); saveData.set("inventory_snares", inv_snares);
saveData.set("inventory_axes", inv_axes); saveData.set("inventory_axes", inv_axes);
@@ -554,6 +576,9 @@ bool save_game_state() {
saveData.set("storage_small_game", storage_small_game); saveData.set("storage_small_game", storage_small_game);
saveData.set("storage_meat", storage_meat); saveData.set("storage_meat", storage_meat);
saveData.set("storage_skins", storage_skins); saveData.set("storage_skins", storage_skins);
saveData.set("storage_feathers", storage_feathers);
saveData.set("storage_down", storage_down);
saveData.set("storage_incense", storage_incense);
saveData.set("storage_spears", storage_spears); saveData.set("storage_spears", storage_spears);
saveData.set("storage_snares", storage_snares); saveData.set("storage_snares", storage_snares);
saveData.set("storage_axes", storage_axes); saveData.set("storage_axes", storage_axes);
@@ -605,6 +630,8 @@ bool save_game_state() {
} }
saveData.set("quest_queue", join_string_array(questData)); saveData.set("quest_queue", join_string_array(questData));
saveData.set("weather_data", serialize_weather());
saveData.set("world_map_size", MAP_SIZE); saveData.set("world_map_size", MAP_SIZE);
saveData.set("world_expanded_area_start", expanded_area_start); saveData.set("world_expanded_area_start", expanded_area_start);
saveData.set("world_expanded_area_end", expanded_area_end); saveData.set("world_expanded_area_end", expanded_area_end);
@@ -685,6 +712,12 @@ bool save_game_state() {
} }
saveData.set("mountains_data", join_string_array(mountainData)); saveData.set("mountains_data", join_string_array(mountainData));
string[] dropData;
for (uint i = 0; i < world_drops.length(); i++) {
dropData.insert_last(world_drops[i].position + "|" + world_drops[i].type);
}
saveData.set("drops_data", join_string_array(dropData));
string rawData = saveData.serialize(); string rawData = saveData.serialize();
string encryptedData = encrypt_save_data(rawData); string encryptedData = encrypt_save_data(rawData);
return save_data(SAVE_FILE_PATH, encryptedData); return save_data(SAVE_FILE_PATH, encryptedData);
@@ -755,6 +788,9 @@ bool load_game_state() {
max_health = int(get_number(saveData, "player_max_health", 10)); max_health = int(get_number(saveData, "player_max_health", 10));
base_max_health = int(get_number(saveData, "player_base_health", max_health)); base_max_health = int(get_number(saveData, "player_base_health", max_health));
favor = get_number(saveData, "player_favor", 0.0); favor = get_number(saveData, "player_favor", 0.0);
incense_hours_remaining = int(get_number(saveData, "incense_hours_remaining", 0));
incense_burning = get_bool(saveData, "incense_burning", false);
if (incense_hours_remaining > 0) incense_burning = true;
if (x < 0) x = 0; if (x < 0) x = 0;
if (x >= MAP_SIZE) x = MAP_SIZE - 1; if (x >= MAP_SIZE) x = MAP_SIZE - 1;
@@ -770,6 +806,9 @@ bool load_game_state() {
inv_small_game = int(get_number(saveData, "inventory_small_game", 0)); inv_small_game = int(get_number(saveData, "inventory_small_game", 0));
inv_meat = int(get_number(saveData, "inventory_meat", 0)); inv_meat = int(get_number(saveData, "inventory_meat", 0));
inv_skins = int(get_number(saveData, "inventory_skins", 0)); inv_skins = int(get_number(saveData, "inventory_skins", 0));
inv_feathers = int(get_number(saveData, "inventory_feathers", 0));
inv_down = int(get_number(saveData, "inventory_down", 0));
inv_incense = int(get_number(saveData, "inventory_incense", 0));
inv_spears = int(get_number(saveData, "inventory_spears", 0)); inv_spears = int(get_number(saveData, "inventory_spears", 0));
inv_snares = int(get_number(saveData, "inventory_snares", 0)); inv_snares = int(get_number(saveData, "inventory_snares", 0));
inv_axes = int(get_number(saveData, "inventory_axes", 0)); inv_axes = int(get_number(saveData, "inventory_axes", 0));
@@ -809,6 +848,9 @@ bool load_game_state() {
storage_small_game = int(get_number(saveData, "storage_small_game", 0)); storage_small_game = int(get_number(saveData, "storage_small_game", 0));
storage_meat = int(get_number(saveData, "storage_meat", 0)); storage_meat = int(get_number(saveData, "storage_meat", 0));
storage_skins = int(get_number(saveData, "storage_skins", 0)); storage_skins = int(get_number(saveData, "storage_skins", 0));
storage_feathers = int(get_number(saveData, "storage_feathers", 0));
storage_down = int(get_number(saveData, "storage_down", 0));
storage_incense = int(get_number(saveData, "storage_incense", 0));
storage_spears = int(get_number(saveData, "storage_spears", 0)); storage_spears = int(get_number(saveData, "storage_spears", 0));
storage_snares = int(get_number(saveData, "storage_snares", 0)); storage_snares = int(get_number(saveData, "storage_snares", 0));
storage_axes = int(get_number(saveData, "storage_axes", 0)); storage_axes = int(get_number(saveData, "storage_axes", 0));
@@ -900,6 +942,13 @@ bool load_game_state() {
is_daytime = (current_hour >= 6 && current_hour < 19); is_daytime = (current_hour >= 6 && current_hour < 19);
hour_timer.restart(); hour_timer.restart();
string weather_data;
if (saveData.get("weather_data", weather_data) && weather_data.length() > 0) {
deserialize_weather(weather_data);
} else {
init_weather();
}
string[] treeData = get_string_list_or_split(saveData, "trees_data"); string[] treeData = get_string_list_or_split(saveData, "trees_data");
for (uint i = 0; i < treeData.length(); i++) { for (uint i = 0; i < treeData.length(); i++) {
string[]@ parts = treeData[i].split("|"); string[]@ parts = treeData[i].split("|");
@@ -1061,6 +1110,13 @@ bool load_game_state() {
world_mountains.insert_last(mountain); world_mountains.insert_last(mountain);
} }
string[] dropData = get_string_list_or_split(saveData, "drops_data");
for (uint i = 0; i < dropData.length(); i++) {
string[]@ parts = dropData[i].split("|");
if (parts.length() < 2) continue;
add_world_drop(parse_int(parts[0]), parts[1]);
}
update_ambience(true); update_ambience(true);
return true; return true;
} }

View File

@@ -104,9 +104,15 @@ void expand_regular_area() {
notify("A " + width_desc + " stream flows through the new area at x " + actual_start + "."); notify("A " + width_desc + " stream flows through the new area at x " + actual_start + ".");
} else { } else {
int tree_pos = random(new_start, new_end); // Try to place a tree with proper spacing
Tree@ t = Tree(tree_pos); for (int attempt = 0; attempt < 20; attempt++) {
trees.insert_last(t); int tree_pos = random(new_start, new_end);
if (!tree_too_close(tree_pos)) {
Tree@ t = Tree(tree_pos);
trees.insert_last(t);
break;
}
}
} }
area_expanded_today = true; area_expanded_today = true;
@@ -210,16 +216,49 @@ void attempt_resident_recruitment() {
return; return;
} }
int chance = random(25, 35); // Check if base is full
if (residents_count >= MAX_RESIDENTS) {
return;
}
// Recruitment chance based on storage buildings
int storage_count = world_storages.length();
int min_chance = 50;
int max_chance = 60;
if (storage_count >= 2) {
// 2+ storage: 75-100% chance
min_chance = 75;
max_chance = 100;
} else if (storage_count == 1) {
// 1 storage: 50-75% chance
min_chance = 50;
max_chance = 75;
}
// 0 storage: 50-60% chance (defaults above)
int chance = random(min_chance, max_chance);
int roll = random(1, 100); int roll = random(1, 100);
if (roll > chance) { if (roll > chance) {
return; return;
} }
int added = random(1, 3); int added = random(1, 3);
// Don't exceed cap
if (residents_count + added > MAX_RESIDENTS) {
added = MAX_RESIDENTS - residents_count;
}
if (added <= 0) return;
residents_count += added; residents_count += added;
string join_message = (added == 1) ? "A survivor joins your base." : "" + added + " survivors join your base."; string join_message = (added == 1) ? "A survivor joins your base." : "" + added + " survivors join your base.";
notify(join_message); notify(join_message);
// Notify if base is now full
if (residents_count >= MAX_RESIDENTS) {
notify("Your base is at maximum capacity.");
}
} }
void end_invasion() { void end_invasion() {
@@ -369,14 +408,34 @@ void update_time() {
} }
attempt_daily_invasion(); attempt_daily_invasion();
keep_base_fires_fed(); keep_base_fires_fed();
update_incense_burning();
attempt_hourly_flying_creature_spawn();
check_scheduled_invasion(); check_scheduled_invasion();
attempt_blessing(); attempt_blessing();
check_weather_transition();
attempt_resident_collection();
} }
// Proactive resident defense with slings
attempt_resident_sling_defense();
// Manage bandits during active invasion // Manage bandits during active invasion
manage_bandits_during_invasion(); manage_bandits_during_invasion();
} }
void update_incense_burning() {
if (!incense_burning || incense_hours_remaining <= 0) return;
favor += INCENSE_FAVOR_PER_HOUR;
incense_hours_remaining--;
if (incense_hours_remaining <= 0) {
incense_hours_remaining = 0;
incense_burning = false;
notify("The incense has burned out.");
}
}
void check_time_input() { void check_time_input() {
if (key_pressed(KEY_T)) { if (key_pressed(KEY_T)) {
screen_reader_speak(get_time_string(), true); screen_reader_speak(get_time_string(), true);

View File

@@ -6,7 +6,8 @@ string ui_input_box(const string title, const string prompt, const string defaul
} }
int ui_question(const string title, const string prompt) { int ui_question(const string title, const string prompt) {
int result = virtual_question(title, prompt); // Put the prompt in both title (for screen reader) and message (for dialog to work)
int result = virtual_question(prompt, prompt);
show_window("Draugnorak"); show_window("Draugnorak");
return result; return result;
} }

492
src/weather.nvgt Normal file
View File

@@ -0,0 +1,492 @@
// Weather System
// Provides ambient wind, rain, and thunder effects
// Weather states
const int WEATHER_CLEAR = 0;
const int WEATHER_WINDY = 1;
const int WEATHER_RAINY = 2;
const int WEATHER_STORMY = 3;
// Intensity levels (0 = none, 1-3 = low/medium/high)
const int INTENSITY_NONE = 0;
const int INTENSITY_LOW = 1;
const int INTENSITY_MEDIUM = 2;
const int INTENSITY_HIGH = 3;
// Audio fade settings
const int WEATHER_FADE_DURATION = 8000; // 8 seconds for smooth transitions
const float WEATHER_MIN_VOLUME = -30.0;
const float WEATHER_MAX_VOLUME = 0.0;
// Rain volume levels by intensity
const float RAIN_VOLUME_LIGHT = -18.0;
const float RAIN_VOLUME_MODERATE = -10.0;
const float RAIN_VOLUME_HEAVY = -3.0;
// Wind gust settings
const int WIND_GUST_MIN_DELAY = 10000; // Min 10 seconds between gusts
const int WIND_GUST_MAX_DELAY = 25000; // Max 25 seconds between gusts
// Thunder timing
const int THUNDER_MIN_INTERVAL = 8000; // Min 8 seconds between thunder
const int THUNDER_MAX_INTERVAL = 35000; // Max 35 seconds between thunder
const int THUNDER_MOVEMENT_SPEED = 2000; // ms per tile movement (slow roll across sky)
const float THUNDER_SOUND_VOLUME_STEP = 2.0; // Gentler volume falloff
const int THUNDER_SPAWN_DISTANCE_MIN = 20; // Min distance from player
const int THUNDER_SPAWN_DISTANCE_MAX = 40; // Max distance from player
// Weather transition chances (out of 100)
const int CHANCE_CLEAR_TO_WINDY = 15;
const int CHANCE_CLEAR_TO_RAINY = 4;
const int CHANCE_CLEAR_TO_STORMY = 2;
const int CHANCE_WINDY_STAY = 55;
const int CHANCE_WINDY_TO_CLEAR = 25;
const int CHANCE_WINDY_TO_STORMY = 12;
const int CHANCE_RAINY_STAY = 45;
const int CHANCE_RAINY_TO_STORMY = 25;
const int CHANCE_STORMY_STAY = 40;
const int CHANCE_STORMY_TO_RAINY = 35;
// State variables
int weather_state = WEATHER_CLEAR;
int wind_intensity = INTENSITY_NONE;
int rain_intensity = INTENSITY_NONE;
bool thunder_enabled = false;
// Wind gust state
timer wind_gust_timer;
int next_wind_gust_delay = 0;
int wind_sound_handle = -1;
// Rain state
int rain_sound_handle = -1;
// Fade state for rain
bool rain_fading = false;
float rain_fade_from_volume = WEATHER_MIN_VOLUME;
float rain_fade_to_volume = WEATHER_MIN_VOLUME;
timer rain_fade_timer;
// Thunder object state
class ThunderStrike {
int position;
int direction; // -1 = moving west, 1 = moving east
int sound_handle;
timer movement_timer;
ThunderStrike(int pos, int dir, int handle) {
position = pos;
direction = dir;
sound_handle = handle;
movement_timer.restart();
}
void update() {
if (movement_timer.elapsed >= THUNDER_MOVEMENT_SPEED) {
position += direction;
movement_timer.restart();
}
// Update sound position
if (sound_handle != -1 && p.sound_is_active(sound_handle)) {
p.update_sound_1d(sound_handle, position);
}
}
bool is_finished() {
return sound_handle == -1 || !p.sound_is_active(sound_handle);
}
}
ThunderStrike@[] active_thunder;
timer thunder_timer;
int next_thunder_interval = 0;
// Sound file paths
string[] wind_sounds = {
"", // INTENSITY_NONE placeholder
"sounds/nature/wind_low.ogg",
"sounds/nature/wind_medium.ogg",
"sounds/nature/wind_high.ogg"
};
string[] thunder_sounds = {
"sounds/nature/thunder_low.ogg",
"sounds/nature/thunder_medium.ogg",
"sounds/nature/thunder_high.ogg"
};
const string RAIN_SOUND = "sounds/nature/rain.ogg";
void init_weather() {
weather_state = WEATHER_CLEAR;
wind_intensity = INTENSITY_NONE;
rain_intensity = INTENSITY_NONE;
thunder_enabled = false;
wind_sound_handle = -1;
rain_sound_handle = -1;
rain_fading = false;
wind_gust_timer.restart();
next_wind_gust_delay = 0;
active_thunder.resize(0);
thunder_timer.restart();
next_thunder_interval = random(THUNDER_MIN_INTERVAL, THUNDER_MAX_INTERVAL);
}
void update_weather() {
update_wind_gusts();
update_rain_fade();
update_thunder();
}
// Called each game hour from time_system
void check_weather_transition() {
int roll = random(1, 100);
if (weather_state == WEATHER_CLEAR) {
if (roll <= CHANCE_CLEAR_TO_STORMY) {
start_storm();
} else if (roll <= CHANCE_CLEAR_TO_STORMY + CHANCE_CLEAR_TO_RAINY) {
start_rain();
} else if (roll <= CHANCE_CLEAR_TO_STORMY + CHANCE_CLEAR_TO_RAINY + CHANCE_CLEAR_TO_WINDY) {
start_wind();
}
} else if (weather_state == WEATHER_WINDY) {
if (roll <= CHANCE_WINDY_TO_CLEAR) {
clear_weather();
} else if (roll <= CHANCE_WINDY_TO_CLEAR + CHANCE_WINDY_TO_STORMY) {
start_storm();
} else if (roll <= CHANCE_WINDY_TO_CLEAR + CHANCE_WINDY_TO_STORMY + CHANCE_WINDY_STAY) {
// Stay windy, maybe change intensity
maybe_change_wind_intensity();
}
} else if (weather_state == WEATHER_RAINY) {
if (roll <= CHANCE_RAINY_TO_STORMY) {
// Escalate to storm
weather_state = WEATHER_STORMY;
thunder_enabled = true;
thunder_timer.restart();
next_thunder_interval = random(THUNDER_MIN_INTERVAL, THUNDER_MAX_INTERVAL);
// Maybe increase rain
if (rain_intensity < INTENSITY_HIGH && random(1, 100) <= 50) {
fade_rain_to_intensity(rain_intensity + 1);
}
} else if (roll <= CHANCE_RAINY_TO_STORMY + CHANCE_RAINY_STAY) {
// Stay rainy, maybe change intensity
maybe_change_rain_intensity();
} else {
// Clear up
clear_weather();
}
} else if (weather_state == WEATHER_STORMY) {
if (roll <= CHANCE_STORMY_TO_RAINY) {
// De-escalate to just rain
weather_state = WEATHER_RAINY;
thunder_enabled = false;
// Clear any active thunder
for (uint i = 0; i < active_thunder.length(); i++) {
if (active_thunder[i].sound_handle != -1) {
p.destroy_sound(active_thunder[i].sound_handle);
}
}
active_thunder.resize(0);
// Maybe reduce rain
if (rain_intensity > INTENSITY_LOW && random(1, 100) <= 40) {
fade_rain_to_intensity(rain_intensity - 1);
}
// Stop wind if present
if (wind_intensity > INTENSITY_NONE) {
wind_intensity = INTENSITY_NONE;
if (wind_sound_handle != -1) {
p.destroy_sound(wind_sound_handle);
wind_sound_handle = -1;
}
}
} else if (roll <= CHANCE_STORMY_TO_RAINY + CHANCE_STORMY_STAY) {
// Stay stormy, maybe change intensities
maybe_change_storm_intensity();
} else {
// Clear up
clear_weather();
}
}
}
void start_wind() {
weather_state = WEATHER_WINDY;
wind_intensity = random(INTENSITY_LOW, INTENSITY_MEDIUM);
wind_gust_timer.restart();
next_wind_gust_delay = random(WIND_GUST_MIN_DELAY, WIND_GUST_MAX_DELAY);
}
void start_rain() {
weather_state = WEATHER_RAINY;
int new_intensity = random(INTENSITY_LOW, INTENSITY_MEDIUM);
fade_rain_to_intensity(new_intensity);
}
void start_storm() {
weather_state = WEATHER_STORMY;
// Start or increase wind
wind_intensity = random(INTENSITY_MEDIUM, INTENSITY_HIGH);
wind_gust_timer.restart();
next_wind_gust_delay = random(WIND_GUST_MIN_DELAY, WIND_GUST_MAX_DELAY);
// Start rain
int new_rain = random(INTENSITY_MEDIUM, INTENSITY_HIGH);
fade_rain_to_intensity(new_rain);
// Enable thunder
thunder_enabled = true;
thunder_timer.restart();
next_thunder_interval = random(THUNDER_MIN_INTERVAL, THUNDER_MAX_INTERVAL);
}
void clear_weather() {
weather_state = WEATHER_CLEAR;
thunder_enabled = false;
wind_intensity = INTENSITY_NONE;
if (wind_sound_handle != -1) {
p.destroy_sound(wind_sound_handle);
wind_sound_handle = -1;
}
if (rain_intensity > INTENSITY_NONE) {
fade_rain_to_intensity(INTENSITY_NONE);
}
// Clear any active thunder
for (uint i = 0; i < active_thunder.length(); i++) {
if (active_thunder[i].sound_handle != -1) {
p.destroy_sound(active_thunder[i].sound_handle);
}
}
active_thunder.resize(0);
}
void maybe_change_wind_intensity() {
if (random(1, 100) <= 30) {
int change = random(-1, 1);
int new_intensity = wind_intensity + change;
if (new_intensity < INTENSITY_LOW) new_intensity = INTENSITY_LOW;
if (new_intensity > INTENSITY_HIGH) new_intensity = INTENSITY_HIGH;
wind_intensity = new_intensity;
}
}
void maybe_change_rain_intensity() {
if (random(1, 100) <= 30) {
int change = random(-1, 1);
int new_intensity = rain_intensity + change;
if (new_intensity < INTENSITY_LOW) new_intensity = INTENSITY_LOW;
if (new_intensity > INTENSITY_HIGH) new_intensity = INTENSITY_HIGH;
if (new_intensity != rain_intensity) {
fade_rain_to_intensity(new_intensity);
}
}
}
void maybe_change_storm_intensity() {
// Possibly change wind
if (random(1, 100) <= 25) {
int new_wind = wind_intensity + random(-1, 1);
if (new_wind < INTENSITY_LOW) new_wind = INTENSITY_LOW;
if (new_wind > INTENSITY_HIGH) new_wind = INTENSITY_HIGH;
wind_intensity = new_wind;
}
// Possibly change rain
if (random(1, 100) <= 25) {
int new_rain = rain_intensity + random(-1, 1);
if (new_rain < INTENSITY_LOW) new_rain = INTENSITY_LOW;
if (new_rain > INTENSITY_HIGH) new_rain = INTENSITY_HIGH;
if (new_rain != rain_intensity) {
fade_rain_to_intensity(new_rain);
}
}
}
// Wind gust implementation
void update_wind_gusts() {
if (wind_intensity == INTENSITY_NONE) return;
// Check if it's time for next gust
if (wind_gust_timer.elapsed >= next_wind_gust_delay) {
play_wind_gust();
wind_gust_timer.restart();
next_wind_gust_delay = random(WIND_GUST_MIN_DELAY, WIND_GUST_MAX_DELAY);
}
}
void play_wind_gust() {
if (wind_intensity == INTENSITY_NONE || wind_intensity > INTENSITY_HIGH) return;
// Play the appropriate wind sound once (non-looping)
wind_sound_handle = p.play_stationary(wind_sounds[wind_intensity], false);
}
// Rain fade implementation
float get_rain_target_volume(int intensity) {
if (intensity == INTENSITY_NONE) return WEATHER_MIN_VOLUME;
if (intensity == INTENSITY_LOW) return RAIN_VOLUME_LIGHT;
if (intensity == INTENSITY_MEDIUM) return RAIN_VOLUME_MODERATE;
return RAIN_VOLUME_HEAVY;
}
void fade_rain_to_intensity(int new_intensity) {
if (new_intensity == rain_intensity && !rain_fading) return;
// Complete any current fade
if (rain_fading) {
complete_rain_fade();
}
rain_fade_from_volume = get_rain_target_volume(rain_intensity);
rain_fade_to_volume = get_rain_target_volume(new_intensity);
// Start rain sound if not playing
if (rain_sound_handle == -1 && new_intensity != INTENSITY_NONE) {
rain_sound_handle = p.play_stationary(RAIN_SOUND, true);
p.update_sound_start_values(rain_sound_handle, 0.0, WEATHER_MIN_VOLUME, 1.0);
rain_fade_from_volume = WEATHER_MIN_VOLUME;
}
rain_fading = true;
rain_fade_timer.restart();
rain_intensity = new_intensity; // Track target intensity
}
void update_rain_fade() {
if (!rain_fading) return;
if (rain_sound_handle == -1) {
rain_fading = false;
return;
}
float progress = float(rain_fade_timer.elapsed) / float(WEATHER_FADE_DURATION);
if (progress > 1.0) progress = 1.0;
float current_vol = rain_fade_from_volume + ((rain_fade_to_volume - rain_fade_from_volume) * progress);
p.update_sound_start_values(rain_sound_handle, 0.0, current_vol, 1.0);
if (progress >= 1.0) {
complete_rain_fade();
}
}
void complete_rain_fade() {
rain_fading = false;
// If faded to silence, destroy sound
if (rain_fade_to_volume <= WEATHER_MIN_VOLUME && rain_sound_handle != -1) {
p.destroy_sound(rain_sound_handle);
rain_sound_handle = -1;
rain_intensity = INTENSITY_NONE;
}
}
// Thunder implementation
void update_thunder() {
// Update existing thunder strikes
for (uint i = 0; i < active_thunder.length(); i++) {
active_thunder[i].update();
}
// Remove finished thunder strikes
for (uint i = 0; i < active_thunder.length(); i++) {
if (active_thunder[i].is_finished()) {
active_thunder.remove_at(i);
i--;
}
}
if (!thunder_enabled) return;
// Check if it's time for new thunder
if (thunder_timer.elapsed >= next_thunder_interval) {
spawn_thunder();
thunder_timer.restart();
next_thunder_interval = random(THUNDER_MIN_INTERVAL, THUNDER_MAX_INTERVAL);
}
}
void spawn_thunder() {
// Pick random thunder sound
int thunder_type = random(0, thunder_sounds.length() - 1);
string thunder_file = thunder_sounds[thunder_type];
// Spawn thunder at random distance from player
int distance = random(THUNDER_SPAWN_DISTANCE_MIN, THUNDER_SPAWN_DISTANCE_MAX);
// Randomly place to left or right of player
int direction = random(0, 1) == 0 ? -1 : 1;
int thunder_pos = x + (distance * direction);
// Play sound at position with custom volume step
int handle = play_1d_with_volume_step(thunder_file, x, thunder_pos, false, THUNDER_SOUND_VOLUME_STEP);
if (handle != -1) {
ThunderStrike@ strike = ThunderStrike(thunder_pos, direction, handle);
active_thunder.insert_last(strike);
}
}
void stop_all_weather_sounds() {
if (wind_sound_handle != -1) {
p.destroy_sound(wind_sound_handle);
wind_sound_handle = -1;
}
if (rain_sound_handle != -1) {
p.destroy_sound(rain_sound_handle);
rain_sound_handle = -1;
}
for (uint i = 0; i < active_thunder.length(); i++) {
if (active_thunder[i].sound_handle != -1) {
p.destroy_sound(active_thunder[i].sound_handle);
}
}
active_thunder.resize(0);
rain_fading = false;
wind_intensity = INTENSITY_NONE;
rain_intensity = INTENSITY_NONE;
thunder_enabled = false;
}
// Save/Load functions
string serialize_weather() {
return weather_state + "|" + wind_intensity + "|" + rain_intensity + "|" + (thunder_enabled ? 1 : 0);
}
void deserialize_weather(string data) {
string[]@ parts = data.split("|");
if (parts.length() < 4) {
init_weather();
return;
}
// Stop any current sounds first
stop_all_weather_sounds();
weather_state = parse_int(parts[0]);
wind_intensity = parse_int(parts[1]);
int saved_rain = parse_int(parts[2]);
thunder_enabled = parse_int(parts[3]) == 1;
// Restore wind gust timer
if (wind_intensity > INTENSITY_NONE && wind_intensity <= INTENSITY_HIGH) {
wind_gust_timer.restart();
next_wind_gust_delay = random(WIND_GUST_MIN_DELAY, WIND_GUST_MAX_DELAY);
}
// Restore rain at saved intensity (no fade, instant)
if (saved_rain > INTENSITY_NONE && saved_rain <= INTENSITY_HIGH) {
rain_sound_handle = p.play_stationary(RAIN_SOUND, true);
float vol = get_rain_target_volume(saved_rain);
p.update_sound_start_values(rain_sound_handle, 0.0, vol, 1.0);
rain_intensity = saved_rain;
}
// Reset thunder timer
thunder_timer.restart();
next_thunder_interval = random(THUNDER_MIN_INTERVAL, THUNDER_MAX_INTERVAL);
}

View File

@@ -9,6 +9,7 @@ int residents_count = 0;
string[] zombie_sounds = {"sounds/enemies/zombie1.ogg"}; string[] zombie_sounds = {"sounds/enemies/zombie1.ogg"};
string[] bandit_sounds = {"sounds/enemies/bandit1.ogg", "sounds/enemies/bandit2.ogg"}; string[] bandit_sounds = {"sounds/enemies/bandit1.ogg", "sounds/enemies/bandit2.ogg"};
string[] goose_sounds = {"sounds/game/goose.ogg"};
class Zombie { class Zombie {
int position; int position;
@@ -82,11 +83,174 @@ class Bandit {
} }
Bandit@[] bandits; Bandit@[] bandits;
class FlyingCreatureConfig {
string id;
string drop_type;
string[] sounds;
string fall_sound;
string impact_sound;
int health;
int move_interval_min;
int move_interval_max;
int min_height;
int max_height;
float sound_volume_step;
int sound_delay_min;
int sound_delay_max;
int fall_speed;
int fly_away_chance;
int max_dist_from_water;
int hourly_spawn_chance;
int max_count;
int sight_range;
bool flee_on_sight;
}
FlyingCreatureConfig@[] flying_creature_configs;
class FlyingCreature {
int position;
int health;
int height;
string state; // "flying", "falling"
int area_start;
int area_end;
string creature_type;
int sound_handle;
int fall_sound_handle;
timer move_timer;
timer sound_timer;
timer fall_timer;
int next_move_delay;
int next_sound_delay;
string voice_sound;
bool fading_out;
bool ready_to_remove;
timer fade_timer;
FlyingCreature(string type, int pos, int home_start, int home_end, FlyingCreatureConfig@ cfg) {
position = pos;
health = cfg.health;
height = random(cfg.min_height, cfg.max_height);
state = "flying";
area_start = home_start;
area_end = home_end;
creature_type = type;
sound_handle = -1;
fall_sound_handle = -1;
if (cfg.sounds.length() > 0) {
voice_sound = cfg.sounds[random(0, cfg.sounds.length() - 1)];
}
move_timer.restart();
sound_timer.restart();
next_move_delay = random(cfg.move_interval_min, cfg.move_interval_max);
next_sound_delay = random(cfg.sound_delay_min, cfg.sound_delay_max);
fading_out = false;
ready_to_remove = false;
}
}
FlyingCreature@[] flying_creatures;
string get_random_small_game() { string get_random_small_game() {
int index = random(0, small_game_types.length() - 1); int index = random(0, small_game_types.length() - 1);
return small_game_types[index]; return small_game_types[index];
} }
class WorldDrop {
int position;
string type;
int sound_handle;
WorldDrop(int pos, string t) {
position = pos;
type = t;
sound_handle = -1;
// Start looping item sound at position
sound_handle = p.play_1d("sounds/items/item.ogg", x, position, true);
if (sound_handle != -1) {
p.update_sound_positioning_values(sound_handle, -1.0, 3.0, true);
}
}
void update() {
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
sound_handle = p.play_1d("sounds/items/item.ogg", x, position, true);
if (sound_handle != -1) {
p.update_sound_positioning_values(sound_handle, -1.0, 3.0, true);
}
} else {
// Update source position for 1d sound
p.update_sound_1d(sound_handle, position);
}
}
void destroy() {
if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
}
}
}
WorldDrop@[] world_drops;
void add_world_drop(int pos, string type) {
WorldDrop@ d = WorldDrop(pos, type);
world_drops.insert_last(d);
}
void update_world_drops() {
for (uint i = 0; i < world_drops.length(); i++) {
world_drops[i].update();
}
}
WorldDrop@ get_drop_at(int pos) {
for (uint i = 0; i < world_drops.length(); i++) {
if (world_drops[i].position == pos) {
return @world_drops[i];
}
}
return null;
}
void remove_drop_at(int pos) {
for (uint i = 0; i < world_drops.length(); i++) {
if (world_drops[i].position == pos) {
world_drops[i].destroy();
world_drops.remove_at(i);
return;
}
}
}
void clear_world_drops() {
for (uint i = 0; i < world_drops.length(); i++) {
world_drops[i].destroy();
}
world_drops.resize(0);
}
bool try_pickup_small_game(string game_type) {
if (inv_small_game >= get_personal_stack_limit()) {
screen_reader_speak("You can't carry any more small game.", true);
return false;
}
inv_small_game++;
inv_small_game_types.insert_last(game_type);
screen_reader_speak("Picked up " + game_type + ".", true);
return true;
}
bool try_pickup_world_drop(WorldDrop@ drop) {
if (get_flying_creature_config_by_drop_type(drop.type) !is null) {
return try_pickup_small_game(drop.type);
}
screen_reader_speak("Picked up " + drop.type + ".", true);
return true;
}
class WorldSnare { class WorldSnare {
int position; int position;
bool has_catch; bool has_catch;
@@ -843,9 +1007,9 @@ bool try_attack_player_bandit(Bandit@ bandit) {
// Play weapon swing sound based on bandit's weapon // Play weapon swing sound based on bandit's weapon
if (bandit.weapon_type == "spear") { if (bandit.weapon_type == "spear") {
p.play_stationary("sounds/weapons/spear_swing.ogg", false); play_creature_attack_sound("sounds/weapons/spear_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
} else if (bandit.weapon_type == "axe") { } else if (bandit.weapon_type == "axe") {
p.play_stationary("sounds/weapons/axe_swing.ogg", false); play_creature_attack_sound("sounds/weapons/axe_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
} }
int damage = random(BANDIT_DAMAGE_MIN, BANDIT_DAMAGE_MAX); int damage = random(BANDIT_DAMAGE_MIN, BANDIT_DAMAGE_MAX);
@@ -856,9 +1020,9 @@ bool try_attack_player_bandit(Bandit@ bandit) {
// Play hit sound // Play hit sound
if (bandit.weapon_type == "spear") { if (bandit.weapon_type == "spear") {
p.play_stationary("sounds/weapons/spear_hit.ogg", false); play_creature_attack_sound("sounds/weapons/spear_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
} else if (bandit.weapon_type == "axe") { } else if (bandit.weapon_type == "axe") {
p.play_stationary("sounds/weapons/axe_hit.ogg", false); play_creature_attack_sound("sounds/weapons/axe_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
} }
return true; return true;
@@ -877,11 +1041,11 @@ void try_attack_barricade_bandit(Bandit@ bandit) {
// Play weapon swing sound // Play weapon swing sound
if (bandit.weapon_type == "spear") { if (bandit.weapon_type == "spear") {
p.play_stationary("sounds/weapons/spear_swing.ogg", false); play_creature_attack_sound("sounds/weapons/spear_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
p.play_stationary("sounds/weapons/spear_hit.ogg", false); play_creature_attack_sound("sounds/weapons/spear_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
} else if (bandit.weapon_type == "axe") { } else if (bandit.weapon_type == "axe") {
p.play_stationary("sounds/weapons/axe_swing.ogg", false); play_creature_attack_sound("sounds/weapons/axe_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
p.play_stationary("sounds/weapons/axe_hit.ogg", false); play_creature_attack_sound("sounds/weapons/axe_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
} }
// Resident defense counter-attack // Resident defense counter-attack
@@ -1261,3 +1425,327 @@ void clear_mountains() {
} }
world_mountains.resize(0); world_mountains.resize(0);
} }
// Flying Creature Functions
void init_flying_creature_configs() {
flying_creature_configs.resize(0);
FlyingCreatureConfig@ goose_cfg = FlyingCreatureConfig();
goose_cfg.id = "goose";
goose_cfg.drop_type = "goose";
goose_cfg.sounds = goose_sounds;
goose_cfg.fall_sound = "sounds/actions/falling.ogg";
goose_cfg.impact_sound = "sounds/game/game_falls.ogg";
goose_cfg.health = GOOSE_HEALTH;
goose_cfg.move_interval_min = GOOSE_MOVE_INTERVAL_MIN;
goose_cfg.move_interval_max = GOOSE_MOVE_INTERVAL_MAX;
goose_cfg.min_height = GOOSE_FLYING_HEIGHT_MIN;
goose_cfg.max_height = GOOSE_FLYING_HEIGHT_MAX;
goose_cfg.sound_volume_step = GOOSE_SOUND_VOLUME_STEP;
goose_cfg.sound_delay_min = GOOSE_FLIGHT_SOUND_DELAY_MIN;
goose_cfg.sound_delay_max = GOOSE_FLIGHT_SOUND_DELAY_MAX;
goose_cfg.fall_speed = GOOSE_FALL_SPEED;
goose_cfg.fly_away_chance = GOOSE_FLY_AWAY_CHANCE;
goose_cfg.max_dist_from_water = GOOSE_MAX_DIST_FROM_WATER;
goose_cfg.hourly_spawn_chance = GOOSE_HOURLY_SPAWN_CHANCE;
goose_cfg.max_count = GOOSE_MAX_COUNT;
goose_cfg.sight_range = GOOSE_SIGHT_RANGE;
goose_cfg.flee_on_sight = false;
flying_creature_configs.insert_last(goose_cfg);
}
FlyingCreatureConfig@ get_flying_creature_config(string creature_type) {
for (uint i = 0; i < flying_creature_configs.length(); i++) {
if (flying_creature_configs[i].id == creature_type) {
return @flying_creature_configs[i];
}
}
return null;
}
FlyingCreatureConfig@ get_flying_creature_config_by_drop_type(string drop_type) {
for (uint i = 0; i < flying_creature_configs.length(); i++) {
if (flying_creature_configs[i].drop_type == drop_type) {
return @flying_creature_configs[i];
}
}
return null;
}
void clear_flying_creatures() {
for (uint i = 0; i < flying_creatures.length(); i++) {
if (flying_creatures[i].sound_handle != -1) {
p.destroy_sound(flying_creatures[i].sound_handle);
flying_creatures[i].sound_handle = -1;
}
if (flying_creatures[i].fall_sound_handle != -1) {
p.destroy_sound(flying_creatures[i].fall_sound_handle);
flying_creatures[i].fall_sound_handle = -1;
}
}
flying_creatures.resize(0);
}
FlyingCreature@ get_flying_creature_at(int pos) {
for (uint i = 0; i < flying_creatures.length(); i++) {
if (flying_creatures[i].position == pos) {
return @flying_creatures[i];
}
}
return null;
}
int get_flying_creature_count(string creature_type) {
int count = 0;
for (uint i = 0; i < flying_creatures.length(); i++) {
if (flying_creatures[i].creature_type == creature_type) {
count++;
}
}
return count;
}
bool get_random_flying_creature_area(FlyingCreatureConfig@ cfg, int &out area_start, int &out area_end) {
int stream_count = int(world_streams.length());
int mountain_stream_count = 0;
for (uint i = 0; i < world_mountains.length(); i++) {
mountain_stream_count += int(world_mountains[i].stream_positions.length());
}
int total_areas = stream_count + mountain_stream_count;
if (total_areas <= 0) return false;
int pick = random(0, total_areas - 1);
if (pick < stream_count) {
area_start = world_streams[pick].start_position;
area_end = world_streams[pick].end_position;
} else {
pick -= stream_count;
for (uint i = 0; i < world_mountains.length(); i++) {
int local_count = int(world_mountains[i].stream_positions.length());
if (pick < local_count) {
int stream_pos = world_mountains[i].start_position + world_mountains[i].stream_positions[pick];
area_start = stream_pos;
area_end = stream_pos;
break;
}
pick -= local_count;
}
}
area_start -= cfg.max_dist_from_water;
area_end += cfg.max_dist_from_water;
if (area_start < 0) area_start = 0;
if (area_end >= MAP_SIZE) area_end = MAP_SIZE - 1;
return true;
}
bool find_flying_creature_spawn(FlyingCreatureConfig@ cfg, int &out spawn_x, int &out area_start, int &out area_end) {
if (!get_random_flying_creature_area(cfg, area_start, area_end)) return false;
for (int attempts = 0; attempts < 20; attempts++) {
int candidate = random(area_start, area_end);
if (get_flying_creature_at(candidate) == null) {
spawn_x = candidate;
return true;
}
}
return false;
}
void fly_away_flying_creature(FlyingCreature@ creature, FlyingCreatureConfig@ cfg) {
creature.state = "fading";
creature.fading_out = true;
creature.ready_to_remove = false;
creature.fade_timer.restart();
creature.health = 0;
if (creature.sound_handle == -1 || !p.sound_is_active(creature.sound_handle)) {
creature.ready_to_remove = true;
}
if (creature.fall_sound_handle != -1) {
p.destroy_sound(creature.fall_sound_handle);
creature.fall_sound_handle = -1;
}
}
bool spawn_flying_creature(string creature_type) {
FlyingCreatureConfig@ cfg = get_flying_creature_config(creature_type);
if (cfg is null) return false;
int spawn_x = -1;
int area_start = 0;
int area_end = 0;
if (!find_flying_creature_spawn(cfg, spawn_x, area_start, area_end)) {
return false;
}
FlyingCreature@ c = FlyingCreature(creature_type, spawn_x, area_start, area_end, cfg);
flying_creatures.insert_last(c);
c.sound_handle = play_creature_voice(c.voice_sound, x, spawn_x, cfg.sound_volume_step);
return true;
}
void update_flying_creature(FlyingCreature@ creature) {
FlyingCreatureConfig@ cfg = get_flying_creature_config(creature.creature_type);
if (cfg is null) return;
if (creature.state == "fading") {
if (!creature.fading_out) {
creature.fading_out = true;
creature.fade_timer.restart();
}
if (creature.sound_handle != -1 && p.sound_is_active(creature.sound_handle)) {
float progress = float(creature.fade_timer.elapsed) / float(FLYING_CREATURE_FADE_OUT_DURATION);
if (progress < 0.0) progress = 0.0;
if (progress > 1.0) progress = 1.0;
float volume = 0.0 + (FLYING_CREATURE_FADE_OUT_MIN_VOLUME * progress);
p.update_sound_start_values(creature.sound_handle, 0.0, volume, 1.0);
}
if (creature.fade_timer.elapsed >= FLYING_CREATURE_FADE_OUT_DURATION) {
if (creature.sound_handle != -1) {
p.destroy_sound(creature.sound_handle);
creature.sound_handle = -1;
}
creature.ready_to_remove = true;
}
return;
}
if (creature.state == "flying") {
if (creature.position < creature.area_start || creature.position > creature.area_end) {
fly_away_flying_creature(creature, cfg);
return;
}
if (creature.sound_timer.elapsed > creature.next_sound_delay) {
creature.sound_timer.restart();
creature.next_sound_delay = random(cfg.sound_delay_min, cfg.sound_delay_max);
creature.sound_handle = play_creature_voice(creature.voice_sound, x, creature.position, cfg.sound_volume_step);
}
if (cfg.fly_away_chance > 0 && random(1, 1000) <= cfg.fly_away_chance) {
fly_away_flying_creature(creature, cfg);
return;
}
if (creature.move_timer.elapsed > creature.next_move_delay) {
creature.move_timer.restart();
creature.next_move_delay = random(cfg.move_interval_min, cfg.move_interval_max);
int dir = 0;
if (cfg.flee_on_sight && cfg.sight_range > 0) {
int distance_to_player = abs(x - creature.position);
if (distance_to_player <= cfg.sight_range) {
if (x > creature.position) dir = -1;
else if (x < creature.position) dir = 1;
}
}
if (dir == 0) dir = random(-1, 1);
if (dir != 0) {
int target_x = creature.position + dir;
if (target_x < creature.area_start || target_x > creature.area_end) {
fly_away_flying_creature(creature, cfg);
return;
}
if (target_x >= 0 && target_x < MAP_SIZE) {
creature.position = target_x;
if (creature.sound_handle != -1 && p.sound_is_active(creature.sound_handle)) {
p.update_sound_1d(creature.sound_handle, creature.position);
}
}
}
}
} else if (creature.state == "falling") {
if (creature.fall_timer.elapsed > cfg.fall_speed) {
creature.fall_timer.restart();
creature.height--;
if (creature.fall_sound_handle != -1) {
p.destroy_sound(creature.fall_sound_handle);
}
float pitch_percent = 50.0 + (50.0 * (float(creature.height) / float(cfg.max_height)));
if (pitch_percent < 50.0) pitch_percent = 50.0;
if (pitch_percent > 100.0) pitch_percent = 100.0;
creature.fall_sound_handle = p.play_extended_1d(cfg.fall_sound, x, creature.position, 0, 0, true, 0, 0.0, 0.0, pitch_percent);
if (creature.fall_sound_handle != -1) {
p.update_sound_positioning_values(creature.fall_sound_handle, -1.0, cfg.sound_volume_step, true);
}
if (creature.height <= 0) {
if (creature.fall_sound_handle != -1) {
p.destroy_sound(creature.fall_sound_handle);
creature.fall_sound_handle = -1;
}
play_creature_death_sound(cfg.impact_sound, x, creature.position, cfg.sound_volume_step);
notify("A " + creature.creature_type + " fell from the sky at " + creature.position + "!");
add_world_drop(creature.position, cfg.drop_type);
creature.health = 0;
}
}
}
}
void update_flying_creatures() {
for (uint i = 0; i < flying_creatures.length(); i++) {
update_flying_creature(flying_creatures[i]);
if (flying_creatures[i].health <= 0) {
if (flying_creatures[i].state == "falling" && flying_creatures[i].height <= 0) {
flying_creatures.remove_at(i);
i--;
} else if (flying_creatures[i].state == "flying") {
flying_creatures.remove_at(i);
i--;
} else if (flying_creatures[i].state == "fading" && flying_creatures[i].ready_to_remove) {
flying_creatures.remove_at(i);
i--;
}
}
}
}
void attempt_hourly_flying_creature_spawn() {
for (uint i = 0; i < flying_creature_configs.length(); i++) {
FlyingCreatureConfig@ cfg = flying_creature_configs[i];
if (get_flying_creature_count(cfg.id) >= cfg.max_count) continue;
if (random(1, 100) <= cfg.hourly_spawn_chance) {
spawn_flying_creature(cfg.id);
}
}
}
bool damage_flying_creature_at(int pos, int damage) {
for (uint i = 0; i < flying_creatures.length(); i++) {
if (flying_creatures[i].position == pos && flying_creatures[i].state == "flying") {
FlyingCreatureConfig@ cfg = get_flying_creature_config(flying_creatures[i].creature_type);
if (cfg is null) return false;
flying_creatures[i].health -= damage;
if (flying_creatures[i].health <= 0) {
flying_creatures[i].state = "falling";
flying_creatures[i].fall_timer.restart();
if (flying_creatures[i].sound_handle != -1) {
p.destroy_sound(flying_creatures[i].sound_handle);
flying_creatures[i].sound_handle = -1;
}
float pitch_percent = 50.0 + (50.0 * (float(flying_creatures[i].height) / float(cfg.max_height)));
flying_creatures[i].fall_sound_handle = p.play_extended_1d(cfg.fall_sound, x, pos, 0, 0, true, 0, 0.0, 0.0, pitch_percent);
if (flying_creatures[i].fall_sound_handle != -1) {
p.update_sound_positioning_values(flying_creatures[i].fall_sound_handle, -1.0, cfg.sound_volume_step, true);
}
}
return true;
}
}
return false;
}