Weather system added, mostly decoration. Some tweaks to residents. Moved altar to its own menu, s for sacrifice. You can no longer burn incense outside the base.
This commit is contained in:
BIN
sounds/game/game_falls.ogg
LFS
Normal file
BIN
sounds/game/game_falls.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/game/goose.ogg
LFS
Normal file
BIN
sounds/game/goose.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/items/item.ogg
LFS
Normal file
BIN
sounds/items/item.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/items/miscellaneous.ogg
LFS
BIN
sounds/items/miscellaneous.ogg
LFS
Binary file not shown.
BIN
sounds/nature/rain.ogg
LFS
Normal file
BIN
sounds/nature/rain.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/nature/thunder_high.ogg
LFS
Normal file
BIN
sounds/nature/thunder_high.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/nature/thunder_low.ogg
LFS
Normal file
BIN
sounds/nature/thunder_low.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/nature/thunder_medium.ogg
LFS
Normal file
BIN
sounds/nature/thunder_medium.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/nature/wind_high.ogg
LFS
Normal file
BIN
sounds/nature/wind_high.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/nature/wind_low.ogg
LFS
Normal file
BIN
sounds/nature/wind_low.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/nature/wind_medium.ogg
LFS
Normal file
BIN
sounds/nature/wind_medium.ogg
LFS
Normal file
Binary file not shown.
@@ -76,16 +76,78 @@ int perform_resident_defense() {
|
|||||||
int damage = 0;
|
int damage = 0;
|
||||||
if (useSpear && storage_spears > 0) {
|
if (useSpear && storage_spears > 0) {
|
||||||
damage = RESIDENT_SPEAR_DAMAGE;
|
damage = RESIDENT_SPEAR_DAMAGE;
|
||||||
play_1d_with_volume_step("sounds/weapons/spear_swing.ogg", x, BASE_END + 1, false, 3.0);
|
// Weapons don't get consumed on use - they break via daily breakage check
|
||||||
|
// Just play the sound
|
||||||
|
play_1d_with_volume_step("sounds/weapons/spear_swing.ogg", x, BASE_END + 1, false, RESIDENT_DEFENSE_VOLUME_STEP);
|
||||||
} else if (storage_slings > 0 && storage_stones > 0) {
|
} else if (storage_slings > 0 && storage_stones > 0) {
|
||||||
damage = random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX);
|
damage = random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX);
|
||||||
|
// Slings use stones as ammo, so consume a stone
|
||||||
storage_stones--;
|
storage_stones--;
|
||||||
play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", x, BASE_END + 1, false, 3.0);
|
play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", x, BASE_END + 1, false, RESIDENT_DEFENSE_VOLUME_STEP);
|
||||||
}
|
}
|
||||||
|
|
||||||
return damage;
|
return damage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Proactive resident sling defense
|
||||||
|
timer resident_sling_timer;
|
||||||
|
const int RESIDENT_SLING_COOLDOWN = 4000; // 4 seconds between shots
|
||||||
|
|
||||||
|
void attempt_resident_sling_defense() {
|
||||||
|
// Only if residents exist and have slings with stones
|
||||||
|
if (residents_count <= 0) return;
|
||||||
|
if (storage_slings <= 0 || storage_stones <= 0) return;
|
||||||
|
|
||||||
|
// Cooldown between shots
|
||||||
|
if (resident_sling_timer.elapsed < RESIDENT_SLING_COOLDOWN) return;
|
||||||
|
|
||||||
|
// Find nearest enemy within sling range
|
||||||
|
int nearestDistance = SLING_RANGE + 1;
|
||||||
|
int targetPos = -1;
|
||||||
|
bool targetIsBandit = false;
|
||||||
|
|
||||||
|
int sling_origin = BASE_END;
|
||||||
|
|
||||||
|
// Check zombies
|
||||||
|
for (uint i = 0; i < zombies.length(); i++) {
|
||||||
|
int dist = abs(zombies[i].position - sling_origin);
|
||||||
|
if (dist > 0 && dist <= SLING_RANGE && dist < nearestDistance) {
|
||||||
|
nearestDistance = dist;
|
||||||
|
targetPos = zombies[i].position;
|
||||||
|
targetIsBandit = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check bandits
|
||||||
|
for (uint i = 0; i < bandits.length(); i++) {
|
||||||
|
int dist = abs(bandits[i].position - sling_origin);
|
||||||
|
if (dist > 0 && dist <= SLING_RANGE && dist < nearestDistance) {
|
||||||
|
nearestDistance = dist;
|
||||||
|
targetPos = bandits[i].position;
|
||||||
|
targetIsBandit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No targets in range
|
||||||
|
if (targetPos == -1) return;
|
||||||
|
|
||||||
|
// Shoot!
|
||||||
|
resident_sling_timer.restart();
|
||||||
|
storage_stones--;
|
||||||
|
|
||||||
|
int damage = random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX);
|
||||||
|
play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", x, targetPos, false, RESIDENT_DEFENSE_VOLUME_STEP);
|
||||||
|
|
||||||
|
if (targetIsBandit) {
|
||||||
|
damage_bandit_at(targetPos, damage);
|
||||||
|
} else {
|
||||||
|
damage_zombie_at(targetPos, damage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play hit sound on enemy
|
||||||
|
play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", x, targetPos, ZOMBIE_SOUND_VOLUME_STEP);
|
||||||
|
}
|
||||||
|
|
||||||
void process_daily_weapon_breakage() {
|
void process_daily_weapon_breakage() {
|
||||||
if (residents_count <= 0) return;
|
if (residents_count <= 0) return;
|
||||||
|
|
||||||
@@ -149,3 +211,53 @@ void process_daily_weapon_breakage() {
|
|||||||
notify(msg);
|
notify(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resident resource collection
|
||||||
|
const int RESIDENT_COLLECTION_CHANCE = 10; // 10% chance per basket per hour
|
||||||
|
|
||||||
|
void attempt_resident_collection() {
|
||||||
|
// Only during daytime
|
||||||
|
if (!is_daytime) return;
|
||||||
|
|
||||||
|
// Need residents
|
||||||
|
if (residents_count <= 0) return;
|
||||||
|
|
||||||
|
// Need baskets in storage to enable collection
|
||||||
|
if (storage_reed_baskets <= 0) return;
|
||||||
|
|
||||||
|
// Number of residents who can collect = min(residents, baskets)
|
||||||
|
int active_collectors = (residents_count < storage_reed_baskets) ? residents_count : storage_reed_baskets;
|
||||||
|
|
||||||
|
// Each active collector has a 10% chance to collect something
|
||||||
|
for (int i = 0; i < active_collectors; i++) {
|
||||||
|
if (random(1, 100) > RESIDENT_COLLECTION_CHANCE) continue;
|
||||||
|
|
||||||
|
// Determine what to collect (weighted random)
|
||||||
|
// Sticks and vines more common, logs and stones less common
|
||||||
|
int roll = random(1, 100);
|
||||||
|
string item_name = "";
|
||||||
|
|
||||||
|
if (roll <= 40) {
|
||||||
|
// 40% chance - stick
|
||||||
|
storage_sticks++;
|
||||||
|
item_name = "stick";
|
||||||
|
} else if (roll <= 70) {
|
||||||
|
// 30% chance - vine
|
||||||
|
storage_vines++;
|
||||||
|
item_name = "vine";
|
||||||
|
} else if (roll <= 85) {
|
||||||
|
// 15% chance - stone
|
||||||
|
storage_stones++;
|
||||||
|
item_name = "stone";
|
||||||
|
} else {
|
||||||
|
// 15% chance - log
|
||||||
|
storage_logs++;
|
||||||
|
item_name = "log";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Announce only if player is in base
|
||||||
|
if (x <= BASE_END) {
|
||||||
|
screen_reader_speak("Resident added " + item_name + " to storage.", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ void release_sling_attack(int player_x) {
|
|||||||
int search_direction = (facing == 1) ? 1 : -1;
|
int search_direction = (facing == 1) ? 1 : -1;
|
||||||
int target_x = -1;
|
int target_x = -1;
|
||||||
bool hit_bandit = false;
|
bool hit_bandit = false;
|
||||||
|
bool hit_flying_creature = false;
|
||||||
|
|
||||||
// Priority: Find nearest enemy (bandit or zombie) first
|
// Priority: Find nearest enemy (bandit or zombie) first
|
||||||
for (int dist = 1; dist <= SLING_RANGE; dist++) {
|
for (int dist = 1; dist <= SLING_RANGE; dist++) {
|
||||||
@@ -151,6 +152,14 @@ void release_sling_attack(int player_x) {
|
|||||||
hit_bandit = false;
|
hit_bandit = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Then check for flying creature (only if flying)
|
||||||
|
FlyingCreature@ creature = get_flying_creature_at(check_x);
|
||||||
|
if (creature != null && creature.state == "flying") {
|
||||||
|
target_x = check_x;
|
||||||
|
hit_flying_creature = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no enemy found, check for trees (but don't damage them)
|
// If no enemy found, check for trees (but don't damage them)
|
||||||
@@ -161,7 +170,7 @@ void release_sling_attack(int player_x) {
|
|||||||
Tree@ tree = get_tree_at(check_x);
|
Tree@ tree = get_tree_at(check_x);
|
||||||
if (tree != null && !tree.is_chopped) {
|
if (tree != null && !tree.is_chopped) {
|
||||||
// Stone hits tree but doesn't damage it
|
// Stone hits tree but doesn't damage it
|
||||||
p.play_1d("sounds/weapons/sling_hit.ogg", player_x, check_x, false);
|
play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, check_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP);
|
||||||
screen_reader_speak("Stone hit tree at " + check_x + ".", true);
|
screen_reader_speak("Stone hit tree at " + check_x + ".", true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -179,11 +188,15 @@ void release_sling_attack(int player_x) {
|
|||||||
// Damage the correct enemy type
|
// Damage the correct enemy type
|
||||||
if (hit_bandit) {
|
if (hit_bandit) {
|
||||||
damage_bandit_at(target_x, damage);
|
damage_bandit_at(target_x, damage);
|
||||||
p.play_1d("sounds/weapons/sling_hit.ogg", player_x, target_x, false);
|
play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP);
|
||||||
play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", player_x, target_x, BANDIT_SOUND_VOLUME_STEP);
|
play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", player_x, target_x, BANDIT_SOUND_VOLUME_STEP);
|
||||||
|
} else if (hit_flying_creature) {
|
||||||
|
damage_flying_creature_at(target_x, damage);
|
||||||
|
play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP);
|
||||||
|
// Falling sound handled by damage_flying_creature_at
|
||||||
} else {
|
} else {
|
||||||
damage_zombie_at(target_x, damage);
|
damage_zombie_at(target_x, damage);
|
||||||
p.play_1d("sounds/weapons/sling_hit.ogg", player_x, target_x, false);
|
play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP);
|
||||||
play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", player_x, target_x, ZOMBIE_SOUND_VOLUME_STEP);
|
play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", player_x, target_x, ZOMBIE_SOUND_VOLUME_STEP);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,22 @@ const int SPEAR_DAMAGE = 3;
|
|||||||
const int AXE_DAMAGE = 4;
|
const int AXE_DAMAGE = 4;
|
||||||
const int SLING_DAMAGE_MIN = 5;
|
const int SLING_DAMAGE_MIN = 5;
|
||||||
const int SLING_DAMAGE_MAX = 8;
|
const int SLING_DAMAGE_MAX = 8;
|
||||||
const int SLING_RANGE = 7;
|
const int SLING_RANGE = 8;
|
||||||
|
|
||||||
|
// Bow settings (for future implementation)
|
||||||
|
// Option 1: Longer range, similar damage
|
||||||
|
// const int BOW_DAMAGE_MIN = 6;
|
||||||
|
// const int BOW_DAMAGE_MAX = 9;
|
||||||
|
// const int BOW_RANGE = 12; // 50% more range than sling
|
||||||
|
//
|
||||||
|
// Option 2: Much longer range, slightly more damage
|
||||||
|
// const int BOW_DAMAGE_MIN = 7;
|
||||||
|
// const int BOW_DAMAGE_MAX = 10;
|
||||||
|
// const int BOW_RANGE = 15; // Nearly double sling range
|
||||||
|
//
|
||||||
|
// Recommendation: Bows should have BOTH more range AND more damage than slings
|
||||||
|
// to justify the likely higher resource cost and complexity to craft.
|
||||||
|
// Suggested balance: BOW_RANGE = 12, damage 6-9 (average 7.5 vs sling's 6.5)
|
||||||
|
|
||||||
// Zombie settings
|
// Zombie settings
|
||||||
const int ZOMBIE_HEALTH = 12;
|
const int ZOMBIE_HEALTH = 12;
|
||||||
@@ -68,6 +83,11 @@ const int STABLE_STONE_COST = 15;
|
|||||||
const int STABLE_VINE_COST = 10;
|
const int STABLE_VINE_COST = 10;
|
||||||
const int ALTAR_STONE_COST = 9;
|
const int ALTAR_STONE_COST = 9;
|
||||||
const int ALTAR_STICK_COST = 3;
|
const int ALTAR_STICK_COST = 3;
|
||||||
|
const int INCENSE_STICK_COST = 6;
|
||||||
|
const int INCENSE_VINE_COST = 2;
|
||||||
|
const int INCENSE_REED_COST = 1;
|
||||||
|
const int INCENSE_HOURS_PER_STICK = 4;
|
||||||
|
const double INCENSE_FAVOR_PER_HOUR = 0.3;
|
||||||
|
|
||||||
// Bandit settings
|
// Bandit settings
|
||||||
const int BANDIT_HEALTH = 4;
|
const int BANDIT_HEALTH = 4;
|
||||||
@@ -99,7 +119,7 @@ const float SNARE_SOUND_PAN_STEP = 4.0; // Stronger pan for direction
|
|||||||
const int SNARE_COLLECT_RANGE = 1;
|
const int SNARE_COLLECT_RANGE = 1;
|
||||||
|
|
||||||
const int FIRE_SOUND_RANGE = 3;
|
const int FIRE_SOUND_RANGE = 3;
|
||||||
const float FIRE_SOUND_VOLUME_STEP = 5.0; // 30 dB over 6 tiles
|
const float FIRE_SOUND_VOLUME_STEP = 5.0; // 15 dB over 3 tiles (FIRE_SOUND_RANGE)
|
||||||
|
|
||||||
const int FIREPIT_SOUND_RANGE = 5;
|
const int FIREPIT_SOUND_RANGE = 5;
|
||||||
const float FIREPIT_SOUND_VOLUME_STEP = 6.0; // 30 dB over 5 tiles
|
const float FIREPIT_SOUND_VOLUME_STEP = 6.0; // 30 dB over 5 tiles
|
||||||
@@ -108,6 +128,12 @@ const int STREAM_SOUND_RANGE = 7;
|
|||||||
const float STREAM_SOUND_VOLUME_STEP = 4.3; // 30 dB over 7 tiles
|
const float STREAM_SOUND_VOLUME_STEP = 4.3; // 30 dB over 7 tiles
|
||||||
|
|
||||||
const float TREE_SOUND_VOLUME_STEP = 4.0; // Similar to snares for good audibility
|
const float TREE_SOUND_VOLUME_STEP = 4.0; // Similar to snares for good audibility
|
||||||
|
const int TREE_SOUND_RANGE = 4;
|
||||||
|
|
||||||
|
const float RESIDENT_DEFENSE_VOLUME_STEP = 3.0; // Default volume for resident counter-attacks
|
||||||
|
const float PLAYER_WEAPON_SOUND_VOLUME_STEP = 3.0;
|
||||||
|
const int FLYING_CREATURE_FADE_OUT_DURATION = 1500; // ms
|
||||||
|
const float FLYING_CREATURE_FADE_OUT_MIN_VOLUME = -40.0; // dB
|
||||||
|
|
||||||
// Mountain configuration
|
// Mountain configuration
|
||||||
const int MOUNTAIN_SIZE = 60;
|
const int MOUNTAIN_SIZE = 60;
|
||||||
@@ -126,8 +152,25 @@ const int QUEST_STONE_SCORE = 6;
|
|||||||
const int QUEST_LOG_SCORE = 10;
|
const int QUEST_LOG_SCORE = 10;
|
||||||
const int QUEST_SKIN_SCORE = 14;
|
const int QUEST_SKIN_SCORE = 14;
|
||||||
|
|
||||||
// Resident defense settings
|
// Resident settings
|
||||||
|
const int MAX_RESIDENTS = 4; // Max residents per base (+ player = 5 total)
|
||||||
const int RESIDENT_WEAPON_BREAK_CHANCE = 10;
|
const int RESIDENT_WEAPON_BREAK_CHANCE = 10;
|
||||||
const int RESIDENT_SPEAR_DAMAGE = 2;
|
const int RESIDENT_SPEAR_DAMAGE = 2;
|
||||||
const int RESIDENT_SLING_DAMAGE_MIN = 3;
|
const int RESIDENT_SLING_DAMAGE_MIN = 3;
|
||||||
const int RESIDENT_SLING_DAMAGE_MAX = 5;
|
const int RESIDENT_SLING_DAMAGE_MAX = 5;
|
||||||
|
|
||||||
|
// Goose settings
|
||||||
|
const int GOOSE_HEALTH = 1;
|
||||||
|
const int GOOSE_MOVE_INTERVAL_MIN = 800; // Faster movement
|
||||||
|
const int GOOSE_MOVE_INTERVAL_MAX = 2000;
|
||||||
|
const int GOOSE_FLYING_HEIGHT_MIN = 10;
|
||||||
|
const int GOOSE_FLYING_HEIGHT_MAX = 30;
|
||||||
|
const float GOOSE_SOUND_VOLUME_STEP = 3.0;
|
||||||
|
const int GOOSE_FLIGHT_SOUND_DELAY_MIN = 2000; // Honk more often
|
||||||
|
const int GOOSE_FLIGHT_SOUND_DELAY_MAX = 5000;
|
||||||
|
const int GOOSE_FALL_SPEED = 100; // ms per foot
|
||||||
|
const int GOOSE_FLY_AWAY_CHANCE = 0; // Chance out of 1000 per tick to fly away
|
||||||
|
const int GOOSE_MAX_DIST_FROM_WATER = 4; // How far they can wander from water
|
||||||
|
const int GOOSE_MAX_COUNT = 3;
|
||||||
|
const int GOOSE_HOURLY_SPAWN_CHANCE = 35; // Percent chance per hour to spawn a goose
|
||||||
|
const int GOOSE_SIGHT_RANGE = 0;
|
||||||
|
|||||||
@@ -11,12 +11,8 @@ void run_crafting_menu() {
|
|||||||
screen_reader_speak("Crafting menu.", true);
|
screen_reader_speak("Crafting menu.", true);
|
||||||
|
|
||||||
int selection = 0;
|
int selection = 0;
|
||||||
string[] categories = {"Weapons", "Tools", "Clothing", "Buildings", "Barricade"};
|
string[] categories = {"Weapons", "Tools", "Materials", "Clothing", "Buildings", "Barricade"};
|
||||||
int[] category_types = {0, 1, 2, 3, 4};
|
int[] category_types = {0, 1, 2, 3, 4, 5};
|
||||||
if (world_altars.length() > 0) {
|
|
||||||
categories.insert_last("Altar");
|
|
||||||
category_types.insert_last(5);
|
|
||||||
}
|
|
||||||
|
|
||||||
while(true) {
|
while(true) {
|
||||||
wait(5);
|
wait(5);
|
||||||
@@ -42,10 +38,10 @@ void run_crafting_menu() {
|
|||||||
int category = category_types[selection];
|
int category = category_types[selection];
|
||||||
if (category == 0) run_weapons_menu();
|
if (category == 0) run_weapons_menu();
|
||||||
else if (category == 1) run_tools_menu();
|
else if (category == 1) run_tools_menu();
|
||||||
else if (category == 2) run_clothing_menu();
|
else if (category == 2) run_materials_menu();
|
||||||
else if (category == 3) run_buildings_menu();
|
else if (category == 3) run_clothing_menu();
|
||||||
else if (category == 4) run_barricade_menu();
|
else if (category == 4) run_buildings_menu();
|
||||||
else if (category == 5) run_altar_menu();
|
else if (category == 5) run_barricade_menu();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,8 +95,7 @@ void run_tools_menu() {
|
|||||||
"Fishing Pole (1 Stick, 2 Vines)",
|
"Fishing Pole (1 Stick, 2 Vines)",
|
||||||
"Rope (3 Vines)",
|
"Rope (3 Vines)",
|
||||||
"Reed Basket (3 Reeds)",
|
"Reed Basket (3 Reeds)",
|
||||||
"Clay Pot (3 Clay)",
|
"Clay Pot (3 Clay)"
|
||||||
"Butcher Small Game (1 Small Game) [Requires Knife and Fire nearby]"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
while(true) {
|
while(true) {
|
||||||
@@ -131,12 +126,47 @@ void run_tools_menu() {
|
|||||||
else if (selection == 4) craft_rope();
|
else if (selection == 4) craft_rope();
|
||||||
else if (selection == 5) craft_reed_basket();
|
else if (selection == 5) craft_reed_basket();
|
||||||
else if (selection == 6) craft_clay_pot();
|
else if (selection == 6) craft_clay_pot();
|
||||||
else if (selection == 7) butcher_small_game();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void run_materials_menu() {
|
||||||
|
screen_reader_speak("Materials.", true);
|
||||||
|
|
||||||
|
int selection = 0;
|
||||||
|
string[] options = {
|
||||||
|
"Butcher Small Game (1 Small Game) [Requires Knife and Fire nearby]",
|
||||||
|
"Incense (6 Sticks, 2 Vines, 1 Reed) [Requires Altar]"
|
||||||
|
};
|
||||||
|
|
||||||
|
while(true) {
|
||||||
|
wait(5);
|
||||||
|
menu_background_tick();
|
||||||
|
if (key_pressed(KEY_ESCAPE)) {
|
||||||
|
screen_reader_speak("Closed.", true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key_pressed(KEY_DOWN)) {
|
||||||
|
selection++;
|
||||||
|
if (selection >= options.length()) selection = 0;
|
||||||
|
screen_reader_speak(options[selection], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key_pressed(KEY_UP)) {
|
||||||
|
selection--;
|
||||||
|
if (selection < 0) selection = options.length() - 1;
|
||||||
|
screen_reader_speak(options[selection], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key_pressed(KEY_RETURN)) {
|
||||||
|
if (selection == 0) butcher_small_game();
|
||||||
|
else if (selection == 1) craft_incense();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
void run_clothing_menu() {
|
void run_clothing_menu() {
|
||||||
screen_reader_speak("Clothing.", true);
|
screen_reader_speak("Clothing.", true);
|
||||||
|
|
||||||
@@ -856,6 +886,33 @@ void craft_clay_pot() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void craft_incense() {
|
||||||
|
if (world_altars.length() == 0) {
|
||||||
|
screen_reader_speak("You need an altar to craft incense.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string missing = "";
|
||||||
|
if (inv_sticks < INCENSE_STICK_COST) missing += INCENSE_STICK_COST + " sticks ";
|
||||||
|
if (inv_vines < INCENSE_VINE_COST) missing += INCENSE_VINE_COST + " vines ";
|
||||||
|
if (inv_reeds < INCENSE_REED_COST) missing += INCENSE_REED_COST + " reed ";
|
||||||
|
|
||||||
|
if (missing == "") {
|
||||||
|
if (inv_incense >= get_personal_stack_limit()) {
|
||||||
|
screen_reader_speak("You can't carry any more incense.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
simulate_crafting(INCENSE_STICK_COST + INCENSE_VINE_COST + INCENSE_REED_COST);
|
||||||
|
inv_sticks -= INCENSE_STICK_COST;
|
||||||
|
inv_vines -= INCENSE_VINE_COST;
|
||||||
|
inv_reeds -= INCENSE_REED_COST;
|
||||||
|
inv_incense++;
|
||||||
|
screen_reader_speak("Crafted incense.", true);
|
||||||
|
} else {
|
||||||
|
screen_reader_speak("Missing: " + missing, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void butcher_small_game() {
|
void butcher_small_game() {
|
||||||
string missing = "";
|
string missing = "";
|
||||||
|
|
||||||
@@ -883,15 +940,23 @@ void butcher_small_game() {
|
|||||||
}
|
}
|
||||||
simulate_crafting(1);
|
simulate_crafting(1);
|
||||||
|
|
||||||
// Get the type of game we're butchering (first in the list)
|
|
||||||
string game_type = inv_small_game_types[0];
|
string game_type = inv_small_game_types[0];
|
||||||
inv_small_game_types.remove_at(0);
|
inv_small_game_types.remove_at(0);
|
||||||
|
|
||||||
inv_small_game--;
|
inv_small_game--;
|
||||||
inv_meat++;
|
|
||||||
inv_skins++;
|
|
||||||
|
|
||||||
screen_reader_speak("Butchered " + game_type + ". Got 1 meat and 1 skin.", true);
|
if (game_type == "goose") {
|
||||||
|
inv_meat++;
|
||||||
|
inv_feathers += random(3, 6);
|
||||||
|
inv_down += random(1, 3);
|
||||||
|
screen_reader_speak("Butchered goose. Got 1 meat, feathers, and down.", true);
|
||||||
|
} else {
|
||||||
|
inv_meat++;
|
||||||
|
inv_skins++;
|
||||||
|
screen_reader_speak("Butchered " + game_type + ". Got 1 meat and 1 skin.", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play sound
|
||||||
|
p.play_stationary("sounds/items/miscellaneous.ogg", false);
|
||||||
} else {
|
} else {
|
||||||
screen_reader_speak("Missing: " + missing, true);
|
screen_reader_speak("Missing: " + missing, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
// Unified creature/enemy audio system
|
// Unified creature/enemy audio system
|
||||||
// Ensures consistent panning and distance behavior for all animated entities
|
// Ensures consistent panning and distance behavior for all animated entities
|
||||||
//
|
//
|
||||||
// USAGE GUIDE:
|
// *** IMPORTANT: See src/creature_template.nvgt for complete step-by-step guide ***
|
||||||
|
// The template file has code examples, a checklist, and enforces consistency.
|
||||||
|
//
|
||||||
|
// QUICK USAGE GUIDE:
|
||||||
// This system provides standardized audio functions for all creatures (zombies, bandits, animals).
|
// This system provides standardized audio functions for all creatures (zombies, bandits, animals).
|
||||||
// Using these functions ensures all creatures sound consistent to the player.
|
// Using these functions ensures all creatures sound consistent to the player.
|
||||||
//
|
//
|
||||||
// When adding new creatures (sheep, cattle, horses, etc):
|
// When adding new creatures (goblins, sheep, cattle, horses, etc):
|
||||||
// 1. Define creature-specific constants in constants.nvgt:
|
// 1. Define creature-specific constants in constants.nvgt:
|
||||||
// const float SHEEP_SOUND_VOLUME_STEP = 3.0;
|
// const float SHEEP_SOUND_VOLUME_STEP = 3.0;
|
||||||
// const int SHEEP_FOOTSTEP_MAX_DISTANCE = 5;
|
// const int SHEEP_FOOTSTEP_MAX_DISTANCE = 5;
|
||||||
|
|||||||
327
src/creature_template.nvgt
Normal file
327
src/creature_template.nvgt
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
// CREATURE CREATION TEMPLATE
|
||||||
|
// Use this as a guide when creating new creatures (goblins, animals, etc.)
|
||||||
|
//
|
||||||
|
// This template ensures all creatures have consistent audio behavior and required features.
|
||||||
|
// Copy the sections you need and fill in creature-specific details.
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
STEP 1: Add constants to src/constants.nvgt
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
// Example for "Goblin" creature:
|
||||||
|
/*
|
||||||
|
// Goblin Configuration
|
||||||
|
const int GOBLIN_HEALTH = 15;
|
||||||
|
const int GOBLIN_DAMAGE_MIN = 2;
|
||||||
|
const int GOBLIN_DAMAGE_MAX = 4;
|
||||||
|
const int GOBLIN_MOVE_INTERVAL = 1500;
|
||||||
|
const int GOBLIN_ATTACK_INTERVAL = 3000;
|
||||||
|
const int GOBLIN_ALERT_MIN_DELAY = 4000;
|
||||||
|
const int GOBLIN_ALERT_MAX_DELAY = 8000;
|
||||||
|
const int GOBLIN_FOOTSTEP_MAX_DISTANCE = 6;
|
||||||
|
const float GOBLIN_SOUND_VOLUME_STEP = 3.0; // Default creature volume
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
STEP 2: Define sound arrays in src/world_state.nvgt (near top with zombies/bandits)
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
// Example:
|
||||||
|
/*
|
||||||
|
string[] goblin_sounds = {
|
||||||
|
"sounds/enemies/goblin_cackle1.ogg",
|
||||||
|
"sounds/enemies/goblin_cackle2.ogg",
|
||||||
|
"sounds/enemies/goblin_cackle3.ogg"
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
STEP 3: Create creature class in src/world_state.nvgt
|
||||||
|
============================================================================
|
||||||
|
|
||||||
|
REQUIRED MEMBERS:
|
||||||
|
- int position (where the creature is on the map)
|
||||||
|
- int health (creature's HP)
|
||||||
|
- int sound_handle (CRITICAL: for tracking active voice/alert sounds)
|
||||||
|
- timer move_timer (for movement intervals)
|
||||||
|
- timer attack_timer (for attack cooldown)
|
||||||
|
- timer voice_timer (for periodic sounds - groans/alerts/cackles)
|
||||||
|
- int next_voice_delay (randomized delay between voice sounds)
|
||||||
|
- string voice_sound (which sound file to play for voice)
|
||||||
|
|
||||||
|
OPTIONAL MEMBERS (creature-specific):
|
||||||
|
- string weapon_type
|
||||||
|
- string behavior_state
|
||||||
|
- int wander_direction
|
||||||
|
- etc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Example:
|
||||||
|
/*
|
||||||
|
class Goblin {
|
||||||
|
int position;
|
||||||
|
int health;
|
||||||
|
int sound_handle; // REQUIRED: Must track for death cleanup
|
||||||
|
string voice_sound;
|
||||||
|
timer move_timer;
|
||||||
|
timer voice_timer; // Renamed from alert_timer/groan_timer for clarity
|
||||||
|
timer attack_timer;
|
||||||
|
int next_voice_delay;
|
||||||
|
|
||||||
|
// Creature-specific behavior
|
||||||
|
string weapon_type; // Example: "dagger" or "club"
|
||||||
|
|
||||||
|
Goblin(int pos) {
|
||||||
|
position = pos;
|
||||||
|
health = GOBLIN_HEALTH;
|
||||||
|
sound_handle = -1; // CRITICAL: Always initialize to -1
|
||||||
|
|
||||||
|
// Choose random voice sound
|
||||||
|
int sound_index = random(0, goblin_sounds.length() - 1);
|
||||||
|
voice_sound = goblin_sounds[sound_index];
|
||||||
|
|
||||||
|
// Choose random weapon
|
||||||
|
weapon_type = (random(0, 1) == 0) ? "dagger" : "club";
|
||||||
|
|
||||||
|
// Initialize timers
|
||||||
|
move_timer.restart();
|
||||||
|
voice_timer.restart();
|
||||||
|
attack_timer.restart();
|
||||||
|
next_voice_delay = random(GOBLIN_ALERT_MIN_DELAY, GOBLIN_ALERT_MAX_DELAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Goblin@[] goblins; // Global array to store all active goblins
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
STEP 4: Spawn function
|
||||||
|
============================================================================
|
||||||
|
|
||||||
|
REQUIRED AUDIO: Use play_creature_voice() and store the handle
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Example:
|
||||||
|
/*
|
||||||
|
void spawn_goblin() {
|
||||||
|
// Find spawn location (avoid duplicates)
|
||||||
|
int spawn_x = -1;
|
||||||
|
for (int attempts = 0; attempts < 20; attempts++) {
|
||||||
|
int candidate = random(BASE_END + 1, MAP_SIZE - 1);
|
||||||
|
if (get_goblin_at(candidate) == null) {
|
||||||
|
spawn_x = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (spawn_x == -1) {
|
||||||
|
spawn_x = random(BASE_END + 1, MAP_SIZE - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Goblin@ g = Goblin(spawn_x);
|
||||||
|
goblins.insert_last(g);
|
||||||
|
|
||||||
|
// REQUIRED: Use creature_audio system and store handle
|
||||||
|
g.sound_handle = play_creature_voice(g.voice_sound, x, spawn_x, GOBLIN_SOUND_VOLUME_STEP);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
STEP 5: Update function
|
||||||
|
============================================================================
|
||||||
|
|
||||||
|
REQUIRED AUDIO:
|
||||||
|
- Periodic voice: play_creature_voice() and store handle
|
||||||
|
- Movement: play_creature_footstep()
|
||||||
|
- Attacks: play_creature_attack_sound()
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Example:
|
||||||
|
/*
|
||||||
|
void update_goblin(Goblin@ goblin) {
|
||||||
|
// Play periodic voice sound
|
||||||
|
if (goblin.voice_timer.elapsed > goblin.next_voice_delay) {
|
||||||
|
goblin.voice_timer.restart();
|
||||||
|
goblin.next_voice_delay = random(GOBLIN_ALERT_MIN_DELAY, GOBLIN_ALERT_MAX_DELAY);
|
||||||
|
// REQUIRED: Store handle for cleanup on death
|
||||||
|
goblin.sound_handle = play_creature_voice(goblin.voice_sound, x, goblin.position, GOBLIN_SOUND_VOLUME_STEP);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to attack player
|
||||||
|
if (try_attack_player_goblin(goblin)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Movement logic
|
||||||
|
if (goblin.move_timer.elapsed < GOBLIN_MOVE_INTERVAL) return;
|
||||||
|
goblin.move_timer.restart();
|
||||||
|
|
||||||
|
// [Your movement AI here - pathfinding toward player, wandering, etc.]
|
||||||
|
|
||||||
|
// When goblin moves:
|
||||||
|
goblin.position = target_x;
|
||||||
|
// REQUIRED: Use creature_audio for footsteps
|
||||||
|
play_creature_footstep(x, goblin.position, BASE_END, GRASS_END, GOBLIN_FOOTSTEP_MAX_DISTANCE, GOBLIN_SOUND_VOLUME_STEP);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
STEP 6: Attack functions
|
||||||
|
============================================================================
|
||||||
|
|
||||||
|
REQUIRED AUDIO: Use play_creature_attack_sound() for weapon sounds
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Example:
|
||||||
|
/*
|
||||||
|
bool try_attack_player_goblin(Goblin@ goblin) {
|
||||||
|
if (player_health <= 0) return false;
|
||||||
|
if (abs(goblin.position - x) > 1) return false;
|
||||||
|
if (goblin.attack_timer.elapsed < GOBLIN_ATTACK_INTERVAL) return false;
|
||||||
|
|
||||||
|
goblin.attack_timer.restart();
|
||||||
|
|
||||||
|
// REQUIRED: Positional weapon sounds using creature_audio
|
||||||
|
if (goblin.weapon_type == "dagger") {
|
||||||
|
play_creature_attack_sound("sounds/weapons/dagger_swing.ogg", x, goblin.position, GOBLIN_SOUND_VOLUME_STEP);
|
||||||
|
} else {
|
||||||
|
play_creature_attack_sound("sounds/weapons/club_swing.ogg", x, goblin.position, GOBLIN_SOUND_VOLUME_STEP);
|
||||||
|
}
|
||||||
|
|
||||||
|
int damage = random(GOBLIN_DAMAGE_MIN, GOBLIN_DAMAGE_MAX);
|
||||||
|
player_health -= damage;
|
||||||
|
if (player_health < 0) player_health = 0;
|
||||||
|
|
||||||
|
// REQUIRED: Hit sound using creature_audio
|
||||||
|
play_creature_attack_sound("sounds/enemies/player_hit.ogg", x, goblin.position, GOBLIN_SOUND_VOLUME_STEP);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
STEP 7: Damage/Death function
|
||||||
|
============================================================================
|
||||||
|
|
||||||
|
CRITICAL: MUST stop creature's sound before playing death sound
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Example:
|
||||||
|
/*
|
||||||
|
bool damage_goblin_at(int pos, int damage) {
|
||||||
|
for (uint i = 0; i < goblins.length(); i++) {
|
||||||
|
if (goblins[i].position == pos) {
|
||||||
|
goblins[i].health -= damage;
|
||||||
|
if (goblins[i].health <= 0) {
|
||||||
|
// CRITICAL: Stop active sounds before death sound
|
||||||
|
if (goblins[i].sound_handle != -1) {
|
||||||
|
p.destroy_sound(goblins[i].sound_handle);
|
||||||
|
goblins[i].sound_handle = -1;
|
||||||
|
}
|
||||||
|
// REQUIRED: Use creature_audio for death sound
|
||||||
|
play_creature_death_sound("sounds/enemies/enemy_falls.ogg", x, pos, GOBLIN_SOUND_VOLUME_STEP);
|
||||||
|
goblins.remove_at(i);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
STEP 8: Helper functions
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
// Example:
|
||||||
|
/*
|
||||||
|
Goblin@ get_goblin_at(int pos) {
|
||||||
|
for (uint i = 0; i < goblins.length(); i++) {
|
||||||
|
if (goblins[i].position == pos) {
|
||||||
|
return @goblins[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_goblins() {
|
||||||
|
for (uint i = 0; i < goblins.length(); i++) {
|
||||||
|
update_goblin(goblins[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear_goblins() {
|
||||||
|
for (uint i = 0; i < goblins.length(); i++) {
|
||||||
|
if (goblins[i].sound_handle != -1) {
|
||||||
|
p.destroy_sound(goblins[i].sound_handle);
|
||||||
|
goblins[i].sound_handle = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
goblins.resize(0);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
STEP 9: Update src/combat.nvgt damage functions
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
// Add to relevant damage functions in src/combat.nvgt:
|
||||||
|
/*
|
||||||
|
// In attack_enemy_ranged():
|
||||||
|
if (damage_goblin_at(check_x, damage)) {
|
||||||
|
return check_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In attack_enemy():
|
||||||
|
if (damage_goblin_at(target_x, damage)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When player damages goblin, play hit sound:
|
||||||
|
play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", player_x, target_x, GOBLIN_SOUND_VOLUME_STEP);
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
CHECKLIST: Required for Every Creature
|
||||||
|
============================================================================
|
||||||
|
|
||||||
|
Constants in constants.nvgt:
|
||||||
|
[ ] CREATURE_HEALTH
|
||||||
|
[ ] CREATURE_DAMAGE_MIN / CREATURE_DAMAGE_MAX
|
||||||
|
[ ] CREATURE_MOVE_INTERVAL
|
||||||
|
[ ] CREATURE_ATTACK_INTERVAL
|
||||||
|
[ ] CREATURE_ALERT_MIN_DELAY / CREATURE_ALERT_MAX_DELAY
|
||||||
|
[ ] CREATURE_FOOTSTEP_MAX_DISTANCE
|
||||||
|
[ ] CREATURE_SOUND_VOLUME_STEP
|
||||||
|
|
||||||
|
Class members:
|
||||||
|
[ ] int position
|
||||||
|
[ ] int health
|
||||||
|
[ ] int sound_handle (initialized to -1)
|
||||||
|
[ ] timer move_timer
|
||||||
|
[ ] timer attack_timer
|
||||||
|
[ ] timer voice_timer
|
||||||
|
[ ] int next_voice_delay
|
||||||
|
[ ] string voice_sound
|
||||||
|
|
||||||
|
Audio usage:
|
||||||
|
[ ] Spawn: play_creature_voice() with stored handle
|
||||||
|
[ ] Update: play_creature_voice() with stored handle
|
||||||
|
[ ] Movement: play_creature_footstep()
|
||||||
|
[ ] Attacks: play_creature_attack_sound() for weapons
|
||||||
|
[ ] Death: Stop sound_handle BEFORE play_creature_death_sound()
|
||||||
|
[ ] Player damages creature: play_creature_hit_sound()
|
||||||
|
|
||||||
|
Functions:
|
||||||
|
[ ] spawn_creature()
|
||||||
|
[ ] update_creature()
|
||||||
|
[ ] try_attack_player_creature()
|
||||||
|
[ ] damage_creature_at()
|
||||||
|
[ ] get_creature_at()
|
||||||
|
[ ] update_creatures()
|
||||||
|
[ ] clear_creatures()
|
||||||
|
|
||||||
|
Integration:
|
||||||
|
[ ] Add creature to combat.nvgt damage checks
|
||||||
|
[ ] Call update_creatures() in main game loop
|
||||||
|
[ ] Handle cleanup (clear_creatures on game over, etc.)
|
||||||
|
|
||||||
|
============================================================================ */
|
||||||
@@ -1,3 +1,34 @@
|
|||||||
|
// Centralized falling damage system
|
||||||
|
// Safe fall height is 10 feet or less
|
||||||
|
// Each foot above 10 has a chance to deal 0-4 damage
|
||||||
|
// This means falling from great heights is VERY dangerous but not guaranteed fatal
|
||||||
|
const int SAFE_FALL_HEIGHT = 10;
|
||||||
|
const int FALL_DAMAGE_MIN = 0;
|
||||||
|
const int FALL_DAMAGE_MAX = 4;
|
||||||
|
|
||||||
|
void apply_falling_damage(int fall_height) {
|
||||||
|
// Always play the hit ground sound
|
||||||
|
p.play_stationary("sounds/actions/hit_ground.ogg", false);
|
||||||
|
|
||||||
|
if (fall_height <= SAFE_FALL_HEIGHT) {
|
||||||
|
screen_reader_speak("Landed safely.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate damage: roll 0-4 for each foot above 10
|
||||||
|
int damage = 0;
|
||||||
|
for (int i = SAFE_FALL_HEIGHT; i < fall_height; i++) {
|
||||||
|
damage += random(FALL_DAMAGE_MIN, FALL_DAMAGE_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply damage
|
||||||
|
player_health -= damage;
|
||||||
|
if (player_health < 0) player_health = 0;
|
||||||
|
|
||||||
|
// Feedback
|
||||||
|
screen_reader_speak("Fell " + fall_height + " feet! Took " + damage + " damage. " + player_health + " health remaining.", true);
|
||||||
|
}
|
||||||
|
|
||||||
// Tree Object
|
// Tree Object
|
||||||
class Tree {
|
class Tree {
|
||||||
int position;
|
int position;
|
||||||
@@ -45,7 +76,7 @@ class Tree {
|
|||||||
int tree_distance = x - position;
|
int tree_distance = x - position;
|
||||||
if (tree_distance < 0) tree_distance = -tree_distance;
|
if (tree_distance < 0) tree_distance = -tree_distance;
|
||||||
|
|
||||||
if (tree_distance <= 4) {
|
if (tree_distance <= TREE_SOUND_RANGE) {
|
||||||
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
|
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
|
||||||
sound_handle = p.play_1d("sounds/environment/tree.ogg", x, position, true);
|
sound_handle = p.play_1d("sounds/environment/tree.ogg", x, position, true);
|
||||||
if (sound_handle != -1) {
|
if (sound_handle != -1) {
|
||||||
@@ -140,10 +171,16 @@ class Tree {
|
|||||||
Tree@[] trees;
|
Tree@[] trees;
|
||||||
|
|
||||||
bool tree_too_close(int pos) {
|
bool tree_too_close(int pos) {
|
||||||
|
// Check distance from base (must be at least 5 tiles away)
|
||||||
|
if (pos <= BASE_END + 5) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check distance from other trees (must be at least 10 tiles apart)
|
||||||
for (uint i = 0; i < trees.length(); i++) {
|
for (uint i = 0; i < trees.length(); i++) {
|
||||||
int distance = trees[i].position - pos;
|
int distance = trees[i].position - pos;
|
||||||
if (distance < 0) distance = -distance;
|
if (distance < 0) distance = -distance;
|
||||||
if (distance <= 5) {
|
if (distance < 10) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,6 +272,19 @@ void damage_tree(int target_x, int damage) {
|
|||||||
|
|
||||||
void perform_search(int current_x)
|
void perform_search(int current_x)
|
||||||
{
|
{
|
||||||
|
// First priority: Check for world drops on this tile or adjacent
|
||||||
|
for (int check_x = current_x - 1; check_x <= current_x + 1; check_x++) {
|
||||||
|
WorldDrop@ drop = get_drop_at(check_x);
|
||||||
|
if (drop != null) {
|
||||||
|
if (!try_pickup_world_drop(drop)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
p.play_stationary("sounds/items/miscellaneous.ogg", false);
|
||||||
|
remove_drop_at(check_x);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check for snares nearby (adjacent within range)
|
// Check for snares nearby (adjacent within range)
|
||||||
for (int check_x = current_x - SNARE_COLLECT_RANGE; check_x <= current_x + SNARE_COLLECT_RANGE; check_x++) {
|
for (int check_x = current_x - SNARE_COLLECT_RANGE; check_x <= current_x + SNARE_COLLECT_RANGE; check_x++) {
|
||||||
// Skip current x? User said "beside". If on top, it breaks.
|
// Skip current x? User said "beside". If on top, it breaks.
|
||||||
@@ -543,23 +593,10 @@ void land_on_ground(int ground_level) {
|
|||||||
fall_sound_handle = -1;
|
fall_sound_handle = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
p.play_stationary("sounds/actions/hit_ground.ogg", false);
|
// Calculate fall damage using centralized function (also plays hit_ground sound)
|
||||||
|
|
||||||
// Calculate fall damage
|
|
||||||
int fall_height = fall_start_y - ground_level;
|
int fall_height = fall_start_y - ground_level;
|
||||||
y = ground_level;
|
y = ground_level;
|
||||||
|
apply_falling_damage(fall_height);
|
||||||
if (fall_height > 10) {
|
|
||||||
int damage = 0;
|
|
||||||
for (int i = 10; i < fall_height; i++) {
|
|
||||||
damage += random(1, 3);
|
|
||||||
}
|
|
||||||
player_health -= damage;
|
|
||||||
screen_reader_speak("Fell " + fall_height + " feet! Took " + damage + " damage. " + player_health + " health remaining.", true);
|
|
||||||
} else {
|
|
||||||
screen_reader_speak("Landed safely.", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
fall_start_y = 0;
|
fall_start_y = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
71
src/flying_creature_template.nvgt
Normal file
71
src/flying_creature_template.nvgt
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// FLYING CREATURE TEMPLATE
|
||||||
|
// Use this as a guide when adding new flying small game (ducks, geese, etc.)
|
||||||
|
//
|
||||||
|
// This template ensures all flying creatures have consistent audio, spawning, and drops.
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
STEP 1: Add creature sound(s) near the top of src/world_state.nvgt
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
// Example:
|
||||||
|
// string[] duck_sounds = {"sounds/game/duck.ogg"};
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
STEP 2: Add constants to src/constants.nvgt
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
// Example for "duck":
|
||||||
|
// const int DUCK_HEALTH = 1;
|
||||||
|
// const int DUCK_MOVE_INTERVAL_MIN = 900;
|
||||||
|
// const int DUCK_MOVE_INTERVAL_MAX = 2200;
|
||||||
|
// const int DUCK_FLYING_HEIGHT_MIN = 8;
|
||||||
|
// const int DUCK_FLYING_HEIGHT_MAX = 25;
|
||||||
|
// const float DUCK_SOUND_VOLUME_STEP = 3.0;
|
||||||
|
// const int DUCK_FLIGHT_SOUND_DELAY_MIN = 2500;
|
||||||
|
// const int DUCK_FLIGHT_SOUND_DELAY_MAX = 6000;
|
||||||
|
// const int DUCK_FALL_SPEED = 100;
|
||||||
|
// const int DUCK_FLY_AWAY_CHANCE = 2;
|
||||||
|
// const int DUCK_MAX_DIST_FROM_WATER = 4;
|
||||||
|
// const int DUCK_MAX_COUNT = 3;
|
||||||
|
// const int DUCK_HOURLY_SPAWN_CHANCE = 30;
|
||||||
|
// const int DUCK_SIGHT_RANGE = 6;
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
STEP 3: Register config in init_flying_creature_configs() (src/world_state.nvgt)
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
// Example:
|
||||||
|
// FlyingCreatureConfig@ duck_cfg = FlyingCreatureConfig();
|
||||||
|
// duck_cfg.id = "duck";
|
||||||
|
// duck_cfg.drop_type = "duck";
|
||||||
|
// duck_cfg.sounds = duck_sounds;
|
||||||
|
// duck_cfg.fall_sound = "sounds/actions/falling.ogg";
|
||||||
|
// duck_cfg.impact_sound = "sounds/game/game_falls.ogg";
|
||||||
|
// duck_cfg.health = DUCK_HEALTH;
|
||||||
|
// duck_cfg.move_interval_min = DUCK_MOVE_INTERVAL_MIN;
|
||||||
|
// duck_cfg.move_interval_max = DUCK_MOVE_INTERVAL_MAX;
|
||||||
|
// duck_cfg.min_height = DUCK_FLYING_HEIGHT_MIN;
|
||||||
|
// duck_cfg.max_height = DUCK_FLYING_HEIGHT_MAX;
|
||||||
|
// duck_cfg.sound_volume_step = DUCK_SOUND_VOLUME_STEP;
|
||||||
|
// duck_cfg.sound_delay_min = DUCK_FLIGHT_SOUND_DELAY_MIN;
|
||||||
|
// duck_cfg.sound_delay_max = DUCK_FLIGHT_SOUND_DELAY_MAX;
|
||||||
|
// duck_cfg.fall_speed = DUCK_FALL_SPEED;
|
||||||
|
// duck_cfg.fly_away_chance = DUCK_FLY_AWAY_CHANCE;
|
||||||
|
// duck_cfg.max_dist_from_water = DUCK_MAX_DIST_FROM_WATER;
|
||||||
|
// duck_cfg.hourly_spawn_chance = DUCK_HOURLY_SPAWN_CHANCE;
|
||||||
|
// duck_cfg.max_count = DUCK_MAX_COUNT;
|
||||||
|
// duck_cfg.sight_range = DUCK_SIGHT_RANGE;
|
||||||
|
// duck_cfg.flee_on_sight = true;
|
||||||
|
// flying_creature_configs.insert_last(duck_cfg);
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
STEP 4: Add butcher behavior (src/crafting.nvgt) if yields differ
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
// Example:
|
||||||
|
// if (game_type == "duck") {
|
||||||
|
// inv_meat++;
|
||||||
|
// inv_feathers += random(2, 4);
|
||||||
|
// inv_down += random(1, 2);
|
||||||
|
// screen_reader_speak("Butchered duck. Got 1 meat, feathers, and down.", true);
|
||||||
|
// }
|
||||||
@@ -10,6 +10,9 @@ string[] inv_small_game_types; // Array to track what types of small game we hav
|
|||||||
|
|
||||||
int inv_meat = 0;
|
int inv_meat = 0;
|
||||||
int inv_skins = 0;
|
int inv_skins = 0;
|
||||||
|
int inv_feathers = 0;
|
||||||
|
int inv_down = 0;
|
||||||
|
int inv_incense = 0;
|
||||||
|
|
||||||
int inv_spears = 0;
|
int inv_spears = 0;
|
||||||
int inv_snares = 0;
|
int inv_snares = 0;
|
||||||
@@ -37,6 +40,10 @@ int storage_small_game = 0;
|
|||||||
string[] storage_small_game_types;
|
string[] storage_small_game_types;
|
||||||
int storage_meat = 0;
|
int storage_meat = 0;
|
||||||
int storage_skins = 0;
|
int storage_skins = 0;
|
||||||
|
int storage_feathers = 0;
|
||||||
|
int storage_down = 0;
|
||||||
|
int storage_incense = 0;
|
||||||
|
|
||||||
int storage_spears = 0;
|
int storage_spears = 0;
|
||||||
int storage_snares = 0;
|
int storage_snares = 0;
|
||||||
int storage_axes = 0;
|
int storage_axes = 0;
|
||||||
@@ -91,6 +98,9 @@ const int ITEM_SKIN_POUCHES = 20;
|
|||||||
const int ITEM_ROPES = 21;
|
const int ITEM_ROPES = 21;
|
||||||
const int ITEM_REED_BASKETS = 22;
|
const int ITEM_REED_BASKETS = 22;
|
||||||
const int ITEM_CLAY_POTS = 23;
|
const int ITEM_CLAY_POTS = 23;
|
||||||
|
const int ITEM_FEATHERS = 24;
|
||||||
|
const int ITEM_DOWN = 25;
|
||||||
|
const int ITEM_INCENSE = 26;
|
||||||
const int HAT_MAX_HEALTH_BONUS = 1;
|
const int HAT_MAX_HEALTH_BONUS = 1;
|
||||||
const int GLOVES_MAX_HEALTH_BONUS = 1;
|
const int GLOVES_MAX_HEALTH_BONUS = 1;
|
||||||
const int PANTS_MAX_HEALTH_BONUS = 3;
|
const int PANTS_MAX_HEALTH_BONUS = 3;
|
||||||
@@ -273,6 +283,9 @@ int get_personal_count(int item_type) {
|
|||||||
if (item_type == ITEM_SMALL_GAME) return inv_small_game;
|
if (item_type == ITEM_SMALL_GAME) return inv_small_game;
|
||||||
if (item_type == ITEM_MEAT) return inv_meat;
|
if (item_type == ITEM_MEAT) return inv_meat;
|
||||||
if (item_type == ITEM_SKINS) return inv_skins;
|
if (item_type == ITEM_SKINS) return inv_skins;
|
||||||
|
if (item_type == ITEM_FEATHERS) return inv_feathers;
|
||||||
|
if (item_type == ITEM_DOWN) return inv_down;
|
||||||
|
if (item_type == ITEM_INCENSE) return inv_incense;
|
||||||
if (item_type == ITEM_SPEARS) return inv_spears;
|
if (item_type == ITEM_SPEARS) return inv_spears;
|
||||||
if (item_type == ITEM_SLINGS) return inv_slings;
|
if (item_type == ITEM_SLINGS) return inv_slings;
|
||||||
if (item_type == ITEM_AXES) return inv_axes;
|
if (item_type == ITEM_AXES) return inv_axes;
|
||||||
@@ -301,6 +314,9 @@ int get_storage_count(int item_type) {
|
|||||||
if (item_type == ITEM_SMALL_GAME) return storage_small_game;
|
if (item_type == ITEM_SMALL_GAME) return storage_small_game;
|
||||||
if (item_type == ITEM_MEAT) return storage_meat;
|
if (item_type == ITEM_MEAT) return storage_meat;
|
||||||
if (item_type == ITEM_SKINS) return storage_skins;
|
if (item_type == ITEM_SKINS) return storage_skins;
|
||||||
|
if (item_type == ITEM_FEATHERS) return storage_feathers;
|
||||||
|
if (item_type == ITEM_DOWN) return storage_down;
|
||||||
|
if (item_type == ITEM_INCENSE) return storage_incense;
|
||||||
if (item_type == ITEM_SPEARS) return storage_spears;
|
if (item_type == ITEM_SPEARS) return storage_spears;
|
||||||
if (item_type == ITEM_SLINGS) return storage_slings;
|
if (item_type == ITEM_SLINGS) return storage_slings;
|
||||||
if (item_type == ITEM_AXES) return storage_axes;
|
if (item_type == ITEM_AXES) return storage_axes;
|
||||||
@@ -329,6 +345,9 @@ string get_item_label(int item_type) {
|
|||||||
if (item_type == ITEM_SMALL_GAME) return "small game";
|
if (item_type == ITEM_SMALL_GAME) return "small game";
|
||||||
if (item_type == ITEM_MEAT) return "meat";
|
if (item_type == ITEM_MEAT) return "meat";
|
||||||
if (item_type == ITEM_SKINS) return "skins";
|
if (item_type == ITEM_SKINS) return "skins";
|
||||||
|
if (item_type == ITEM_FEATHERS) return "feathers";
|
||||||
|
if (item_type == ITEM_DOWN) return "down";
|
||||||
|
if (item_type == ITEM_INCENSE) return "incense";
|
||||||
if (item_type == ITEM_SPEARS) return "spears";
|
if (item_type == ITEM_SPEARS) return "spears";
|
||||||
if (item_type == ITEM_SLINGS) return "slings";
|
if (item_type == ITEM_SLINGS) return "slings";
|
||||||
if (item_type == ITEM_AXES) return "axes";
|
if (item_type == ITEM_AXES) return "axes";
|
||||||
@@ -366,6 +385,9 @@ string get_item_label_singular(int item_type) {
|
|||||||
if (item_type == ITEM_SMALL_GAME) return "small game";
|
if (item_type == ITEM_SMALL_GAME) return "small game";
|
||||||
if (item_type == ITEM_MEAT) return "meat";
|
if (item_type == ITEM_MEAT) return "meat";
|
||||||
if (item_type == ITEM_SKINS) return "skin";
|
if (item_type == ITEM_SKINS) return "skin";
|
||||||
|
if (item_type == ITEM_FEATHERS) return "feather";
|
||||||
|
if (item_type == ITEM_DOWN) return "down";
|
||||||
|
if (item_type == ITEM_INCENSE) return "incense stick";
|
||||||
if (item_type == ITEM_SPEARS) return "spear";
|
if (item_type == ITEM_SPEARS) return "spear";
|
||||||
if (item_type == ITEM_SLINGS) return "sling";
|
if (item_type == ITEM_SLINGS) return "sling";
|
||||||
if (item_type == ITEM_AXES) return "axe";
|
if (item_type == ITEM_AXES) return "axe";
|
||||||
@@ -394,6 +416,9 @@ double get_item_favor_value(int item_type) {
|
|||||||
if (item_type == ITEM_SMALL_GAME) return 0.20;
|
if (item_type == ITEM_SMALL_GAME) return 0.20;
|
||||||
if (item_type == ITEM_MEAT) return 0.15;
|
if (item_type == ITEM_MEAT) return 0.15;
|
||||||
if (item_type == ITEM_SKINS) return 0.15;
|
if (item_type == ITEM_SKINS) return 0.15;
|
||||||
|
if (item_type == ITEM_FEATHERS) return 0.05;
|
||||||
|
if (item_type == ITEM_DOWN) return 0.05;
|
||||||
|
if (item_type == ITEM_INCENSE) return 0.10;
|
||||||
if (item_type == ITEM_SPEARS) return 1.00;
|
if (item_type == ITEM_SPEARS) return 1.00;
|
||||||
if (item_type == ITEM_SLINGS) return 2.00;
|
if (item_type == ITEM_SLINGS) return 2.00;
|
||||||
if (item_type == ITEM_AXES) return 1.50;
|
if (item_type == ITEM_AXES) return 1.50;
|
||||||
|
|||||||
@@ -257,6 +257,9 @@ void deposit_item(int item_type) {
|
|||||||
else if (item_type == ITEM_SMALL_GAME) { inv_small_game -= amount; storage_small_game += amount; move_small_game_to_storage(amount); }
|
else if (item_type == ITEM_SMALL_GAME) { inv_small_game -= amount; storage_small_game += amount; move_small_game_to_storage(amount); }
|
||||||
else if (item_type == ITEM_MEAT) { inv_meat -= amount; storage_meat += amount; }
|
else if (item_type == ITEM_MEAT) { inv_meat -= amount; storage_meat += amount; }
|
||||||
else if (item_type == ITEM_SKINS) { inv_skins -= amount; storage_skins += amount; }
|
else if (item_type == ITEM_SKINS) { inv_skins -= amount; storage_skins += amount; }
|
||||||
|
else if (item_type == ITEM_FEATHERS) { inv_feathers -= amount; storage_feathers += amount; }
|
||||||
|
else if (item_type == ITEM_DOWN) { inv_down -= amount; storage_down += amount; }
|
||||||
|
else if (item_type == ITEM_INCENSE) { inv_incense -= amount; storage_incense += amount; }
|
||||||
else if (item_type == ITEM_SPEARS) { inv_spears -= amount; storage_spears += amount; }
|
else if (item_type == ITEM_SPEARS) { inv_spears -= amount; storage_spears += amount; }
|
||||||
else if (item_type == ITEM_SLINGS) { inv_slings -= amount; storage_slings += amount; }
|
else if (item_type == ITEM_SLINGS) { inv_slings -= amount; storage_slings += amount; }
|
||||||
else if (item_type == ITEM_AXES) { inv_axes -= amount; storage_axes += amount; }
|
else if (item_type == ITEM_AXES) { inv_axes -= amount; storage_axes += amount; }
|
||||||
@@ -301,6 +304,9 @@ void withdraw_item(int item_type) {
|
|||||||
else if (item_type == ITEM_SMALL_GAME) { storage_small_game -= amount; inv_small_game += amount; move_small_game_to_personal(amount); }
|
else if (item_type == ITEM_SMALL_GAME) { storage_small_game -= amount; inv_small_game += amount; move_small_game_to_personal(amount); }
|
||||||
else if (item_type == ITEM_MEAT) { storage_meat -= amount; inv_meat += amount; }
|
else if (item_type == ITEM_MEAT) { storage_meat -= amount; inv_meat += amount; }
|
||||||
else if (item_type == ITEM_SKINS) { storage_skins -= amount; inv_skins += amount; }
|
else if (item_type == ITEM_SKINS) { storage_skins -= amount; inv_skins += amount; }
|
||||||
|
else if (item_type == ITEM_FEATHERS) { storage_feathers -= amount; inv_feathers += amount; }
|
||||||
|
else if (item_type == ITEM_DOWN) { storage_down -= amount; inv_down += amount; }
|
||||||
|
else if (item_type == ITEM_INCENSE) { storage_incense -= amount; inv_incense += amount; }
|
||||||
else if (item_type == ITEM_SPEARS) { storage_spears -= amount; inv_spears += amount; }
|
else if (item_type == ITEM_SPEARS) { storage_spears -= amount; inv_spears += amount; }
|
||||||
else if (item_type == ITEM_SLINGS) { storage_slings -= amount; inv_slings += amount; }
|
else if (item_type == ITEM_SLINGS) { storage_slings -= amount; inv_slings += amount; }
|
||||||
else if (item_type == ITEM_AXES) { storage_axes -= amount; inv_axes += amount; }
|
else if (item_type == ITEM_AXES) { storage_axes -= amount; inv_axes += amount; }
|
||||||
@@ -341,6 +347,9 @@ void sacrifice_item(int item_type) {
|
|||||||
}
|
}
|
||||||
else if (item_type == ITEM_MEAT) inv_meat--;
|
else if (item_type == ITEM_MEAT) inv_meat--;
|
||||||
else if (item_type == ITEM_SKINS) inv_skins--;
|
else if (item_type == ITEM_SKINS) inv_skins--;
|
||||||
|
else if (item_type == ITEM_FEATHERS) inv_feathers--;
|
||||||
|
else if (item_type == ITEM_DOWN) inv_down--;
|
||||||
|
else if (item_type == ITEM_INCENSE) inv_incense--;
|
||||||
else if (item_type == ITEM_SPEARS) inv_spears--;
|
else if (item_type == ITEM_SPEARS) inv_spears--;
|
||||||
else if (item_type == ITEM_SLINGS) inv_slings--;
|
else if (item_type == ITEM_SLINGS) inv_slings--;
|
||||||
else if (item_type == ITEM_AXES) inv_axes--;
|
else if (item_type == ITEM_AXES) inv_axes--;
|
||||||
@@ -375,6 +384,9 @@ void build_personal_inventory_options(string[]@ options, int[]@ item_types) {
|
|||||||
options.insert_last("Small Game: " + inv_small_game); item_types.insert_last(ITEM_SMALL_GAME);
|
options.insert_last("Small Game: " + inv_small_game); item_types.insert_last(ITEM_SMALL_GAME);
|
||||||
options.insert_last("Meat: " + inv_meat); item_types.insert_last(ITEM_MEAT);
|
options.insert_last("Meat: " + inv_meat); item_types.insert_last(ITEM_MEAT);
|
||||||
options.insert_last("Skins: " + inv_skins); item_types.insert_last(ITEM_SKINS);
|
options.insert_last("Skins: " + inv_skins); item_types.insert_last(ITEM_SKINS);
|
||||||
|
options.insert_last("Feathers: " + inv_feathers); item_types.insert_last(ITEM_FEATHERS);
|
||||||
|
options.insert_last("Down: " + inv_down); item_types.insert_last(ITEM_DOWN);
|
||||||
|
options.insert_last("Incense: " + inv_incense); item_types.insert_last(ITEM_INCENSE);
|
||||||
options.insert_last("Spears: " + inv_spears); item_types.insert_last(ITEM_SPEARS);
|
options.insert_last("Spears: " + inv_spears); item_types.insert_last(ITEM_SPEARS);
|
||||||
options.insert_last("Slings: " + inv_slings); item_types.insert_last(ITEM_SLINGS);
|
options.insert_last("Slings: " + inv_slings); item_types.insert_last(ITEM_SLINGS);
|
||||||
options.insert_last("Axes: " + inv_axes); item_types.insert_last(ITEM_AXES);
|
options.insert_last("Axes: " + inv_axes); item_types.insert_last(ITEM_AXES);
|
||||||
@@ -404,6 +416,9 @@ void build_storage_inventory_options(string[]@ options, int[]@ item_types) {
|
|||||||
options.insert_last("Small Game: " + storage_small_game); item_types.insert_last(ITEM_SMALL_GAME);
|
options.insert_last("Small Game: " + storage_small_game); item_types.insert_last(ITEM_SMALL_GAME);
|
||||||
options.insert_last("Meat: " + storage_meat); item_types.insert_last(ITEM_MEAT);
|
options.insert_last("Meat: " + storage_meat); item_types.insert_last(ITEM_MEAT);
|
||||||
options.insert_last("Skins: " + storage_skins); item_types.insert_last(ITEM_SKINS);
|
options.insert_last("Skins: " + storage_skins); item_types.insert_last(ITEM_SKINS);
|
||||||
|
options.insert_last("Feathers: " + storage_feathers); item_types.insert_last(ITEM_FEATHERS);
|
||||||
|
options.insert_last("Down: " + storage_down); item_types.insert_last(ITEM_DOWN);
|
||||||
|
options.insert_last("Incense: " + storage_incense); item_types.insert_last(ITEM_INCENSE);
|
||||||
options.insert_last("Spears: " + storage_spears); item_types.insert_last(ITEM_SPEARS);
|
options.insert_last("Spears: " + storage_spears); item_types.insert_last(ITEM_SPEARS);
|
||||||
options.insert_last("Slings: " + storage_slings); item_types.insert_last(ITEM_SLINGS);
|
options.insert_last("Slings: " + storage_slings); item_types.insert_last(ITEM_SLINGS);
|
||||||
options.insert_last("Axes: " + storage_axes); item_types.insert_last(ITEM_AXES);
|
options.insert_last("Axes: " + storage_axes); item_types.insert_last(ITEM_AXES);
|
||||||
@@ -431,7 +446,10 @@ void show_inventory() {
|
|||||||
info += inv_clay + " clay, ";
|
info += inv_clay + " clay, ";
|
||||||
info += inv_small_game + " small game, ";
|
info += inv_small_game + " small game, ";
|
||||||
info += inv_meat + " meat, ";
|
info += inv_meat + " meat, ";
|
||||||
info += inv_skins + " skins. ";
|
info += inv_skins + " skins, ";
|
||||||
|
info += inv_feathers + " feathers, ";
|
||||||
|
info += inv_down + " down, ";
|
||||||
|
info += inv_incense + " incense. ";
|
||||||
info += "Tools: " + inv_spears + " spears, " + inv_slings + " slings, " + inv_axes + " axes, " + inv_snares + " snares, " + inv_knives + " knives, " + inv_fishing_poles + " fishing poles, " + inv_ropes + " ropes, " + inv_reed_baskets + " reed baskets, " + inv_clay_pots + " clay pots. ";
|
info += "Tools: " + inv_spears + " spears, " + inv_slings + " slings, " + inv_axes + " axes, " + inv_snares + " snares, " + inv_knives + " knives, " + inv_fishing_poles + " fishing poles, " + inv_ropes + " ropes, " + inv_reed_baskets + " reed baskets, " + inv_clay_pots + " clay pots. ";
|
||||||
info += "Clothing: " + inv_skin_hats + " hats, " + inv_skin_gloves + " gloves, " + inv_skin_pants + " pants, " + inv_skin_tunics + " tunics, " + inv_moccasins + " moccasins, " + inv_skin_pouches + " skin pouches.";
|
info += "Clothing: " + inv_skin_hats + " hats, " + inv_skin_gloves + " gloves, " + inv_skin_pants + " pants, " + inv_skin_tunics + " tunics, " + inv_moccasins + " moccasins, " + inv_skin_pouches + " skin pouches.";
|
||||||
screen_reader_speak(info, true);
|
screen_reader_speak(info, true);
|
||||||
@@ -550,12 +568,25 @@ void run_storage_menu() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void run_altar_menu() {
|
void check_altar_menu(int player_x) {
|
||||||
|
if (!key_pressed(KEY_S)) return;
|
||||||
|
|
||||||
|
// Must be in base
|
||||||
|
if (player_x > BASE_END) {
|
||||||
|
screen_reader_speak("Must be in base to use altar.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must have altar
|
||||||
if (world_altars.length() == 0) {
|
if (world_altars.length() == 0) {
|
||||||
screen_reader_speak("No altar built.", true);
|
screen_reader_speak("No altar built.", true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
run_altar_menu();
|
||||||
|
}
|
||||||
|
|
||||||
|
void run_altar_menu() {
|
||||||
screen_reader_speak("Altar. Favor " + format_favor(favor) + ".", true);
|
screen_reader_speak("Altar. Favor " + format_favor(favor) + ".", true);
|
||||||
|
|
||||||
int selection = 0;
|
int selection = 0;
|
||||||
@@ -635,6 +666,26 @@ void try_feed_fire_log(WorldFire@ fire) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void try_burn_incense() {
|
||||||
|
if (world_altars.length() == 0) {
|
||||||
|
screen_reader_speak("No altar built.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (inv_clay_pots <= 0) {
|
||||||
|
screen_reader_speak("You need a clay pot to burn incense.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (inv_incense <= 0) {
|
||||||
|
screen_reader_speak("No incense to burn.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inv_incense--;
|
||||||
|
incense_hours_remaining += INCENSE_HOURS_PER_STICK;
|
||||||
|
incense_burning = true;
|
||||||
|
screen_reader_speak("Incense burning. " + incense_hours_remaining + " hours remaining.", true);
|
||||||
|
}
|
||||||
|
|
||||||
void check_equipment_menu() {
|
void check_equipment_menu() {
|
||||||
if (key_pressed(KEY_E)) {
|
if (key_pressed(KEY_E)) {
|
||||||
// Check if player has any equipment
|
// Check if player has any equipment
|
||||||
@@ -678,6 +729,11 @@ void run_action_menu(int x) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (x <= BASE_END && world_altars.length() > 0 && inv_incense > 0) {
|
||||||
|
options.insert_last("Burn incense");
|
||||||
|
action_types.insert_last(4);
|
||||||
|
}
|
||||||
|
|
||||||
while(true) {
|
while(true) {
|
||||||
wait(5);
|
wait(5);
|
||||||
menu_background_tick();
|
menu_background_tick();
|
||||||
@@ -708,6 +764,8 @@ void run_action_menu(int x) {
|
|||||||
try_feed_fire_vine(nearby_fire);
|
try_feed_fire_vine(nearby_fire);
|
||||||
} else if (action == 3) {
|
} else if (action == 3) {
|
||||||
try_feed_fire_log(nearby_fire);
|
try_feed_fire_log(nearby_fire);
|
||||||
|
} else if (action == 4) {
|
||||||
|
try_burn_incense();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
// Notification System
|
// Notification System
|
||||||
string[] notification_history;
|
string[] notification_history;
|
||||||
const int MAX_NOTIFICATIONS = 10;
|
const int MAX_NOTIFICATIONS = 10;
|
||||||
|
const int NOTIFICATION_DELAY = 3000; // 3 seconds between notifications
|
||||||
int current_notification_index = -1;
|
int current_notification_index = -1;
|
||||||
string[] notification_queue;
|
string[] notification_queue;
|
||||||
int[] notification_sound_handles;
|
timer notification_timer;
|
||||||
|
bool notification_active = false;
|
||||||
|
int notification_sound_handle = -1;
|
||||||
|
|
||||||
void notify(string message) {
|
void notify(string message) {
|
||||||
// Play notification sound
|
// Add to queue (don't play yet)
|
||||||
int sound_handle = p.play_stationary("sounds/notify.ogg", false);
|
|
||||||
notification_queue.insert_last(message);
|
notification_queue.insert_last(message);
|
||||||
notification_sound_handles.insert_last(sound_handle);
|
|
||||||
|
|
||||||
// Add to history
|
// Add to history immediately so it appears in history even if queued
|
||||||
notification_history.insert_last(message);
|
notification_history.insert_last(message);
|
||||||
|
|
||||||
// Keep only last 10 notifications
|
// Keep only last 10 notifications
|
||||||
@@ -25,17 +26,35 @@ void notify(string message) {
|
|||||||
|
|
||||||
void update_notifications() {
|
void update_notifications() {
|
||||||
if (notification_queue.length() == 0) {
|
if (notification_queue.length() == 0) {
|
||||||
|
notification_active = false;
|
||||||
|
notification_sound_handle = -1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int sound_handle = notification_sound_handles[0];
|
// If a notification is currently active, wait for delay
|
||||||
if (sound_handle != -1 && p.sound_is_playing(sound_handle)) {
|
if (notification_active && notification_timer.elapsed < NOTIFICATION_DELAY) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
screen_reader_speak(notification_queue[0], true);
|
// If we're waiting for the notification sound to finish playing
|
||||||
notification_queue.remove_at(0);
|
if (notification_sound_handle != -1) {
|
||||||
notification_sound_handles.remove_at(0);
|
// Check if sound is still playing
|
||||||
|
if (p.sound_is_active(notification_sound_handle)) {
|
||||||
|
return; // Still playing, wait
|
||||||
|
}
|
||||||
|
// Sound finished, now speak
|
||||||
|
screen_reader_speak(notification_queue[0], true);
|
||||||
|
notification_queue.remove_at(0);
|
||||||
|
notification_sound_handle = -1;
|
||||||
|
|
||||||
|
// Start timer for next notification
|
||||||
|
notification_timer.restart();
|
||||||
|
notification_active = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play next notification sound (don't speak yet)
|
||||||
|
notification_sound_handle = p.play_stationary("sounds/notify.ogg", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void check_notification_keys() {
|
void check_notification_keys() {
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ int last_sling_stage = -1; // Track which stage we're in to avoid duplicate soun
|
|||||||
|
|
||||||
// Favor system
|
// Favor system
|
||||||
double favor = 0.0;
|
double favor = 0.0;
|
||||||
|
int incense_hours_remaining = 0;
|
||||||
|
bool incense_burning = false;
|
||||||
bool blessing_speed_active = false;
|
bool blessing_speed_active = false;
|
||||||
timer blessing_speed_timer;
|
timer blessing_speed_timer;
|
||||||
|
|
||||||
|
|||||||
@@ -145,7 +145,9 @@ void clear_world_objects() {
|
|||||||
|
|
||||||
clear_zombies();
|
clear_zombies();
|
||||||
clear_bandits();
|
clear_bandits();
|
||||||
|
clear_flying_creatures();
|
||||||
clear_mountains();
|
clear_mountains();
|
||||||
|
clear_world_drops();
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset_game_state() {
|
void reset_game_state() {
|
||||||
@@ -174,6 +176,8 @@ void reset_game_state() {
|
|||||||
base_max_health = 10;
|
base_max_health = 10;
|
||||||
max_health = 10;
|
max_health = 10;
|
||||||
favor = 0.0;
|
favor = 0.0;
|
||||||
|
incense_hours_remaining = 0;
|
||||||
|
incense_burning = false;
|
||||||
blessing_speed_active = false;
|
blessing_speed_active = false;
|
||||||
|
|
||||||
inv_stones = 0;
|
inv_stones = 0;
|
||||||
@@ -186,6 +190,9 @@ void reset_game_state() {
|
|||||||
inv_small_game_types.resize(0);
|
inv_small_game_types.resize(0);
|
||||||
inv_meat = 0;
|
inv_meat = 0;
|
||||||
inv_skins = 0;
|
inv_skins = 0;
|
||||||
|
inv_feathers = 0;
|
||||||
|
inv_down = 0;
|
||||||
|
inv_incense = 0;
|
||||||
inv_spears = 0;
|
inv_spears = 0;
|
||||||
inv_snares = 0;
|
inv_snares = 0;
|
||||||
inv_axes = 0;
|
inv_axes = 0;
|
||||||
@@ -211,6 +218,9 @@ void reset_game_state() {
|
|||||||
storage_small_game_types.resize(0);
|
storage_small_game_types.resize(0);
|
||||||
storage_meat = 0;
|
storage_meat = 0;
|
||||||
storage_skins = 0;
|
storage_skins = 0;
|
||||||
|
storage_feathers = 0;
|
||||||
|
storage_down = 0;
|
||||||
|
storage_incense = 0;
|
||||||
storage_spears = 0;
|
storage_spears = 0;
|
||||||
storage_snares = 0;
|
storage_snares = 0;
|
||||||
storage_axes = 0;
|
storage_axes = 0;
|
||||||
@@ -283,6 +293,7 @@ void start_new_game() {
|
|||||||
spawn_trees(5, 19);
|
spawn_trees(5, 19);
|
||||||
init_barricade();
|
init_barricade();
|
||||||
init_time();
|
init_time();
|
||||||
|
init_weather();
|
||||||
save_game_state();
|
save_game_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,6 +441,8 @@ bool load_game_state_from_raw(const string&in rawData) {
|
|||||||
if (get_raw_number(rawData, "player_base_health", value)) base_max_health = value;
|
if (get_raw_number(rawData, "player_base_health", value)) base_max_health = value;
|
||||||
if (get_raw_number(rawData, "player_max_health", value)) max_health = value;
|
if (get_raw_number(rawData, "player_max_health", value)) max_health = value;
|
||||||
if (get_raw_number(rawData, "player_favor", value)) favor = value;
|
if (get_raw_number(rawData, "player_favor", value)) favor = value;
|
||||||
|
if (get_raw_number(rawData, "incense_hours_remaining", value)) incense_hours_remaining = value;
|
||||||
|
if (get_raw_bool(rawData, "incense_burning", bool_value)) incense_burning = bool_value;
|
||||||
if (get_raw_number(rawData, "time_current_hour", value)) current_hour = value;
|
if (get_raw_number(rawData, "time_current_hour", value)) current_hour = value;
|
||||||
if (get_raw_number(rawData, "world_map_size", value)) MAP_SIZE = value;
|
if (get_raw_number(rawData, "world_map_size", value)) MAP_SIZE = value;
|
||||||
if (get_raw_number(rawData, "world_expanded_area_start", value)) expanded_area_start = value;
|
if (get_raw_number(rawData, "world_expanded_area_start", value)) expanded_area_start = value;
|
||||||
@@ -459,6 +472,9 @@ bool load_game_state_from_raw(const string&in rawData) {
|
|||||||
if (get_raw_number(rawData, "inventory_small_game", value)) inv_small_game = value;
|
if (get_raw_number(rawData, "inventory_small_game", value)) inv_small_game = value;
|
||||||
if (get_raw_number(rawData, "inventory_meat", value)) inv_meat = value;
|
if (get_raw_number(rawData, "inventory_meat", value)) inv_meat = value;
|
||||||
if (get_raw_number(rawData, "inventory_skins", value)) inv_skins = value;
|
if (get_raw_number(rawData, "inventory_skins", value)) inv_skins = value;
|
||||||
|
if (get_raw_number(rawData, "inventory_feathers", value)) inv_feathers = value;
|
||||||
|
if (get_raw_number(rawData, "inventory_down", value)) inv_down = value;
|
||||||
|
if (get_raw_number(rawData, "inventory_incense", value)) inv_incense = value;
|
||||||
if (get_raw_number(rawData, "inventory_spears", value)) inv_spears = value;
|
if (get_raw_number(rawData, "inventory_spears", value)) inv_spears = value;
|
||||||
if (get_raw_number(rawData, "inventory_snares", value)) inv_snares = value;
|
if (get_raw_number(rawData, "inventory_snares", value)) inv_snares = value;
|
||||||
if (get_raw_number(rawData, "inventory_axes", value)) inv_axes = value;
|
if (get_raw_number(rawData, "inventory_axes", value)) inv_axes = value;
|
||||||
@@ -487,6 +503,7 @@ bool load_game_state_from_raw(const string&in rawData) {
|
|||||||
|
|
||||||
if (equipped_arms != EQUIP_POUCH) equipped_arms = EQUIP_NONE;
|
if (equipped_arms != EQUIP_POUCH) equipped_arms = EQUIP_NONE;
|
||||||
if (equipped_arms == EQUIP_POUCH && inv_skin_pouches <= 0) equipped_arms = EQUIP_NONE;
|
if (equipped_arms == EQUIP_POUCH && inv_skin_pouches <= 0) equipped_arms = EQUIP_NONE;
|
||||||
|
if (incense_hours_remaining > 0) incense_burning = true;
|
||||||
|
|
||||||
if (inv_small_game_types.length() == 0 && inv_small_game > 0) {
|
if (inv_small_game_types.length() == 0 && inv_small_game > 0) {
|
||||||
for (int i = 0; i < inv_small_game; i++) {
|
for (int i = 0; i < inv_small_game; i++) {
|
||||||
@@ -518,6 +535,8 @@ bool save_game_state() {
|
|||||||
saveData.set("player_base_health", base_max_health);
|
saveData.set("player_base_health", base_max_health);
|
||||||
saveData.set("player_max_health", max_health);
|
saveData.set("player_max_health", max_health);
|
||||||
saveData.set("player_favor", favor);
|
saveData.set("player_favor", favor);
|
||||||
|
saveData.set("incense_hours_remaining", incense_hours_remaining);
|
||||||
|
saveData.set("incense_burning", incense_burning);
|
||||||
|
|
||||||
saveData.set("inventory_stones", inv_stones);
|
saveData.set("inventory_stones", inv_stones);
|
||||||
saveData.set("inventory_sticks", inv_sticks);
|
saveData.set("inventory_sticks", inv_sticks);
|
||||||
@@ -528,6 +547,9 @@ bool save_game_state() {
|
|||||||
saveData.set("inventory_small_game", inv_small_game);
|
saveData.set("inventory_small_game", inv_small_game);
|
||||||
saveData.set("inventory_meat", inv_meat);
|
saveData.set("inventory_meat", inv_meat);
|
||||||
saveData.set("inventory_skins", inv_skins);
|
saveData.set("inventory_skins", inv_skins);
|
||||||
|
saveData.set("inventory_feathers", inv_feathers);
|
||||||
|
saveData.set("inventory_down", inv_down);
|
||||||
|
saveData.set("inventory_incense", inv_incense);
|
||||||
saveData.set("inventory_spears", inv_spears);
|
saveData.set("inventory_spears", inv_spears);
|
||||||
saveData.set("inventory_snares", inv_snares);
|
saveData.set("inventory_snares", inv_snares);
|
||||||
saveData.set("inventory_axes", inv_axes);
|
saveData.set("inventory_axes", inv_axes);
|
||||||
@@ -554,6 +576,9 @@ bool save_game_state() {
|
|||||||
saveData.set("storage_small_game", storage_small_game);
|
saveData.set("storage_small_game", storage_small_game);
|
||||||
saveData.set("storage_meat", storage_meat);
|
saveData.set("storage_meat", storage_meat);
|
||||||
saveData.set("storage_skins", storage_skins);
|
saveData.set("storage_skins", storage_skins);
|
||||||
|
saveData.set("storage_feathers", storage_feathers);
|
||||||
|
saveData.set("storage_down", storage_down);
|
||||||
|
saveData.set("storage_incense", storage_incense);
|
||||||
saveData.set("storage_spears", storage_spears);
|
saveData.set("storage_spears", storage_spears);
|
||||||
saveData.set("storage_snares", storage_snares);
|
saveData.set("storage_snares", storage_snares);
|
||||||
saveData.set("storage_axes", storage_axes);
|
saveData.set("storage_axes", storage_axes);
|
||||||
@@ -605,6 +630,8 @@ bool save_game_state() {
|
|||||||
}
|
}
|
||||||
saveData.set("quest_queue", join_string_array(questData));
|
saveData.set("quest_queue", join_string_array(questData));
|
||||||
|
|
||||||
|
saveData.set("weather_data", serialize_weather());
|
||||||
|
|
||||||
saveData.set("world_map_size", MAP_SIZE);
|
saveData.set("world_map_size", MAP_SIZE);
|
||||||
saveData.set("world_expanded_area_start", expanded_area_start);
|
saveData.set("world_expanded_area_start", expanded_area_start);
|
||||||
saveData.set("world_expanded_area_end", expanded_area_end);
|
saveData.set("world_expanded_area_end", expanded_area_end);
|
||||||
@@ -685,6 +712,12 @@ bool save_game_state() {
|
|||||||
}
|
}
|
||||||
saveData.set("mountains_data", join_string_array(mountainData));
|
saveData.set("mountains_data", join_string_array(mountainData));
|
||||||
|
|
||||||
|
string[] dropData;
|
||||||
|
for (uint i = 0; i < world_drops.length(); i++) {
|
||||||
|
dropData.insert_last(world_drops[i].position + "|" + world_drops[i].type);
|
||||||
|
}
|
||||||
|
saveData.set("drops_data", join_string_array(dropData));
|
||||||
|
|
||||||
string rawData = saveData.serialize();
|
string rawData = saveData.serialize();
|
||||||
string encryptedData = encrypt_save_data(rawData);
|
string encryptedData = encrypt_save_data(rawData);
|
||||||
return save_data(SAVE_FILE_PATH, encryptedData);
|
return save_data(SAVE_FILE_PATH, encryptedData);
|
||||||
@@ -755,6 +788,9 @@ bool load_game_state() {
|
|||||||
max_health = int(get_number(saveData, "player_max_health", 10));
|
max_health = int(get_number(saveData, "player_max_health", 10));
|
||||||
base_max_health = int(get_number(saveData, "player_base_health", max_health));
|
base_max_health = int(get_number(saveData, "player_base_health", max_health));
|
||||||
favor = get_number(saveData, "player_favor", 0.0);
|
favor = get_number(saveData, "player_favor", 0.0);
|
||||||
|
incense_hours_remaining = int(get_number(saveData, "incense_hours_remaining", 0));
|
||||||
|
incense_burning = get_bool(saveData, "incense_burning", false);
|
||||||
|
if (incense_hours_remaining > 0) incense_burning = true;
|
||||||
|
|
||||||
if (x < 0) x = 0;
|
if (x < 0) x = 0;
|
||||||
if (x >= MAP_SIZE) x = MAP_SIZE - 1;
|
if (x >= MAP_SIZE) x = MAP_SIZE - 1;
|
||||||
@@ -770,6 +806,9 @@ bool load_game_state() {
|
|||||||
inv_small_game = int(get_number(saveData, "inventory_small_game", 0));
|
inv_small_game = int(get_number(saveData, "inventory_small_game", 0));
|
||||||
inv_meat = int(get_number(saveData, "inventory_meat", 0));
|
inv_meat = int(get_number(saveData, "inventory_meat", 0));
|
||||||
inv_skins = int(get_number(saveData, "inventory_skins", 0));
|
inv_skins = int(get_number(saveData, "inventory_skins", 0));
|
||||||
|
inv_feathers = int(get_number(saveData, "inventory_feathers", 0));
|
||||||
|
inv_down = int(get_number(saveData, "inventory_down", 0));
|
||||||
|
inv_incense = int(get_number(saveData, "inventory_incense", 0));
|
||||||
inv_spears = int(get_number(saveData, "inventory_spears", 0));
|
inv_spears = int(get_number(saveData, "inventory_spears", 0));
|
||||||
inv_snares = int(get_number(saveData, "inventory_snares", 0));
|
inv_snares = int(get_number(saveData, "inventory_snares", 0));
|
||||||
inv_axes = int(get_number(saveData, "inventory_axes", 0));
|
inv_axes = int(get_number(saveData, "inventory_axes", 0));
|
||||||
@@ -809,6 +848,9 @@ bool load_game_state() {
|
|||||||
storage_small_game = int(get_number(saveData, "storage_small_game", 0));
|
storage_small_game = int(get_number(saveData, "storage_small_game", 0));
|
||||||
storage_meat = int(get_number(saveData, "storage_meat", 0));
|
storage_meat = int(get_number(saveData, "storage_meat", 0));
|
||||||
storage_skins = int(get_number(saveData, "storage_skins", 0));
|
storage_skins = int(get_number(saveData, "storage_skins", 0));
|
||||||
|
storage_feathers = int(get_number(saveData, "storage_feathers", 0));
|
||||||
|
storage_down = int(get_number(saveData, "storage_down", 0));
|
||||||
|
storage_incense = int(get_number(saveData, "storage_incense", 0));
|
||||||
storage_spears = int(get_number(saveData, "storage_spears", 0));
|
storage_spears = int(get_number(saveData, "storage_spears", 0));
|
||||||
storage_snares = int(get_number(saveData, "storage_snares", 0));
|
storage_snares = int(get_number(saveData, "storage_snares", 0));
|
||||||
storage_axes = int(get_number(saveData, "storage_axes", 0));
|
storage_axes = int(get_number(saveData, "storage_axes", 0));
|
||||||
@@ -900,6 +942,13 @@ bool load_game_state() {
|
|||||||
is_daytime = (current_hour >= 6 && current_hour < 19);
|
is_daytime = (current_hour >= 6 && current_hour < 19);
|
||||||
hour_timer.restart();
|
hour_timer.restart();
|
||||||
|
|
||||||
|
string weather_data;
|
||||||
|
if (saveData.get("weather_data", weather_data) && weather_data.length() > 0) {
|
||||||
|
deserialize_weather(weather_data);
|
||||||
|
} else {
|
||||||
|
init_weather();
|
||||||
|
}
|
||||||
|
|
||||||
string[] treeData = get_string_list_or_split(saveData, "trees_data");
|
string[] treeData = get_string_list_or_split(saveData, "trees_data");
|
||||||
for (uint i = 0; i < treeData.length(); i++) {
|
for (uint i = 0; i < treeData.length(); i++) {
|
||||||
string[]@ parts = treeData[i].split("|");
|
string[]@ parts = treeData[i].split("|");
|
||||||
@@ -1061,6 +1110,13 @@ bool load_game_state() {
|
|||||||
world_mountains.insert_last(mountain);
|
world_mountains.insert_last(mountain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string[] dropData = get_string_list_or_split(saveData, "drops_data");
|
||||||
|
for (uint i = 0; i < dropData.length(); i++) {
|
||||||
|
string[]@ parts = dropData[i].split("|");
|
||||||
|
if (parts.length() < 2) continue;
|
||||||
|
add_world_drop(parse_int(parts[0]), parts[1]);
|
||||||
|
}
|
||||||
|
|
||||||
update_ambience(true);
|
update_ambience(true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,9 +104,15 @@ void expand_regular_area() {
|
|||||||
|
|
||||||
notify("A " + width_desc + " stream flows through the new area at x " + actual_start + ".");
|
notify("A " + width_desc + " stream flows through the new area at x " + actual_start + ".");
|
||||||
} else {
|
} else {
|
||||||
int tree_pos = random(new_start, new_end);
|
// Try to place a tree with proper spacing
|
||||||
Tree@ t = Tree(tree_pos);
|
for (int attempt = 0; attempt < 20; attempt++) {
|
||||||
trees.insert_last(t);
|
int tree_pos = random(new_start, new_end);
|
||||||
|
if (!tree_too_close(tree_pos)) {
|
||||||
|
Tree@ t = Tree(tree_pos);
|
||||||
|
trees.insert_last(t);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
area_expanded_today = true;
|
area_expanded_today = true;
|
||||||
@@ -210,16 +216,49 @@ void attempt_resident_recruitment() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int chance = random(25, 35);
|
// Check if base is full
|
||||||
|
if (residents_count >= MAX_RESIDENTS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recruitment chance based on storage buildings
|
||||||
|
int storage_count = world_storages.length();
|
||||||
|
int min_chance = 50;
|
||||||
|
int max_chance = 60;
|
||||||
|
|
||||||
|
if (storage_count >= 2) {
|
||||||
|
// 2+ storage: 75-100% chance
|
||||||
|
min_chance = 75;
|
||||||
|
max_chance = 100;
|
||||||
|
} else if (storage_count == 1) {
|
||||||
|
// 1 storage: 50-75% chance
|
||||||
|
min_chance = 50;
|
||||||
|
max_chance = 75;
|
||||||
|
}
|
||||||
|
// 0 storage: 50-60% chance (defaults above)
|
||||||
|
|
||||||
|
int chance = random(min_chance, max_chance);
|
||||||
int roll = random(1, 100);
|
int roll = random(1, 100);
|
||||||
if (roll > chance) {
|
if (roll > chance) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int added = random(1, 3);
|
int added = random(1, 3);
|
||||||
|
// Don't exceed cap
|
||||||
|
if (residents_count + added > MAX_RESIDENTS) {
|
||||||
|
added = MAX_RESIDENTS - residents_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (added <= 0) return;
|
||||||
|
|
||||||
residents_count += added;
|
residents_count += added;
|
||||||
string join_message = (added == 1) ? "A survivor joins your base." : "" + added + " survivors join your base.";
|
string join_message = (added == 1) ? "A survivor joins your base." : "" + added + " survivors join your base.";
|
||||||
notify(join_message);
|
notify(join_message);
|
||||||
|
|
||||||
|
// Notify if base is now full
|
||||||
|
if (residents_count >= MAX_RESIDENTS) {
|
||||||
|
notify("Your base is at maximum capacity.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void end_invasion() {
|
void end_invasion() {
|
||||||
@@ -369,14 +408,34 @@ void update_time() {
|
|||||||
}
|
}
|
||||||
attempt_daily_invasion();
|
attempt_daily_invasion();
|
||||||
keep_base_fires_fed();
|
keep_base_fires_fed();
|
||||||
|
update_incense_burning();
|
||||||
|
attempt_hourly_flying_creature_spawn();
|
||||||
check_scheduled_invasion();
|
check_scheduled_invasion();
|
||||||
attempt_blessing();
|
attempt_blessing();
|
||||||
|
check_weather_transition();
|
||||||
|
attempt_resident_collection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Proactive resident defense with slings
|
||||||
|
attempt_resident_sling_defense();
|
||||||
|
|
||||||
// Manage bandits during active invasion
|
// Manage bandits during active invasion
|
||||||
manage_bandits_during_invasion();
|
manage_bandits_during_invasion();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void update_incense_burning() {
|
||||||
|
if (!incense_burning || incense_hours_remaining <= 0) return;
|
||||||
|
|
||||||
|
favor += INCENSE_FAVOR_PER_HOUR;
|
||||||
|
incense_hours_remaining--;
|
||||||
|
|
||||||
|
if (incense_hours_remaining <= 0) {
|
||||||
|
incense_hours_remaining = 0;
|
||||||
|
incense_burning = false;
|
||||||
|
notify("The incense has burned out.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void check_time_input() {
|
void check_time_input() {
|
||||||
if (key_pressed(KEY_T)) {
|
if (key_pressed(KEY_T)) {
|
||||||
screen_reader_speak(get_time_string(), true);
|
screen_reader_speak(get_time_string(), true);
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ string ui_input_box(const string title, const string prompt, const string defaul
|
|||||||
}
|
}
|
||||||
|
|
||||||
int ui_question(const string title, const string prompt) {
|
int ui_question(const string title, const string prompt) {
|
||||||
int result = virtual_question(title, prompt);
|
// Put the prompt in both title (for screen reader) and message (for dialog to work)
|
||||||
|
int result = virtual_question(prompt, prompt);
|
||||||
show_window("Draugnorak");
|
show_window("Draugnorak");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
492
src/weather.nvgt
Normal file
492
src/weather.nvgt
Normal file
@@ -0,0 +1,492 @@
|
|||||||
|
// Weather System
|
||||||
|
// Provides ambient wind, rain, and thunder effects
|
||||||
|
|
||||||
|
// Weather states
|
||||||
|
const int WEATHER_CLEAR = 0;
|
||||||
|
const int WEATHER_WINDY = 1;
|
||||||
|
const int WEATHER_RAINY = 2;
|
||||||
|
const int WEATHER_STORMY = 3;
|
||||||
|
|
||||||
|
// Intensity levels (0 = none, 1-3 = low/medium/high)
|
||||||
|
const int INTENSITY_NONE = 0;
|
||||||
|
const int INTENSITY_LOW = 1;
|
||||||
|
const int INTENSITY_MEDIUM = 2;
|
||||||
|
const int INTENSITY_HIGH = 3;
|
||||||
|
|
||||||
|
// Audio fade settings
|
||||||
|
const int WEATHER_FADE_DURATION = 8000; // 8 seconds for smooth transitions
|
||||||
|
const float WEATHER_MIN_VOLUME = -30.0;
|
||||||
|
const float WEATHER_MAX_VOLUME = 0.0;
|
||||||
|
|
||||||
|
// Rain volume levels by intensity
|
||||||
|
const float RAIN_VOLUME_LIGHT = -18.0;
|
||||||
|
const float RAIN_VOLUME_MODERATE = -10.0;
|
||||||
|
const float RAIN_VOLUME_HEAVY = -3.0;
|
||||||
|
|
||||||
|
// Wind gust settings
|
||||||
|
const int WIND_GUST_MIN_DELAY = 10000; // Min 10 seconds between gusts
|
||||||
|
const int WIND_GUST_MAX_DELAY = 25000; // Max 25 seconds between gusts
|
||||||
|
|
||||||
|
// Thunder timing
|
||||||
|
const int THUNDER_MIN_INTERVAL = 8000; // Min 8 seconds between thunder
|
||||||
|
const int THUNDER_MAX_INTERVAL = 35000; // Max 35 seconds between thunder
|
||||||
|
const int THUNDER_MOVEMENT_SPEED = 2000; // ms per tile movement (slow roll across sky)
|
||||||
|
const float THUNDER_SOUND_VOLUME_STEP = 2.0; // Gentler volume falloff
|
||||||
|
const int THUNDER_SPAWN_DISTANCE_MIN = 20; // Min distance from player
|
||||||
|
const int THUNDER_SPAWN_DISTANCE_MAX = 40; // Max distance from player
|
||||||
|
|
||||||
|
// Weather transition chances (out of 100)
|
||||||
|
const int CHANCE_CLEAR_TO_WINDY = 15;
|
||||||
|
const int CHANCE_CLEAR_TO_RAINY = 4;
|
||||||
|
const int CHANCE_CLEAR_TO_STORMY = 2;
|
||||||
|
const int CHANCE_WINDY_STAY = 55;
|
||||||
|
const int CHANCE_WINDY_TO_CLEAR = 25;
|
||||||
|
const int CHANCE_WINDY_TO_STORMY = 12;
|
||||||
|
const int CHANCE_RAINY_STAY = 45;
|
||||||
|
const int CHANCE_RAINY_TO_STORMY = 25;
|
||||||
|
const int CHANCE_STORMY_STAY = 40;
|
||||||
|
const int CHANCE_STORMY_TO_RAINY = 35;
|
||||||
|
|
||||||
|
// State variables
|
||||||
|
int weather_state = WEATHER_CLEAR;
|
||||||
|
int wind_intensity = INTENSITY_NONE;
|
||||||
|
int rain_intensity = INTENSITY_NONE;
|
||||||
|
bool thunder_enabled = false;
|
||||||
|
|
||||||
|
// Wind gust state
|
||||||
|
timer wind_gust_timer;
|
||||||
|
int next_wind_gust_delay = 0;
|
||||||
|
int wind_sound_handle = -1;
|
||||||
|
|
||||||
|
// Rain state
|
||||||
|
int rain_sound_handle = -1;
|
||||||
|
|
||||||
|
// Fade state for rain
|
||||||
|
bool rain_fading = false;
|
||||||
|
float rain_fade_from_volume = WEATHER_MIN_VOLUME;
|
||||||
|
float rain_fade_to_volume = WEATHER_MIN_VOLUME;
|
||||||
|
timer rain_fade_timer;
|
||||||
|
|
||||||
|
// Thunder object state
|
||||||
|
class ThunderStrike {
|
||||||
|
int position;
|
||||||
|
int direction; // -1 = moving west, 1 = moving east
|
||||||
|
int sound_handle;
|
||||||
|
timer movement_timer;
|
||||||
|
|
||||||
|
ThunderStrike(int pos, int dir, int handle) {
|
||||||
|
position = pos;
|
||||||
|
direction = dir;
|
||||||
|
sound_handle = handle;
|
||||||
|
movement_timer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
void update() {
|
||||||
|
if (movement_timer.elapsed >= THUNDER_MOVEMENT_SPEED) {
|
||||||
|
position += direction;
|
||||||
|
movement_timer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update sound position
|
||||||
|
if (sound_handle != -1 && p.sound_is_active(sound_handle)) {
|
||||||
|
p.update_sound_1d(sound_handle, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_finished() {
|
||||||
|
return sound_handle == -1 || !p.sound_is_active(sound_handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ThunderStrike@[] active_thunder;
|
||||||
|
timer thunder_timer;
|
||||||
|
int next_thunder_interval = 0;
|
||||||
|
|
||||||
|
// Sound file paths
|
||||||
|
string[] wind_sounds = {
|
||||||
|
"", // INTENSITY_NONE placeholder
|
||||||
|
"sounds/nature/wind_low.ogg",
|
||||||
|
"sounds/nature/wind_medium.ogg",
|
||||||
|
"sounds/nature/wind_high.ogg"
|
||||||
|
};
|
||||||
|
|
||||||
|
string[] thunder_sounds = {
|
||||||
|
"sounds/nature/thunder_low.ogg",
|
||||||
|
"sounds/nature/thunder_medium.ogg",
|
||||||
|
"sounds/nature/thunder_high.ogg"
|
||||||
|
};
|
||||||
|
|
||||||
|
const string RAIN_SOUND = "sounds/nature/rain.ogg";
|
||||||
|
|
||||||
|
void init_weather() {
|
||||||
|
weather_state = WEATHER_CLEAR;
|
||||||
|
wind_intensity = INTENSITY_NONE;
|
||||||
|
rain_intensity = INTENSITY_NONE;
|
||||||
|
thunder_enabled = false;
|
||||||
|
wind_sound_handle = -1;
|
||||||
|
rain_sound_handle = -1;
|
||||||
|
rain_fading = false;
|
||||||
|
wind_gust_timer.restart();
|
||||||
|
next_wind_gust_delay = 0;
|
||||||
|
active_thunder.resize(0);
|
||||||
|
thunder_timer.restart();
|
||||||
|
next_thunder_interval = random(THUNDER_MIN_INTERVAL, THUNDER_MAX_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_weather() {
|
||||||
|
update_wind_gusts();
|
||||||
|
update_rain_fade();
|
||||||
|
update_thunder();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called each game hour from time_system
|
||||||
|
void check_weather_transition() {
|
||||||
|
int roll = random(1, 100);
|
||||||
|
|
||||||
|
if (weather_state == WEATHER_CLEAR) {
|
||||||
|
if (roll <= CHANCE_CLEAR_TO_STORMY) {
|
||||||
|
start_storm();
|
||||||
|
} else if (roll <= CHANCE_CLEAR_TO_STORMY + CHANCE_CLEAR_TO_RAINY) {
|
||||||
|
start_rain();
|
||||||
|
} else if (roll <= CHANCE_CLEAR_TO_STORMY + CHANCE_CLEAR_TO_RAINY + CHANCE_CLEAR_TO_WINDY) {
|
||||||
|
start_wind();
|
||||||
|
}
|
||||||
|
} else if (weather_state == WEATHER_WINDY) {
|
||||||
|
if (roll <= CHANCE_WINDY_TO_CLEAR) {
|
||||||
|
clear_weather();
|
||||||
|
} else if (roll <= CHANCE_WINDY_TO_CLEAR + CHANCE_WINDY_TO_STORMY) {
|
||||||
|
start_storm();
|
||||||
|
} else if (roll <= CHANCE_WINDY_TO_CLEAR + CHANCE_WINDY_TO_STORMY + CHANCE_WINDY_STAY) {
|
||||||
|
// Stay windy, maybe change intensity
|
||||||
|
maybe_change_wind_intensity();
|
||||||
|
}
|
||||||
|
} else if (weather_state == WEATHER_RAINY) {
|
||||||
|
if (roll <= CHANCE_RAINY_TO_STORMY) {
|
||||||
|
// Escalate to storm
|
||||||
|
weather_state = WEATHER_STORMY;
|
||||||
|
thunder_enabled = true;
|
||||||
|
thunder_timer.restart();
|
||||||
|
next_thunder_interval = random(THUNDER_MIN_INTERVAL, THUNDER_MAX_INTERVAL);
|
||||||
|
// Maybe increase rain
|
||||||
|
if (rain_intensity < INTENSITY_HIGH && random(1, 100) <= 50) {
|
||||||
|
fade_rain_to_intensity(rain_intensity + 1);
|
||||||
|
}
|
||||||
|
} else if (roll <= CHANCE_RAINY_TO_STORMY + CHANCE_RAINY_STAY) {
|
||||||
|
// Stay rainy, maybe change intensity
|
||||||
|
maybe_change_rain_intensity();
|
||||||
|
} else {
|
||||||
|
// Clear up
|
||||||
|
clear_weather();
|
||||||
|
}
|
||||||
|
} else if (weather_state == WEATHER_STORMY) {
|
||||||
|
if (roll <= CHANCE_STORMY_TO_RAINY) {
|
||||||
|
// De-escalate to just rain
|
||||||
|
weather_state = WEATHER_RAINY;
|
||||||
|
thunder_enabled = false;
|
||||||
|
// Clear any active thunder
|
||||||
|
for (uint i = 0; i < active_thunder.length(); i++) {
|
||||||
|
if (active_thunder[i].sound_handle != -1) {
|
||||||
|
p.destroy_sound(active_thunder[i].sound_handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
active_thunder.resize(0);
|
||||||
|
// Maybe reduce rain
|
||||||
|
if (rain_intensity > INTENSITY_LOW && random(1, 100) <= 40) {
|
||||||
|
fade_rain_to_intensity(rain_intensity - 1);
|
||||||
|
}
|
||||||
|
// Stop wind if present
|
||||||
|
if (wind_intensity > INTENSITY_NONE) {
|
||||||
|
wind_intensity = INTENSITY_NONE;
|
||||||
|
if (wind_sound_handle != -1) {
|
||||||
|
p.destroy_sound(wind_sound_handle);
|
||||||
|
wind_sound_handle = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (roll <= CHANCE_STORMY_TO_RAINY + CHANCE_STORMY_STAY) {
|
||||||
|
// Stay stormy, maybe change intensities
|
||||||
|
maybe_change_storm_intensity();
|
||||||
|
} else {
|
||||||
|
// Clear up
|
||||||
|
clear_weather();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void start_wind() {
|
||||||
|
weather_state = WEATHER_WINDY;
|
||||||
|
wind_intensity = random(INTENSITY_LOW, INTENSITY_MEDIUM);
|
||||||
|
wind_gust_timer.restart();
|
||||||
|
next_wind_gust_delay = random(WIND_GUST_MIN_DELAY, WIND_GUST_MAX_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
void start_rain() {
|
||||||
|
weather_state = WEATHER_RAINY;
|
||||||
|
int new_intensity = random(INTENSITY_LOW, INTENSITY_MEDIUM);
|
||||||
|
fade_rain_to_intensity(new_intensity);
|
||||||
|
}
|
||||||
|
|
||||||
|
void start_storm() {
|
||||||
|
weather_state = WEATHER_STORMY;
|
||||||
|
|
||||||
|
// Start or increase wind
|
||||||
|
wind_intensity = random(INTENSITY_MEDIUM, INTENSITY_HIGH);
|
||||||
|
wind_gust_timer.restart();
|
||||||
|
next_wind_gust_delay = random(WIND_GUST_MIN_DELAY, WIND_GUST_MAX_DELAY);
|
||||||
|
|
||||||
|
// Start rain
|
||||||
|
int new_rain = random(INTENSITY_MEDIUM, INTENSITY_HIGH);
|
||||||
|
fade_rain_to_intensity(new_rain);
|
||||||
|
|
||||||
|
// Enable thunder
|
||||||
|
thunder_enabled = true;
|
||||||
|
thunder_timer.restart();
|
||||||
|
next_thunder_interval = random(THUNDER_MIN_INTERVAL, THUNDER_MAX_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear_weather() {
|
||||||
|
weather_state = WEATHER_CLEAR;
|
||||||
|
thunder_enabled = false;
|
||||||
|
|
||||||
|
wind_intensity = INTENSITY_NONE;
|
||||||
|
if (wind_sound_handle != -1) {
|
||||||
|
p.destroy_sound(wind_sound_handle);
|
||||||
|
wind_sound_handle = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rain_intensity > INTENSITY_NONE) {
|
||||||
|
fade_rain_to_intensity(INTENSITY_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any active thunder
|
||||||
|
for (uint i = 0; i < active_thunder.length(); i++) {
|
||||||
|
if (active_thunder[i].sound_handle != -1) {
|
||||||
|
p.destroy_sound(active_thunder[i].sound_handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
active_thunder.resize(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void maybe_change_wind_intensity() {
|
||||||
|
if (random(1, 100) <= 30) {
|
||||||
|
int change = random(-1, 1);
|
||||||
|
int new_intensity = wind_intensity + change;
|
||||||
|
if (new_intensity < INTENSITY_LOW) new_intensity = INTENSITY_LOW;
|
||||||
|
if (new_intensity > INTENSITY_HIGH) new_intensity = INTENSITY_HIGH;
|
||||||
|
wind_intensity = new_intensity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void maybe_change_rain_intensity() {
|
||||||
|
if (random(1, 100) <= 30) {
|
||||||
|
int change = random(-1, 1);
|
||||||
|
int new_intensity = rain_intensity + change;
|
||||||
|
if (new_intensity < INTENSITY_LOW) new_intensity = INTENSITY_LOW;
|
||||||
|
if (new_intensity > INTENSITY_HIGH) new_intensity = INTENSITY_HIGH;
|
||||||
|
if (new_intensity != rain_intensity) {
|
||||||
|
fade_rain_to_intensity(new_intensity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void maybe_change_storm_intensity() {
|
||||||
|
// Possibly change wind
|
||||||
|
if (random(1, 100) <= 25) {
|
||||||
|
int new_wind = wind_intensity + random(-1, 1);
|
||||||
|
if (new_wind < INTENSITY_LOW) new_wind = INTENSITY_LOW;
|
||||||
|
if (new_wind > INTENSITY_HIGH) new_wind = INTENSITY_HIGH;
|
||||||
|
wind_intensity = new_wind;
|
||||||
|
}
|
||||||
|
// Possibly change rain
|
||||||
|
if (random(1, 100) <= 25) {
|
||||||
|
int new_rain = rain_intensity + random(-1, 1);
|
||||||
|
if (new_rain < INTENSITY_LOW) new_rain = INTENSITY_LOW;
|
||||||
|
if (new_rain > INTENSITY_HIGH) new_rain = INTENSITY_HIGH;
|
||||||
|
if (new_rain != rain_intensity) {
|
||||||
|
fade_rain_to_intensity(new_rain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wind gust implementation
|
||||||
|
void update_wind_gusts() {
|
||||||
|
if (wind_intensity == INTENSITY_NONE) return;
|
||||||
|
|
||||||
|
// Check if it's time for next gust
|
||||||
|
if (wind_gust_timer.elapsed >= next_wind_gust_delay) {
|
||||||
|
play_wind_gust();
|
||||||
|
wind_gust_timer.restart();
|
||||||
|
next_wind_gust_delay = random(WIND_GUST_MIN_DELAY, WIND_GUST_MAX_DELAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void play_wind_gust() {
|
||||||
|
if (wind_intensity == INTENSITY_NONE || wind_intensity > INTENSITY_HIGH) return;
|
||||||
|
|
||||||
|
// Play the appropriate wind sound once (non-looping)
|
||||||
|
wind_sound_handle = p.play_stationary(wind_sounds[wind_intensity], false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rain fade implementation
|
||||||
|
float get_rain_target_volume(int intensity) {
|
||||||
|
if (intensity == INTENSITY_NONE) return WEATHER_MIN_VOLUME;
|
||||||
|
if (intensity == INTENSITY_LOW) return RAIN_VOLUME_LIGHT;
|
||||||
|
if (intensity == INTENSITY_MEDIUM) return RAIN_VOLUME_MODERATE;
|
||||||
|
return RAIN_VOLUME_HEAVY;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fade_rain_to_intensity(int new_intensity) {
|
||||||
|
if (new_intensity == rain_intensity && !rain_fading) return;
|
||||||
|
|
||||||
|
// Complete any current fade
|
||||||
|
if (rain_fading) {
|
||||||
|
complete_rain_fade();
|
||||||
|
}
|
||||||
|
|
||||||
|
rain_fade_from_volume = get_rain_target_volume(rain_intensity);
|
||||||
|
rain_fade_to_volume = get_rain_target_volume(new_intensity);
|
||||||
|
|
||||||
|
// Start rain sound if not playing
|
||||||
|
if (rain_sound_handle == -1 && new_intensity != INTENSITY_NONE) {
|
||||||
|
rain_sound_handle = p.play_stationary(RAIN_SOUND, true);
|
||||||
|
p.update_sound_start_values(rain_sound_handle, 0.0, WEATHER_MIN_VOLUME, 1.0);
|
||||||
|
rain_fade_from_volume = WEATHER_MIN_VOLUME;
|
||||||
|
}
|
||||||
|
|
||||||
|
rain_fading = true;
|
||||||
|
rain_fade_timer.restart();
|
||||||
|
rain_intensity = new_intensity; // Track target intensity
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_rain_fade() {
|
||||||
|
if (!rain_fading) return;
|
||||||
|
if (rain_sound_handle == -1) {
|
||||||
|
rain_fading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float progress = float(rain_fade_timer.elapsed) / float(WEATHER_FADE_DURATION);
|
||||||
|
if (progress > 1.0) progress = 1.0;
|
||||||
|
|
||||||
|
float current_vol = rain_fade_from_volume + ((rain_fade_to_volume - rain_fade_from_volume) * progress);
|
||||||
|
p.update_sound_start_values(rain_sound_handle, 0.0, current_vol, 1.0);
|
||||||
|
|
||||||
|
if (progress >= 1.0) {
|
||||||
|
complete_rain_fade();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void complete_rain_fade() {
|
||||||
|
rain_fading = false;
|
||||||
|
|
||||||
|
// If faded to silence, destroy sound
|
||||||
|
if (rain_fade_to_volume <= WEATHER_MIN_VOLUME && rain_sound_handle != -1) {
|
||||||
|
p.destroy_sound(rain_sound_handle);
|
||||||
|
rain_sound_handle = -1;
|
||||||
|
rain_intensity = INTENSITY_NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thunder implementation
|
||||||
|
void update_thunder() {
|
||||||
|
// Update existing thunder strikes
|
||||||
|
for (uint i = 0; i < active_thunder.length(); i++) {
|
||||||
|
active_thunder[i].update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove finished thunder strikes
|
||||||
|
for (uint i = 0; i < active_thunder.length(); i++) {
|
||||||
|
if (active_thunder[i].is_finished()) {
|
||||||
|
active_thunder.remove_at(i);
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!thunder_enabled) return;
|
||||||
|
|
||||||
|
// Check if it's time for new thunder
|
||||||
|
if (thunder_timer.elapsed >= next_thunder_interval) {
|
||||||
|
spawn_thunder();
|
||||||
|
thunder_timer.restart();
|
||||||
|
next_thunder_interval = random(THUNDER_MIN_INTERVAL, THUNDER_MAX_INTERVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void spawn_thunder() {
|
||||||
|
// Pick random thunder sound
|
||||||
|
int thunder_type = random(0, thunder_sounds.length() - 1);
|
||||||
|
string thunder_file = thunder_sounds[thunder_type];
|
||||||
|
|
||||||
|
// Spawn thunder at random distance from player
|
||||||
|
int distance = random(THUNDER_SPAWN_DISTANCE_MIN, THUNDER_SPAWN_DISTANCE_MAX);
|
||||||
|
// Randomly place to left or right of player
|
||||||
|
int direction = random(0, 1) == 0 ? -1 : 1;
|
||||||
|
int thunder_pos = x + (distance * direction);
|
||||||
|
|
||||||
|
// Play sound at position with custom volume step
|
||||||
|
int handle = play_1d_with_volume_step(thunder_file, x, thunder_pos, false, THUNDER_SOUND_VOLUME_STEP);
|
||||||
|
|
||||||
|
if (handle != -1) {
|
||||||
|
ThunderStrike@ strike = ThunderStrike(thunder_pos, direction, handle);
|
||||||
|
active_thunder.insert_last(strike);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop_all_weather_sounds() {
|
||||||
|
if (wind_sound_handle != -1) {
|
||||||
|
p.destroy_sound(wind_sound_handle);
|
||||||
|
wind_sound_handle = -1;
|
||||||
|
}
|
||||||
|
if (rain_sound_handle != -1) {
|
||||||
|
p.destroy_sound(rain_sound_handle);
|
||||||
|
rain_sound_handle = -1;
|
||||||
|
}
|
||||||
|
for (uint i = 0; i < active_thunder.length(); i++) {
|
||||||
|
if (active_thunder[i].sound_handle != -1) {
|
||||||
|
p.destroy_sound(active_thunder[i].sound_handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
active_thunder.resize(0);
|
||||||
|
rain_fading = false;
|
||||||
|
wind_intensity = INTENSITY_NONE;
|
||||||
|
rain_intensity = INTENSITY_NONE;
|
||||||
|
thunder_enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save/Load functions
|
||||||
|
string serialize_weather() {
|
||||||
|
return weather_state + "|" + wind_intensity + "|" + rain_intensity + "|" + (thunder_enabled ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void deserialize_weather(string data) {
|
||||||
|
string[]@ parts = data.split("|");
|
||||||
|
if (parts.length() < 4) {
|
||||||
|
init_weather();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop any current sounds first
|
||||||
|
stop_all_weather_sounds();
|
||||||
|
|
||||||
|
weather_state = parse_int(parts[0]);
|
||||||
|
wind_intensity = parse_int(parts[1]);
|
||||||
|
int saved_rain = parse_int(parts[2]);
|
||||||
|
thunder_enabled = parse_int(parts[3]) == 1;
|
||||||
|
|
||||||
|
// Restore wind gust timer
|
||||||
|
if (wind_intensity > INTENSITY_NONE && wind_intensity <= INTENSITY_HIGH) {
|
||||||
|
wind_gust_timer.restart();
|
||||||
|
next_wind_gust_delay = random(WIND_GUST_MIN_DELAY, WIND_GUST_MAX_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore rain at saved intensity (no fade, instant)
|
||||||
|
if (saved_rain > INTENSITY_NONE && saved_rain <= INTENSITY_HIGH) {
|
||||||
|
rain_sound_handle = p.play_stationary(RAIN_SOUND, true);
|
||||||
|
float vol = get_rain_target_volume(saved_rain);
|
||||||
|
p.update_sound_start_values(rain_sound_handle, 0.0, vol, 1.0);
|
||||||
|
rain_intensity = saved_rain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset thunder timer
|
||||||
|
thunder_timer.restart();
|
||||||
|
next_thunder_interval = random(THUNDER_MIN_INTERVAL, THUNDER_MAX_INTERVAL);
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ int residents_count = 0;
|
|||||||
|
|
||||||
string[] zombie_sounds = {"sounds/enemies/zombie1.ogg"};
|
string[] zombie_sounds = {"sounds/enemies/zombie1.ogg"};
|
||||||
string[] bandit_sounds = {"sounds/enemies/bandit1.ogg", "sounds/enemies/bandit2.ogg"};
|
string[] bandit_sounds = {"sounds/enemies/bandit1.ogg", "sounds/enemies/bandit2.ogg"};
|
||||||
|
string[] goose_sounds = {"sounds/game/goose.ogg"};
|
||||||
|
|
||||||
class Zombie {
|
class Zombie {
|
||||||
int position;
|
int position;
|
||||||
@@ -82,11 +83,174 @@ class Bandit {
|
|||||||
}
|
}
|
||||||
Bandit@[] bandits;
|
Bandit@[] bandits;
|
||||||
|
|
||||||
|
class FlyingCreatureConfig {
|
||||||
|
string id;
|
||||||
|
string drop_type;
|
||||||
|
string[] sounds;
|
||||||
|
string fall_sound;
|
||||||
|
string impact_sound;
|
||||||
|
int health;
|
||||||
|
int move_interval_min;
|
||||||
|
int move_interval_max;
|
||||||
|
int min_height;
|
||||||
|
int max_height;
|
||||||
|
float sound_volume_step;
|
||||||
|
int sound_delay_min;
|
||||||
|
int sound_delay_max;
|
||||||
|
int fall_speed;
|
||||||
|
int fly_away_chance;
|
||||||
|
int max_dist_from_water;
|
||||||
|
int hourly_spawn_chance;
|
||||||
|
int max_count;
|
||||||
|
int sight_range;
|
||||||
|
bool flee_on_sight;
|
||||||
|
}
|
||||||
|
FlyingCreatureConfig@[] flying_creature_configs;
|
||||||
|
|
||||||
|
class FlyingCreature {
|
||||||
|
int position;
|
||||||
|
int health;
|
||||||
|
int height;
|
||||||
|
string state; // "flying", "falling"
|
||||||
|
int area_start;
|
||||||
|
int area_end;
|
||||||
|
string creature_type;
|
||||||
|
int sound_handle;
|
||||||
|
int fall_sound_handle;
|
||||||
|
timer move_timer;
|
||||||
|
timer sound_timer;
|
||||||
|
timer fall_timer;
|
||||||
|
int next_move_delay;
|
||||||
|
int next_sound_delay;
|
||||||
|
string voice_sound;
|
||||||
|
bool fading_out;
|
||||||
|
bool ready_to_remove;
|
||||||
|
timer fade_timer;
|
||||||
|
|
||||||
|
FlyingCreature(string type, int pos, int home_start, int home_end, FlyingCreatureConfig@ cfg) {
|
||||||
|
position = pos;
|
||||||
|
health = cfg.health;
|
||||||
|
height = random(cfg.min_height, cfg.max_height);
|
||||||
|
state = "flying";
|
||||||
|
area_start = home_start;
|
||||||
|
area_end = home_end;
|
||||||
|
creature_type = type;
|
||||||
|
sound_handle = -1;
|
||||||
|
fall_sound_handle = -1;
|
||||||
|
|
||||||
|
if (cfg.sounds.length() > 0) {
|
||||||
|
voice_sound = cfg.sounds[random(0, cfg.sounds.length() - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
move_timer.restart();
|
||||||
|
sound_timer.restart();
|
||||||
|
|
||||||
|
next_move_delay = random(cfg.move_interval_min, cfg.move_interval_max);
|
||||||
|
next_sound_delay = random(cfg.sound_delay_min, cfg.sound_delay_max);
|
||||||
|
fading_out = false;
|
||||||
|
ready_to_remove = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FlyingCreature@[] flying_creatures;
|
||||||
|
|
||||||
string get_random_small_game() {
|
string get_random_small_game() {
|
||||||
int index = random(0, small_game_types.length() - 1);
|
int index = random(0, small_game_types.length() - 1);
|
||||||
return small_game_types[index];
|
return small_game_types[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class WorldDrop {
|
||||||
|
int position;
|
||||||
|
string type;
|
||||||
|
int sound_handle;
|
||||||
|
|
||||||
|
WorldDrop(int pos, string t) {
|
||||||
|
position = pos;
|
||||||
|
type = t;
|
||||||
|
sound_handle = -1;
|
||||||
|
// Start looping item sound at position
|
||||||
|
sound_handle = p.play_1d("sounds/items/item.ogg", x, position, true);
|
||||||
|
if (sound_handle != -1) {
|
||||||
|
p.update_sound_positioning_values(sound_handle, -1.0, 3.0, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void update() {
|
||||||
|
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
|
||||||
|
sound_handle = p.play_1d("sounds/items/item.ogg", x, position, true);
|
||||||
|
if (sound_handle != -1) {
|
||||||
|
p.update_sound_positioning_values(sound_handle, -1.0, 3.0, true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Update source position for 1d sound
|
||||||
|
p.update_sound_1d(sound_handle, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void destroy() {
|
||||||
|
if (sound_handle != -1) {
|
||||||
|
p.destroy_sound(sound_handle);
|
||||||
|
sound_handle = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WorldDrop@[] world_drops;
|
||||||
|
|
||||||
|
void add_world_drop(int pos, string type) {
|
||||||
|
WorldDrop@ d = WorldDrop(pos, type);
|
||||||
|
world_drops.insert_last(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_world_drops() {
|
||||||
|
for (uint i = 0; i < world_drops.length(); i++) {
|
||||||
|
world_drops[i].update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WorldDrop@ get_drop_at(int pos) {
|
||||||
|
for (uint i = 0; i < world_drops.length(); i++) {
|
||||||
|
if (world_drops[i].position == pos) {
|
||||||
|
return @world_drops[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove_drop_at(int pos) {
|
||||||
|
for (uint i = 0; i < world_drops.length(); i++) {
|
||||||
|
if (world_drops[i].position == pos) {
|
||||||
|
world_drops[i].destroy();
|
||||||
|
world_drops.remove_at(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear_world_drops() {
|
||||||
|
for (uint i = 0; i < world_drops.length(); i++) {
|
||||||
|
world_drops[i].destroy();
|
||||||
|
}
|
||||||
|
world_drops.resize(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool try_pickup_small_game(string game_type) {
|
||||||
|
if (inv_small_game >= get_personal_stack_limit()) {
|
||||||
|
screen_reader_speak("You can't carry any more small game.", true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
inv_small_game++;
|
||||||
|
inv_small_game_types.insert_last(game_type);
|
||||||
|
screen_reader_speak("Picked up " + game_type + ".", true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool try_pickup_world_drop(WorldDrop@ drop) {
|
||||||
|
if (get_flying_creature_config_by_drop_type(drop.type) !is null) {
|
||||||
|
return try_pickup_small_game(drop.type);
|
||||||
|
}
|
||||||
|
screen_reader_speak("Picked up " + drop.type + ".", true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
class WorldSnare {
|
class WorldSnare {
|
||||||
int position;
|
int position;
|
||||||
bool has_catch;
|
bool has_catch;
|
||||||
@@ -843,9 +1007,9 @@ bool try_attack_player_bandit(Bandit@ bandit) {
|
|||||||
|
|
||||||
// Play weapon swing sound based on bandit's weapon
|
// Play weapon swing sound based on bandit's weapon
|
||||||
if (bandit.weapon_type == "spear") {
|
if (bandit.weapon_type == "spear") {
|
||||||
p.play_stationary("sounds/weapons/spear_swing.ogg", false);
|
play_creature_attack_sound("sounds/weapons/spear_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
||||||
} else if (bandit.weapon_type == "axe") {
|
} else if (bandit.weapon_type == "axe") {
|
||||||
p.play_stationary("sounds/weapons/axe_swing.ogg", false);
|
play_creature_attack_sound("sounds/weapons/axe_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
||||||
}
|
}
|
||||||
|
|
||||||
int damage = random(BANDIT_DAMAGE_MIN, BANDIT_DAMAGE_MAX);
|
int damage = random(BANDIT_DAMAGE_MIN, BANDIT_DAMAGE_MAX);
|
||||||
@@ -856,9 +1020,9 @@ bool try_attack_player_bandit(Bandit@ bandit) {
|
|||||||
|
|
||||||
// Play hit sound
|
// Play hit sound
|
||||||
if (bandit.weapon_type == "spear") {
|
if (bandit.weapon_type == "spear") {
|
||||||
p.play_stationary("sounds/weapons/spear_hit.ogg", false);
|
play_creature_attack_sound("sounds/weapons/spear_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
||||||
} else if (bandit.weapon_type == "axe") {
|
} else if (bandit.weapon_type == "axe") {
|
||||||
p.play_stationary("sounds/weapons/axe_hit.ogg", false);
|
play_creature_attack_sound("sounds/weapons/axe_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -877,11 +1041,11 @@ void try_attack_barricade_bandit(Bandit@ bandit) {
|
|||||||
|
|
||||||
// Play weapon swing sound
|
// Play weapon swing sound
|
||||||
if (bandit.weapon_type == "spear") {
|
if (bandit.weapon_type == "spear") {
|
||||||
p.play_stationary("sounds/weapons/spear_swing.ogg", false);
|
play_creature_attack_sound("sounds/weapons/spear_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
||||||
p.play_stationary("sounds/weapons/spear_hit.ogg", false);
|
play_creature_attack_sound("sounds/weapons/spear_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
||||||
} else if (bandit.weapon_type == "axe") {
|
} else if (bandit.weapon_type == "axe") {
|
||||||
p.play_stationary("sounds/weapons/axe_swing.ogg", false);
|
play_creature_attack_sound("sounds/weapons/axe_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
||||||
p.play_stationary("sounds/weapons/axe_hit.ogg", false);
|
play_creature_attack_sound("sounds/weapons/axe_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resident defense counter-attack
|
// Resident defense counter-attack
|
||||||
@@ -1261,3 +1425,327 @@ void clear_mountains() {
|
|||||||
}
|
}
|
||||||
world_mountains.resize(0);
|
world_mountains.resize(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flying Creature Functions
|
||||||
|
|
||||||
|
void init_flying_creature_configs() {
|
||||||
|
flying_creature_configs.resize(0);
|
||||||
|
|
||||||
|
FlyingCreatureConfig@ goose_cfg = FlyingCreatureConfig();
|
||||||
|
goose_cfg.id = "goose";
|
||||||
|
goose_cfg.drop_type = "goose";
|
||||||
|
goose_cfg.sounds = goose_sounds;
|
||||||
|
goose_cfg.fall_sound = "sounds/actions/falling.ogg";
|
||||||
|
goose_cfg.impact_sound = "sounds/game/game_falls.ogg";
|
||||||
|
goose_cfg.health = GOOSE_HEALTH;
|
||||||
|
goose_cfg.move_interval_min = GOOSE_MOVE_INTERVAL_MIN;
|
||||||
|
goose_cfg.move_interval_max = GOOSE_MOVE_INTERVAL_MAX;
|
||||||
|
goose_cfg.min_height = GOOSE_FLYING_HEIGHT_MIN;
|
||||||
|
goose_cfg.max_height = GOOSE_FLYING_HEIGHT_MAX;
|
||||||
|
goose_cfg.sound_volume_step = GOOSE_SOUND_VOLUME_STEP;
|
||||||
|
goose_cfg.sound_delay_min = GOOSE_FLIGHT_SOUND_DELAY_MIN;
|
||||||
|
goose_cfg.sound_delay_max = GOOSE_FLIGHT_SOUND_DELAY_MAX;
|
||||||
|
goose_cfg.fall_speed = GOOSE_FALL_SPEED;
|
||||||
|
goose_cfg.fly_away_chance = GOOSE_FLY_AWAY_CHANCE;
|
||||||
|
goose_cfg.max_dist_from_water = GOOSE_MAX_DIST_FROM_WATER;
|
||||||
|
goose_cfg.hourly_spawn_chance = GOOSE_HOURLY_SPAWN_CHANCE;
|
||||||
|
goose_cfg.max_count = GOOSE_MAX_COUNT;
|
||||||
|
goose_cfg.sight_range = GOOSE_SIGHT_RANGE;
|
||||||
|
goose_cfg.flee_on_sight = false;
|
||||||
|
flying_creature_configs.insert_last(goose_cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
FlyingCreatureConfig@ get_flying_creature_config(string creature_type) {
|
||||||
|
for (uint i = 0; i < flying_creature_configs.length(); i++) {
|
||||||
|
if (flying_creature_configs[i].id == creature_type) {
|
||||||
|
return @flying_creature_configs[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlyingCreatureConfig@ get_flying_creature_config_by_drop_type(string drop_type) {
|
||||||
|
for (uint i = 0; i < flying_creature_configs.length(); i++) {
|
||||||
|
if (flying_creature_configs[i].drop_type == drop_type) {
|
||||||
|
return @flying_creature_configs[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear_flying_creatures() {
|
||||||
|
for (uint i = 0; i < flying_creatures.length(); i++) {
|
||||||
|
if (flying_creatures[i].sound_handle != -1) {
|
||||||
|
p.destroy_sound(flying_creatures[i].sound_handle);
|
||||||
|
flying_creatures[i].sound_handle = -1;
|
||||||
|
}
|
||||||
|
if (flying_creatures[i].fall_sound_handle != -1) {
|
||||||
|
p.destroy_sound(flying_creatures[i].fall_sound_handle);
|
||||||
|
flying_creatures[i].fall_sound_handle = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flying_creatures.resize(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
FlyingCreature@ get_flying_creature_at(int pos) {
|
||||||
|
for (uint i = 0; i < flying_creatures.length(); i++) {
|
||||||
|
if (flying_creatures[i].position == pos) {
|
||||||
|
return @flying_creatures[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_flying_creature_count(string creature_type) {
|
||||||
|
int count = 0;
|
||||||
|
for (uint i = 0; i < flying_creatures.length(); i++) {
|
||||||
|
if (flying_creatures[i].creature_type == creature_type) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_random_flying_creature_area(FlyingCreatureConfig@ cfg, int &out area_start, int &out area_end) {
|
||||||
|
int stream_count = int(world_streams.length());
|
||||||
|
int mountain_stream_count = 0;
|
||||||
|
for (uint i = 0; i < world_mountains.length(); i++) {
|
||||||
|
mountain_stream_count += int(world_mountains[i].stream_positions.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
int total_areas = stream_count + mountain_stream_count;
|
||||||
|
if (total_areas <= 0) return false;
|
||||||
|
|
||||||
|
int pick = random(0, total_areas - 1);
|
||||||
|
if (pick < stream_count) {
|
||||||
|
area_start = world_streams[pick].start_position;
|
||||||
|
area_end = world_streams[pick].end_position;
|
||||||
|
} else {
|
||||||
|
pick -= stream_count;
|
||||||
|
for (uint i = 0; i < world_mountains.length(); i++) {
|
||||||
|
int local_count = int(world_mountains[i].stream_positions.length());
|
||||||
|
if (pick < local_count) {
|
||||||
|
int stream_pos = world_mountains[i].start_position + world_mountains[i].stream_positions[pick];
|
||||||
|
area_start = stream_pos;
|
||||||
|
area_end = stream_pos;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pick -= local_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
area_start -= cfg.max_dist_from_water;
|
||||||
|
area_end += cfg.max_dist_from_water;
|
||||||
|
if (area_start < 0) area_start = 0;
|
||||||
|
if (area_end >= MAP_SIZE) area_end = MAP_SIZE - 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool find_flying_creature_spawn(FlyingCreatureConfig@ cfg, int &out spawn_x, int &out area_start, int &out area_end) {
|
||||||
|
if (!get_random_flying_creature_area(cfg, area_start, area_end)) return false;
|
||||||
|
|
||||||
|
for (int attempts = 0; attempts < 20; attempts++) {
|
||||||
|
int candidate = random(area_start, area_end);
|
||||||
|
if (get_flying_creature_at(candidate) == null) {
|
||||||
|
spawn_x = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fly_away_flying_creature(FlyingCreature@ creature, FlyingCreatureConfig@ cfg) {
|
||||||
|
creature.state = "fading";
|
||||||
|
creature.fading_out = true;
|
||||||
|
creature.ready_to_remove = false;
|
||||||
|
creature.fade_timer.restart();
|
||||||
|
creature.health = 0;
|
||||||
|
|
||||||
|
if (creature.sound_handle == -1 || !p.sound_is_active(creature.sound_handle)) {
|
||||||
|
creature.ready_to_remove = true;
|
||||||
|
}
|
||||||
|
if (creature.fall_sound_handle != -1) {
|
||||||
|
p.destroy_sound(creature.fall_sound_handle);
|
||||||
|
creature.fall_sound_handle = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool spawn_flying_creature(string creature_type) {
|
||||||
|
FlyingCreatureConfig@ cfg = get_flying_creature_config(creature_type);
|
||||||
|
if (cfg is null) return false;
|
||||||
|
|
||||||
|
int spawn_x = -1;
|
||||||
|
int area_start = 0;
|
||||||
|
int area_end = 0;
|
||||||
|
|
||||||
|
if (!find_flying_creature_spawn(cfg, spawn_x, area_start, area_end)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlyingCreature@ c = FlyingCreature(creature_type, spawn_x, area_start, area_end, cfg);
|
||||||
|
flying_creatures.insert_last(c);
|
||||||
|
c.sound_handle = play_creature_voice(c.voice_sound, x, spawn_x, cfg.sound_volume_step);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_flying_creature(FlyingCreature@ creature) {
|
||||||
|
FlyingCreatureConfig@ cfg = get_flying_creature_config(creature.creature_type);
|
||||||
|
if (cfg is null) return;
|
||||||
|
|
||||||
|
if (creature.state == "fading") {
|
||||||
|
if (!creature.fading_out) {
|
||||||
|
creature.fading_out = true;
|
||||||
|
creature.fade_timer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (creature.sound_handle != -1 && p.sound_is_active(creature.sound_handle)) {
|
||||||
|
float progress = float(creature.fade_timer.elapsed) / float(FLYING_CREATURE_FADE_OUT_DURATION);
|
||||||
|
if (progress < 0.0) progress = 0.0;
|
||||||
|
if (progress > 1.0) progress = 1.0;
|
||||||
|
float volume = 0.0 + (FLYING_CREATURE_FADE_OUT_MIN_VOLUME * progress);
|
||||||
|
p.update_sound_start_values(creature.sound_handle, 0.0, volume, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (creature.fade_timer.elapsed >= FLYING_CREATURE_FADE_OUT_DURATION) {
|
||||||
|
if (creature.sound_handle != -1) {
|
||||||
|
p.destroy_sound(creature.sound_handle);
|
||||||
|
creature.sound_handle = -1;
|
||||||
|
}
|
||||||
|
creature.ready_to_remove = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (creature.state == "flying") {
|
||||||
|
if (creature.position < creature.area_start || creature.position > creature.area_end) {
|
||||||
|
fly_away_flying_creature(creature, cfg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (creature.sound_timer.elapsed > creature.next_sound_delay) {
|
||||||
|
creature.sound_timer.restart();
|
||||||
|
creature.next_sound_delay = random(cfg.sound_delay_min, cfg.sound_delay_max);
|
||||||
|
creature.sound_handle = play_creature_voice(creature.voice_sound, x, creature.position, cfg.sound_volume_step);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.fly_away_chance > 0 && random(1, 1000) <= cfg.fly_away_chance) {
|
||||||
|
fly_away_flying_creature(creature, cfg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (creature.move_timer.elapsed > creature.next_move_delay) {
|
||||||
|
creature.move_timer.restart();
|
||||||
|
creature.next_move_delay = random(cfg.move_interval_min, cfg.move_interval_max);
|
||||||
|
|
||||||
|
int dir = 0;
|
||||||
|
if (cfg.flee_on_sight && cfg.sight_range > 0) {
|
||||||
|
int distance_to_player = abs(x - creature.position);
|
||||||
|
if (distance_to_player <= cfg.sight_range) {
|
||||||
|
if (x > creature.position) dir = -1;
|
||||||
|
else if (x < creature.position) dir = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dir == 0) dir = random(-1, 1);
|
||||||
|
if (dir != 0) {
|
||||||
|
int target_x = creature.position + dir;
|
||||||
|
if (target_x < creature.area_start || target_x > creature.area_end) {
|
||||||
|
fly_away_flying_creature(creature, cfg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target_x >= 0 && target_x < MAP_SIZE) {
|
||||||
|
creature.position = target_x;
|
||||||
|
if (creature.sound_handle != -1 && p.sound_is_active(creature.sound_handle)) {
|
||||||
|
p.update_sound_1d(creature.sound_handle, creature.position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (creature.state == "falling") {
|
||||||
|
if (creature.fall_timer.elapsed > cfg.fall_speed) {
|
||||||
|
creature.fall_timer.restart();
|
||||||
|
creature.height--;
|
||||||
|
|
||||||
|
if (creature.fall_sound_handle != -1) {
|
||||||
|
p.destroy_sound(creature.fall_sound_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
float pitch_percent = 50.0 + (50.0 * (float(creature.height) / float(cfg.max_height)));
|
||||||
|
if (pitch_percent < 50.0) pitch_percent = 50.0;
|
||||||
|
if (pitch_percent > 100.0) pitch_percent = 100.0;
|
||||||
|
|
||||||
|
creature.fall_sound_handle = p.play_extended_1d(cfg.fall_sound, x, creature.position, 0, 0, true, 0, 0.0, 0.0, pitch_percent);
|
||||||
|
if (creature.fall_sound_handle != -1) {
|
||||||
|
p.update_sound_positioning_values(creature.fall_sound_handle, -1.0, cfg.sound_volume_step, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (creature.height <= 0) {
|
||||||
|
if (creature.fall_sound_handle != -1) {
|
||||||
|
p.destroy_sound(creature.fall_sound_handle);
|
||||||
|
creature.fall_sound_handle = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
play_creature_death_sound(cfg.impact_sound, x, creature.position, cfg.sound_volume_step);
|
||||||
|
notify("A " + creature.creature_type + " fell from the sky at " + creature.position + "!");
|
||||||
|
add_world_drop(creature.position, cfg.drop_type);
|
||||||
|
creature.health = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_flying_creatures() {
|
||||||
|
for (uint i = 0; i < flying_creatures.length(); i++) {
|
||||||
|
update_flying_creature(flying_creatures[i]);
|
||||||
|
|
||||||
|
if (flying_creatures[i].health <= 0) {
|
||||||
|
if (flying_creatures[i].state == "falling" && flying_creatures[i].height <= 0) {
|
||||||
|
flying_creatures.remove_at(i);
|
||||||
|
i--;
|
||||||
|
} else if (flying_creatures[i].state == "flying") {
|
||||||
|
flying_creatures.remove_at(i);
|
||||||
|
i--;
|
||||||
|
} else if (flying_creatures[i].state == "fading" && flying_creatures[i].ready_to_remove) {
|
||||||
|
flying_creatures.remove_at(i);
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void attempt_hourly_flying_creature_spawn() {
|
||||||
|
for (uint i = 0; i < flying_creature_configs.length(); i++) {
|
||||||
|
FlyingCreatureConfig@ cfg = flying_creature_configs[i];
|
||||||
|
if (get_flying_creature_count(cfg.id) >= cfg.max_count) continue;
|
||||||
|
if (random(1, 100) <= cfg.hourly_spawn_chance) {
|
||||||
|
spawn_flying_creature(cfg.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool damage_flying_creature_at(int pos, int damage) {
|
||||||
|
for (uint i = 0; i < flying_creatures.length(); i++) {
|
||||||
|
if (flying_creatures[i].position == pos && flying_creatures[i].state == "flying") {
|
||||||
|
FlyingCreatureConfig@ cfg = get_flying_creature_config(flying_creatures[i].creature_type);
|
||||||
|
if (cfg is null) return false;
|
||||||
|
|
||||||
|
flying_creatures[i].health -= damage;
|
||||||
|
if (flying_creatures[i].health <= 0) {
|
||||||
|
flying_creatures[i].state = "falling";
|
||||||
|
flying_creatures[i].fall_timer.restart();
|
||||||
|
|
||||||
|
if (flying_creatures[i].sound_handle != -1) {
|
||||||
|
p.destroy_sound(flying_creatures[i].sound_handle);
|
||||||
|
flying_creatures[i].sound_handle = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
float pitch_percent = 50.0 + (50.0 * (float(flying_creatures[i].height) / float(cfg.max_height)));
|
||||||
|
flying_creatures[i].fall_sound_handle = p.play_extended_1d(cfg.fall_sound, x, pos, 0, 0, true, 0, 0.0, 0.0, pitch_percent);
|
||||||
|
if (flying_creatures[i].fall_sound_handle != -1) {
|
||||||
|
p.update_sound_positioning_values(flying_creatures[i].fall_sound_handle, -1.0, cfg.sound_volume_step, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user