Multiple characters allowed. Starting base health lowered. Fylgjr system implemented.
This commit is contained in:
144
src/bosses/adventure_combat.nvgt
Normal file
144
src/bosses/adventure_combat.nvgt
Normal file
@@ -0,0 +1,144 @@
|
||||
// Shared combat helpers for adventures
|
||||
|
||||
const int ADVENTURE_WEAPON_BOW = 1;
|
||||
const int ADVENTURE_WEAPON_SLING = 2;
|
||||
const int ADVENTURE_WEAPON_SPEAR = 3;
|
||||
const int ADVENTURE_WEAPON_AXE = 4;
|
||||
|
||||
funcdef int AdventureRangedReleaseCallback(int player_x, int direction, int range, int weapon_type, int damage);
|
||||
|
||||
bool adventure_arrow_recover_pending = false;
|
||||
|
||||
void reset_adventure_combat_state() {
|
||||
bow_drawing = false;
|
||||
sling_charging = false;
|
||||
adventure_arrow_recover_pending = false;
|
||||
stop_bow_shot_audio();
|
||||
safe_destroy_sound(sling_sound_handle);
|
||||
last_sling_stage = -1;
|
||||
}
|
||||
|
||||
void update_weapon_range_audio_with_listener(int listener_x, int creature_pos, bool &inout was_in_range) {
|
||||
int range = get_current_weapon_range();
|
||||
bool in_range = (range >= 0) && (abs(creature_pos - listener_x) <= range);
|
||||
if (in_range && !was_in_range) {
|
||||
play_weapon_range_sound("sounds/enemies/enter_range.ogg", creature_pos);
|
||||
} else if (!in_range && was_in_range) {
|
||||
play_weapon_range_sound("sounds/enemies/exit_range.ogg", creature_pos);
|
||||
}
|
||||
was_in_range = in_range;
|
||||
}
|
||||
|
||||
void adventure_start_bow_shot_audio(int listener_x, int start_x, int end_x, int hit_x, int duration_ms) {
|
||||
stop_bow_shot_audio();
|
||||
bow_shot_active = true;
|
||||
bow_shot_timer.restart();
|
||||
bow_shot_start_x = start_x;
|
||||
bow_shot_end_x = end_x;
|
||||
bow_shot_hit_x = hit_x;
|
||||
bow_shot_duration_ms = duration_ms;
|
||||
if (bow_shot_duration_ms < 1) bow_shot_duration_ms = 1;
|
||||
|
||||
bow_shot_sound_handle = play_1d_with_volume_step(
|
||||
"sounds/weapons/arrow_flies.ogg",
|
||||
listener_x,
|
||||
bow_shot_start_x,
|
||||
false,
|
||||
PLAYER_WEAPON_SOUND_VOLUME_STEP
|
||||
);
|
||||
}
|
||||
|
||||
void adventure_update_bow_shot(int listener_x) {
|
||||
if (!bow_shot_active) return;
|
||||
if (bow_shot_duration_ms < 1) bow_shot_duration_ms = 1;
|
||||
|
||||
int elapsed = bow_shot_timer.elapsed;
|
||||
float progress = float(elapsed) / float(bow_shot_duration_ms);
|
||||
if (progress > 1.0f) progress = 1.0f;
|
||||
|
||||
int travel = int(float(bow_shot_end_x - bow_shot_start_x) * progress);
|
||||
int current_pos = bow_shot_start_x + travel;
|
||||
if (bow_shot_sound_handle != -1) {
|
||||
p.update_sound_1d(bow_shot_sound_handle, current_pos);
|
||||
}
|
||||
|
||||
if (elapsed >= bow_shot_duration_ms) {
|
||||
int hit_x = bow_shot_hit_x;
|
||||
bool recover_pending = adventure_arrow_recover_pending;
|
||||
stop_bow_shot_audio();
|
||||
adventure_arrow_recover_pending = false;
|
||||
if (hit_x >= 0) {
|
||||
play_1d_with_volume_step(
|
||||
"sounds/weapons/arrow_hit.ogg",
|
||||
listener_x,
|
||||
hit_x,
|
||||
false,
|
||||
PLAYER_WEAPON_SOUND_VOLUME_STEP
|
||||
);
|
||||
}
|
||||
if (recover_pending) {
|
||||
add_personal_count(ITEM_ARROWS, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void adventure_release_bow_attack(int player_x, int player_facing, AdventureRangedReleaseCallback@ ranged_callback) {
|
||||
if (get_personal_count(ITEM_ARROWS) <= 0) {
|
||||
speak_ammo_blocked("No arrows.");
|
||||
return;
|
||||
}
|
||||
|
||||
int damage = get_bow_draw_damage(bow_draw_timer.elapsed);
|
||||
add_personal_count(ITEM_ARROWS, -1);
|
||||
p.play_stationary("sounds/weapons/bow_fire.ogg", false);
|
||||
|
||||
int search_direction = (player_facing == 1) ? 1 : -1;
|
||||
int target_x = -1;
|
||||
if (@ranged_callback !is null) {
|
||||
target_x = ranged_callback(player_x, search_direction, BOW_RANGE, ADVENTURE_WEAPON_BOW, damage);
|
||||
}
|
||||
|
||||
int end_x = (target_x != -1)
|
||||
? target_x
|
||||
: (player_x + (search_direction * (BOW_RANGE + BOW_MISS_EXTRA_TILES)));
|
||||
|
||||
int duration_ms = ARROW_FLIES_DURATION_MS;
|
||||
if (target_x != -1) {
|
||||
int distance = abs(target_x - player_x);
|
||||
if (distance < 1) distance = 1;
|
||||
duration_ms = int(float(ARROW_FLIES_DURATION_MS) * (float(distance) / float(BOW_RANGE)));
|
||||
if (duration_ms < 1) duration_ms = 1;
|
||||
}
|
||||
|
||||
adventure_arrow_recover_pending = (random(1, 100) <= 25);
|
||||
adventure_start_bow_shot_audio(player_x, player_x, end_x, target_x, duration_ms);
|
||||
}
|
||||
|
||||
void adventure_release_sling_attack(int player_x, int player_facing, AdventureRangedReleaseCallback@ ranged_callback) {
|
||||
add_personal_count(ITEM_STONES, -1);
|
||||
|
||||
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
|
||||
|
||||
if (stage != 1) {
|
||||
speak_with_history("Stone missed.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
int search_direction = (player_facing == 1) ? 1 : -1;
|
||||
int target_x = -1;
|
||||
int damage = random(SLING_DAMAGE_MIN, SLING_DAMAGE_MAX);
|
||||
|
||||
if (@ranged_callback !is null) {
|
||||
target_x = ranged_callback(player_x, search_direction, SLING_RANGE, ADVENTURE_WEAPON_SLING, damage);
|
||||
}
|
||||
|
||||
if (target_x == -1) {
|
||||
speak_with_history("Stone missed.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Adventure System
|
||||
// Handles triggering and managing terrain-specific adventures
|
||||
|
||||
#include "src/bosses/adventure_combat.nvgt"
|
||||
#include "src/bosses/unicorn/unicorn_boss.nvgt"
|
||||
|
||||
void check_adventure_menu(int player_x) {
|
||||
|
||||
@@ -16,7 +16,7 @@ class UnicornBoss {
|
||||
}
|
||||
|
||||
void reset() {
|
||||
health = 10000;
|
||||
health = 450;
|
||||
speed = UNICORN_SPEED;
|
||||
facing = 0; // Start facing west (toward player)
|
||||
x = 0;
|
||||
@@ -33,6 +33,8 @@ 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
|
||||
const bool UNICORN_BOW_CAN_DAMAGE_SUPPORTS = false;
|
||||
const bool UNICORN_SLING_CAN_DAMAGE_SUPPORTS = false;
|
||||
|
||||
// State
|
||||
UnicornBoss unicorn;
|
||||
@@ -46,16 +48,23 @@ 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 = "";
|
||||
bool unicorn_defeated = false;
|
||||
bool unicorn_defeated_by_fall = false;
|
||||
bool unicorn_in_weapon_range = false;
|
||||
|
||||
void init_unicorn_adventure() {
|
||||
unicorn.reset();
|
||||
unicorn.x = UNICORN_ARENA_SIZE - 1; // Start at east end
|
||||
|
||||
reset_adventure_combat_state();
|
||||
player_arena_x = 0; // Start player at west end
|
||||
player_arena_y = 0;
|
||||
player_arena_jumping = false;
|
||||
bridge_collapsed = false;
|
||||
current_unicorn_sound = "";
|
||||
unicorn_defeated = false;
|
||||
unicorn_defeated_by_fall = false;
|
||||
unicorn_in_weapon_range = false;
|
||||
|
||||
// Initialize supports
|
||||
bridge_supports_health.resize(2);
|
||||
@@ -65,6 +74,7 @@ void init_unicorn_adventure() {
|
||||
|
||||
void cleanup_unicorn_adventure() {
|
||||
p.destroy_all();
|
||||
reset_adventure_combat_state();
|
||||
if (unicorn.sound_handle != -1) {
|
||||
p.destroy_sound(unicorn.sound_handle);
|
||||
unicorn.sound_handle = -1;
|
||||
@@ -90,6 +100,7 @@ void run_unicorn_adventure() {
|
||||
intro.insert_last("Strategy:");
|
||||
intro.insert_last(" - Use your axe to destroy a bridge support");
|
||||
intro.insert_last(" - Lure the Unicorn onto the bridge");
|
||||
intro.insert_last(" - Or fight the Unicorn directly (it has massive health)");
|
||||
intro.insert_last(" - Jump (UP arrow) to avoid being trampled");
|
||||
intro.insert_last(" - When the bridge collapses with the Unicorn on it, you win!");
|
||||
intro.insert_last("");
|
||||
@@ -130,10 +141,24 @@ void run_unicorn_adventure() {
|
||||
// Updates
|
||||
update_player_jump();
|
||||
update_unicorn();
|
||||
adventure_update_bow_shot(player_arena_x);
|
||||
update_unicorn_weapon_range_audio();
|
||||
|
||||
// Check Conditions - unicorn falls when on collapsed bridge
|
||||
if (bridge_collapsed && unicorn.x >= BRIDGE_START && unicorn.x <= BRIDGE_END) {
|
||||
play_unicorn_death_sequence();
|
||||
if (!unicorn_defeated && bridge_collapsed && unicorn.x >= BRIDGE_START && unicorn.x <= BRIDGE_END) {
|
||||
mark_unicorn_defeated(true);
|
||||
}
|
||||
|
||||
if (!unicorn_defeated && unicorn.health <= 0) {
|
||||
mark_unicorn_defeated(false);
|
||||
}
|
||||
|
||||
if (unicorn_defeated) {
|
||||
if (unicorn_defeated_by_fall) {
|
||||
play_unicorn_death_sequence();
|
||||
} else {
|
||||
play_unicorn_ground_death_sequence();
|
||||
}
|
||||
cleanup_unicorn_adventure();
|
||||
give_unicorn_rewards();
|
||||
return;
|
||||
@@ -247,55 +272,207 @@ 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;
|
||||
bool ctrl_down = (key_down(KEY_LCTRL) || key_down(KEY_RCTRL));
|
||||
|
||||
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);
|
||||
// Bow draw detection
|
||||
if (bow_equipped) {
|
||||
if (ctrl_down && !bow_drawing) {
|
||||
if (get_personal_count(ITEM_ARROWS) > 0) {
|
||||
bow_drawing = true;
|
||||
bow_draw_timer.restart();
|
||||
p.play_stationary("sounds/weapons/bow_draw.ogg", false);
|
||||
} else {
|
||||
p.play_stationary("sounds/weapons/axe_swing.ogg", false);
|
||||
speak_ammo_blocked("No arrows.");
|
||||
}
|
||||
}
|
||||
|
||||
if (bow_drawing && !ctrl_down) {
|
||||
adventure_release_bow_attack(player_arena_x, player_arena_facing, @unicorn_ranged_attack);
|
||||
bow_drawing = false;
|
||||
}
|
||||
}
|
||||
if (!bow_equipped && bow_drawing) {
|
||||
bow_drawing = false;
|
||||
}
|
||||
|
||||
// Sling charge detection
|
||||
if (!bow_equipped && sling_equipped && ctrl_down && !sling_charging) {
|
||||
if (get_personal_count(ITEM_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 {
|
||||
speak_ammo_blocked("No stones.");
|
||||
}
|
||||
}
|
||||
|
||||
// Update sling charge state while holding
|
||||
if (sling_charging && ctrl_down) {
|
||||
update_sling_charge();
|
||||
}
|
||||
|
||||
// Sling release detection
|
||||
if (sling_charging && !ctrl_down) {
|
||||
adventure_release_sling_attack(player_arena_x, player_arena_facing, @unicorn_ranged_attack);
|
||||
sling_charging = false;
|
||||
safe_destroy_sound(sling_sound_handle);
|
||||
}
|
||||
|
||||
// Non-sling weapon attacks (existing pattern)
|
||||
if (!bow_equipped && !bow_drawing && !sling_equipped && !sling_charging) {
|
||||
if (fishing_pole_equipped) return;
|
||||
|
||||
int weapon_type = get_unicorn_melee_weapon_type();
|
||||
if (weapon_type == -1) return;
|
||||
|
||||
int attack_cooldown = 1000;
|
||||
if (weapon_type == ADVENTURE_WEAPON_SPEAR) attack_cooldown = 800;
|
||||
if (weapon_type == ADVENTURE_WEAPON_AXE) attack_cooldown = 1600;
|
||||
|
||||
if (ctrl_down && arena_attack_timer.elapsed > attack_cooldown) {
|
||||
arena_attack_timer.restart();
|
||||
play_unicorn_melee_swing(weapon_type);
|
||||
if (unicorn_melee_hit(weapon_type)) {
|
||||
play_unicorn_melee_hit(weapon_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int get_unicorn_melee_weapon_type() {
|
||||
if (spear_equipped) return ADVENTURE_WEAPON_SPEAR;
|
||||
if (axe_equipped) return ADVENTURE_WEAPON_AXE;
|
||||
return -1;
|
||||
}
|
||||
|
||||
void play_unicorn_melee_swing(int weapon_type) {
|
||||
if (weapon_type == ADVENTURE_WEAPON_SPEAR) {
|
||||
p.play_stationary("sounds/weapons/spear_swing.ogg", false);
|
||||
} else if (weapon_type == ADVENTURE_WEAPON_AXE) {
|
||||
p.play_stationary("sounds/weapons/axe_swing.ogg", false);
|
||||
}
|
||||
}
|
||||
|
||||
void play_unicorn_melee_hit(int weapon_type) {
|
||||
if (weapon_type == ADVENTURE_WEAPON_SPEAR) {
|
||||
p.play_stationary("sounds/weapons/spear_hit.ogg", false);
|
||||
} else if (weapon_type == ADVENTURE_WEAPON_AXE) {
|
||||
p.play_stationary("sounds/weapons/axe_hit.ogg", false);
|
||||
}
|
||||
}
|
||||
|
||||
bool unicorn_melee_hit(int weapon_type) {
|
||||
int target_support = -1;
|
||||
if (abs(player_arena_x - BRIDGE_START) <= 1) {
|
||||
target_support = 0;
|
||||
} else if (abs(player_arena_x - BRIDGE_END) <= 1) {
|
||||
target_support = 1;
|
||||
}
|
||||
|
||||
if (target_support != -1 && bridge_supports_health[target_support] > 0) {
|
||||
if (weapon_type == ADVENTURE_WEAPON_AXE) {
|
||||
bridge_supports_health[target_support] -= AXE_DAMAGE;
|
||||
if (bridge_supports_health[target_support] <= 0) {
|
||||
check_bridge_collapse();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (abs(player_arena_x - unicorn.x) <= 1) {
|
||||
int damage = (weapon_type == ADVENTURE_WEAPON_SPEAR) ? SPEAR_DAMAGE : AXE_DAMAGE;
|
||||
apply_unicorn_damage(damage);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int find_unicorn_ranged_target(int player_x, int direction, int range) {
|
||||
int unicorn_distance = (unicorn.x - player_x) * direction;
|
||||
if (unicorn_distance > 0 && unicorn_distance <= range) {
|
||||
return unicorn.x;
|
||||
}
|
||||
|
||||
for (int dist = 1; dist <= range; dist++) {
|
||||
int check_x = player_x + (dist * direction);
|
||||
if (check_x < 0 || check_x >= UNICORN_ARENA_SIZE) break;
|
||||
|
||||
if (bridge_supports_health[0] > 0 && check_x == BRIDGE_START) {
|
||||
return check_x;
|
||||
}
|
||||
|
||||
if (bridge_supports_health[1] > 0 && check_x == BRIDGE_END) {
|
||||
return check_x;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int unicorn_ranged_attack(int player_x, int direction, int range, int weapon_type, int damage) {
|
||||
int target_x = find_unicorn_ranged_target(player_x, direction, range);
|
||||
if (target_x == -1) return -1;
|
||||
|
||||
if (target_x == unicorn.x) {
|
||||
apply_unicorn_damage(damage);
|
||||
return target_x;
|
||||
}
|
||||
|
||||
if (target_x == BRIDGE_START || target_x == BRIDGE_END) {
|
||||
int support_index = (target_x == BRIDGE_START) ? 0 : 1;
|
||||
bool can_damage = false;
|
||||
if (weapon_type == ADVENTURE_WEAPON_BOW) {
|
||||
can_damage = UNICORN_BOW_CAN_DAMAGE_SUPPORTS;
|
||||
} else if (weapon_type == ADVENTURE_WEAPON_SLING) {
|
||||
can_damage = UNICORN_SLING_CAN_DAMAGE_SUPPORTS;
|
||||
}
|
||||
|
||||
if (can_damage && bridge_supports_health[support_index] > 0) {
|
||||
bridge_supports_health[support_index] -= damage;
|
||||
if (bridge_supports_health[support_index] <= 0) {
|
||||
check_bridge_collapse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return target_x;
|
||||
}
|
||||
|
||||
void mark_unicorn_defeated(bool by_fall) {
|
||||
if (unicorn_defeated) return;
|
||||
unicorn_defeated = true;
|
||||
unicorn_defeated_by_fall = by_fall;
|
||||
if (by_fall) {
|
||||
unicorn.health = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void apply_unicorn_damage(int damage) {
|
||||
if (damage <= 0) return;
|
||||
if (unicorn.health <= 0) return;
|
||||
|
||||
unicorn.health -= damage;
|
||||
if (unicorn.health <= 0) {
|
||||
unicorn.health = 0;
|
||||
mark_unicorn_defeated(false);
|
||||
}
|
||||
}
|
||||
|
||||
void play_unicorn_ground_death_sequence() {
|
||||
if (unicorn.sound_handle != -1) {
|
||||
p.destroy_sound(unicorn.sound_handle);
|
||||
unicorn.sound_handle = -1;
|
||||
}
|
||||
p.play_stationary("sounds/actions/hit_ground.ogg", false);
|
||||
wait(800);
|
||||
}
|
||||
|
||||
void update_unicorn_weapon_range_audio() {
|
||||
update_weapon_range_audio_with_listener(player_arena_x, unicorn.x, unicorn_in_weapon_range);
|
||||
}
|
||||
|
||||
void check_bridge_collapse() {
|
||||
// Bridge collapses when any support is destroyed
|
||||
if (bridge_supports_health[0] <= 0 || bridge_supports_health[1] <= 0) {
|
||||
@@ -339,6 +516,7 @@ void update_unicorn() {
|
||||
if (unicorn.x == player_arena_x && player_arena_y == 0) {
|
||||
player_health -= 10;
|
||||
p.play_stationary("sounds/actions/hit_ground.ogg", false);
|
||||
play_player_damage_sound();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -440,6 +618,8 @@ void give_unicorn_rewards() {
|
||||
rewards.insert_last("You have already mastered the Rune of Swiftness.");
|
||||
}
|
||||
|
||||
append_adventure_completion_rewards(ADVENTURE_UNICORN, rewards);
|
||||
|
||||
// Display rewards in text reader
|
||||
text_reader_lines(rewards, "Unicorn Victory", true);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user