A couple of terrain types added. First adventure added, incomplete.

This commit is contained in:
Storm Dragon
2026-01-22 14:43:25 -05:00
parent d2387b4506
commit 3097a245ca
17 changed files with 733 additions and 5 deletions

View File

@@ -34,6 +34,7 @@ sound_pool p(100);
#include "src/creature_audio.nvgt" #include "src/creature_audio.nvgt"
#include "src/notify.nvgt" #include "src/notify.nvgt"
#include "src/speech_history.nvgt" #include "src/speech_history.nvgt"
#include "src/bosses/adventure_system.nvgt"
int run_main_menu() { int run_main_menu() {
speak_with_history("Draugnorak. Main menu.", true); speak_with_history("Draugnorak. Main menu.", true);
@@ -192,6 +193,7 @@ void main()
// Inventory & Actions // Inventory & Actions
check_inventory_keys(x); check_inventory_keys(x);
check_action_menu(x); check_action_menu(x);
check_adventure_menu(x);
check_crafting_menu(x, BASE_END); check_crafting_menu(x, BASE_END);
check_altar_menu(x); check_altar_menu(x);
check_equipment_menu(); check_equipment_menu();
@@ -209,7 +211,11 @@ void main()
// Coordinates Key // Coordinates Key
if (key_pressed(KEY_X)) { if (key_pressed(KEY_X)) {
string direction_label = (facing == 1) ? "east" : "west"; string direction_label = (facing == 1) ? "east" : "west";
speak_with_history(direction_label + ", x " + x + ", y " + y, true); string terrain = get_terrain_at_position(x);
if (get_mountain_at(x) !is null) {
terrain += ". Mountains";
}
speak_with_history(direction_label + ", x " + x + ", y " + y + ", terrain " + terrain, true);
} }
// Base Info Key (base only) // Base Info Key (base only)

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
sounds/terrain/deep_forest.ogg LFS Normal file

Binary file not shown.

BIN
sounds/terrain/forest.ogg LFS Normal file

Binary file not shown.

View File

@@ -37,6 +37,10 @@ string get_footstep_sound(int current_x, int base_end, int grass_end)
return "sounds/terrain/gravel.ogg"; return "sounds/terrain/gravel.ogg";
} else if (terrain == "snow") { } else if (terrain == "snow") {
return "sounds/terrain/snow.ogg"; return "sounds/terrain/snow.ogg";
} else if (terrain == "forest") {
return "sounds/terrain/forest.ogg";
} else if (terrain == "deep_forest") {
return "sounds/terrain/deep_forest.ogg";
} }
} }
@@ -57,6 +61,10 @@ string get_footstep_sound(int current_x, int base_end, int grass_end)
return "sounds/terrain/snow.ogg"; return "sounds/terrain/snow.ogg";
} else if (terrain == "gravel") { } else if (terrain == "gravel") {
return "sounds/terrain/gravel.ogg"; return "sounds/terrain/gravel.ogg";
} else if (terrain == "forest") {
return "sounds/terrain/forest.ogg";
} else if (terrain == "deep_forest") {
return "sounds/terrain/deep_forest.ogg";
} }
} }
} }

View File

