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

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.ogg filter=lfs diff=lfs merge=lfs -text

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
nvgt
*.zip
lib/
lib_mac/
lib_windows/
stub/
include/
save.dat
save.dat_robust

108
AGENTS.md Normal file
View File

@@ -0,0 +1,108 @@
# Draugnorak Project Guidelines
## Asset Usage
**CRITICAL**: The `bloodshed/` directory is for reference only. DO NOT use any assets from the bloodshed directory in this game. It exists solely to help with coding examples and understanding patterns, but none of its files should be included or referenced in the actual game code.
## Project Structure
- `src/` - Game source code modules
- `constants.nvgt` - Game configuration and constants
- `player.nvgt` - Player state and variables
- `world_state.nvgt` - World objects (snares, fires, firepits, herb gardens)
- `inventory.nvgt` - Inventory, crafting, equipment menus
- `environment.nvgt` - Trees and environmental interactions
- `combat.nvgt` - Combat system
- `time_system.nvgt` - In-game time tracking
- `audio_utils.nvgt` - Audio helper functions
- `notify.nvgt` - Notification system
- `sounds/` - Game audio assets
- `draugnorak.nvgt` - Main game file
## Game Mechanics
### Inventory Limits
- All item stacks are capped at 9 until backpacks are implemented
### Map Layout
- Base area: x 0-4 (wood terrain)
- Grass area: x 5-19 (grass terrain, contains trees)
- Gravel area: x 20-34 (gravel terrain, contains stones)
### Healing System
- Base area provides passive healing when player health < max
- Without herb garden: 1 HP every 2.5 minutes (150000ms)
- With herb garden: 1 HP every 30 seconds (30000ms)
- Herb garden can only be built in base area (x <= 4)
- Only one herb garden allowed per base
### Fire System
- Fires require a firepit to build
- New fires start with 12 minutes (720000ms) of fuel
- Warning at 30 seconds remaining
- Fire goes out when fuel depletes
- Fire sound plays within 2 tiles distance
- Jumping over fire prevents damage
### Snare System
- Snares become active when player moves away
- Check every minute for catching/escaping rabbits
- Catch chance starts at 5%, increases by 5% per minute (max 75%)
- Escape chance starts at 5%, increases by 5% per minute (max 95%)
- Snare sound plays within 2 tiles distance
### Tree System
- Trees regenerate after 4 minutes (240000ms) when depleted or chopped
- Tree ambient sound plays within 3 tiles distance
- Trees only play sound when not chopped
- Climb down only when in a tree (Down arrow does nothing during jumps or when not in a tree)
### Notification System
- Important events use `notify()` function (plays sounds/notify.ogg + speaks message)
- Last 10 notifications stored in history
- Navigation keys:
- `\` - Repeat most recent notification
- `]` - Next (newer) notification
- `[` (Shift+Comma) - Previous (older) notification
- Navigating history speaks message without notification sound
### Search System
- Hold Shift for 1 second to search
- Search completes after 1-second delay
### Undead System
- At night, zombies spawn (5 max) and wander outside the base
- Zombies cannot enter the base while barricade health > 0
- Zombies attack the barricade for 4-6 damage when they reach it; play `sounds/enemies/zombie_hit.ogg`
- Zombies vanish at daybreak
- Player can hit zombies with spear (1-tile range) or sling (5-tile range)
### Barricade System
- Base starts with 100 barricade health, capped at 500
- In base, press B to report barricade health
- Crafting menu includes Barricade category for reinforcement
- Reinforcement costs and health: 3 sticks (+10), 5 vines (+15), 1 log (+30), 5 stones (+20)
- Barricade health does not reset during gameplay
## Menu Structure
### Crafting Menu (C key, base only)
Organized into three categories:
- **Weapons**: Spear
- **Tools**: Stone Knife, Snare, Stone Axe
- **Buildings**: Firepit, Fire, Herb Garden
### Equipment Menu (E key)
- Only shows items player actually has
- Shows equipped status
- Says "Nothing to equip" if inventory is empty
## Testing
- Always use `./nvgt -c draugnorak.nvgt` to compile without opening the game window
- This prevents the window from taking over the terminal during testing
## Code Standards
- Use `notify()` for important game events that should be reviewable
- Use `screen_reader_speak()` for immediate feedback that doesn't need to be stored

28
README.md Normal file
View File

@@ -0,0 +1,28 @@
# Draugnorak
A survival audio game built with NVGT.
## Controls
- **Arrow Keys**: Move (Left/Right) and Jump (Up).
- **Shift**: Search the current area for resources.
- **Control**: Attack with equipped weapon.
- **C**: Open Crafting Menu (Base area only).
- **E**: Open Equipment Menu.
- **A**: Open Action Menu (Place objects, feed fire).
- **I**: Check Inventory.
- **H**: Check Health.
- **T**: Check Time.
- **X**: Check Coordinates.
- **Escape**: Exit game.
## Gameplay
- **Gathering**: Search in grass for sticks and vines. Search in gravel for stones.
- **Crafting**: Create tools like Spears, Axes, Snares, and Firepits (base only).
- **Combat**: Defend yourself or hunt.
- **Survival**: Build fires and set snares to survive.
- **Fire Management**:
- Build a firepit (9 stones) in the base area - it's crafted in place
- Build fires (2 sticks + 1 log) only on tiles with a firepit
- Fires burn out over time and need fuel (vines, sticks, or logs)
- Stand next to a fire and press A to feed it
- Standing on a fire damages your health!

317
draugnorak.nvgt Normal file
View File

@@ -0,0 +1,317 @@
#include "include/bgt_compat.nvgt"
#include "include/sound_pool.nvgt"
#include "include/virtual_dialogs.nvgt"
// Audio
sound_pool p(100);
#include "src/constants.nvgt"
#include "src/player.nvgt"
#include "src/world_state.nvgt"
#include "src/inventory.nvgt"
#include "src/environment.nvgt"
#include "src/combat.nvgt"
#include "src/save_system.nvgt"
#include "src/time_system.nvgt"
#include "src/audio_utils.nvgt"
#include "src/notify.nvgt"
int run_main_menu() {
screen_reader_speak("Draugnorak. Main menu.", true);
int selection = 0;
string load_label = has_save_game() ? "Load Game" : "Load Game (no save found)";
string[] options = {"New Game", load_label, "Exit"};
screen_reader_speak(options[selection], true);
while(true) {
wait(5);
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)) {
return selection;
}
if (key_pressed(KEY_ESCAPE)) {
return 2;
}
}
}
void main()
{
show_window("Draugnorak");
bool game_started = false;
while (!game_started) {
int selection = run_main_menu();
if (selection == 0) {
start_new_game();
screen_reader_speak("New game started.", true);
game_started = true;
} else if (selection == 1) {
if (load_game_state()) {
screen_reader_speak("Game loaded.", true);
game_started = true;
} else {
if (has_save_game()) {
screen_reader_speak("Unable to load save.", true);
} else {
screen_reader_speak("No save found.", true);
}
}
} else {
exit();
}
}
while(true)
{
wait(5);
if(key_pressed(KEY_ESCAPE))
{
int really_exit = virtual_question("Draugnorak", "Really exit?");
if (really_exit == 1) {
exit();
}
// Restore focus to the game window
show_window("Draugnorak");
}
// Time & Environment updates
update_time();
update_environment();
update_snares();
update_streams();
update_fires();
update_zombies();
update_bandits();
// 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();
}
// Inventory & Actions
check_inventory_keys(x);
check_action_menu(x);
check_crafting_menu(x, BASE_END);
check_equipment_menu();
check_time_input();
check_notification_keys();
// Health Key
if (key_pressed(KEY_H)) {
screen_reader_speak(player_health + " health of " + max_health, true);
}
// Coordinates Key
if (key_pressed(KEY_X)) {
string direction_label = (facing == 1) ? "east" : "west";
screen_reader_speak(direction_label + ", x " + x + ", y " + y, true);
}
// Barricade Key (base only)
if (key_pressed(KEY_B)) {
if (x <= BASE_END) {
screen_reader_speak("Barricade health " + barricade_health + " of " + BARRICADE_MAX_HEALTH, true);
} else {
screen_reader_speak("You are not in the base.", true);
}
}
// Climbing and Falling Updates
update_climbing();
update_falling();
// Down arrow to climb down from tree
if (key_pressed(KEY_DOWN)) {
Tree@ tree = get_tree_at(x);
if (tree != null && !tree.is_chopped && y > 0 && !jumping && !climbing && !falling) {
climb_down_tree();
}
}
// Jumping Logic
if(key_pressed(KEY_UP))
{
if(!jumping && !climbing && !falling)
{
// Check if on tree tile
Tree@ tree = get_tree_at(x);
if (tree != null && !tree.is_chopped && y == 0) {
// Start climbing the tree
start_climbing_tree(x);
} else if (y == 0) {
// Normal jump
p.play_stationary("sounds/jump.ogg", false);
jumping = true;
jumptimer.restart();
}
}
}
if(jumping && jumptimer.elapsed > 850)
{
jumping = false;
y = 0; // Reset y after jump
play_land_sound(x, BASE_END, GRASS_END);
// Check for snare on landing?
check_snare_collision(x);
}
// Set y to 3 during jump
if(jumping && y == 0) {
y = 3;
}
movetime = jumping ? jump_speed : walk_speed;
// Movement Logic
if (key_pressed(KEY_LEFT) && facing != 0 && !climbing && !falling) {
facing = 0;
screen_reader_speak("west", true);
walktimer.restart();
}
if (key_pressed(KEY_RIGHT) && facing != 1 && !climbing && !falling) {
facing = 1;
screen_reader_speak("east", true);
walktimer.restart();
}
if(walktimer.elapsed > movetime)
{
int old_x = x;
// Check if trying to move left/right while in tree
if((key_down(KEY_LEFT) || key_down(KEY_RIGHT)) && y > 0 && !jumping && !falling) {
// Fall out of tree
climbing = false;
start_falling();
}
if(key_down(KEY_LEFT) && x > 0 && !climbing && !falling)
{
facing = 0;
x--;
walktimer.restart();
if(!jumping) {
play_footstep(x, BASE_END, GRASS_END);
check_snare_collision(x); // Check when moving onto a tile
}
}
else if(key_down(KEY_RIGHT) && x < MAP_SIZE - 1 && !climbing && !falling)
{
facing = 1;
x++;
walktimer.restart();
if(!jumping) {
play_footstep(x, BASE_END, GRASS_END);
check_snare_collision(x); // Check when moving onto a tile
}
}
}
// Reset search timer if shift is used with other keys
if((key_down(KEY_LSHIFT) || key_down(KEY_RSHIFT))) {
if(key_pressed(KEY_COMMA) || key_pressed(KEY_PERIOD)) {
searching = false;
search_delay_timer.restart();
}
}
// Searching Logic
if((key_down(KEY_LSHIFT) || key_down(KEY_RSHIFT)) && search_timer.elapsed > 2000 && !searching)
{
searching = true;
search_delay_timer.restart();
}
// Complete search after delay
if(searching && search_delay_timer.elapsed >= 1000)
{
searching = false;
search_timer.restart();
perform_search(x);
}
// Sling charge detection
if (sling_equipped && (key_down(KEY_LCONTROL) || key_down(KEY_RCONTROL)) && !sling_charging) {
if (inv_stones > 0) {
sling_charging = true;
sling_charge_timer.restart();
sling_sound_handle = p.play_stationary("sounds/weapons/sling_swing.ogg", true);
last_sling_stage = -1;
} else {
screen_reader_speak("No stones.", true);
}
}
// Update sling charge state while holding
if (sling_charging && (key_down(KEY_LCONTROL) || key_down(KEY_RCONTROL))) {
update_sling_charge();
}
// Sling release detection
if (sling_charging && (!key_down(KEY_LCONTROL) && !key_down(KEY_RCONTROL))) {
release_sling_attack(x);
sling_charging = false;
if (sling_sound_handle != -1) {
p.destroy_sound(sling_sound_handle);
sling_sound_handle = -1;
}
}
// Non-sling weapon attacks (existing pattern)
if (!sling_equipped && !sling_charging) {
int attack_cooldown = 1000;
if (spear_equipped) attack_cooldown = 800;
if (axe_equipped) attack_cooldown = 1600;
if((key_down(KEY_LCONTROL) || key_down(KEY_RCONTROL)) && attack_timer.elapsed > attack_cooldown)
{
attack_timer.restart();
perform_attack(x);
}
}
// Audio Listener Update
p.update_listener_1d(x);
}
}

BIN
sounds/actions/break_snare.ogg LFS Normal file

Binary file not shown.

BIN
sounds/actions/climb_tree.ogg LFS Normal file

Binary file not shown.

BIN
sounds/actions/falling.ogg LFS Normal file

Binary file not shown.

BIN
sounds/actions/feed_fire.ogg LFS Normal file

Binary file not shown.

BIN
sounds/actions/hit_ground.ogg LFS Normal file

Binary file not shown.

BIN
sounds/actions/set_snare.ogg LFS Normal file

Binary file not shown.

BIN
sounds/crafting.ogg LFS Normal file

Binary file not shown.

BIN
sounds/crafting_complete.ogg LFS Normal file

Binary file not shown.

BIN
sounds/enemies/bandit1.ogg LFS Normal file

Binary file not shown.

BIN
sounds/enemies/bandit2.ogg LFS Normal file

Binary file not shown.

BIN
sounds/enemies/enemy_falls.ogg LFS Normal file

Binary file not shown.

BIN
sounds/enemies/invasion.ogg LFS Normal file

Binary file not shown.

BIN
sounds/enemies/zombie1.ogg LFS Normal file

Binary file not shown.

BIN
sounds/enemies/zombie_hit.ogg LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
sounds/environment/tree.ogg LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
sounds/items/fire.ogg LFS Normal file

Binary file not shown.

BIN
sounds/items/miscellaneous.ogg LFS Normal file

Binary file not shown.

BIN
sounds/items/stick.ogg LFS Normal file

Binary file not shown.

BIN
sounds/items/stone.ogg LFS Normal file

Binary file not shown.

BIN
sounds/items/tree.ogg LFS Normal file

Binary file not shown.

BIN
sounds/items/vine.ogg LFS Normal file

Binary file not shown.

BIN
sounds/jump.ogg LFS Normal file

Binary file not shown.

BIN
sounds/nature/day.ogg LFS Normal file

Binary file not shown.

BIN
sounds/nature/night.ogg LFS Normal file

Binary file not shown.

BIN
sounds/notify.ogg LFS Normal file

Binary file not shown.

BIN
sounds/terrain/grass.ogg LFS Normal file

Binary file not shown.

BIN
sounds/terrain/gravel.ogg LFS Normal file

Binary file not shown.

BIN
sounds/terrain/shallow_water.ogg LFS Normal file

Binary file not shown.

BIN
sounds/terrain/snow.ogg LFS Normal file

Binary file not shown.

BIN
sounds/terrain/stone.ogg LFS Normal file

Binary file not shown.

BIN
sounds/terrain/stream.ogg LFS Normal file

Binary file not shown.

BIN
sounds/terrain/wood.ogg LFS Normal file

Binary file not shown.

BIN
sounds/weapons/axe_hit.ogg LFS Normal file

Binary file not shown.

BIN
sounds/weapons/axe_swing.ogg LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
sounds/weapons/sling_hit.ogg LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
sounds/weapons/sling_swing.ogg LFS Normal file

Binary file not shown.

Binary file not shown.

BIN
sounds/weapons/spear_hit.ogg LFS Normal file

Binary file not shown.

BIN
sounds/weapons/spear_swing.ogg LFS Normal file

Binary file not shown.

87
src/audio_utils.nvgt Normal file
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
src/combat.nvgt Normal file
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
src/constants.nvgt Normal file
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
src/environment.nvgt Normal file
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
src/inventory.nvgt Normal file
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
src/notify.nvgt Normal file
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
src/player.nvgt Normal file
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
src/save_system.nvgt Normal file
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
src/time_system.nvgt Normal file
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
src/world_state.nvgt Normal file
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;
}