Initial commit.
This commit is contained in:
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.ogg filter=lfs diff=lfs merge=lfs -text
|
||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
nvgt
|
||||
*.zip
|
||||
lib/
|
||||
lib_mac/
|
||||
lib_windows/
|
||||
stub/
|
||||
include/
|
||||
save.dat
|
||||
save.dat_robust
|
||||
108
AGENTS.md
Normal file
108
AGENTS.md
Normal 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
28
README.md
Normal 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
317
draugnorak.nvgt
Normal 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
BIN
sounds/actions/break_snare.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/actions/climb_tree.ogg
LFS
Normal file
BIN
sounds/actions/climb_tree.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/actions/falling.ogg
LFS
Normal file
BIN
sounds/actions/falling.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/actions/feed_fire.ogg
LFS
Normal file
BIN
sounds/actions/feed_fire.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/actions/hit_ground.ogg
LFS
Normal file
BIN
sounds/actions/hit_ground.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/actions/set_snare.ogg
LFS
Normal file
BIN
sounds/actions/set_snare.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/crafting.ogg
LFS
Normal file
BIN
sounds/crafting.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/crafting_complete.ogg
LFS
Normal file
BIN
sounds/crafting_complete.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/enemies/bandit1.ogg
LFS
Normal file
BIN
sounds/enemies/bandit1.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/enemies/bandit2.ogg
LFS
Normal file
BIN
sounds/enemies/bandit2.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/enemies/enemy_falls.ogg
LFS
Normal file
BIN
sounds/enemies/enemy_falls.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/enemies/invasion.ogg
LFS
Normal file
BIN
sounds/enemies/invasion.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/enemies/zombie1.ogg
LFS
Normal file
BIN
sounds/enemies/zombie1.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/enemies/zombie_hit.ogg
LFS
Normal file
BIN
sounds/enemies/zombie_hit.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/enemies/zombie_hits_player.ogg
LFS
Normal file
BIN
sounds/enemies/zombie_hits_player.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/environment/tree.ogg
LFS
Normal file
BIN
sounds/environment/tree.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/environment/tree.ogg.bak
Normal file
BIN
sounds/environment/tree.ogg.bak
Normal file
Binary file not shown.
BIN
sounds/items/fire.ogg
LFS
Normal file
BIN
sounds/items/fire.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/items/miscellaneous.ogg
LFS
Normal file
BIN
sounds/items/miscellaneous.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/items/stick.ogg
LFS
Normal file
BIN
sounds/items/stick.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/items/stone.ogg
LFS
Normal file
BIN
sounds/items/stone.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/items/tree.ogg
LFS
Normal file
BIN
sounds/items/tree.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/items/vine.ogg
LFS
Normal file
BIN
sounds/items/vine.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/jump.ogg
LFS
Normal file
BIN
sounds/jump.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/nature/day.ogg
LFS
Normal file
BIN
sounds/nature/day.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/nature/night.ogg
LFS
Normal file
BIN
sounds/nature/night.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/notify.ogg
LFS
Normal file
BIN
sounds/notify.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/terrain/grass.ogg
LFS
Normal file
BIN
sounds/terrain/grass.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/terrain/gravel.ogg
LFS
Normal file
BIN
sounds/terrain/gravel.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/terrain/shallow_water.ogg
LFS
Normal file
BIN
sounds/terrain/shallow_water.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/terrain/snow.ogg
LFS
Normal file
BIN
sounds/terrain/snow.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/terrain/stone.ogg
LFS
Normal file
BIN
sounds/terrain/stone.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/terrain/stream.ogg
LFS
Normal file
BIN
sounds/terrain/stream.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/terrain/wood.ogg
LFS
Normal file
BIN
sounds/terrain/wood.ogg
LFS
Normal file
Binary file not shown.
Binary file not shown.
BIN
sounds/weapons/axe_hit.ogg
LFS
Normal file
BIN
sounds/weapons/axe_hit.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/weapons/axe_swing.ogg
LFS
Normal file
BIN
sounds/weapons/axe_swing.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/weapons/sling_high_range.ogg
LFS
Normal file
BIN
sounds/weapons/sling_high_range.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/weapons/sling_hit.ogg
LFS
Normal file
BIN
sounds/weapons/sling_hit.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/weapons/sling_low_range.ogg
LFS
Normal file
BIN
sounds/weapons/sling_low_range.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/weapons/sling_swing.ogg
LFS
Normal file
BIN
sounds/weapons/sling_swing.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/weapons/sling_swing.wav
Normal file
BIN
sounds/weapons/sling_swing.wav
Normal file
Binary file not shown.
BIN
sounds/weapons/spear_hit.ogg
LFS
Normal file
BIN
sounds/weapons/spear_hit.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/weapons/spear_swing.ogg
LFS
Normal file
BIN
sounds/weapons/spear_swing.ogg
LFS
Normal file
Binary file not shown.
87
src/audio_utils.nvgt
Normal file
87
src/audio_utils.nvgt
Normal 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
189
src/combat.nvgt
Normal 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
63
src/constants.nvgt
Normal 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
456
src/environment.nvgt
Normal 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
886
src/inventory.nvgt
Normal 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
72
src/notify.nvgt
Normal 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
34
src/player.nvgt
Normal 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
485
src/save_system.nvgt
Normal 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
278
src/time_system.nvgt
Normal 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
845
src/world_state.nvgt
Normal 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user