@@ -0,0 +1,81 @@
// Adventure System
// Handles triggering and managing terrain-specific adventures
#include "src/bosses/unicorn/unicorn_boss.nvgt"
void check_adventure_menu(int player_x) {
if (key_pressed(KEY_TAB)) {
run_adventure_menu(player_x);
}
}
void run_adventure_menu(int player_x) {
if (player_x <= BASE_END) {
speak_with_history("No adventures available in the base.", true);
return;
}
if (last_adventure_day == current_day) {
speak_with_history("You have already attempted an adventure today.", true);
return;
}
string terrain = get_terrain_at_position(player_x);
MountainRange@ mountain = get_mountain_at(player_x);
// Check available adventures based on terrain
string[] options;
int[] adventure_ids; // 1 = Unicorn
if (mountain !is null) {
// Mountain terrain
options.insert_last("Unicorn Hunt (Mountain Boss)");
adventure_ids.insert_last(1);
}
if (options.length() == 0) {
speak_with_history("No adventures found in this area.", true);
return;
}
// Show Menu
speak_with_history("Adventure Menu.", true);
int selection = 0;
speak_with_history(options[selection], true);
while (true) {
wait(5);
if (key_pressed(KEY_ESCAPE)) {
speak_with_history("Closed.", true);
return;
}
if (key_pressed(KEY_DOWN)) {
selection++;
if (selection >= options.length()) selection = 0;
speak_with_history(options[selection], true);
}
if (key_pressed(KEY_UP)) {
selection--;
if (selection < 0) selection = options.length() - 1;
speak_with_history(options[selection], true);
}
if (key_pressed(KEY_RETURN)) {
start_adventure(adventure_ids[selection]);
// Adventure has finished (it blocks until done)
// Resume main game ambience
update_ambience(true);
return;
}
}
}
void start_adventure(int adventure_id) {
last_adventure_day = current_day;
if (adventure_id == 1) {
run_unicorn_adventure();
}
}

View File

