A couple of terrain types added. First adventure added, incomplete.
This commit is contained in:
@@ -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.
BIN
sounds/bosses/unicorn/unicorn_falls.ogg
LFS
Normal file
BIN
sounds/bosses/unicorn/unicorn_falls.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/bosses/unicorn/unicorn_galloping.ogg
LFS
Normal file
BIN
sounds/bosses/unicorn/unicorn_galloping.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/bosses/unicorn/unicorn_on_bridge.ogg
LFS
Normal file
BIN
sounds/bosses/unicorn/unicorn_on_bridge.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/terrain/deep_forest.ogg
LFS
Normal file
BIN
sounds/terrain/deep_forest.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/terrain/forest.ogg
LFS
Normal file
BIN
sounds/terrain/forest.ogg
LFS
Normal file
Binary file not shown.
@@ -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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
81
src/bosses/adventure_system.nvgt
Normal file
81
src/bosses/adventure_system.nvgt
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
400
src/bosses/unicorn/unicorn_boss.nvgt
Normal file
400
src/bosses/unicorn/unicorn_boss.nvgt
Normal 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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
114
src/text_reader.nvgt
Normal 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;
|
||||||
|
}
|
||||||
@@ -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++) {
|
||||||
|
|||||||
35
src/ui.nvgt
35
src/ui.nvgt
@@ -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");
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user