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
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
+114
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+46
-3
@@ -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;
|
||||
|
||||
+83
-18
@@ -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;
|
||||
|
||||
@@ -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.)
|
||||
|
||||
============================================================================ */
|
||||
+54
-17
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+29
-10
@@ -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;
|
||||
}
|
||||
|
||||
+63
-4
@@ -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);
|
||||
|
||||
+2
-1
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
+496
-8
@@ -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