Initial commit.

This commit is contained in:
Storm Dragon
2026-01-17 22:51:22 -05:00
commit 4acd6edbf0
59 changed files with 3981 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
string get_footstep_sound(int current_x, int base_end, int grass_end)
{
// Check if in water first (overrides all other terrain)
if (is_position_in_water(current_x)) {
return "sounds/terrain/shallow_water.ogg";
}
if (current_x <= base_end)
{
// Base area
return "sounds/terrain/wood.ogg";
}
else if (current_x <= grass_end)
{
// Grass area
return "sounds/terrain/grass.ogg";
}
else if (current_x <= GRAVEL_END)
{
// Gravel area
return "sounds/terrain/gravel.ogg";
}
else if (expanded_area_start != -1 && current_x >= expanded_area_start && current_x <= expanded_area_end)
{
// Expanded area - check terrain type
int index = current_x - expanded_area_start;
if (index >= 0 && index < expanded_terrain_types.length())
{
string terrain = expanded_terrain_types[index];
if (terrain == "stone") {
return "sounds/terrain/stone.ogg";
} else if (terrain == "grass") {
return "sounds/terrain/grass.ogg";
} else if (terrain == "snow") {
return "sounds/terrain/snow.ogg";
}
}
}
// Default to gravel
return "sounds/terrain/gravel.ogg";
}
void play_footstep(int current_x, int base_end, int grass_end)
{
string sound_file = get_footstep_sound(current_x, base_end, grass_end);
if(file_exists(sound_file)) {
p.play_stationary(sound_file, false);
}
}
int play_1d_with_volume_step(string sound_file, int listener_x, int sound_x, bool looping, float volume_step)
{
int slot = p.play_1d(sound_file, listener_x, sound_x, looping);
if (slot != -1) {
p.update_sound_positioning_values(slot, -1.0, volume_step, true);
}
return slot;
}
void play_positional_footstep(int listener_x, int step_x, int base_end, int grass_end, int max_distance, float volume_step)
{
int distance = step_x - listener_x;
if (distance < 0) {
distance = -distance;
}
if (distance > max_distance) {
return;
}
string sound_file = get_footstep_sound(step_x, base_end, grass_end);
if(file_exists(sound_file)) {
play_1d_with_volume_step(sound_file, listener_x, step_x, false, volume_step);
}
}
void play_land_sound(int current_x, int base_end, int grass_end)
{
// Reusing the same logic to play the terrain sound on landing
string sound_file = get_footstep_sound(current_x, base_end, grass_end);
if(file_exists(sound_file)) {
p.play_stationary(sound_file, false);
}
}
+189
View File
@@ -0,0 +1,189 @@
void perform_attack(int current_x) {
if (sling_equipped) {
perform_sling_attack(current_x);
} else if (spear_equipped) {
perform_spear_attack(current_x);
} else if (axe_equipped) {
perform_axe_attack(current_x);
} else {
// Optional: Punch logic
// p.play_stationary("sounds/weapons/fist_swing.ogg", false);
}
}
int attack_enemy_ranged(int start_x, int end_x, int damage) {
for (int check_x = start_x; check_x <= end_x; check_x++) {
// Check for bandits first (priority during daytime)
if (damage_bandit_at(check_x, damage)) {
return check_x;
}
// Then check zombies
if (damage_zombie_at(check_x, damage)) {
return check_x;
}
}
return -1;
}
bool attack_enemy(int target_x, int damage) {
// Check for bandits first
if (damage_bandit_at(target_x, damage)) {
return true;
}
// Then check zombies
return damage_zombie_at(target_x, damage);
}
void perform_spear_attack(int current_x) {
p.play_stationary("sounds/weapons/spear_swing.ogg", false);
int hit_pos = attack_enemy_ranged(current_x - 1, current_x + 1, SPEAR_DAMAGE);
if (hit_pos != -1) {
p.play_stationary("sounds/weapons/spear_hit.ogg", false);
// Play hit sound based on enemy type (both use same hit sound for now)
if (get_bandit_at(hit_pos) != null) {
play_1d_with_volume_step("sounds/enemies/zombie_hit.ogg", x, hit_pos, false, BANDIT_SOUND_VOLUME_STEP);
} else {
play_1d_with_volume_step("sounds/enemies/zombie_hit.ogg", x, hit_pos, false, ZOMBIE_SOUND_VOLUME_STEP);
}
return;
}
// Hit tree with spear (sound only, 0 damage)
hit_tree_with_spear(current_x);
}
void perform_axe_attack(int current_x) {
p.play_stationary("sounds/weapons/axe_swing.ogg", false);
if (attack_enemy(current_x, AXE_DAMAGE)) {
p.play_stationary("sounds/weapons/axe_hit.ogg", false);
// Play hit sound based on enemy type
if (get_bandit_at(current_x) != null) {
play_1d_with_volume_step("sounds/enemies/zombie_hit.ogg", x, current_x, false, BANDIT_SOUND_VOLUME_STEP);
} else {
play_1d_with_volume_step("sounds/enemies/zombie_hit.ogg", x, current_x, false, ZOMBIE_SOUND_VOLUME_STEP);
}
return;
}
// Axe Attack Logic
// Range: current_x (0 distance)
// Damage: 4
// Target: Trees
damage_tree(current_x, 4);
}
void perform_sling_attack(int current_x) {
// Sling uses charge/release mechanism, not direct attack
// This function is called from main loop when sling is released
release_sling_attack(current_x);
}
void hit_tree_with_spear(int target_x) {
Tree@ target = get_tree_at(target_x);
if (@target != null && !target.is_chopped) {
p.play_stationary("sounds/weapons/spear_hit.ogg", false);
}
}
void update_sling_charge() {
int elapsed = sling_charge_timer.elapsed;
int cycle_time = 1500; // 1.5 seconds
int stage_duration = 500; // 0.5 seconds per stage
// Loop the charge cycle
int time_in_cycle = elapsed % cycle_time;
// Determine stage: 0=low, 1=in-range, 2=high
int current_stage = time_in_cycle / stage_duration;
// Play stage indicator sounds (only once per stage change)
if (current_stage != last_sling_stage) {
if (current_stage == 1) {
// Entering in-range window
p.play_stationary("sounds/weapons/sling_low_range.ogg", false);
} else if (current_stage == 2) {
// Entering too-high window
p.play_stationary("sounds/weapons/sling_high_range.ogg", false);
}
last_sling_stage = current_stage;
}
}
void release_sling_attack(int player_x) {
// Consume stone
inv_stones--;
int elapsed = sling_charge_timer.elapsed;
int cycle_time = 1500;
int time_in_cycle = elapsed % cycle_time;
int stage = time_in_cycle / 500; // 0=low, 1=in-range, 2=high
// Only hit if released during in-range window (stage 1)
if (stage != 1) {
screen_reader_speak("Stone missed.", true);
return;
}
// Find target in facing direction (5 tiles)
int search_direction = (facing == 1) ? 1 : -1;
int target_x = -1;
bool hit_bandit = false;
// Priority: Find nearest enemy (bandit or zombie) first
for (int dist = 1; dist <= 5; dist++) {
int check_x = player_x + (dist * search_direction);
if (check_x < 0 || check_x >= MAP_SIZE) break;
// Check for bandit first
Bandit@ bandit = get_bandit_at(check_x);
if (bandit != null) {
target_x = check_x;
hit_bandit = true;
break;
}
// Then check for zombie
Zombie@ zombie = get_zombie_at(check_x);
if (zombie != null) {
target_x = check_x;
hit_bandit = false;
break;
}
}
// If no enemy found, check for trees (but don't damage them)
if (target_x == -1) {
for (int dist = 1; dist <= 5; dist++) {
int check_x = player_x + (dist * search_direction);
if (check_x < 0 || check_x >= MAP_SIZE) break;
Tree@ tree = get_tree_at(check_x);
if (tree != null && !tree.is_chopped) {
// Stone hits tree but doesn't damage it
p.play_1d("sounds/weapons/sling_hit.ogg", player_x, check_x, false);
screen_reader_speak("Stone hit tree at " + check_x + ".", true);
return;
}
}
}
// No target found
if (target_x == -1) {
screen_reader_speak("Stone missed.", true);
return;
}
int damage = random(SLING_DAMAGE_MIN, SLING_DAMAGE_MAX);
// Damage the correct enemy type
if (hit_bandit) {
damage_bandit_at(target_x, damage);
p.play_1d("sounds/weapons/sling_hit.ogg", player_x, target_x, false);
play_1d_with_volume_step("sounds/enemies/zombie_hit.ogg", player_x, target_x, false, BANDIT_SOUND_VOLUME_STEP);
} else {
damage_zombie_at(target_x, damage);
p.play_1d("sounds/weapons/sling_hit.ogg", player_x, target_x, false);
play_1d_with_volume_step("sounds/enemies/zombie_hit.ogg", player_x, target_x, false, ZOMBIE_SOUND_VOLUME_STEP);
}
}
+63
View File
@@ -0,0 +1,63 @@
// Map configuration
int MAP_SIZE = 35;
const int BASE_END = 4; // 0-4
const int GRASS_END = 19; // 5-19
const int GRAVEL_END = 34; // 20-34
// Expansion configuration
const int EXPANSION_SIZE = 30;
const int EXPANSION_CHANCE = 30; // 30% chance per hour before noon
int expanded_area_start = -1; // -1 means not expanded yet
int expanded_area_end = -1;
// Movement configuration
int movetime = 400; // Time between steps/movements
int walk_speed = 400;
int jump_speed = 170;
const int MAX_ITEM_STACK = 9;
// Weapon damage
const int SPEAR_DAMAGE = 3;
const int AXE_DAMAGE = 4;
const int SLING_DAMAGE_MIN = 5;
const int SLING_DAMAGE_MAX = 8;
// Zombie settings
const int ZOMBIE_HEALTH = 12;
const int ZOMBIE_MAX_COUNT = 5;
const int ZOMBIE_MOVE_INTERVAL = 1000;
const int ZOMBIE_ATTACK_INTERVAL = 1600;
const int ZOMBIE_DAMAGE_MIN = 4;
const int ZOMBIE_DAMAGE_MAX = 6;
const int ZOMBIE_GROAN_MIN_DELAY = 2000;
const int ZOMBIE_GROAN_MAX_DELAY = 3000;
const int ZOMBIE_FOOTSTEP_MAX_DISTANCE = 5;
const float ZOMBIE_SOUND_VOLUME_STEP = 3.0;
const int ZOMBIE_ATTACK_MAX_HEIGHT = 6;
// Barricade configuration
const int BARRICADE_BASE_HEALTH = 100;
const int BARRICADE_MAX_HEALTH = 500;
const int BARRICADE_STICK_COST = 3;
const int BARRICADE_STICK_HEALTH = 10;
const int BARRICADE_VINE_COST = 5;
const int BARRICADE_VINE_HEALTH = 15;
const int BARRICADE_LOG_COST = 1;
const int BARRICADE_LOG_HEALTH = 30;
const int BARRICADE_STONE_COST = 5;
const int BARRICADE_STONE_HEALTH = 20;
// Bandit settings
const int BANDIT_HEALTH = 4;
const int BANDIT_MAX_COUNT = 3;
const int BANDIT_MOVE_INTERVAL_MIN = 600;
const int BANDIT_MOVE_INTERVAL_MAX = 800;
const int BANDIT_ATTACK_INTERVAL = 1200;
const int BANDIT_DAMAGE_MIN = 1;
const int BANDIT_DAMAGE_MAX = 2;
const int BANDIT_ALERT_MIN_DELAY = 3000;
const int BANDIT_ALERT_MAX_DELAY = 5000;
const int BANDIT_FOOTSTEP_MAX_DISTANCE = 7;
const float BANDIT_SOUND_VOLUME_STEP = 3.0;
const int BANDIT_ATTACK_MAX_HEIGHT = 6;
const int INVASION_DURATION_HOURS = 1;
+456
View File
@@ -0,0 +1,456 @@
// Tree Object
class Tree {
int position;
int sticks;
int vines;
int health;
int height; // Height in feet
int sound_handle;
timer regen_timer;
bool depleted;
bool is_chopped;
int minutes_since_depletion; // Track minutes for gradual replenishment
Tree(int pos) {
position = pos;
depleted = false;
is_chopped = false;
sound_handle = -1;
minutes_since_depletion = 0;
refill();
}
void refill() {
sticks = random(2, 3);
vines = random(1, 2);
health = random(25, 40);
height = random(8, 30); // Random height between 8 and 30 feet
depleted = false;
is_chopped = false;
minutes_since_depletion = 0;
}
void respawn(int grass_start, int grass_end) {
if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
}
position = random(grass_start, grass_end);
refill();
}
void update() {
// Only play tree sound if not chopped and within 3 tiles distance (2 tiles on either side)
if (!is_chopped) {
if (abs(x - position) <= 3) {
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
sound_handle = p.play_1d("sounds/environment/tree.ogg", x, position, true);
}
} else {
if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
}
}
}
}
void try_regen() {
// Skip if tree is fully stocked
if (!depleted && !is_chopped) return;
// Check every minute (60000ms)
if (regen_timer.elapsed < 60000) return;
// Advance to next minute
regen_timer.restart();
minutes_since_depletion++;
if (is_chopped) {
if (minutes_since_depletion >= 5) {
respawn(BASE_END + 1, GRASS_END);
}
return;
}
// At minute 5, completely refill
if (minutes_since_depletion >= 5) {
refill();
return;
}
// Skip if already fully stocked
if (sticks >= 3 && vines >= 2) {
depleted = false;
is_chopped = false;
return;
}
// Determine base chance based on minutes elapsed
int base_chance = 0;
if (minutes_since_depletion == 1) base_chance = 25;
else if (minutes_since_depletion == 2) base_chance = 50;
else if (minutes_since_depletion == 3) base_chance = 75;
else if (minutes_since_depletion == 4) base_chance = 100;
// Try to add items with decreasing probability
int current_chance = base_chance;
while (current_chance >= 25) {
// Check if we can add anything
if (sticks >= 3 && vines >= 2) break;
// Roll for success
int roll = random(1, 100);
if (roll <= current_chance) {
// Decide what to add (70% stick, 30% vine)
int item_roll = random(1, 100);
if (item_roll <= 70 && sticks < 3) {
// Add stick
sticks++;
} else if (vines < 2) {
// Add vine
vines++;
} else if (sticks < 3) {
// Vine is full but stick isn't, add stick
sticks++;
}
} else {
// Failed roll, stop trying
break;
}
// Reduce chance by 25% for next attempt (minimum 25%)
current_chance -= 25;
}
// Mark as no longer depleted if we have at least one item
if (sticks > 0 || vines > 0) {
depleted = false;
is_chopped = false;
}
}
}
Tree@[] trees;
void spawn_trees(int grass_start, int grass_end) {
int pos = random(grass_start, grass_end);
Tree@ t = Tree(pos);
trees.insert_last(t);
}
void update_environment() {
for(uint i = 0; i < trees.length(); i++) {
trees[i].update();
trees[i].try_regen();
}
}
Tree@ get_tree_at(int target_x) {
for(uint i=0; i<trees.length(); i++) {
if(trees[i].position == target_x) {
return @trees[i];
}
}
return null;
}
void damage_tree(int target_x, int damage) {
Tree@ target = null;
for(uint i=0; i<trees.length(); i++) {
if(trees[i].position == target_x) {
@target = @trees[i];
break;
}
}
if(@target != null) {
if(!target.is_chopped) {
target.health -= damage;
p.play_stationary("sounds/weapons/axe_hit.ogg", false);
if(target.health <= 0) {
target.is_chopped = true;
target.depleted = true;
target.regen_timer.restart();
target.minutes_since_depletion = 0;
// Stop the looping sound
if (target.sound_handle != -1) {
p.destroy_sound(target.sound_handle);
target.sound_handle = -1;
}
// Play the falling sound at the tree's position
p.play_1d("sounds/items/tree.ogg", x, target.position, false);
int sticks_dropped = random(1, 3);
int vines_dropped = random(1, 2);
int sticks_added = add_to_stack(inv_sticks, sticks_dropped);
int vines_added = add_to_stack(inv_vines, vines_dropped);
int logs_added = add_to_stack(inv_logs, 1);
inv_sticks += sticks_added;
inv_vines += vines_added;
inv_logs += logs_added;
string drop_message = "Tree fell!";
if (sticks_added > 0 || vines_added > 0 || logs_added > 0) {
string log_label = (logs_added == 1) ? " log" : " logs";
drop_message += " Got " + sticks_added + " sticks, " + vines_added + " vines, and " + logs_added + log_label + ".";
}
if (sticks_added < sticks_dropped || vines_added < vines_dropped || logs_added < 1) {
drop_message += " Inventory full.";
}
p.play_stationary("sounds/items/stick.ogg", false);
screen_reader_speak(drop_message, true);
}
}
}
}
void perform_search(int current_x)
{
// Check for Snares nearby (Current or Adjacent)
// "Shift beside the snare will collect the snare" -> adjacent
// We check current and +/- 1
for (int check_x = current_x - 1; check_x <= current_x + 1; check_x++) {
// Skip current x? User said "beside". If on top, it breaks.
// But if I stand adjacent and shift...
if (check_x == current_x) continue; // Safety against collecting own snare you stand on? (Collision happens on move)
// Actually, collision happens when *moving onto* it. If you placed it, you are on it.
// If active is false (just placed), you can pick it up.
// If active is true (you moved away), moving back breaks it.
// So checking adjacent is correct.
WorldSnare@ s = get_snare_at(check_x);
if (s != null) {
if (s.has_catch) {
if (inv_small_game >= MAX_ITEM_STACK) {
screen_reader_speak("You can't carry any more small game.", true);
return;
}
if (inv_snares >= MAX_ITEM_STACK) {
screen_reader_speak("You can't carry any more snares.", true);
return;
}
inv_small_game++;
inv_small_game_types.insert_last(s.catch_type);
inv_snares++; // Recover snare
screen_reader_speak("Collected " + s.catch_type + " and snare.", true);
} else {
if (inv_snares >= MAX_ITEM_STACK) {
screen_reader_speak("You can't carry any more snares.", true);
return;
}
inv_snares++; // Recover snare
screen_reader_speak("Collected snare.", true);
}
p.play_stationary("sounds/items/miscellaneous.ogg", false);
remove_snare_at(check_x);
return; // Action taken, stop searching
}
}
// Gravel Area - Stones (20-34)
if (current_x >= 20 && current_x <= 34)
{
if (inv_stones < MAX_ITEM_STACK)
{
inv_stones++;
p.play_stationary("sounds/items/stone.ogg", false);
screen_reader_speak("Found a stone.", true);
}
else
{
screen_reader_speak("You can't carry any more stones.", true);
}
return;
}
// Grass Area - Trees (Sticks/Vines) (5-19)
if (current_x >= 5 && current_x <= 19)
{
Tree@ nearest = null;
for(uint i=0; i<trees.length(); i++)
{
if(trees[i].position >= current_x - 1 && trees[i].position <= current_x + 1)
{
@nearest = @trees[i];
break;
}
}
if(@nearest != null)
{
if(nearest.is_chopped) {
screen_reader_speak("This tree has been cut down.", true);
return;
}
if (nearest.depleted) {
screen_reader_speak("This tree is empty.", true);
return;
}
if(nearest.sticks > 0 || nearest.vines > 0)
{
bool find_stick = (nearest.vines <= 0) || (nearest.sticks > 0 && random(0, 1) == 0);
if(find_stick)
{
if (inv_sticks >= MAX_ITEM_STACK) {
screen_reader_speak("You can't carry any more sticks.", true);
return;
}
nearest.sticks--;
inv_sticks++;
p.play_stationary("sounds/items/stick.ogg", false);
screen_reader_speak("Found a stick.", true);
}
else
{
if (inv_vines >= MAX_ITEM_STACK) {
screen_reader_speak("You can't carry any more vines.", true);
return;
}
nearest.vines--;
inv_vines++;
p.play_stationary("sounds/items/vine.ogg", false);
screen_reader_speak("Found a vine.", true);
}
if(nearest.sticks == 0 && nearest.vines == 0) {
nearest.depleted = true;
nearest.regen_timer.restart();
nearest.minutes_since_depletion = 0;
}
}
else
{
screen_reader_speak("This area has nothing left.", true);
}
}
else
{
screen_reader_speak("Found nothing.", true);
}
return;
}
screen_reader_speak("Found nothing.", true);
}
// Climbing functions
void start_climbing_tree(int target_x) {
Tree@ tree = get_tree_at(target_x);
if (tree == null || tree.is_chopped) {
return;
}
climbing = true;
climb_target_y = tree.height;
climb_timer.restart();
screen_reader_speak("Started climbing tree. Height is " + tree.height + " feet.", true);
}
void update_climbing() {
if (!climbing) return;
// Climb at 1 foot per 500ms
if (climb_timer.elapsed > 500) {
climb_timer.restart();
// Climbing up
if (y < climb_target_y) {
y++;
p.play_stationary("sounds/actions/climb_tree.ogg", false);
if (y >= climb_target_y) {
climbing = false;
screen_reader_speak("Reached the top at " + y + " feet.", true);
}
}
// Climbing down
else if (y > climb_target_y) {
y--;
p.play_stationary("sounds/actions/climb_tree.ogg", false);
if (y <= 0) {
climbing = false;
y = 0;
screen_reader_speak("Safely reached the ground.", true);
}
}
}
}
void climb_down_tree() {
if (y == 0 || climbing) return;
climbing = true;
climb_target_y = 0;
climb_timer.restart();
screen_reader_speak("Climbing down.", true);
}
void start_falling() {
if (y <= 0 || falling) return;
falling = true;
fall_start_y = y; // Remember where we started falling from
fall_timer.restart();
// Start looping falling sound
fall_sound_handle = p.play_stationary("sounds/actions/falling.ogg", true);
}
void update_falling() {
if (!falling) return;
// Fall faster than climbing - 1 foot per 100ms
if (fall_timer.elapsed > 100) {
fall_timer.restart();
if (y > 0) {
y--;
// Restart falling sound with decreasing pitch each foot
if (fall_sound_handle != -1) {
p.destroy_sound(fall_sound_handle);
}
// Pitch ranges from 100 (high up) to 50 (near ground)
// Calculate based on current y position
float pitch_percent = 50.0 + (50.0 * (y / 30.0));
if (pitch_percent < 50.0) pitch_percent = 50.0;
if (pitch_percent > 100.0) pitch_percent = 100.0;
fall_sound_handle = p.play_stationary_extended("sounds/actions/falling.ogg", true, 0, 0, 0, pitch_percent);
} else {
// Hit the ground
falling = false;
// Stop falling sound
if (fall_sound_handle != -1) {
p.destroy_sound(fall_sound_handle);
fall_sound_handle = -1;
}
p.play_stationary("sounds/actions/hit_ground.ogg", false);
// Calculate fall damage
int fall_height = fall_start_y;
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;
}
}
}
+886
View File
@@ -0,0 +1,886 @@
// Inventory
int inv_stones = 0;
int inv_sticks = 0;
int inv_vines = 0;
int inv_logs = 0;
int inv_small_game = 0; // Total small game caught (any type)
string[] inv_small_game_types; // Array to track what types of small game we have
int inv_meat = 0;
int inv_skins = 0;
int inv_spears = 0;
int inv_snares = 0;
int inv_axes = 0;
int inv_knives = 0;
int inv_fishing_poles = 0;
int inv_slings = 0;
bool spear_equipped = false;
bool axe_equipped = false;
bool sling_equipped = false;
int add_to_stack(int current, int amount) {
if (amount <= 0) return 0;
int space = MAX_ITEM_STACK - current;
if (space <= 0) return 0;
if (amount > space) return space;
return amount;
}
void check_crafting_menu(int x, int base_end_tile) {
if (x <= base_end_tile) {
if (key_pressed(KEY_C)) {
run_crafting_menu();
}
}
}
void check_inventory_keys(int x) {
if (key_pressed(KEY_I)) {
run_inventory_menu();
}
}
void check_action_menu(int x) {
if (key_pressed(KEY_A)) {
run_action_menu(x);
}
}
void menu_background_tick() {
update_time();
update_environment();
update_snares();
update_fires();
update_zombies();
// Fire damage check (only if not jumping)
WorldFire@ fire_on_tile = get_fire_at(x);
if (fire_on_tile != null && !jumping && fire_damage_timer.elapsed > 1000) {
player_health--;
fire_damage_timer.restart();
screen_reader_speak("Burning! " + player_health + " health remaining.", true);
}
// Healing in base area
if (x <= BASE_END && player_health < max_health) {
WorldHerbGarden@ herb_garden = get_herb_garden_at_base();
int heal_interval = (herb_garden != null) ? 30000 : 150000; // 30 seconds with garden, 2.5 minutes without
if (healing_timer.elapsed > heal_interval) {
player_health++;
healing_timer.restart();
screen_reader_speak(player_health + " health.", true);
}
}
// Death check
if (player_health <= 0) {
screen_reader_speak("You have died.", true);
wait(2000);
exit();
}
}
void show_inventory() {
string info = "Inventory: ";
info += inv_sticks + " sticks, ";
info += inv_vines + " vines, ";
info += inv_stones + " stones, ";
info += inv_logs + " logs, ";
info += inv_small_game + " small game, ";
info += inv_meat + " meat, ";
info += inv_skins + " skins. ";
info += "Tools: " + inv_spears + " spears, " + inv_slings + " slings, " + inv_axes + " axes, " + inv_snares + " snares, " + inv_knives + " knives, " + inv_fishing_poles + " fishing poles.";
screen_reader_speak(info, true);
}
void run_inventory_menu() {
screen_reader_speak("Inventory menu.", true);
int selection = 0;
string[] options = {
"Sticks: " + inv_sticks,
"Vines: " + inv_vines,
"Stones: " + inv_stones,
"Logs: " + inv_logs,
"Small Game: " + inv_small_game,
"Meat: " + inv_meat,
"Skins: " + inv_skins,
"Spears: " + inv_spears,
"Slings: " + inv_slings,
"Axes: " + inv_axes,
"Snares: " + inv_snares,
"Knives: " + inv_knives,
"Fishing Poles: " + inv_fishing_poles
};
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);
}
}
}
void try_place_snare(int x) {
if (inv_snares > 0) {
// Prevent placing if one already exists here
if (get_snare_at(x) != null) {
screen_reader_speak("There is already a snare here.", true);
return;
}
inv_snares--;
add_world_snare(x);
screen_reader_speak("Snare set.", true);
} else {
screen_reader_speak("No snares to place.", true);
}
}
void try_feed_fire_stick(WorldFire@ fire) {
if (inv_sticks > 0 && fire != null) {
inv_sticks--;
fire.add_fuel(300000); // 5 minutes
screen_reader_speak("You dump an arm load of sticks into the fire.", true);
p.play_stationary("sounds/actions/fed_fire.ogg", false);
}
}
void try_feed_fire_vine(WorldFire@ fire) {
if (inv_vines > 0 && fire != null) {
inv_vines--;
fire.add_fuel(60000); // 1 minute
screen_reader_speak("You toss a fiew vines and leaves into the fire.", true);
p.play_stationary("sounds/actions/fed_fire.ogg", false);
}
}
void try_feed_fire_log(WorldFire@ fire) {
if (inv_logs > 0 && fire != null) {
inv_logs--;
fire.add_fuel(720000); // 12 minutes
screen_reader_speak("You heave a log into the fire.", true);
p.play_stationary("sounds/actions/fed_fire.ogg", false);
}
}
void check_equipment_menu() {
if (key_pressed(KEY_E)) {
// Check if player has any equipment
if (inv_spears == 0 && inv_axes == 0 && inv_slings == 0) {
screen_reader_speak("Nothing to equip.", true);
} else {
run_equipment_menu();
}
}
}
void run_action_menu(int x) {
screen_reader_speak("Action menu.", true);
int selection = 0;
string[] options;
int[] action_types; // Track what action each option corresponds to
// Check if fire is nearby
WorldFire@ nearby_fire = get_fire_near(x);
bool can_feed_fire = nearby_fire != null;
// Build menu options dynamically
options.insert_last("Place Snare");
action_types.insert_last(0);
if (can_feed_fire) {
if (inv_sticks > 0) {
options.insert_last("Feed fire with stick");
action_types.insert_last(1);
}
if (inv_vines > 0) {
options.insert_last("Feed fire with vine");
action_types.insert_last(2);
}
if (inv_logs > 0) {
options.insert_last("Feed fire with log");
action_types.insert_last(3);
}
}
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)) {
int action = action_types[selection];
if (action == 0) {
try_place_snare(x);
} else if (action == 1) {
try_feed_fire_stick(nearby_fire);
} else if (action == 2) {
try_feed_fire_vine(nearby_fire);
} else if (action == 3) {
try_feed_fire_log(nearby_fire);
}
break;
}
}
}
void run_crafting_menu() {
screen_reader_speak("Crafting menu.", true);
int selection = 0;
string[] categories = {"Weapons", "Tools", "Buildings", "Barricade"};
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 >= categories.length()) selection = 0;
screen_reader_speak(categories[selection], true);
}
if (key_pressed(KEY_UP)) {
selection--;
if (selection < 0) selection = categories.length() - 1;
screen_reader_speak(categories[selection], true);
}
if (key_pressed(KEY_RETURN)) {
if (selection == 0) run_weapons_menu();
else if (selection == 1) run_tools_menu();
else if (selection == 2) run_buildings_menu();
else if (selection == 3) run_barricade_menu();
break;
}
}
}
void run_weapons_menu() {
screen_reader_speak("Weapons.", true);
int selection = 0;
string[] options = {
"Spear (1 Stick, 1 Vine, 1 Stone) [Requires Knife]",
"Sling (1 Skin, 2 Vines)"
};
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) craft_spear();
else if (selection == 1) craft_sling();
break;
}
}
}
void run_tools_menu() {
screen_reader_speak("Tools.", true);
int selection = 0;
string[] options = {
"Stone Knife (2 Stones)",
"Snare (1 Stick, 2 Vines)",
"Stone Axe (1 Stick, 1 Vine, 2 Stones) [Requires Knife]",
"Fishing Pole (1 Stick, 2 Vines)",
"Butcher Small Game (1 Small Game) [Requires Knife and Fire nearby]"
};
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) craft_knife();
else if (selection == 1) craft_snare();
else if (selection == 2) craft_axe();
else if (selection == 3) craft_fishing_pole();
else if (selection == 4) butcher_small_game();
break;
}
}
}
void run_buildings_menu() {
screen_reader_speak("Buildings.", true);
int selection = 0;
string[] options = {
"Firepit (9 Stones)",
"Fire (2 Sticks, 1 Log) [Requires Firepit]",
"Herb Garden (9 Stones, 3 Vines, 2 Logs) [Base Only]"
};
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) craft_firepit();
else if (selection == 1) craft_campfire();
else if (selection == 2) craft_herb_garden();
break;
}
}
}
void run_barricade_menu() {
if (barricade_health >= BARRICADE_MAX_HEALTH) {
screen_reader_speak("Barricade is already at full health.", true);
return;
}
screen_reader_speak("Barricade.", true);
int selection = 0;
string[] options;
int[] action_types; // 0 = sticks, 1 = vines, 2 = log, 3 = stones
if (inv_sticks >= BARRICADE_STICK_COST) {
options.insert_last("Reinforce with sticks (" + BARRICADE_STICK_COST + " sticks, +" + BARRICADE_STICK_HEALTH + " health)");
action_types.insert_last(0);
}
if (inv_vines >= BARRICADE_VINE_COST) {
options.insert_last("Reinforce with vines (" + BARRICADE_VINE_COST + " vines, +" + BARRICADE_VINE_HEALTH + " health)");
action_types.insert_last(1);
}
if (inv_logs >= BARRICADE_LOG_COST) {
options.insert_last("Reinforce with log (" + BARRICADE_LOG_COST + " log, +" + BARRICADE_LOG_HEALTH + " health)");
action_types.insert_last(2);
}
if (inv_stones >= BARRICADE_STONE_COST) {
options.insert_last("Reinforce with stones (" + BARRICADE_STONE_COST + " stones, +" + BARRICADE_STONE_HEALTH + " health)");
action_types.insert_last(3);
}
if (options.length() == 0) {
screen_reader_speak("No materials to reinforce the barricade.", true);
return;
}
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)) {
int action = action_types[selection];
if (action == 0) reinforce_barricade_with_sticks();
else if (action == 1) reinforce_barricade_with_vines();
else if (action == 2) reinforce_barricade_with_log();
else if (action == 3) reinforce_barricade_with_stones();
break;
}
}
}
void run_equipment_menu() {
screen_reader_speak("Equipment menu.", true);
int selection = 0;
string[] options;
int[] equipment_types; // 0 = spear, 1 = axe, 2 = sling
// Build menu dynamically based on what player has
if (inv_spears > 0) {
string status = spear_equipped ? " (equipped)" : "";
options.insert_last("Spear" + status);
equipment_types.insert_last(0);
}
if (inv_slings > 0) {
string status = sling_equipped ? " (equipped)" : "";
options.insert_last("Sling" + status);
equipment_types.insert_last(2);
}
if (inv_axes > 0) {
string status = axe_equipped ? " (equipped)" : "";
options.insert_last("Stone Axe" + status);
equipment_types.insert_last(1);
}
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)) {
int equip_type = equipment_types[selection];
if (equip_type == 0) {
// Spear
if (!spear_equipped) {
spear_equipped = true;
axe_equipped = false;
sling_equipped = false;
screen_reader_speak("Spear equipped.", true);
} else {
spear_equipped = false;
screen_reader_speak("Spear unequipped.", true);
}
} else if (equip_type == 1) {
// Axe
if (!axe_equipped) {
axe_equipped = true;
spear_equipped = false;
sling_equipped = false;
screen_reader_speak("Stone Axe equipped.", true);
} else {
axe_equipped = false;
screen_reader_speak("Stone Axe unequipped.", true);
}
} else if (equip_type == 2) {
// Sling
if (!sling_equipped) {
sling_equipped = true;
spear_equipped = false;
axe_equipped = false;
screen_reader_speak("Sling equipped.", true);
} else {
sling_equipped = false;
screen_reader_speak("Sling unequipped.", true);
}
}
break;
}
}
}
void simulate_crafting() {
screen_reader_speak("Crafting...", true);
timer t;
int duration = 4000;
int next_sound = 0;
while(t.elapsed < duration) {
if(t.elapsed > next_sound) {
float pitch = random(85, 115);
p.play_stationary_extended("sounds/crafting.ogg", false, 0, 0, 0, pitch);
next_sound = t.elapsed + 800;
}
wait(5);
menu_background_tick();
}
p.play_stationary("sounds/crafting_complete.ogg", false);
}
void craft_knife() {
string missing = "";
if (inv_stones < 2) missing += "2 stones ";
if (missing == "") {
if (inv_knives >= MAX_ITEM_STACK) {
screen_reader_speak("You can't carry any more stone knives.", true);
return;
}
simulate_crafting();
inv_stones -= 2;
inv_knives++;
screen_reader_speak("Crafted a Stone Knife.", true);
} else {
screen_reader_speak("Missing: " + missing, true);
}
}
void craft_spear() {
string missing = "";
if (inv_knives < 1) missing += "Stone Knife ";
if (inv_sticks < 1) missing += "1 stick ";
if (inv_vines < 1) missing += "1 vine ";
if (inv_stones < 1) missing += "1 stone ";
if (missing == "") {
if (inv_spears >= MAX_ITEM_STACK) {
screen_reader_speak("You can't carry any more spears.", true);
return;
}
simulate_crafting();
inv_sticks--;
inv_vines--;
inv_stones--;
inv_spears++;
screen_reader_speak("Crafted a Spear.", true);
} else {
screen_reader_speak("Missing: " + missing, true);
}
}
void craft_sling() {
string missing = "";
if (inv_skins < 1) missing += "1 skin ";
if (inv_vines < 2) missing += "2 vines ";
if (missing == "") {
if (inv_slings >= MAX_ITEM_STACK) {
screen_reader_speak("You can't carry any more slings.", true);
return;
}
simulate_crafting();
inv_skins--;
inv_vines -= 2;
inv_slings++;
screen_reader_speak("Crafted a Sling.", true);
} else {
screen_reader_speak("Missing: " + missing, true);
}
}
void craft_snare() {
string missing = "";
if (inv_sticks < 1) missing += "1 stick ";
if (inv_vines < 2) missing += "2 vines ";
if (missing == "") {
if (inv_snares >= MAX_ITEM_STACK) {
screen_reader_speak("You can't carry any more snares.", true);
return;
}
simulate_crafting();
inv_sticks--;
inv_vines -= 2;
inv_snares++;
screen_reader_speak("Crafted a Snare.", true);
} else {
screen_reader_speak("Missing: " + missing, true);
}
}
void craft_axe() {
string missing = "";
if (inv_knives < 1) missing += "Stone Knife ";
if (inv_sticks < 1) missing += "1 stick ";
if (inv_vines < 1) missing += "1 vine ";
if (inv_stones < 2) missing += "2 stones ";
if (missing == "") {
if (inv_axes >= MAX_ITEM_STACK) {
screen_reader_speak("You can't carry any more stone axes.", true);
return;
}
simulate_crafting();
inv_sticks--;
inv_vines--;
inv_stones -= 2;
inv_axes++;
screen_reader_speak("Crafted a Stone Axe.", true);
} else {
screen_reader_speak("Missing: " + missing, true);
}
}
void craft_firepit() {
// Check if there's already a firepit here
if (get_firepit_at(x) != null) {
screen_reader_speak("There is already a firepit here.", true);
return;
}
string missing = "";
if (inv_stones < 9) missing += "9 stones ";
if (missing == "") {
simulate_crafting();
inv_stones -= 9;
add_world_firepit(x);
screen_reader_speak("Firepit built here.", true);
} else {
screen_reader_speak("Missing: " + missing, true);
}
}
void craft_campfire() {
// Check if there's a firepit within 2 tiles
WorldFirepit@ firepit = get_firepit_near(x, 2);
if (firepit == null) {
screen_reader_speak("You need a firepit within 2 tiles to build a fire.", true);
return;
}
string missing = "";
if (inv_logs < 1) missing += "1 log ";
if (inv_sticks < 2) missing += "2 sticks ";
if (missing == "") {
simulate_crafting();
inv_logs--;
inv_sticks -= 2;
// Build the fire at the firepit location, not player location
add_world_fire(firepit.position);
screen_reader_speak("Fire built at firepit.", true);
} else {
screen_reader_speak("Missing: " + missing, true);
}
}
void craft_herb_garden() {
// Can only build in base area
if (x > BASE_END) {
screen_reader_speak("Herb garden can only be built in the base area.", true);
return;
}
// Check if there's already an herb garden in the base
if (get_herb_garden_at_base() != null) {
screen_reader_speak("There is already an herb garden in the base.", true);
return;
}
string missing = "";
if (inv_stones < 9) missing += "9 stones ";
if (inv_vines < 3) missing += "3 vines ";
if (inv_logs < 2) missing += "2 logs ";
if (missing == "") {
simulate_crafting();
inv_stones -= 9;
inv_vines -= 3;
inv_logs -= 2;
add_world_herb_garden(x);
screen_reader_speak("Herb garden built. The base now heals faster.", true);
} else {
screen_reader_speak("Missing: " + missing, true);
}
}
void reinforce_barricade_with_sticks() {
if (barricade_health >= BARRICADE_MAX_HEALTH) {
screen_reader_speak("Barricade is already at full health.", true);
return;
}
if (inv_sticks < BARRICADE_STICK_COST) {
screen_reader_speak("Not enough sticks.", true);
return;
}
simulate_crafting();
inv_sticks -= BARRICADE_STICK_COST;
int gained = add_barricade_health(BARRICADE_STICK_HEALTH);
screen_reader_speak("Reinforced barricade with sticks. +" + gained + " health. Now " + barricade_health + " of " + BARRICADE_MAX_HEALTH + ".", true);
}
void reinforce_barricade_with_vines() {
if (barricade_health >= BARRICADE_MAX_HEALTH) {
screen_reader_speak("Barricade is already at full health.", true);
return;
}
if (inv_vines < BARRICADE_VINE_COST) {
screen_reader_speak("Not enough vines.", true);
return;
}
simulate_crafting();
inv_vines -= BARRICADE_VINE_COST;
int gained = add_barricade_health(BARRICADE_VINE_HEALTH);
screen_reader_speak("Reinforced barricade with vines. +" + gained + " health. Now " + barricade_health + " of " + BARRICADE_MAX_HEALTH + ".", true);
}
void reinforce_barricade_with_log() {
if (barricade_health >= BARRICADE_MAX_HEALTH) {
screen_reader_speak("Barricade is already at full health.", true);
return;
}
if (inv_logs < BARRICADE_LOG_COST) {
screen_reader_speak("Not enough logs.", true);
return;
}
simulate_crafting();
inv_logs -= BARRICADE_LOG_COST;
int gained = add_barricade_health(BARRICADE_LOG_HEALTH);
screen_reader_speak("Reinforced barricade with log. +" + gained + " health. Now " + barricade_health + " of " + BARRICADE_MAX_HEALTH + ".", true);
}
void reinforce_barricade_with_stones() {
if (barricade_health >= BARRICADE_MAX_HEALTH) {
screen_reader_speak("Barricade is already at full health.", true);
return;
}
if (inv_stones < BARRICADE_STONE_COST) {
screen_reader_speak("Not enough stones.", true);
return;
}
simulate_crafting();
inv_stones -= BARRICADE_STONE_COST;
int gained = add_barricade_health(BARRICADE_STONE_HEALTH);
screen_reader_speak("Reinforced barricade with stones. +" + gained + " health. Now " + barricade_health + " of " + BARRICADE_MAX_HEALTH + ".", true);
}
void craft_fishing_pole() {
string missing = "";
if (inv_sticks < 1) missing += "1 stick ";
if (inv_vines < 2) missing += "2 vines ";
if (missing == "") {
if (inv_fishing_poles >= MAX_ITEM_STACK) {
screen_reader_speak("You can't carry any more fishing poles.", true);
return;
}
simulate_crafting();
inv_sticks--;
inv_vines -= 2;
inv_fishing_poles++;
screen_reader_speak("Crafted a Fishing Pole.", true);
} else {
screen_reader_speak("Missing: " + missing, true);
}
}
void butcher_small_game() {
string missing = "";
// Check for knife
if (inv_knives < 1) missing += "Stone Knife ";
// Check for small game
if (inv_small_game < 1) missing += "Small Game ";
// Check for fire within 3 tiles (can hear it)
WorldFire@ fire = get_fire_within_range(x, 3);
if (fire == null) {
screen_reader_speak("You need a fire within 3 tiles to butcher.", true);
return;
}
if (missing == "") {
if (inv_meat >= MAX_ITEM_STACK) {
screen_reader_speak("You can't carry any more meat.", true);
return;
}
if (inv_skins >= MAX_ITEM_STACK) {
screen_reader_speak("You can't carry any more skins.", true);
return;
}
simulate_crafting();
// Get the type of game we're butchering (first in the list)
string game_type = inv_small_game_types[0];
inv_small_game_types.remove_at(0);
inv_small_game--;
inv_meat++;
inv_skins++;
screen_reader_speak("Butchered " + game_type + ". Got 1 meat and 1 skin.", true);
} else {
screen_reader_speak("Missing: " + missing, true);
}
}
+72
View File
@@ -0,0 +1,72 @@
// Notification System
string[] notification_history;
const int MAX_NOTIFICATIONS = 10;
int current_notification_index = -1;
void notify(string message) {
// Play notification sound
p.play_stationary("sounds/notify.ogg", false);
// Speak the message
screen_reader_speak(message, true);
// Add to history
notification_history.insert_last(message);
// Keep only last 10 notifications
if (notification_history.length() > MAX_NOTIFICATIONS) {
notification_history.remove_at(0);
}
// Reset index to most recent
current_notification_index = notification_history.length() - 1;
}
void check_notification_keys() {
// [ for previous notification (older) with position
if (key_pressed(KEY_LBRACKET)) {
if (notification_history.length() == 0) {
screen_reader_speak("No notifications.", true);
return;
}
current_notification_index--;
if (current_notification_index < 0) {
current_notification_index = 0;
screen_reader_speak("Oldest notification.", true);
return;
}
int position = current_notification_index + 1;
screen_reader_speak(notification_history[current_notification_index] + " " + position + " of " + notification_history.length(), true);
return;
}
// ] for next notification (newer) with position
if (key_pressed(KEY_RBRACKET)) {
if (notification_history.length() == 0) {
screen_reader_speak("No notifications.", true);
return;
}
current_notification_index++;
if (current_notification_index >= notification_history.length()) {
current_notification_index = notification_history.length() - 1;
screen_reader_speak("Most recent notification.", true);
return;
}
int position = current_notification_index + 1;
screen_reader_speak(notification_history[current_notification_index] + " " + position + " of " + notification_history.length(), true);
return;
}
// \ for most recent notification (without position)
if (key_pressed(KEY_BACKSLASH)) {
if (notification_history.length() == 0) {
screen_reader_speak("No notifications.", true);
return;
}
current_notification_index = notification_history.length() - 1;
screen_reader_speak(notification_history[current_notification_index], true);
}
}
+34
View File
@@ -0,0 +1,34 @@
// Player state
int x = 0;
int y = 0;
int facing = 1; // 0: left, 1: right
bool jumping = false;
bool climbing = false;
bool falling = false;
int climb_target_y = 0; // Target height when climbing
int fall_start_y = 0; // Height where fall started
int fall_sound_handle = -1; // Handle for looping fall sound
timer fall_timer; // For fall sound pitch
timer climb_timer; // For climb speed
// Health System
int player_health = 10;
int max_health = 10;
timer fire_damage_timer;
timer healing_timer;
// Sling state
bool sling_charging = false;
timer sling_charge_timer;
int sling_sound_handle = -1;
int last_sling_stage = -1; // Track which stage we're in to avoid duplicate sounds
// Timers
timer walktimer;
timer jumptimer;
timer search_timer;
timer search_delay_timer;
timer attack_timer;
// Search state
bool searching = false;
+485
View File
@@ -0,0 +1,485 @@
// Save system
const string SAVE_FILE_PATH = "save.dat";
const string SAVE_BACKUP_SUFFIX = "_robust";
const string SAVE_ENCRYPTION_KEY = "draugnorak_save_v1";
const int SAVE_VERSION = 1;
bool has_save_game() {
return file_exists(SAVE_FILE_PATH);
}
string encrypt_save_data(const string&in rawData) {
return string_aes_encrypt(rawData, SAVE_ENCRYPTION_KEY);
}
string decrypt_save_data(const string&in encryptedData) {
return string_aes_decrypt(encryptedData, SAVE_ENCRYPTION_KEY);
}
bool save_robust_data(const string&in filename, const string&in data) {
if (data.length() == 0) {
return false;
}
string backupPath = filename + SAVE_BACKUP_SUFFIX;
if (file_exists(filename)) {
if (!file_copy(filename, backupPath, true)) {
return false;
}
}
file tmp;
if (!tmp.open(filename, "wb")) {
if (file_exists(backupPath)) {
file_copy(backupPath, filename, true);
file_delete(backupPath);
}
return false;
}
if (tmp.write(data) < data.length()) {
tmp.close();
if (file_exists(backupPath)) {
file_copy(backupPath, filename, true);
file_delete(backupPath);
}
return false;
}
tmp.close();
file_delete(backupPath);
return true;
}
bool read_file_string(const string&in filename, string&out data) {
file tmp;
if (!tmp.open(filename, "rb")) {
return false;
}
data = tmp.read();
tmp.close();
return data.length() > 0;
}
double get_number(dictionary@ data, const string&in key, double defaultValue) {
double value;
if (@data == null) return defaultValue;
if (!data.get(key, value)) return defaultValue;
return value;
}
bool get_bool(dictionary@ data, const string&in key, bool defaultValue) {
bool value;
if (@data == null) return defaultValue;
if (!data.get(key, value)) return defaultValue;
return value;
}
string[] get_string_list(dictionary@ data, const string&in key) {
string[] result;
if (@data == null) return result;
if (!data.get(key, result)) return result;
return result;
}
void stop_active_sounds() {
if (day_sound_handle != -1) {
p.destroy_sound(day_sound_handle);
day_sound_handle = -1;
}
if (night_sound_handle != -1) {
p.destroy_sound(night_sound_handle);
night_sound_handle = -1;
}
if (fall_sound_handle != -1) {
p.destroy_sound(fall_sound_handle);
fall_sound_handle = -1;
}
if (sling_sound_handle != -1) {
p.destroy_sound(sling_sound_handle);
sling_sound_handle = -1;
}
}
void clear_world_objects() {
for (uint i = 0; i < world_snares.length(); i++) {
world_snares[i].destroy();
}
world_snares.resize(0);
for (uint i = 0; i < world_fires.length(); i++) {
world_fires[i].destroy();
}
world_fires.resize(0);
for (uint i = 0; i < world_streams.length(); i++) {
world_streams[i].destroy();
}
world_streams.resize(0);
world_firepits.resize(0);
world_herb_gardens.resize(0);
for (uint i = 0; i < trees.length(); i++) {
if (trees[i].sound_handle != -1) {
p.destroy_sound(trees[i].sound_handle);
trees[i].sound_handle = -1;
}
}
trees.resize(0);
clear_zombies();
clear_bandits();
}
void reset_game_state() {
stop_active_sounds();
clear_world_objects();
x = 0;
y = 0;
facing = 1;
jumping = false;
climbing = false;
falling = false;
climb_target_y = 0;
fall_start_y = 0;
sling_charging = false;
searching = false;
player_health = 10;
max_health = 10;
inv_stones = 0;
inv_sticks = 0;
inv_vines = 0;
inv_logs = 0;
inv_small_game = 0;
inv_small_game_types.resize(0);
inv_meat = 0;
inv_skins = 0;
inv_spears = 0;
inv_snares = 0;
inv_axes = 0;
inv_knives = 0;
inv_fishing_poles = 0;
inv_slings = 0;
spear_equipped = false;
axe_equipped = false;
sling_equipped = false;
MAP_SIZE = 35;
expanded_area_start = -1;
expanded_area_end = -1;
expanded_terrain_types.resize(0);
barricade_health = 0;
barricade_initialized = false;
current_hour = 8;
current_day = 1;
is_daytime = true;
sun_setting_warned = false;
sunrise_warned = false;
area_expanded_today = false;
invasion_active = false;
invasion_start_hour = -1;
walktimer.restart();
jumptimer.restart();
search_timer.restart();
search_delay_timer.restart();
attack_timer.restart();
hour_timer.restart();
fire_damage_timer.restart();
healing_timer.restart();
sling_charge_timer.restart();
fall_timer.restart();
climb_timer.restart();
}
void start_new_game() {
reset_game_state();
spawn_trees(5, 19);
init_barricade();
init_time();
}
string serialize_bool(bool value) {
return value ? "1" : "0";
}
string serialize_tree(Tree@ tree) {
return tree.position + "|" + tree.sticks + "|" + tree.vines + "|" + tree.health + "|" + tree.height + "|" + serialize_bool(tree.depleted) + "|" + serialize_bool(tree.is_chopped) + "|" + tree.minutes_since_depletion;
}
string serialize_snare(WorldSnare@ snare) {
return snare.position + "|" + serialize_bool(snare.has_catch) + "|" + snare.catch_type + "|" + snare.catch_chance + "|" + snare.escape_chance + "|" + serialize_bool(snare.active);
}
string serialize_fire(WorldFire@ fire) {
return fire.position + "|" + fire.fuel_remaining + "|" + serialize_bool(fire.low_fuel_warned);
}
string serialize_stream(WorldStream@ stream) {
return stream.start_position + "|" + stream.get_width();
}
bool save_game_state() {
dictionary saveData;
saveData.set("version", SAVE_VERSION);
saveData.set("player_x", x);
saveData.set("player_y", y);
saveData.set("player_facing", facing);
saveData.set("player_health", player_health);
saveData.set("player_max_health", max_health);
saveData.set("inventory_stones", inv_stones);
saveData.set("inventory_sticks", inv_sticks);
saveData.set("inventory_vines", inv_vines);
saveData.set("inventory_logs", inv_logs);
saveData.set("inventory_small_game", inv_small_game);
saveData.set("inventory_meat", inv_meat);
saveData.set("inventory_skins", inv_skins);
saveData.set("inventory_spears", inv_spears);
saveData.set("inventory_snares", inv_snares);
saveData.set("inventory_axes", inv_axes);
saveData.set("inventory_knives", inv_knives);
saveData.set("inventory_fishing_poles", inv_fishing_poles);
saveData.set("inventory_slings", inv_slings);
saveData.set("inventory_small_game_types", inv_small_game_types);
saveData.set("equipment_spear_equipped", spear_equipped);
saveData.set("equipment_axe_equipped", axe_equipped);
saveData.set("equipment_sling_equipped", sling_equipped);
saveData.set("time_current_hour", current_hour);
saveData.set("time_current_day", current_day);
saveData.set("time_is_daytime", is_daytime);
saveData.set("time_sun_setting_warned", sun_setting_warned);
saveData.set("time_sunrise_warned", sunrise_warned);
saveData.set("time_area_expanded_today", area_expanded_today);
saveData.set("time_invasion_active", invasion_active);
saveData.set("time_invasion_start_hour", invasion_start_hour);
saveData.set("world_map_size", MAP_SIZE);
saveData.set("world_expanded_area_start", expanded_area_start);
saveData.set("world_expanded_area_end", expanded_area_end);
saveData.set("world_barricade_initialized", barricade_initialized);
saveData.set("world_barricade_health", barricade_health);
saveData.set("world_expanded_terrain_types", expanded_terrain_types);
string[] treeData;
for (uint i = 0; i < trees.length(); i++) {
treeData.insert_last(serialize_tree(trees[i]));
}
saveData.set("trees_data", treeData);
string[] snareData;
for (uint i = 0; i < world_snares.length(); i++) {
snareData.insert_last(serialize_snare(world_snares[i]));
}
saveData.set("snares_data", snareData);
string[] fireData;
for (uint i = 0; i < world_fires.length(); i++) {
fireData.insert_last(serialize_fire(world_fires[i]));
}
saveData.set("fires_data", fireData);
string[] firepitPositions;
for (uint i = 0; i < world_firepits.length(); i++) {
firepitPositions.insert_last("" + world_firepits[i].position);
}
saveData.set("firepits_positions", firepitPositions);
string[] herbPositions;
for (uint i = 0; i < world_herb_gardens.length(); i++) {
herbPositions.insert_last("" + world_herb_gardens[i].position);
}
saveData.set("herb_gardens_positions", herbPositions);
string[] streamData;
for (uint i = 0; i < world_streams.length(); i++) {
streamData.insert_last(serialize_stream(world_streams[i]));
}
saveData.set("streams_data", streamData);
string rawData = saveData.serialize();
string encryptedData = encrypt_save_data(rawData);
return save_robust_data(SAVE_FILE_PATH, encryptedData);
}
bool load_game_state() {
if (!file_exists(SAVE_FILE_PATH)) {
return false;
}
string encryptedData;
if (!read_file_string(SAVE_FILE_PATH, encryptedData)) {
return false;
}
string rawData = decrypt_save_data(encryptedData);
if (rawData.length() == 0) {
return false;
}
dictionary@ saveData = deserialize(rawData);
if (@saveData == null) {
return false;
}
reset_game_state();
MAP_SIZE = int(get_number(saveData, "world_map_size", 35));
expanded_area_start = int(get_number(saveData, "world_expanded_area_start", -1));
expanded_area_end = int(get_number(saveData, "world_expanded_area_end", -1));
string[] loadedTerrain = get_string_list(saveData, "world_expanded_terrain_types");
expanded_terrain_types.resize(0);
for (uint i = 0; i < loadedTerrain.length(); i++) {
expanded_terrain_types.insert_last(loadedTerrain[i]);
}
barricade_initialized = get_bool(saveData, "world_barricade_initialized", true);
barricade_health = int(get_number(saveData, "world_barricade_health", BARRICADE_BASE_HEALTH));
if (!barricade_initialized) {
init_barricade();
} else {
if (barricade_health < 0) barricade_health = 0;
if (barricade_health > BARRICADE_MAX_HEALTH) barricade_health = BARRICADE_MAX_HEALTH;
}
x = int(get_number(saveData, "player_x", 0));
y = int(get_number(saveData, "player_y", 0));
facing = int(get_number(saveData, "player_facing", 1));
player_health = int(get_number(saveData, "player_health", 10));
max_health = int(get_number(saveData, "player_max_health", 10));
if (x < 0) x = 0;
if (x >= MAP_SIZE) x = MAP_SIZE - 1;
if (y < 0) y = 0;
if (facing != 0 && facing != 1) facing = 1;
inv_stones = int(get_number(saveData, "inventory_stones", 0));
inv_sticks = int(get_number(saveData, "inventory_sticks", 0));
inv_vines = int(get_number(saveData, "inventory_vines", 0));
inv_logs = int(get_number(saveData, "inventory_logs", 0));
inv_small_game = int(get_number(saveData, "inventory_small_game", 0));
inv_meat = int(get_number(saveData, "inventory_meat", 0));
inv_skins = int(get_number(saveData, "inventory_skins", 0));
inv_spears = int(get_number(saveData, "inventory_spears", 0));
inv_snares = int(get_number(saveData, "inventory_snares", 0));
inv_axes = int(get_number(saveData, "inventory_axes", 0));
inv_knives = int(get_number(saveData, "inventory_knives", 0));
inv_fishing_poles = int(get_number(saveData, "inventory_fishing_poles", 0));
inv_slings = int(get_number(saveData, "inventory_slings", 0));
string[] loadedSmallGameTypes = get_string_list(saveData, "inventory_small_game_types");
inv_small_game_types.resize(0);
for (uint i = 0; i < loadedSmallGameTypes.length(); i++) {
inv_small_game_types.insert_last(loadedSmallGameTypes[i]);
}
if (inv_small_game_types.length() == 0 && inv_small_game > 0) {
for (int i = 0; i < inv_small_game; i++) {
inv_small_game_types.insert_last("small game");
}
} else {
inv_small_game = inv_small_game_types.length();
}
spear_equipped = get_bool(saveData, "equipment_spear_equipped", false);
axe_equipped = get_bool(saveData, "equipment_axe_equipped", false);
sling_equipped = get_bool(saveData, "equipment_sling_equipped", false);
current_hour = int(get_number(saveData, "time_current_hour", 8));
current_day = int(get_number(saveData, "time_current_day", 1));
sun_setting_warned = get_bool(saveData, "time_sun_setting_warned", false);
sunrise_warned = get_bool(saveData, "time_sunrise_warned", false);
area_expanded_today = get_bool(saveData, "time_area_expanded_today", false);
invasion_active = get_bool(saveData, "time_invasion_active", false);
invasion_start_hour = int(get_number(saveData, "time_invasion_start_hour", -1));
if (current_hour < 0) current_hour = 0;
if (current_hour > 23) current_hour = 23;
if (current_day < 1) current_day = 1;
is_daytime = (current_hour >= 6 && current_hour < 19);
hour_timer.restart();
string[] treeData = get_string_list(saveData, "trees_data");
for (uint i = 0; i < treeData.length(); i++) {
string[]@ parts = treeData[i].split("|");
if (parts.length() < 8) continue;
int pos = parse_int(parts[0]);
Tree@ tree = Tree(pos);
tree.sticks = parse_int(parts[1]);
tree.vines = parse_int(parts[2]);
tree.health = parse_int(parts[3]);
tree.height = parse_int(parts[4]);
tree.depleted = (parse_int(parts[5]) == 1);
tree.is_chopped = (parse_int(parts[6]) == 1);
tree.minutes_since_depletion = parse_int(parts[7]);
tree.regen_timer.restart();
trees.insert_last(tree);
}
string[] snareData = get_string_list(saveData, "snares_data");
for (uint i = 0; i < snareData.length(); i++) {
string[]@ parts = snareData[i].split("|");
if (parts.length() < 6) continue;
int pos = parse_int(parts[0]);
WorldSnare@ snare = WorldSnare(pos);
snare.has_catch = (parse_int(parts[1]) == 1);
snare.catch_type = parts[2];
snare.catch_chance = parse_int(parts[3]);
snare.escape_chance = parse_int(parts[4]);
snare.active = (parse_int(parts[5]) == 1);
snare.minute_timer.restart();
world_snares.insert_last(snare);
}
string[] fireData = get_string_list(saveData, "fires_data");
for (uint i = 0; i < fireData.length(); i++) {
string[]@ parts = fireData[i].split("|");
if (parts.length() < 3) continue;
int pos = parse_int(parts[0]);
WorldFire@ fire = WorldFire(pos);
fire.fuel_remaining = parse_int(parts[1]);
fire.low_fuel_warned = (parse_int(parts[2]) == 1);
fire.fuel_timer.restart();
world_fires.insert_last(fire);
}
string[] firepitPositions = get_string_list(saveData, "firepits_positions");
for (uint i = 0; i < firepitPositions.length(); i++) {
add_world_firepit(parse_int(firepitPositions[i]));
}
string[] herbPositions = get_string_list(saveData, "herb_gardens_positions");
for (uint i = 0; i < herbPositions.length(); i++) {
add_world_herb_garden(parse_int(herbPositions[i]));
}
string[] streamData = get_string_list(saveData, "streams_data");
for (uint i = 0; i < streamData.length(); i++) {
string[]@ parts = streamData[i].split("|");
if (parts.length() < 2) continue;
int startPos = parse_int(parts[0]);
int width = parse_int(parts[1]);
if (width < 1) width = 1;
add_world_stream(startPos, width);
}
update_ambience(true);
return true;
}
+278
View File
@@ -0,0 +1,278 @@
// Time System
// 1 real minute = 1 in-game hour
const int MS_PER_HOUR = 60000;
int current_hour = 8; // Start at 8 AM
int current_day = 1; // Track current day
timer hour_timer;
int day_sound_handle = -1;
int night_sound_handle = -1;
bool is_daytime = true;
bool sun_setting_warned = false;
bool sunrise_warned = false;
// Expansion and invasion tracking
bool area_expanded_today = false;
bool invasion_active = false;
int invasion_start_hour = -1;
string[] expanded_terrain_types;
void init_time() {
current_hour = 8;
current_day = 1;
hour_timer.restart();
is_daytime = true;
sun_setting_warned = false;
sunrise_warned = false;
area_expanded_today = false;
invasion_active = false;
invasion_start_hour = -1;
update_ambience(true); // Force start
}
void expand_area() {
if (expanded_area_start != -1) {
return; // Already expanded
}
// Play invasion sound
p.play_stationary("sounds/enemies/invasion.ogg", false);
// Calculate new area
expanded_area_start = MAP_SIZE;
expanded_area_end = MAP_SIZE + EXPANSION_SIZE - 1;
MAP_SIZE += EXPANSION_SIZE;
// Generate random terrain for the 30 new tiles
expanded_terrain_types.resize(EXPANSION_SIZE);
for (int i = 0; i < EXPANSION_SIZE; i++) {
int terrain_roll = random(0, 2);
if (terrain_roll == 0) {
expanded_terrain_types[i] = "stone";
} else if (terrain_roll == 1) {
expanded_terrain_types[i] = "grass";
} else {
expanded_terrain_types[i] = "snow";
}
}
// Generate streams (30% chance for a stream)
int stream_roll = random(1, 100);
if (stream_roll <= 30) {
// Determine stream width (1-5 tiles)
int stream_width = random(1, 5);
// Find a valid starting position for the stream
// Stream can only be in grass, stone, or snow areas
int attempts = 0;
int stream_start = -1;
while (attempts < 50) {
int candidate_start = random(0, EXPANSION_SIZE - stream_width);
bool valid_position = true;
// Check if all tiles in this range are valid for a stream
for (int i = 0; i < stream_width; i++) {
string terrain = expanded_terrain_types[candidate_start + i];
// Streams can be in any terrain type
if (terrain != "grass" && terrain != "stone" && terrain != "snow") {
valid_position = false;
break;
}
}
if (valid_position) {
stream_start = candidate_start;
break;
}
attempts++;
}
// Create the stream if we found a valid position
if (stream_start != -1) {
int actual_start = expanded_area_start + stream_start;
add_world_stream(actual_start, stream_width);
string width_desc = "very small";
if (stream_width == 2) width_desc = "small";
else if (stream_width == 3) width_desc = "medium";
else if (stream_width == 4) width_desc = "wide";
else if (stream_width == 5) width_desc = "very wide";
notify("A " + width_desc + " stream flows through the new area at x " + actual_start + ".");
}
}
// Generate trees in grass areas (50% chance for each grass tile)
for (int i = 0; i < EXPANSION_SIZE; i++) {
if (expanded_terrain_types[i] == "grass") {
int tile_pos = expanded_area_start + i;
// Skip if this position has a stream
if (is_position_in_water(tile_pos)) {
continue;
}
// 50% chance to spawn a tree
int tree_roll = random(1, 100);
if (tree_roll <= 50) {
Tree@ t = Tree(tile_pos);
trees.insert_last(t);
}
}
}
area_expanded_today = true;
notify("The area has expanded! New territory discovered to the east.");
}
void start_invasion() {
invasion_active = true;
invasion_start_hour = current_hour;
notify("Bandits are invading from the new area!");
}
void end_invasion() {
invasion_active = false;
invasion_start_hour = -1;
clear_bandits();
notify("The bandit invasion has ended.");
}
void check_invasion_status() {
if (!invasion_active) return;
// Check if invasion duration has elapsed
int hours_elapsed = current_hour - invasion_start_hour;
if (hours_elapsed < 0) {
hours_elapsed += 24; // Handle day wrap
}
if (hours_elapsed >= INVASION_DURATION_HOURS) {
end_invasion();
}
}
void manage_bandits_during_invasion() {
if (!invasion_active) return;
if (expanded_area_start == -1) return;
// Bandits only appear during daytime (6 AM to 7 PM)
if (!is_daytime) {
clear_bandits();
return;
}
// Maintain BANDIT_MAX_COUNT bandits during invasion
while (bandits.length() < BANDIT_MAX_COUNT) {
spawn_bandit(expanded_area_start, expanded_area_end);
}
}
void update_time() {
if (hour_timer.elapsed >= MS_PER_HOUR) {
hour_timer.restart();
current_hour++;
if (current_hour >= 24) {
current_hour = 0;
current_day++;
area_expanded_today = false; // Reset for new day
}
if (current_hour == 18 && !sun_setting_warned) {
notify("The sun is setting.");
sun_setting_warned = true;
} else if (current_hour == 19) {
sun_setting_warned = false;
}
if (current_hour == 5 && !sunrise_warned) {
notify("Sunrise isn't far away.");
sunrise_warned = true;
} else if (current_hour == 6) {
sunrise_warned = false;
}
// Check for area expansion (day 2+, daytime morning before noon, not yet expanded today)
if (current_day >= 2 && current_hour >= 6 && current_hour < 12 && !area_expanded_today && expanded_area_start == -1) {
int roll = random(1, 100);
if (roll <= EXPANSION_CHANCE) {
expand_area();
// Start invasion immediately after expansion (morning, during daytime)
start_invasion();
}
}
// Check invasion status
check_invasion_status();
check_ambience_transition();
if (current_hour == 6) {
save_game_state();
}
}
// Manage bandits during active invasion
manage_bandits_during_invasion();
}
void check_time_input() {
if (key_pressed(KEY_T)) {
screen_reader_speak(get_time_string(), true);
}
}
string get_time_string() {
int display_hour = current_hour;
string period = "am";
if (display_hour == 0) {
display_hour = 12;
} else if (display_hour == 12) {
period = "pm";
} else if (display_hour > 12) {
display_hour -= 12;
period = "pm";
}
return display_hour + " oclock " + period;
}
void check_ambience_transition() {
// Definition of Day: 6 AM to 7 PM (19:00) ?
// Let's say Day is 6 (6AM) to 19 (7PM). Night is 20 (8PM) to 5 (5AM).
// Or simpler: 6 to 18 (6PM).
bool now_day = (current_hour >= 6 && current_hour < 19);
if (now_day != is_daytime) {
is_daytime = now_day;
update_ambience(false);
}
}
void update_ambience(bool force_restart) {
if (is_daytime) {
// Transition to Day
if (night_sound_handle != -1) {
p.destroy_sound(night_sound_handle);
night_sound_handle = -1;
}
if (day_sound_handle == -1 || !p.sound_is_active(day_sound_handle)) {
// Play looped, stationary (or relative to player x?)
// Usually ambience is 2D/Global. play_stationary_extended allows looping.
// Or play_1d at player position if we want panning?
// "sounds/nature/day.ogg"
day_sound_handle = p.play_stationary("sounds/nature/day.ogg", true);
}
} else {
// Transition to Night
if (day_sound_handle != -1) {
p.destroy_sound(day_sound_handle);
day_sound_handle = -1;
}
if (night_sound_handle == -1 || !p.sound_is_active(night_sound_handle)) {
night_sound_handle = p.play_stationary("sounds/nature/night.ogg", true);
}
}
}
+845
View File
@@ -0,0 +1,845 @@
// World Objects
// Small game types that can be caught in snares
string[] small_game_types = {"rabbit", "squirrel", "raccoon", "opossum", "groundhog"};
int barricade_health = 0;
bool barricade_initialized = false;
string[] zombie_sounds = {"sounds/enemies/zombie1.ogg"};
string[] bandit_sounds = {"sounds/enemies/bandit1.ogg", "sounds/enemies/bandit2.ogg"};
class Zombie {
int position;
int health;
string voice_sound;
timer move_timer;
timer groan_timer;
timer attack_timer;
int next_groan_delay;
Zombie(int pos) {
position = pos;
health = ZOMBIE_HEALTH;
int sound_index = random(0, zombie_sounds.length() - 1);
voice_sound = zombie_sounds[sound_index];
move_timer.restart();
groan_timer.restart();
attack_timer.restart();
next_groan_delay = random(ZOMBIE_GROAN_MIN_DELAY, ZOMBIE_GROAN_MAX_DELAY);
}
}
Zombie@[] zombies;
class Bandit {
int position;
int health;
string alert_sound;
string weapon_type; // "spear" or "axe"
timer move_timer;
timer alert_timer;
timer attack_timer;
int next_alert_delay;
int move_interval;
Bandit(int pos, int expansion_start, int expansion_end) {
// Spawn somewhere in the expanded area
position = random(expansion_start, expansion_end);
health = BANDIT_HEALTH;
// Choose random alert sound
int sound_index = random(0, bandit_sounds.length() - 1);
alert_sound = bandit_sounds[sound_index];
// Choose random weapon
weapon_type = (random(0, 1) == 0) ? "spear" : "axe";
// Random movement speed within range
move_interval = random(BANDIT_MOVE_INTERVAL_MIN, BANDIT_MOVE_INTERVAL_MAX);
move_timer.restart();
alert_timer.restart();
attack_timer.restart();
next_alert_delay = random(BANDIT_ALERT_MIN_DELAY, BANDIT_ALERT_MAX_DELAY);
}
}
Bandit@[] bandits;
string get_random_small_game() {
int index = random(0, small_game_types.length() - 1);
return small_game_types[index];
}
class WorldSnare {
int position;
bool has_catch;
string catch_type; // What type of small game was caught
int catch_chance;
int escape_chance;
int sound_handle;
timer minute_timer;
bool active; // To prevent immediate breakage on placement
WorldSnare(int pos) {
position = pos;
has_catch = false;
catch_type = "";
catch_chance = 5;
escape_chance = 5;
active = false; // Becomes active when player steps off
sound_handle = -1;
minute_timer.restart();
}
void update() {
// Activate if player moves away
if (!active && x != position) {
active = true;
minute_timer.restart();
}
// Limit snare sound to 2 tiles distance
if (abs(x - position) <= 2) {
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
sound_handle = p.play_1d("sounds/actions/set_snare.ogg", x, position, true);
}
} else {
if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
}
}
// Every minute logic (only when active)
if (active && minute_timer.elapsed >= 60000) {
minute_timer.restart();
if (has_catch) {
// Animal trying to escape
if (escape_chance < 95) escape_chance += 5;
int roll = random(1, 100);
if (roll <= escape_chance) {
// Animal escaped!
has_catch = false;
notify("A " + catch_type + " escaped from your snare at " + position + "!");
catch_type = "";
catch_chance = 5;
}
} else {
// Trying to catch small game
if (catch_chance < 75) catch_chance += 5;
int roll = random(1, 100);
if (roll <= catch_chance) {
// Caught something!
has_catch = true;
catch_type = get_random_small_game();
escape_chance = 5; // Reset escape chance
notify(catch_type + " caught in snare at x " + position + " y 0!");
}
}
}
}
void destroy() {
if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
}
}
}
WorldSnare@[] world_snares;
class WorldFire {
int position;
int sound_handle;
timer fuel_timer;
int fuel_remaining;
bool low_fuel_warned;
WorldFire(int pos) {
position = pos;
sound_handle = -1;
fuel_remaining = 720000; // Start with 12 minutes (12 hours in-game)
low_fuel_warned = false;
fuel_timer.restart();
}
void add_fuel(int amount) {
fuel_remaining += amount;
low_fuel_warned = false;
}
bool is_burning() {
return fuel_remaining > 0;
}
void update() {
// Update fuel
if (fuel_remaining > 0) {
int elapsed = fuel_timer.elapsed;
fuel_timer.restart();
fuel_remaining -= elapsed;
// Warn when fuel is low (30 seconds remaining)
if (!low_fuel_warned && fuel_remaining <= 30000 && fuel_remaining > 0) {
low_fuel_warned = true;
notify("Fire at " + position + " is getting low!");
}
// Fire went out
if (fuel_remaining <= 0) {
fuel_remaining = 0;
notify("Fire at " + position + " has gone out.");
if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
}
return;
}
}
// Limit fire sound to 2 tiles distance (only if burning)
if (is_burning()) {
if (abs(x - position) <= 2) {
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
sound_handle = p.play_1d("sounds/items/fire.ogg", x, position, true);
}
} else {
if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
}
}
}
}
void destroy() {
if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
}
}
}
WorldFire@[] world_fires;
class WorldFirepit {
int position;
WorldFirepit(int pos) {
position = pos;
}
}
WorldFirepit@[] world_firepits;
class WorldHerbGarden {
int position;
WorldHerbGarden(int pos) {
position = pos;
}
}
WorldHerbGarden@[] world_herb_gardens;
class WorldStream {
int start_position;
int end_position;
int sound_handle;
WorldStream(int start_pos, int width) {
start_position = start_pos;
end_position = start_pos + width - 1;
sound_handle = -1;
}
bool contains_position(int pos) {
return pos >= start_position && pos <= end_position;
}
int get_width() {
return end_position - start_position + 1;
}
int get_center_position() {
return (start_position + end_position) / 2;
}
void update() {
int center = get_center_position();
// Play stream sound within 3 tiles distance from center
if (abs(x - center) <= 3) {
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
sound_handle = p.play_1d("sounds/terrain/stream.ogg", x, center, true);
}
} else {
if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
}
}
}
void destroy() {
if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
}
}
}
WorldStream@[] world_streams;
void add_world_snare(int pos) {
WorldSnare@ s = WorldSnare(pos);
world_snares.insert_last(s);
}
void add_world_fire(int pos) {
WorldFire@ f = WorldFire(pos);
world_fires.insert_last(f);
}
void add_world_firepit(int pos) {
WorldFirepit@ fp = WorldFirepit(pos);
world_firepits.insert_last(fp);
}
void update_world_objects() {
for (uint i = 0; i < world_snares.length(); i++) {
world_snares[i].update();
}
for (uint i = 0; i < world_fires.length(); i++) {
world_fires[i].update();
}
}
WorldSnare@ get_snare_at(int pos) {
for (uint i = 0; i < world_snares.length(); i++) {
if (world_snares[i].position == pos) {
return @world_snares[i];
}
}
return null;
}
void remove_snare_at(int pos) {
for (uint i = 0; i < world_snares.length(); i++) {
if (world_snares[i].position == pos) {
world_snares[i].destroy();
world_snares.remove_at(i);
return;
}
}
}
// Called when player moves onto a tile
void check_snare_collision(int player_x) {
WorldSnare@ s = get_snare_at(player_x);
if (s != null && s.active) {
// Break the snare
p.play_stationary("sounds/actions/break_snare.ogg", false);
if (s.has_catch) {
screen_reader_speak("You stepped on your snare! The " + s.catch_type + " escaped.", true);
} else {
screen_reader_speak("You stepped on your snare and broke it!", true);
}
remove_snare_at(player_x);
}
}
void update_snares() {
for (uint i = 0; i < world_snares.length(); i++) {
world_snares[i].update();
}
}
void update_streams() {
for (uint i = 0; i < world_streams.length(); i++) {
world_streams[i].update();
}
}
void update_fires() {
// Update all fires and remove any that have burned out
for (uint i = 0; i < world_fires.length(); i++) {
world_fires[i].update();
}
// Remove dead fires
for (uint i = 0; i < world_fires.length(); i++) {
if (!world_fires[i].is_burning()) {
world_fires[i].destroy();
world_fires.remove_at(i);
i--;
}
}
}
WorldFire@ get_fire_at(int pos) {
for (uint i = 0; i < world_fires.length(); i++) {
if (world_fires[i].position == pos) {
return @world_fires[i];
}
}
return null;
}
WorldFire@ get_fire_near(int pos) {
// Check for fire at current position or adjacent tiles
for (int check_x = pos - 1; check_x <= pos + 1; check_x++) {
WorldFire@ fire = get_fire_at(check_x);
if (fire != null && fire.is_burning()) {
return @fire;
}
}
return null;
}
WorldFire@ get_fire_within_range(int pos, int range) {
// Check for fire within specified range
for (int check_x = pos - range; check_x <= pos + range; check_x++) {
WorldFire@ fire = get_fire_at(check_x);
if (fire != null && fire.is_burning()) {
return @fire;
}
}
return null;
}
WorldFirepit@ get_firepit_at(int pos) {
for (uint i = 0; i < world_firepits.length(); i++) {
if (world_firepits[i].position == pos) {
return @world_firepits[i];
}
}
return null;
}
WorldFirepit@ get_firepit_near(int pos, int range) {
// Check for firepit within specified range
for (int check_x = pos - range; check_x <= pos + range; check_x++) {
WorldFirepit@ firepit = get_firepit_at(check_x);
if (firepit != null) {
return @firepit;
}
}
return null;
}
void add_world_herb_garden(int pos) {
WorldHerbGarden@ hg = WorldHerbGarden(pos);
world_herb_gardens.insert_last(hg);
}
void init_barricade() {
if (barricade_initialized) {
return;
}
barricade_health = BARRICADE_BASE_HEALTH;
barricade_initialized = true;
}
int add_barricade_health(int amount) {
if (amount <= 0) {
return 0;
}
int before = barricade_health;
barricade_health += amount;
if (barricade_health > BARRICADE_MAX_HEALTH) {
barricade_health = BARRICADE_MAX_HEALTH;
}
return barricade_health - before;
}
void clear_zombies() {
if (zombies.length() == 0) return;
zombies.resize(0);
}
Zombie@ get_zombie_at(int pos) {
for (uint i = 0; i < zombies.length(); i++) {
if (zombies[i].position == pos) {
return @zombies[i];
}
}
return null;
}
void spawn_zombie() {
int spawn_x = -1;
for (int attempts = 0; attempts < 20; attempts++) {
int candidate = random(BASE_END + 1, MAP_SIZE - 1);
if (get_zombie_at(candidate) == null) {
spawn_x = candidate;
break;
}
}
if (spawn_x == -1) {
spawn_x = random(BASE_END + 1, MAP_SIZE - 1);
}
Zombie@ z = Zombie(spawn_x);
zombies.insert_last(z);
play_1d_with_volume_step(z.voice_sound, x, spawn_x, false, ZOMBIE_SOUND_VOLUME_STEP);
}
void try_attack_barricade(Zombie@ zombie) {
if (barricade_health <= 0) return;
if (zombie.attack_timer.elapsed < ZOMBIE_ATTACK_INTERVAL) return;
zombie.attack_timer.restart();
int damage = random(ZOMBIE_DAMAGE_MIN, ZOMBIE_DAMAGE_MAX);
barricade_health -= damage;
if (barricade_health < 0) barricade_health = 0;
play_1d_with_volume_step("sounds/enemies/zombie_hits_player.ogg", x, zombie.position, false, ZOMBIE_SOUND_VOLUME_STEP);
if (barricade_health == 0) {
notify("The barricade has fallen!");
}
}
bool can_zombie_attack_player(Zombie@ zombie) {
if (player_health <= 0) {
return false;
}
if (barricade_health > 0 && x <= BASE_END) {
return false;
}
if (abs(zombie.position - x) > 1) {
return false;
}
return y <= ZOMBIE_ATTACK_MAX_HEIGHT;
}
bool try_attack_player(Zombie@ zombie) {
if (!can_zombie_attack_player(zombie)) {
return false;
}
if (zombie.attack_timer.elapsed < ZOMBIE_ATTACK_INTERVAL) {
return false;
}
zombie.attack_timer.restart();
int damage = random(ZOMBIE_DAMAGE_MIN, ZOMBIE_DAMAGE_MAX);
player_health -= damage;
if (player_health < 0) {
player_health = 0;
}
play_1d_with_volume_step("sounds/enemies/zombie_hits_player.ogg", x, zombie.position, false, ZOMBIE_SOUND_VOLUME_STEP);
return true;
}
void update_zombie(Zombie@ zombie) {
if (zombie.groan_timer.elapsed > zombie.next_groan_delay) {
zombie.groan_timer.restart();
zombie.next_groan_delay = random(ZOMBIE_GROAN_MIN_DELAY, ZOMBIE_GROAN_MAX_DELAY);
play_1d_with_volume_step(zombie.voice_sound, x, zombie.position, false, ZOMBIE_SOUND_VOLUME_STEP);
}
if (try_attack_player(zombie)) {
return;
}
if (zombie.move_timer.elapsed < ZOMBIE_MOVE_INTERVAL) return;
zombie.move_timer.restart();
if (barricade_health > 0 && zombie.position == BASE_END + 1) {
try_attack_barricade(zombie);
return;
}
int direction = 0;
if (x > BASE_END) {
if (x > zombie.position) {
direction = 1;
} else if (x < zombie.position) {
direction = -1;
} else {
return;
}
} else {
direction = random(-1, 1);
if (direction == 0) return;
}
int target_x = zombie.position + direction;
if (target_x < 0 || target_x >= MAP_SIZE) return;
if (target_x <= BASE_END && barricade_health > 0) {
try_attack_barricade(zombie);
return;
}
zombie.position = target_x;
play_positional_footstep(x, zombie.position, BASE_END, GRASS_END, ZOMBIE_FOOTSTEP_MAX_DISTANCE, ZOMBIE_SOUND_VOLUME_STEP);
}
void update_zombies() {
if (is_daytime) {
clear_zombies();
return;
}
while (zombies.length() < ZOMBIE_MAX_COUNT) {
spawn_zombie();
}
for (uint i = 0; i < zombies.length(); i++) {
update_zombie(zombies[i]);
}
}
bool damage_zombie_at(int pos, int damage) {
for (uint i = 0; i < zombies.length(); i++) {
if (zombies[i].position == pos) {
zombies[i].health -= damage;
if (zombies[i].health <= 0) {
play_1d_with_volume_step("sounds/enemies/enemy_falls.ogg", x, pos, false, ZOMBIE_SOUND_VOLUME_STEP);
zombies.remove_at(i);
screen_reader_speak("Zombie killed.", true);
} else {
screen_reader_speak("Hit zombie.", true);
}
return true;
}
}
return false;
}
WorldHerbGarden@ get_herb_garden_at(int pos) {
for (uint i = 0; i < world_herb_gardens.length(); i++) {
if (world_herb_gardens[i].position == pos) {
return @world_herb_gardens[i];
}
}
return null;
}
WorldHerbGarden@ get_herb_garden_at_base() {
// Check if herb garden exists anywhere in base area (0-4)
for (uint i = 0; i < world_herb_gardens.length(); i++) {
if (world_herb_gardens[i].position <= BASE_END) {
return @world_herb_gardens[i];
}
}
return null;
}
// Bandit Functions
void clear_bandits() {
if (bandits.length() == 0) return;
bandits.resize(0);
}
Bandit@ get_bandit_at(int pos) {
for (uint i = 0; i < bandits.length(); i++) {
if (bandits[i].position == pos) {
return @bandits[i];
}
}
return null;
}
void spawn_bandit(int expansion_start, int expansion_end) {
int spawn_x = -1;
for (int attempts = 0; attempts < 20; attempts++) {
int candidate = random(expansion_start, expansion_end);
if (get_bandit_at(candidate) == null) {
spawn_x = candidate;
break;
}
}
if (spawn_x == -1) {
spawn_x = random(expansion_start, expansion_end);
}
Bandit@ b = Bandit(spawn_x, expansion_start, expansion_end);
bandits.insert_last(b);
play_1d_with_volume_step(b.alert_sound, x, spawn_x, false, BANDIT_SOUND_VOLUME_STEP);
}
bool can_bandit_attack_player(Bandit@ bandit) {
if (player_health <= 0) {
return false;
}
// Cannot attack player if barricade is up and player is in base
if (barricade_health > 0 && x <= BASE_END) {
return false;
}
if (abs(bandit.position - x) > 1) {
return false;
}
return y <= BANDIT_ATTACK_MAX_HEIGHT;
}
bool try_attack_player_bandit(Bandit@ bandit) {
if (!can_bandit_attack_player(bandit)) {
return false;
}
if (bandit.attack_timer.elapsed < BANDIT_ATTACK_INTERVAL) {
return false;
}
bandit.attack_timer.restart();
// Play weapon swing sound based on bandit's weapon
if (bandit.weapon_type == "spear") {
p.play_stationary("sounds/weapons/spear_swing.ogg", false);
} else if (bandit.weapon_type == "axe") {
p.play_stationary("sounds/weapons/axe_swing.ogg", false);
}
int damage = random(BANDIT_DAMAGE_MIN, BANDIT_DAMAGE_MAX);
player_health -= damage;
if (player_health < 0) {
player_health = 0;
}
// Play hit sound
if (bandit.weapon_type == "spear") {
p.play_stationary("sounds/weapons/spear_hit.ogg", false);
} else if (bandit.weapon_type == "axe") {
p.play_stationary("sounds/weapons/axe_hit.ogg", false);
}
return true;
}
void try_attack_barricade_bandit(Bandit@ bandit) {
if (barricade_health <= 0) return;
if (bandit.attack_timer.elapsed < BANDIT_ATTACK_INTERVAL) return;
bandit.attack_timer.restart();
// Bandits do 1-2 damage to barricade
int damage = random(BANDIT_DAMAGE_MIN, BANDIT_DAMAGE_MAX);
barricade_health -= damage;
if (barricade_health < 0) barricade_health = 0;
// Play weapon swing sound
if (bandit.weapon_type == "spear") {
p.play_stationary("sounds/weapons/spear_swing.ogg", false);
p.play_stationary("sounds/weapons/spear_hit.ogg", false);
} else if (bandit.weapon_type == "axe") {
p.play_stationary("sounds/weapons/axe_swing.ogg", false);
p.play_stationary("sounds/weapons/axe_hit.ogg", false);
}
if (barricade_health == 0) {
notify("The barricade has fallen!");
}
}
void update_bandit(Bandit@ bandit) {
// Play alert sound at intervals
if (bandit.alert_timer.elapsed > bandit.next_alert_delay) {
bandit.alert_timer.restart();
bandit.next_alert_delay = random(BANDIT_ALERT_MIN_DELAY, BANDIT_ALERT_MAX_DELAY);
play_1d_with_volume_step(bandit.alert_sound, x, bandit.position, false, BANDIT_SOUND_VOLUME_STEP);
}
if (try_attack_player_bandit(bandit)) {
return;
}
if (bandit.move_timer.elapsed < bandit.move_interval) return;
bandit.move_timer.restart();
// If barricade is up and bandit is at the edge of base, attack barricade
if (barricade_health > 0 && bandit.position == BASE_END + 1) {
try_attack_barricade_bandit(bandit);
return;
}
// Move toward player
int direction = 0;
if (x > BASE_END) {
// Player is outside base, move toward them
if (x > bandit.position) {
direction = 1;
} else if (x < bandit.position) {
direction = -1;
} else {
return;
}
} else {
// Player is in base, move toward base edge
if (bandit.position > BASE_END + 1) {
direction = -1;
} else {
return; // Already at base edge
}
}
int target_x = bandit.position + direction;
if (target_x < 0 || target_x >= MAP_SIZE) return;
// Don't enter base if barricade is up
if (target_x <= BASE_END && barricade_health > 0) {
try_attack_barricade_bandit(bandit);
return;
}
bandit.position = target_x;
play_positional_footstep(x, bandit.position, BASE_END, GRASS_END, BANDIT_FOOTSTEP_MAX_DISTANCE, BANDIT_SOUND_VOLUME_STEP);
}
void update_bandits() {
for (uint i = 0; i < bandits.length(); i++) {
update_bandit(bandits[i]);
}
}
bool damage_bandit_at(int pos, int damage) {
for (uint i = 0; i < bandits.length(); i++) {
if (bandits[i].position == pos) {
bandits[i].health -= damage;
if (bandits[i].health <= 0) {
play_1d_with_volume_step("sounds/enemies/enemy_falls.ogg", x, pos, false, BANDIT_SOUND_VOLUME_STEP);
bandits.remove_at(i);
screen_reader_speak("Bandit killed.", true);
} else {
screen_reader_speak("Hit bandit.", true);
}
return true;
}
}
return false;
}
// Stream Functions
void add_world_stream(int start_pos, int width) {
WorldStream@ s = WorldStream(start_pos, width);
world_streams.insert_last(s);
}
WorldStream@ get_stream_at(int pos) {
for (uint i = 0; i < world_streams.length(); i++) {
if (world_streams[i].contains_position(pos)) {
return @world_streams[i];
}
}
return null;
}
bool is_position_in_water(int pos) {
return get_stream_at(pos) != null;
}