@@ -0,0 +1,400 @@
// Unicorn Boss Adventure logic
// Terrain: Mountain
// Objective: Destroy the bridge supports to defeat the charging unicorn.
class UnicornBoss {
int x;
int facing; // 0 = left, 1 = right
int health;
int speed;
timer move_timer;
int sound_handle;
bool on_bridge;
UnicornBoss() {
reset();
}
void reset() {
health = 10000;
speed = UNICORN_SPEED;
facing = 0; // Start facing west (toward player)
x = 0;
on_bridge = false;
sound_handle = -1;
move_timer.restart();
}
}
// Adventure Arena Constants
const int UNICORN_ARENA_SIZE = 100;
const int BRIDGE_START = 45;
const int BRIDGE_END = 54;
const int BRIDGE_SUPPORT_MAX_HEALTH = 100;
const float UNICORN_SOUND_VOLUME_STEP = 2.5; // Lower = audible from further away
const int UNICORN_SPEED = 80; // ms per tile, 100 tiles * 80ms = 8 seconds per charge
// State
UnicornBoss unicorn;
int player_arena_x = 0;
int player_arena_y = 0; // 0 = ground, >0 = jump
bool player_arena_jumping = false;
timer arena_jump_timer;
timer arena_walk_timer;
timer arena_attack_timer;
int player_arena_facing = 1; // 0 = west, 1 = east
int[] bridge_supports_health; // 2 supports: Left (start) and Right (end)
bool bridge_collapsed = false;
string current_unicorn_sound = "";
void init_unicorn_adventure() {
unicorn.reset();
unicorn.x = UNICORN_ARENA_SIZE - 1; // Start at east end
player_arena_x = 0; // Start player at west end
player_arena_y = 0;
player_arena_jumping = false;
bridge_collapsed = false;
current_unicorn_sound = "";
// Initialize supports
bridge_supports_health.resize(2);
bridge_supports_health[0] = BRIDGE_SUPPORT_MAX_HEALTH; // Left support at BRIDGE_START
bridge_supports_health[1] = BRIDGE_SUPPORT_MAX_HEALTH; // Right support at BRIDGE_END
}
void cleanup_unicorn_adventure() {
p.destroy_all();
if (unicorn.sound_handle != -1) {
p.destroy_sound(unicorn.sound_handle);
unicorn.sound_handle = -1;
}
}
void run_unicorn_adventure() {
// Stop main game sounds
p.destroy_all();
init_unicorn_adventure();
speak_with_history("You enter a mountain pass. A massive Unicorn blocks the path! A wooden bridge spans a chasm ahead.", true);
// Adventure Loop
while (true) {
wait(5);
// Input Handling
if (key_pressed(KEY_ESCAPE)) {
cleanup_unicorn_adventure();
speak_with_history("You flee the encounter.", true);
return;
}
// Standard game keys
check_quick_slot_keys();
check_notification_keys();
check_speech_history_keys();
// Health
if (key_pressed(KEY_H)) {
speak_with_history(player_health + " health of " + max_health, true);
}
// Coordinates
if (key_pressed(KEY_X)) {
string facing_dir = (unicorn.facing == 1) ? "east" : "west";
string terrain = (player_arena_x >= BRIDGE_START && player_arena_x <= BRIDGE_END && !bridge_collapsed) ? "wood" : "grass";
speak_with_history("x " + player_arena_x + ", terrain " + terrain + ". Unicorn facing " + facing_dir, true);
}
handle_player_movement();
handle_player_actions();
// Updates
update_player_jump();
update_unicorn();
// Check Conditions - unicorn falls when on collapsed bridge
if (bridge_collapsed && unicorn.x >= BRIDGE_START && unicorn.x <= BRIDGE_END) {
play_unicorn_death_sequence();
cleanup_unicorn_adventure();
give_unicorn_rewards();
return;
}
if (player_health <= 0) {
cleanup_unicorn_adventure();
speak_with_history("The Unicorn trampled you.", true);
// Player death will be handled by main game loop checking player_health <= 0
return;
}
// Audio
p.update_listener_1d(player_arena_x);
update_unicorn_audio();
}
}
void handle_player_movement() {
// Direction change announces
if (key_pressed(KEY_LEFT) && player_arena_facing != 0) {
player_arena_facing = 0;
speak_with_history("west", true);
arena_walk_timer.restart();
}
if (key_pressed(KEY_RIGHT) && player_arena_facing != 1) {
player_arena_facing = 1;
speak_with_history("east", true);
arena_walk_timer.restart();
}
// Movement with walk timer (like main game)
if (arena_walk_timer.elapsed > walk_speed && !player_arena_jumping) {
if (key_down(KEY_LEFT) && player_arena_x > 0) {
player_arena_facing = 0;
player_arena_x--;
arena_walk_timer.restart();
check_player_chasm_fall();
if (player_health > 0) play_footstep_sound();
} else if (key_down(KEY_RIGHT) && player_arena_x < UNICORN_ARENA_SIZE - 1) {
player_arena_facing = 1;
player_arena_x++;
arena_walk_timer.restart();
check_player_chasm_fall();
if (player_health > 0) play_footstep_sound();
}
}
// Jumping
if (key_pressed(KEY_UP) && !player_arena_jumping) {
player_arena_jumping = true;
arena_jump_timer.restart();
p.play_stationary("sounds/jump.ogg", false);
player_arena_y = 1;
}
}
void play_footstep_sound() {
if (player_arena_x >= BRIDGE_START && player_arena_x <= BRIDGE_END && !bridge_collapsed) {
p.play_stationary("sounds/terrain/wood.ogg", false);
} else {
p.play_stationary("sounds/terrain/grass.ogg", false);
}
}
void check_player_chasm_fall() {
// If bridge is collapsed and player walks into the chasm, they fall 100 feet
if (bridge_collapsed && player_arena_x >= BRIDGE_START && player_arena_x <= BRIDGE_END) {
// Play falling sequence like falling from a 100-foot tree
timer fall_timer;
int fall_handle = -1;
int feet_fallen = 0;
int total_fall = 100;
while (feet_fallen < total_fall) {
if (fall_timer.elapsed >= 100) {
fall_timer.restart();
feet_fallen++;
if (fall_handle != -1) {
p.destroy_sound(fall_handle);
}
float height_remaining = float(total_fall - feet_fallen);
float pitch_percent = 50.0 + (50.0 * (height_remaining / float(total_fall)));
if (pitch_percent < 50.0) pitch_percent = 50.0;
if (pitch_percent > 100.0) pitch_percent = 100.0;
fall_handle = p.play_stationary_extended("sounds/actions/falling.ogg", true, 0, 0, 0, pitch_percent);
}
wait(5);
}
if (fall_handle != -1) {
p.destroy_sound(fall_handle);
}
p.play_stationary("sounds/actions/hit_ground.ogg", false);
player_health = 0;
}
}
void update_player_jump() {
if (player_arena_jumping && arena_jump_timer.elapsed > 800) {
player_arena_jumping = false;
player_arena_y = 0;
play_footstep_sound(); // Land sound
}
}
void handle_player_actions() {
// Can't attack while jumping
if (player_arena_jumping) return;
// Attack cooldown like main game
int attack_cooldown = 1000;
if (spear_equipped) attack_cooldown = 800;
if (axe_equipped) attack_cooldown = 1600;
if ((key_down(KEY_LCTRL) || key_down(KEY_RCTRL)) && arena_attack_timer.elapsed > attack_cooldown) {
arena_attack_timer.restart();
// Check for bridge supports
int target_support = -1;
// Check Left Support (at BRIDGE_START)
if (abs(player_arena_x - BRIDGE_START) <= 1) {
target_support = 0;
}
// Check Right Support (at BRIDGE_END)
else if (abs(player_arena_x - BRIDGE_END) <= 1) {
target_support = 1;
}
if (target_support != -1) {
if (bridge_supports_health[target_support] > 0) {
// Only axe can damage supports (like chopping trees)
if (axe_equipped) {
bridge_supports_health[target_support] -= AXE_DAMAGE;
p.play_stationary("sounds/weapons/axe_hit.ogg", false);
if (bridge_supports_health[target_support] <= 0) {
check_bridge_collapse();
}
} else if (spear_equipped) {
// Spear just makes sound, no damage (like hitting trees)
p.play_stationary("sounds/weapons/spear_hit.ogg", false);
} else {
// No weapon or sling - swing sound, no effect
p.play_stationary("sounds/weapons/axe_swing.ogg", false);
}
}
} else {
// Normal attack (useless vs unicorn but gives feedback)
if (abs(player_arena_x - unicorn.x) <= 1) {
p.play_stationary("sounds/weapons/axe_hit.ogg", false);
} else {
p.play_stationary("sounds/weapons/axe_swing.ogg", false);
}
}
}
}
void check_bridge_collapse() {
// Bridge collapses when any support is destroyed
if (bridge_supports_health[0] <= 0 || bridge_supports_health[1] <= 0) {
bridge_collapsed = true;
p.play_stationary("sounds/actions/break_snare.ogg", false);
// If player is on bridge, they fall too
if (player_arena_x >= BRIDGE_START && player_arena_x <= BRIDGE_END) {
check_player_chasm_fall();
}
}
}
void update_unicorn() {
if (unicorn.move_timer.elapsed >= unicorn.speed) {
unicorn.move_timer.restart();
// Move
if (unicorn.facing == 1) {
unicorn.x++;
if (unicorn.x >= UNICORN_ARENA_SIZE) {
unicorn.facing = 0; // Turn around
unicorn.x = UNICORN_ARENA_SIZE - 1;
}
} else {
unicorn.x--;
if (unicorn.x < 0) {
unicorn.facing = 1; // Turn around
unicorn.x = 0;
}
}
// Bridge Logic
if (unicorn.x >= BRIDGE_START && unicorn.x <= BRIDGE_END && !bridge_collapsed) {
unicorn.on_bridge = true;
} else {
unicorn.on_bridge = false;
}
// Collision with Player
if (unicorn.x == player_arena_x && player_arena_y == 0) {
player_health -= 10;
p.play_stationary("sounds/actions/hit_ground.ogg", false);
}
}
}
void update_unicorn_audio() {
// Determine sound based on surface
string sound_file = "sounds/bosses/unicorn/unicorn_galloping.ogg";
if (unicorn.on_bridge) {
sound_file = "sounds/bosses/unicorn/unicorn_on_bridge.ogg";
}
// Check if we need to switch sounds (different file or no active sound)
bool need_new_sound = (unicorn.sound_handle == -1 || !p.sound_is_active(unicorn.sound_handle) || current_unicorn_sound != sound_file);
if (need_new_sound) {
// Stop old sound if playing
if (unicorn.sound_handle != -1) {
p.destroy_sound(unicorn.sound_handle);
}
// Start new positioned sound using shared helper
unicorn.sound_handle = play_1d_with_volume_step(sound_file, player_arena_x, unicorn.x, true, UNICORN_SOUND_VOLUME_STEP);
current_unicorn_sound = sound_file;
} else {
// Update position of existing sound
p.update_sound_1d(unicorn.sound_handle, unicorn.x);
}
}
void play_unicorn_death_sequence() {
// Stop unicorn's normal sound
if (unicorn.sound_handle != -1) {
p.destroy_sound(unicorn.sound_handle);
unicorn.sound_handle = -1;
}
// Simulate 100-foot fall, updating every 100ms like normal falling
timer fall_timer;
int fall_handle = -1;
int feet_fallen = 0;
int total_fall = 100;
while (feet_fallen < total_fall) {
if (fall_timer.elapsed >= 100) {
fall_timer.restart();
feet_fallen++;
// Restart falling sound with decreasing pitch each foot
if (fall_handle != -1) {
p.destroy_sound(fall_handle);
}
// Pitch ranges from 100 (start) to 50 (end) like normal falling
float height_remaining = float(total_fall - feet_fallen);
float pitch_percent = 50.0 + (50.0 * (height_remaining / float(total_fall)));
if (pitch_percent < 50.0) pitch_percent = 50.0;
if (pitch_percent > 100.0) pitch_percent = 100.0;
fall_handle = p.play_extended_1d("sounds/actions/falling.ogg", player_arena_x, unicorn.x, 0, 0, true, 0, 0.0, 0.0, pitch_percent);
if (fall_handle != -1) {
p.update_sound_positioning_values(fall_handle, -1.0, UNICORN_SOUND_VOLUME_STEP, true);
}
}
wait(5);
}
// Cleanup falling sound and play impact
if (fall_handle != -1) {
p.destroy_sound(fall_handle);
}
play_1d_with_volume_step("sounds/bosses/unicorn/unicorn_falls.ogg", player_arena_x, unicorn.x, false, UNICORN_SOUND_VOLUME_STEP);
}
void give_unicorn_rewards() {
speak_with_history("Victory!", true);
favor += 50;
}

