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:
BIN
sounds/game/game_falls.ogg
LFS
Normal file
BIN
sounds/game/game_falls.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/game/goose.ogg
LFS
Normal file
BIN
sounds/game/goose.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/items/item.ogg
LFS
Normal file
BIN
sounds/items/item.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/items/miscellaneous.ogg
LFS
BIN
sounds/items/miscellaneous.ogg
LFS
Binary file not shown.
BIN
sounds/nature/rain.ogg
LFS
Normal file
BIN
sounds/nature/rain.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/nature/thunder_high.ogg
LFS
Normal file
BIN
sounds/nature/thunder_high.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/nature/thunder_low.ogg
LFS
Normal file
BIN
sounds/nature/thunder_low.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/nature/thunder_medium.ogg
LFS
Normal file
BIN
sounds/nature/thunder_medium.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/nature/wind_high.ogg
LFS
Normal file
BIN
sounds/nature/wind_high.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/nature/wind_low.ogg
LFS
Normal file
BIN
sounds/nature/wind_low.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/nature/wind_medium.ogg
LFS
Normal file
BIN
sounds/nature/wind_medium.ogg
LFS
Normal file
Binary file not shown.
@@ -76,16 +76,78 @@ int perform_resident_defense() {
|
||||
int damage = 0;
|
||||
if (useSpear && storage_spears > 0) {
|
||||
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) {
|
||||
damage = random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX);
|
||||
// Slings use stones as ammo, so consume a stone
|
||||
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;
|
||||
}
|
||||
|
||||
// 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() {
|
||||
if (residents_count <= 0) return;
|
||||
|
||||
@@ -149,3 +211,53 @@ void process_daily_weapon_breakage() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +130,7 @@ void release_sling_attack(int player_x) {
|
||||
int search_direction = (facing == 1) ? 1 : -1;
|
||||
int target_x = -1;
|
||||
bool hit_bandit = false;
|
||||
bool hit_flying_creature = false;
|
||||
|
||||
// Priority: Find nearest enemy (bandit or zombie) first
|
||||
for (int dist = 1; dist <= SLING_RANGE; dist++) {
|
||||
@@ -151,6 +152,14 @@ void release_sling_attack(int player_x) {
|
||||
hit_bandit = false;
|
||||
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)
|
||||
@@ -161,7 +170,7 @@ void release_sling_attack(int player_x) {
|
||||
Tree@ tree = get_tree_at(check_x);
|
||||
if (tree != null && !tree.is_chopped) {
|
||||
// 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);
|
||||
return;
|
||||
}
|
||||
@@ -179,11 +188,15 @@ void release_sling_attack(int player_x) {
|
||||
// Damage the correct enemy type
|
||||
if (hit_bandit) {
|
||||
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);
|
||||
} 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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,22 @@ const int SPEAR_DAMAGE = 3;
|
||||
const int AXE_DAMAGE = 4;
|
||||
const int SLING_DAMAGE_MIN = 5;
|
||||
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
|
||||
const int ZOMBIE_HEALTH = 12;
|
||||
@@ -68,6 +83,11 @@ const int STABLE_STONE_COST = 15;
|
||||
const int STABLE_VINE_COST = 10;
|
||||
const int ALTAR_STONE_COST = 9;
|
||||
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
|
||||
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 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 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 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
|
||||
const int MOUNTAIN_SIZE = 60;
|
||||
@@ -126,8 +152,25 @@ const int QUEST_STONE_SCORE = 6;
|
||||
const int QUEST_LOG_SCORE = 10;
|
||||
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_SPEAR_DAMAGE = 2;
|
||||
const int RESIDENT_SLING_DAMAGE_MIN = 3;
|
||||
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;
|
||||
|
||||
@@ -11,12 +11,8 @@ void run_crafting_menu() {
|
||||
screen_reader_speak("Crafting menu.", true);
|
||||
|
||||
int selection = 0;
|
||||
string[] categories = {"Weapons", "Tools", "Clothing", "Buildings", "Barricade"};
|
||||
int[] category_types = {0, 1, 2, 3, 4};
|
||||
if (world_altars.length() > 0) {
|
||||
categories.insert_last("Altar");
|
||||
category_types.insert_last(5);
|
||||
}
|
||||
string[] categories = {"Weapons", "Tools", "Materials", "Clothing", "Buildings", "Barricade"};
|
||||
int[] category_types = {0, 1, 2, 3, 4, 5};
|
||||
|
||||
while(true) {
|
||||
wait(5);
|
||||
@@ -42,10 +38,10 @@ void run_crafting_menu() {
|
||||
int category = category_types[selection];
|
||||
if (category == 0) run_weapons_menu();
|
||||
else if (category == 1) run_tools_menu();
|
||||
else if (category == 2) run_clothing_menu();
|
||||
else if (category == 3) run_buildings_menu();
|
||||
else if (category == 4) run_barricade_menu();
|
||||
else if (category == 5) run_altar_menu();
|
||||
else if (category == 2) run_materials_menu();
|
||||
else if (category == 3) run_clothing_menu();
|
||||
else if (category == 4) run_buildings_menu();
|
||||
else if (category == 5) run_barricade_menu();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -99,8 +95,7 @@ void run_tools_menu() {
|
||||
"Fishing Pole (1 Stick, 2 Vines)",
|
||||
"Rope (3 Vines)",
|
||||
"Reed Basket (3 Reeds)",
|
||||
"Clay Pot (3 Clay)",
|
||||
"Butcher Small Game (1 Small Game) [Requires Knife and Fire nearby]"
|
||||
"Clay Pot (3 Clay)"
|
||||
};
|
||||
|
||||
while(true) {
|
||||
@@ -131,12 +126,47 @@ void run_tools_menu() {
|
||||
else if (selection == 4) craft_rope();
|
||||
else if (selection == 5) craft_reed_basket();
|
||||
else if (selection == 6) craft_clay_pot();
|
||||
else if (selection == 7) butcher_small_game();
|
||||
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() {
|
||||
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() {
|
||||
string missing = "";
|
||||
|
||||
@@ -883,15 +940,23 @@ void butcher_small_game() {
|
||||
}
|
||||
simulate_crafting(1);
|
||||
|
||||
// Get the type of game we're butchering (first in the list)
|
||||
string game_type = inv_small_game_types[0];
|
||||
inv_small_game_types.remove_at(0);
|
||||
|
||||
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 {
|
||||
screen_reader_speak("Missing: " + missing, true);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
// Unified creature/enemy audio system
|
||||
// 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).
|
||||
// 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:
|
||||
// const float SHEEP_SOUND_VOLUME_STEP = 3.0;
|
||||
// const int SHEEP_FOOTSTEP_MAX_DISTANCE = 5;
|
||||
|
||||
327
src/creature_template.nvgt
Normal file
327
src/creature_template.nvgt
Normal 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.)
|
||||
|
||||
============================================================================ */
|
||||
@@ -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
|
||||
class Tree {
|
||||
int position;
|
||||
@@ -45,7 +76,7 @@ class Tree {
|
||||
int tree_distance = x - position;
|
||||
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)) {
|
||||
sound_handle = p.play_1d("sounds/environment/tree.ogg", x, position, true);
|
||||
if (sound_handle != -1) {
|
||||
@@ -140,10 +171,16 @@ class Tree {
|
||||
Tree@[] trees;
|
||||
|
||||
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++) {
|
||||
int distance = trees[i].position - pos;
|
||||
if (distance < 0) distance = -distance;
|
||||
if (distance <= 5) {
|
||||
if (distance < 10) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -235,6 +272,19 @@ void damage_tree(int target_x, int damage) {
|
||||
|
||||
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)
|
||||
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.
|
||||
@@ -543,23 +593,10 @@ void land_on_ground(int ground_level) {
|
||||
fall_sound_handle = -1;
|
||||
}
|
||||
|
||||
p.play_stationary("sounds/actions/hit_ground.ogg", false);
|
||||
|
||||
// Calculate fall damage
|
||||
// Calculate fall damage using centralized function (also plays hit_ground sound)
|
||||
int fall_height = fall_start_y - ground_level;
|
||||
y = ground_level;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
apply_falling_damage(fall_height);
|
||||
fall_start_y = 0;
|
||||
}
|
||||
|
||||
|
||||
71
src/flying_creature_template.nvgt
Normal file
71
src/flying_creature_template.nvgt
Normal 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);
|
||||
// }
|
||||
@@ -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_skins = 0;
|
||||
int inv_feathers = 0;
|
||||
int inv_down = 0;
|
||||
int inv_incense = 0;
|
||||
|
||||
int inv_spears = 0;
|
||||
int inv_snares = 0;
|
||||
@@ -37,6 +40,10 @@ int storage_small_game = 0;
|
||||
string[] storage_small_game_types;
|
||||
int storage_meat = 0;
|
||||
int storage_skins = 0;
|
||||
int storage_feathers = 0;
|
||||
int storage_down = 0;
|
||||
int storage_incense = 0;
|
||||
|
||||
int storage_spears = 0;
|
||||
int storage_snares = 0;
|
||||
int storage_axes = 0;
|
||||
@@ -91,6 +98,9 @@ const int ITEM_SKIN_POUCHES = 20;
|
||||
const int ITEM_ROPES = 21;
|
||||
const int ITEM_REED_BASKETS = 22;
|
||||
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 GLOVES_MAX_HEALTH_BONUS = 1;
|
||||
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_MEAT) return inv_meat;
|
||||
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_SLINGS) return inv_slings;
|
||||
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_MEAT) return storage_meat;
|
||||
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_SLINGS) return storage_slings;
|
||||
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_MEAT) return "meat";
|
||||
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_SLINGS) return "slings";
|
||||
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_MEAT) return "meat";
|
||||
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_SLINGS) return "sling";
|
||||
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_MEAT) 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_SLINGS) return 2.00;
|
||||
if (item_type == ITEM_AXES) return 1.50;
|
||||
|
||||
@@ -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_MEAT) { inv_meat -= amount; storage_meat += 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_SLINGS) { inv_slings -= amount; storage_slings += 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_MEAT) { storage_meat -= amount; inv_meat += 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_SLINGS) { storage_slings -= amount; inv_slings += 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_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_SLINGS) inv_slings--;
|
||||
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("Meat: " + inv_meat); item_types.insert_last(ITEM_MEAT);
|
||||
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("Slings: " + inv_slings); item_types.insert_last(ITEM_SLINGS);
|
||||
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("Meat: " + storage_meat); item_types.insert_last(ITEM_MEAT);
|
||||
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("Slings: " + storage_slings); item_types.insert_last(ITEM_SLINGS);
|
||||
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_small_game + " small game, ";
|
||||
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 += "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);
|
||||
@@ -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) {
|
||||
screen_reader_speak("No altar built.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
run_altar_menu();
|
||||
}
|
||||
|
||||
void run_altar_menu() {
|
||||
screen_reader_speak("Altar. Favor " + format_favor(favor) + ".", true);
|
||||
|
||||
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() {
|
||||
if (key_pressed(KEY_E)) {
|
||||
// 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) {
|
||||
wait(5);
|
||||
menu_background_tick();
|
||||
@@ -708,6 +764,8 @@ void run_action_menu(int x) {
|
||||
try_feed_fire_vine(nearby_fire);
|
||||
} else if (action == 3) {
|
||||
try_feed_fire_log(nearby_fire);
|
||||
} else if (action == 4) {
|
||||
try_burn_incense();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
// Notification System
|
||||
string[] notification_history;
|
||||
const int MAX_NOTIFICATIONS = 10;
|
||||
const int NOTIFICATION_DELAY = 3000; // 3 seconds between notifications
|
||||
int current_notification_index = -1;
|
||||
string[] notification_queue;
|
||||
int[] notification_sound_handles;
|
||||
timer notification_timer;
|
||||
bool notification_active = false;
|
||||
int notification_sound_handle = -1;
|
||||
|
||||
void notify(string message) {
|
||||
// Play notification sound
|
||||
int sound_handle = p.play_stationary("sounds/notify.ogg", false);
|
||||
// Add to queue (don't play yet)
|
||||
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);
|
||||
|
||||
// Keep only last 10 notifications
|
||||
@@ -25,17 +26,35 @@ void notify(string message) {
|
||||
|
||||
void update_notifications() {
|
||||
if (notification_queue.length() == 0) {
|
||||
notification_active = false;
|
||||
notification_sound_handle = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
int sound_handle = notification_sound_handles[0];
|
||||
if (sound_handle != -1 && p.sound_is_playing(sound_handle)) {
|
||||
// If a notification is currently active, wait for delay
|
||||
if (notification_active && notification_timer.elapsed < NOTIFICATION_DELAY) {
|
||||
return;
|
||||
}
|
||||
|
||||
screen_reader_speak(notification_queue[0], true);
|
||||
notification_queue.remove_at(0);
|
||||
notification_sound_handles.remove_at(0);
|
||||
// If we're waiting for the notification sound to finish playing
|
||||
if (notification_sound_handle != -1) {
|
||||
// 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() {
|
||||
|
||||
@@ -36,6 +36,8 @@ int last_sling_stage = -1; // Track which stage we're in to avoid duplicate soun
|
||||
|
||||
// Favor system
|
||||
double favor = 0.0;
|
||||
int incense_hours_remaining = 0;
|
||||
bool incense_burning = false;
|
||||
bool blessing_speed_active = false;
|
||||
timer blessing_speed_timer;
|
||||
|
||||
|
||||
@@ -145,7 +145,9 @@ void clear_world_objects() {
|
||||
|
||||
clear_zombies();
|
||||
clear_bandits();
|
||||
clear_flying_creatures();
|
||||
clear_mountains();
|
||||
clear_world_drops();
|
||||
}
|
||||
|
||||
void reset_game_state() {
|
||||
@@ -174,6 +176,8 @@ void reset_game_state() {
|
||||
base_max_health = 10;
|
||||
max_health = 10;
|
||||
favor = 0.0;
|
||||
incense_hours_remaining = 0;
|
||||
incense_burning = false;
|
||||
blessing_speed_active = false;
|
||||
|
||||
inv_stones = 0;
|
||||
@@ -186,6 +190,9 @@ void reset_game_state() {
|
||||
inv_small_game_types.resize(0);
|
||||
inv_meat = 0;
|
||||
inv_skins = 0;
|
||||
inv_feathers = 0;
|
||||
inv_down = 0;
|
||||
inv_incense = 0;
|
||||
inv_spears = 0;
|
||||
inv_snares = 0;
|
||||
inv_axes = 0;
|
||||
@@ -211,6 +218,9 @@ void reset_game_state() {
|
||||
storage_small_game_types.resize(0);
|
||||
storage_meat = 0;
|
||||
storage_skins = 0;
|
||||
storage_feathers = 0;
|
||||
storage_down = 0;
|
||||
storage_incense = 0;
|
||||
storage_spears = 0;
|
||||
storage_snares = 0;
|
||||
storage_axes = 0;
|
||||
@@ -283,6 +293,7 @@ void start_new_game() {
|
||||
spawn_trees(5, 19);
|
||||
init_barricade();
|
||||
init_time();
|
||||
init_weather();
|
||||
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_max_health", value)) max_health = 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, "world_map_size", value)) MAP_SIZE = 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_meat", value)) inv_meat = 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_snares", value)) inv_snares = 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 && 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) {
|
||||
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_max_health", max_health);
|
||||
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_sticks", inv_sticks);
|
||||
@@ -528,6 +547,9 @@ bool save_game_state() {
|
||||
saveData.set("inventory_small_game", inv_small_game);
|
||||
saveData.set("inventory_meat", inv_meat);
|
||||
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_snares", inv_snares);
|
||||
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_meat", storage_meat);
|
||||
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_snares", storage_snares);
|
||||
saveData.set("storage_axes", storage_axes);
|
||||
@@ -605,6 +630,8 @@ bool save_game_state() {
|
||||
}
|
||||
saveData.set("quest_queue", join_string_array(questData));
|
||||
|
||||
saveData.set("weather_data", serialize_weather());
|
||||
|
||||
saveData.set("world_map_size", MAP_SIZE);
|
||||
saveData.set("world_expanded_area_start", expanded_area_start);
|
||||
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));
|
||||
|
||||
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 encryptedData = encrypt_save_data(rawData);
|
||||
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));
|
||||
base_max_health = int(get_number(saveData, "player_base_health", max_health));
|
||||
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 >= 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_meat = int(get_number(saveData, "inventory_meat", 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_snares = int(get_number(saveData, "inventory_snares", 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_meat = int(get_number(saveData, "storage_meat", 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_snares = int(get_number(saveData, "storage_snares", 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);
|
||||
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");
|
||||
for (uint i = 0; i < treeData.length(); i++) {
|
||||
string[]@ parts = treeData[i].split("|");
|
||||
@@ -1061,6 +1110,13 @@ bool load_game_state() {
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -104,9 +104,15 @@ void expand_regular_area() {
|
||||
|
||||
notify("A " + width_desc + " stream flows through the new area at x " + actual_start + ".");
|
||||
} else {
|
||||
int tree_pos = random(new_start, new_end);
|
||||
Tree@ t = Tree(tree_pos);
|
||||
trees.insert_last(t);
|
||||
// Try to place a tree with proper spacing
|
||||
for (int attempt = 0; attempt < 20; attempt++) {
|
||||
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;
|
||||
@@ -210,16 +216,49 @@ void attempt_resident_recruitment() {
|
||||
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);
|
||||
if (roll > chance) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
string join_message = (added == 1) ? "A survivor joins your base." : "" + added + " survivors join your base.";
|
||||
notify(join_message);
|
||||
|
||||
// Notify if base is now full
|
||||
if (residents_count >= MAX_RESIDENTS) {
|
||||
notify("Your base is at maximum capacity.");
|
||||
}
|
||||
}
|
||||
|
||||
void end_invasion() {
|
||||
@@ -369,14 +408,34 @@ void update_time() {
|
||||
}
|
||||
attempt_daily_invasion();
|
||||
keep_base_fires_fed();
|
||||
update_incense_burning();
|
||||
attempt_hourly_flying_creature_spawn();
|
||||
check_scheduled_invasion();
|
||||
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_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() {
|
||||
if (key_pressed(KEY_T)) {
|
||||
screen_reader_speak(get_time_string(), true);
|
||||
|
||||
@@ -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 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");
|
||||
return result;
|
||||
}
|
||||
|
||||
492
src/weather.nvgt
Normal file
492
src/weather.nvgt
Normal 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);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ int residents_count = 0;
|
||||
|
||||
string[] zombie_sounds = {"sounds/enemies/zombie1.ogg"};
|
||||
string[] bandit_sounds = {"sounds/enemies/bandit1.ogg", "sounds/enemies/bandit2.ogg"};
|
||||
string[] goose_sounds = {"sounds/game/goose.ogg"};
|
||||
|
||||
class Zombie {
|
||||
int position;
|
||||
@@ -82,11 +83,174 @@ class Bandit {
|
||||
}
|
||||
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() {
|
||||
int index = random(0, small_game_types.length() - 1);
|
||||
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 {
|
||||
int position;
|
||||
bool has_catch;
|
||||
@@ -843,9 +1007,9 @@ bool try_attack_player_bandit(Bandit@ bandit) {
|
||||
|
||||
// Play weapon swing sound based on bandit's weapon
|
||||
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") {
|
||||
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);
|
||||
@@ -856,9 +1020,9 @@ bool try_attack_player_bandit(Bandit@ bandit) {
|
||||
|
||||
// Play hit sound
|
||||
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") {
|
||||
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;
|
||||
@@ -877,11 +1041,11 @@ void try_attack_barricade_bandit(Bandit@ bandit) {
|
||||
|
||||
// Play weapon swing sound
|
||||
if (bandit.weapon_type == "spear") {
|
||||
p.play_stationary("sounds/weapons/spear_swing.ogg", false);
|
||||
p.play_stationary("sounds/weapons/spear_hit.ogg", false);
|
||||
play_creature_attack_sound("sounds/weapons/spear_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
||||
play_creature_attack_sound("sounds/weapons/spear_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
||||
} else if (bandit.weapon_type == "axe") {
|
||||
p.play_stationary("sounds/weapons/axe_swing.ogg", false);
|
||||
p.play_stationary("sounds/weapons/axe_hit.ogg", false);
|
||||
play_creature_attack_sound("sounds/weapons/axe_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
||||
play_creature_attack_sound("sounds/weapons/axe_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
||||
}
|
||||
|
||||
// Resident defense counter-attack
|
||||
@@ -1261,3 +1425,327 @@ void clear_mountains() {
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user