View File

@@ -687,6 +687,58 @@ void perform_search(int current_x)
} }
return; return;
} }
// Forest terrain - check for sticks and vines
bool is_forest_terrain = false;
string current_terrain = "";
// Check expanded areas for forest/deep_forest
if (expanded_area_start != -1 && current_x >= expanded_area_start && current_x <= expanded_area_end) {
// Check for mountain terrain first
MountainRange@ mountain = get_mountain_at(current_x);
if (mountain !is null) {
current_terrain = mountain.get_terrain_at(current_x);
} else {
// Regular expanded area - check terrain type
int index = current_x - expanded_area_start;
if (index >= 0 && index < int(expanded_terrain_types.length())) {
current_terrain = expanded_terrain_types[index];
// Handle "mountain:terrain" format from older saves
if (current_terrain.find("mountain:") == 0) {
current_terrain = current_terrain.substr(9);
}
}
}
if (current_terrain == "forest" || current_terrain == "deep_forest") {
is_forest_terrain = true;
}
}
if (is_forest_terrain)
{
bool can_find_stick = inv_sticks < get_personal_stack_limit();
bool can_find_vine = inv_vines < get_personal_stack_limit();
if (!can_find_stick && !can_find_vine) {
speak_with_history("You can't carry any more sticks or vines.", true);
return;
}
// Random choice: if both available, 50/50. If only one, find that.
bool find_stick = can_find_stick && (!can_find_vine || random(0, 1) == 0);
if (find_stick) {
inv_sticks++;
p.play_stationary("sounds/items/stick.ogg", false);
speak_with_history("Found a stick.", true);
} else {
inv_vines++;
p.play_stationary("sounds/items/vine.ogg", false);
speak_with_history("Found a vine.", true);
}
return;
}
speak_with_history("Found nothing.", true); speak_with_history("Found nothing.", true);
} }

View File

@@ -51,6 +51,9 @@ timer attack_timer;
// Search state // Search state
bool searching = false; bool searching = false;
// Adventure state
int last_adventure_day = -1;
// Pause state // Pause state
bool game_paused = false; bool game_paused = false;

View File

@@ -176,6 +176,7 @@ void reset_game_state() {
base_max_health = 10; base_max_health = 10;
max_health = 10; max_health = 10;
favor = 0.0; favor = 0.0;
last_adventure_day = -1;
incense_hours_remaining = 0; incense_hours_remaining = 0;
incense_burning = false; incense_burning = false;
blessing_speed_active = false; blessing_speed_active = false;
@@ -442,6 +443,7 @@ bool load_game_state_from_raw(const string&in rawData) {
if (get_raw_number(rawData, "player_base_health", value)) base_max_health = value; if (get_raw_number(rawData, "player_base_health", value)) base_max_health = value;
if (get_raw_number(rawData, "player_max_health", value)) max_health = value; if (get_raw_number(rawData, "player_max_health", value)) max_health = value;
if (get_raw_number(rawData, "player_favor", value)) favor = value; if (get_raw_number(rawData, "player_favor", value)) favor = value;
if (get_raw_number(rawData, "player_last_adventure_day", value)) last_adventure_day = value;
if (get_raw_number(rawData, "incense_hours_remaining", value)) incense_hours_remaining = value; if (get_raw_number(rawData, "incense_hours_remaining", value)) incense_hours_remaining = value;
if (get_raw_bool(rawData, "incense_burning", bool_value)) incense_burning = bool_value; if (get_raw_bool(rawData, "incense_burning", bool_value)) incense_burning = bool_value;
if (get_raw_number(rawData, "time_current_hour", value)) current_hour = value; if (get_raw_number(rawData, "time_current_hour", value)) current_hour = value;
@@ -537,6 +539,7 @@ bool save_game_state() {
saveData.set("player_base_health", base_max_health); saveData.set("player_base_health", base_max_health);
saveData.set("player_max_health", max_health); saveData.set("player_max_health", max_health);
saveData.set("player_favor", favor); saveData.set("player_favor", favor);
saveData.set("player_last_adventure_day", last_adventure_day);
saveData.set("incense_hours_remaining", incense_hours_remaining); saveData.set("incense_hours_remaining", incense_hours_remaining);
saveData.set("incense_burning", incense_burning); saveData.set("incense_burning", incense_burning);
@@ -567,6 +570,7 @@ bool save_game_state() {
saveData.set("inventory_skin_tunics", inv_skin_tunics); saveData.set("inventory_skin_tunics", inv_skin_tunics);
saveData.set("inventory_moccasins", inv_moccasins); saveData.set("inventory_moccasins", inv_moccasins);
saveData.set("inventory_skin_pouches", inv_skin_pouches); saveData.set("inventory_skin_pouches", inv_skin_pouches);
saveData.set("inventory_backpacks", inv_backpacks);
saveData.set("inventory_small_game_types", join_string_array(inv_small_game_types)); saveData.set("inventory_small_game_types", join_string_array(inv_small_game_types));
saveData.set("storage_stones", storage_stones); saveData.set("storage_stones", storage_stones);
@@ -596,6 +600,7 @@ bool save_game_state() {
saveData.set("storage_skin_tunics", storage_skin_tunics); saveData.set("storage_skin_tunics", storage_skin_tunics);
saveData.set("storage_moccasins", storage_moccasins); saveData.set("storage_moccasins", storage_moccasins);
saveData.set("storage_skin_pouches", storage_skin_pouches); saveData.set("storage_skin_pouches", storage_skin_pouches);
saveData.set("storage_backpacks", storage_backpacks);
saveData.set("storage_small_game_types", join_string_array(storage_small_game_types)); saveData.set("storage_small_game_types", join_string_array(storage_small_game_types));
saveData.set("equipment_spear_equipped", spear_equipped); saveData.set("equipment_spear_equipped", spear_equipped);
@@ -790,6 +795,7 @@ bool load_game_state() {
max_health = int(get_number(saveData, "player_max_health", 10)); max_health = int(get_number(saveData, "player_max_health", 10));
base_max_health = int(get_number(saveData, "player_base_health", max_health)); base_max_health = int(get_number(saveData, "player_base_health", max_health));
favor = get_number(saveData, "player_favor", 0.0); favor = get_number(saveData, "player_favor", 0.0);
last_adventure_day = int(get_number(saveData, "player_last_adventure_day", -1));
incense_hours_remaining = int(get_number(saveData, "incense_hours_remaining", 0)); incense_hours_remaining = int(get_number(saveData, "incense_hours_remaining", 0));
incense_burning = get_bool(saveData, "incense_burning", false); incense_burning = get_bool(saveData, "incense_burning", false);
if (incense_hours_remaining > 0) incense_burning = true; if (incense_hours_remaining > 0) incense_burning = true;
@@ -826,6 +832,7 @@ bool load_game_state() {
inv_skin_tunics = int(get_number(saveData, "inventory_skin_tunics", 0)); inv_skin_tunics = int(get_number(saveData, "inventory_skin_tunics", 0));
inv_moccasins = int(get_number(saveData, "inventory_moccasins", 0)); inv_moccasins = int(get_number(saveData, "inventory_moccasins", 0));
inv_skin_pouches = int(get_number(saveData, "inventory_skin_pouches", 0)); inv_skin_pouches = int(get_number(saveData, "inventory_skin_pouches", 0));
inv_backpacks = int(get_number(saveData, "inventory_backpacks", 0));
string[] loadedSmallGameTypes = get_string_list_or_split(saveData, "inventory_small_game_types"); string[] loadedSmallGameTypes = get_string_list_or_split(saveData, "inventory_small_game_types");
inv_small_game_types.resize(0); inv_small_game_types.resize(0);
@@ -868,6 +875,7 @@ bool load_game_state() {
storage_skin_tunics = int(get_number(saveData, "storage_skin_tunics", 0)); storage_skin_tunics = int(get_number(saveData, "storage_skin_tunics", 0));
storage_moccasins = int(get_number(saveData, "storage_moccasins", 0)); storage_moccasins = int(get_number(saveData, "storage_moccasins", 0));
storage_skin_pouches = int(get_number(saveData, "storage_skin_pouches", 0)); storage_skin_pouches = int(get_number(saveData, "storage_skin_pouches", 0));
storage_backpacks = int(get_number(saveData, "storage_backpacks", 0));
string[] loadedStorageSmallGameTypes = get_string_list_or_split(saveData, "storage_small_game_types"); string[] loadedStorageSmallGameTypes = get_string_list_or_split(saveData, "storage_small_game_types");
storage_small_game_types.resize(0); storage_small_game_types.resize(0);

114
src/text_reader.nvgt Normal file
View File

@@ -0,0 +1,114 @@
// text_reader.nvgt - Simple text document reader/editor using NVGT's audio_form
// Provides accessible navigation through text documents with optional editing
#include "form.nvgt"
// Opens a text reader/editor window with string content
// Parameters:
// content: The text content to display (can be file contents or direct string)
// title: Window title (default: "Text Reader")
// readonly: If true, text cannot be edited (default: true)
// Returns: The modified text if readonly=false and user presses OK, empty string if canceled or readonly=true
string text_reader(string content, string title = "Text Reader", bool readonly = true) {
audio_form f;
f.create_window(title, false, true);
// Create the multiline input box
// In readonly mode, it's still navigable with arrows/home/end/etc
// In edit mode, user can modify text
int text_control = f.create_input_box(
(readonly ? "Document (read only)" : "Document (editable)"),
content,
"", // no password mask
0, // no max length
readonly,
true, // multiline = true
true // multiline_enter = true (Ctrl+Enter for newlines)
);
int ok_button = -1;
int close_button = -1;
if (readonly) {
// In readonly mode, just have a Close button
close_button = f.create_button("&Close", true, true);
} else {
// In edit mode, have OK and Cancel buttons
ok_button = f.create_button("&OK", true);
close_button = f.create_button("&Cancel", false, true);
}
f.focus(text_control);
// Monitor loop
while (true) {
f.monitor();
wait(5);
// Check if user pressed OK (edit mode only)
if (!readonly && ok_button != -1 && f.is_pressed(ok_button)) {
return f.get_text(text_control);
}
// Check if user pressed Close/Cancel
if (close_button != -1 && f.is_pressed(close_button)) {
return "";
}
// Check for Escape key as alternative close method
if (key_pressed(KEY_ESCAPE)) {
return "";
}
}
return "";
}
// Opens a text reader/editor window with an array of lines
// Parameters:
// lines: Array of text lines to display
// title: Window title (default: "Text Reader")
// readonly: If true, text cannot be edited (default: true)
// Returns: The modified text if readonly=false and user presses OK, empty string if canceled or readonly=true
string text_reader_lines(string[] lines, string title = "Text Reader", bool readonly = true) {
string content = join(lines, "\n");
return text_reader(content, title, readonly);
}
// Convenience function to read a file and display it in the text reader
// Parameters:
// file_path: Path to the file to read
// title: Window title (default: uses file_path as title)
// readonly: If true, text cannot be edited (default: true)
// Returns: The modified text if readonly=false and user saves, empty string otherwise
string text_reader_file(string file_path, string title = "", bool readonly = true) {
file f;
if (!f.open(file_path, "rb")) {
screen_reader_speak("Failed to open file: " + file_path, true);
return "";
}
string content = f.read();
f.close();
// Use file_path as title if no custom title provided
if (title == "") {
title = file_path;
}
string result = text_reader(content, title, readonly);
// If in edit mode and user pressed OK, save the file
if (!readonly && result != "") {
if (f.open(file_path, "wb")) {
f.write(result);
f.close();
screen_reader_speak("File saved successfully", true);
return result;
} else {
screen_reader_speak("Failed to save file", true);
}
}
return result;
}

View File

@@ -74,13 +74,17 @@ void expand_regular_area() {
// Generate a single terrain type for the entire new area // Generate a single terrain type for the entire new area
string terrain_type; string terrain_type;
int terrain_roll = random(0, 2); int terrain_roll = random(0, 4);
if (terrain_roll == 0) { if (terrain_roll == 0) {
terrain_type = "stone"; terrain_type = "stone";
} else if (terrain_roll == 1) { } else if (terrain_roll == 1) {
terrain_type = "grass"; terrain_type = "grass";
} else { } else if (terrain_roll == 2) {
terrain_type = "snow"; terrain_type = "snow";
} else if (terrain_roll == 3) {
terrain_type = "forest";
} else {
terrain_type = "deep_forest";
} }
for (int i = 0; i < EXPANSION_SIZE; i++) { for (int i = 0; i < EXPANSION_SIZE; i++) {

View File

@@ -1,4 +1,39 @@
// UI helpers // UI helpers
string get_terrain_at_position(int pos_x) {
// Check for water first (streams in expanded areas or mountain streams)
if (is_position_in_water(pos_x) || is_mountain_stream_at(pos_x)) {
return "water";
}
// Check mountain terrain
MountainRange@ mountain = get_mountain_at(pos_x);
if (mountain !is null) {
return mountain.get_terrain_at(pos_x);
}
// Base area
if (pos_x <= BASE_END) return "wood";
// Grass area
if (pos_x <= GRASS_END) return "grass";
// Gravel area
if (pos_x <= GRAVEL_END) return "gravel";
// Expanded areas
int index = pos_x - expanded_area_start;
if (index >= 0 && index < int(expanded_terrain_types.length())) {
string terrain = expanded_terrain_types[index];
// Handle "mountain:terrain" format from older saves
if (terrain.find("mountain:") == 0) {
terrain = terrain.substr(9);
}
return terrain;
}
return "unknown";
}
string ui_input_box(const string title, const string prompt, const string default_value) { string ui_input_box(const string title, const string prompt, const string default_value) {
string result = virtual_input_box(title, prompt, default_value); string result = virtual_input_box(title, prompt, default_value);
show_window("Draugnorak"); show_window("Draugnorak");

View File

@@ -46,10 +46,12 @@ class MountainRange {
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
if (elevations[i] > 20) { if (elevations[i] > 20) {
terrain_types[i] = "snow"; terrain_types[i] = "snow";
} else if (elevations[i] > 8) { } else if (elevations[i] > 12) {
terrain_types[i] = "stone"; terrain_types[i] = "stone";
} else if (elevations[i] > 6) {
terrain_types[i] = "forest";
} else { } else {
terrain_types[i] = "gravel"; terrain_types[i] = "deep_forest";
} }
} }