Bow mechanics added, documentation updated.

This commit is contained in:
Storm Dragon
2026-01-26 12:48:14 -05:00
parent 837deacf6c
commit efd90b596c
16 changed files with 484 additions and 100 deletions

View File

@@ -1,10 +1,10 @@
# Draugnorak
A survival audio game built with NVGT. Explore, gather, craft, and defend your base as day and night cycle on.
A survival audio game built with NVGT. Explore, gather, craft, and defend your base as day and night cycle.
## Installation
- Unzip the file for your operating system.
- On Linux and Mac make sure the dragnorak executable is marked executable
- On Linux and Mac, make sure the draugnorak executable is marked executable.
``` bash
chmod +x draugnorak
```
@@ -16,14 +16,12 @@ chmod +x draugnorak
- Set snares and build fires to survive.
Some of the first things you will want are a stone knife, spear, stone axe, and firepit.
## Controls
- **Left/Right**: Move.
- **Up**: Jump, climb trees, start rope climbs when prompted.
- **Down**: Climb down trees or descend rope climbs when prompted.
- **Shift (hold)**: Search area (1-second hold, 1-second delay).
- **Control (hold/release)**: Attack with equipped weapon. Sling uses a charge window on release.
- **Control (hold/release)**: Attack with equipped weapon. Sling uses a charge window.
- **A**: Action menu (place snare, feed fire, burn incense).
- **C**: Crafting menu (base only).
- **I**: Inventory.
@@ -66,9 +64,9 @@ Some of the first things you will want are a stone knife, spear, stone axe, and
Crafting is only available in the base (C).
### Categories
- **Weapons**: Spear, Sling
- **Weapons**: Spear, Sling, Bow
- **Tools**: Knife, Snare, Stone Axe, Fishing Pole, Rope, Quiver, Canoe, Reed Basket, Clay Pot
- **Materials**: Arrows, Butcher Game, Incense
- **Materials**: Butcher Game, Smoke Fish, Bowstring, Arrows, Incense
- **Clothing**: Skin gear, Moccasins, Pouch, Backpack
- **Buildings**: Firepit, Fire, Herb Garden, Storage, Pasture, Stable, Altar
- **Barricade**: Reinforcement options
@@ -79,8 +77,10 @@ Crafting is only available in the base (C).
- **Spear**: 1-tile range.
- **Axe**: Melee; also chops trees.
- **Sling**: Ranged (uses stones). Hold Control to charge; release on the “in-range” audio cue.
- **Bow**: Ranged (uses arrows). Hold Control to draw; release to fire. Damage scales with draw time (010).
- Enemies include zombies (night), bandits (invasions), and boars (daytime).
- Bow combat is planned; arrows are stockpiled for future bow use.
- **25% chance** to recover a fired arrow as a world drop.
- If your base has an altar, each undead kill grants **+0.2 favor**.
## Base and Survival
- **Passive healing** in the base when below max health.
@@ -94,7 +94,7 @@ Crafting is only available in the base (C).
Residents can automate survival tasks, defend your base, and process resources. To get the most out of them, ensure you have the necessary tools and facilities in your base storage.
### Recruitment and Care
- **Recruitment**: Residents join automatically after invasions. If you have **Storage** built the likelihood for survivors to become residents increases. You can have a maximum of 4 residents.
- **Recruitment**: Residents join automatically after invasions. If you have **Storage** built, the likelihood for survivors to become residents increases. **12 survivors** can join at a time, up to a maximum of **4 residents**.
- **Food**: Each resident consumes **1 unit of food every 8 hours** (3 per day).
- Priority: Meat > Smoked Fish > Basket of Fruits and Nuts.
- If no food is available, residents will go hungry and stop working.
@@ -103,10 +103,11 @@ Residents can automate survival tasks, defend your base, and process resources.
### Activities and Requirements
Residents automatically perform these tasks if the requirements are met in your base storage.
| Activity | Requires (in Storage) | details |
| Activity | Requires (in Storage) | Details |
| :--- | :--- | :--- |
| **Defense (Melee)** | **Spear** | Deals damage to attackers. Spears may break. |
| **Defense (Ranged)** | **Sling** + **Stones** | Attacks enemies from a distance. Consumes stones. Slings have a chance to break. |
| **Defense (Ranged - Bow)** | **Bow** + **Arrows** | Preferred ranged defense. Consumes arrows. Bows have a chance to break. |
| **Defense (Ranged - Sling)** | **Sling** + **Stones** | Attacks enemies from a distance. Consumes stones. Slings have a chance to break. |
| **Repairs** | N/A | Repairs the **Barricade** during the day. |
| **Gathering** | **Reed Basket** | Collects Sticks, Vines, Stones, and Logs. |
| **Foraging** | **Reed Basket** | Occurs at 6 AM and 12 PM. Produces **Basket of Fruits and Nuts**. |
@@ -115,7 +116,7 @@ Residents automatically perform these tasks if the requirements are met in your
| **Butchering** | **Knife** + **Game** | Requires a **burning fire**. Processes Small Game or Boar Carcasses into Meat, Skins, etc. |
| **Snare Check** | **Placed Snares** | Checks active snares, retrieves **Small Game**, and resets the snare. |
> **Note**: Tools (Spears, Slings, Baskets, Knives, Fishing Poles) have a chance to break when used by residents. Keep a stock of spares!
> **Note**: Tools (Spears, Slings, Bows, Baskets, Knives, Fishing Poles) have a chance to break when used by residents. Keep a stock of spares!
## Time, Weather, and Events
- **1 real minute = 1 in-game hour**.
@@ -141,6 +142,7 @@ Residents automatically perform these tasks if the requirements are met in your
- Personal inventory stacks are capped at 9.
- Skin pouches add +2 stack capacity, backpacks add +9.
- Arrows require quivers. Each quiver adds capacity for 12 arrows.
- Bowstrings are crafted from **sinew at a fire** and used to craft bows.
- Storage is separate and accessed in the base (Inventory menu when storage exists).
## Saving and Loading

View File

@@ -407,9 +407,33 @@ void main()
}
update_fishing();
update_bow_shot();
bool ctrl_down = (key_down(KEY_LCTRL) || key_down(KEY_RCTRL));
// 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 {
speak_ammo_blocked("No arrows.");
}
}
if (bow_drawing && !ctrl_down) {
release_bow_attack(x);
bow_drawing = false;
}
}
if (!bow_equipped && bow_drawing) {
bow_drawing = false;
}
// Sling charge detection
if (sling_equipped && (key_down(KEY_LCTRL) || key_down(KEY_RCTRL)) && !sling_charging) {
if (!bow_equipped && sling_equipped && ctrl_down && !sling_charging) {
if (get_personal_count(ITEM_STONES) > 0) {
sling_charging = true;
sling_charge_timer.restart();
@@ -421,12 +445,12 @@ void main()
}
// Update sling charge state while holding
if (sling_charging && (key_down(KEY_LCTRL) || key_down(KEY_RCTRL))) {
if (sling_charging && ctrl_down) {
update_sling_charge();
}
// Sling release detection
if (sling_charging && (!key_down(KEY_LCTRL) && !key_down(KEY_RCTRL))) {
if (sling_charging && !ctrl_down) {
release_sling_attack(x);
sling_charging = false;
if (sling_sound_handle != -1) {
@@ -436,20 +460,12 @@ void main()
}
// Non-sling weapon attacks (existing pattern)
if (!sling_equipped && !sling_charging) {
if (!bow_equipped && !bow_drawing && !sling_equipped && !sling_charging) {
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 (bow_equipped && ctrl_down && attack_timer.elapsed > attack_cooldown) {
if (get_personal_count(ITEM_ARROWS) <= 0) {
speak_ammo_blocked("No arrows.");
} else {
attack_timer.restart();
perform_attack(x);
}
} else if (!bow_equipped && !fishing_pole_equipped && ctrl_down && attack_timer.elapsed > attack_cooldown) {
if (!fishing_pole_equipped && ctrl_down && attack_timer.elapsed > attack_cooldown) {
attack_timer.restart();
perform_attack(x);
}

BIN
sounds/weapons/arrow_flies.ogg LFS Normal file

Binary file not shown.

BIN
sounds/weapons/arrow_hit.ogg LFS Normal file

Binary file not shown.

BIN
sounds/weapons/bow_draw.ogg LFS Normal file

Binary file not shown.

BIN
sounds/weapons/bow_fire.ogg LFS Normal file

Binary file not shown.

View File

@@ -198,12 +198,20 @@ void keep_base_fires_fed() {
}
// Resident defense functions
const int RESIDENT_WEAPON_SPEAR = 0;
const int RESIDENT_WEAPON_SLING = 1;
const int RESIDENT_WEAPON_BOW = 2;
int get_available_defense_weapons() {
int count = get_storage_count(ITEM_SPEARS);
// Slings only count if stones are available
if (get_storage_count(ITEM_SLINGS) > 0 && get_storage_count(ITEM_STONES) > 0) {
count += get_storage_count(ITEM_SLINGS);
}
// Bows only count if arrows are available
if (get_storage_count(ITEM_BOWS) > 0 && get_storage_count(ITEM_ARROWS) > 0) {
count += get_storage_count(ITEM_BOWS);
}
return count;
}
@@ -212,33 +220,42 @@ bool can_residents_defend() {
return get_available_defense_weapons() > 0;
}
bool choose_defense_weapon() {
// Returns true for spear, false for sling
int choose_defense_weapon_type() {
// Prefer bows if available
int bowCount = (get_storage_count(ITEM_BOWS) > 0 && get_storage_count(ITEM_ARROWS) > 0)
? get_storage_count(ITEM_BOWS)
: 0;
if (bowCount > 0) return RESIDENT_WEAPON_BOW;
int spearCount = get_storage_count(ITEM_SPEARS);
int slingCount = (get_storage_count(ITEM_SLINGS) > 0 && get_storage_count(ITEM_STONES) > 0) ? get_storage_count(ITEM_SLINGS) : 0;
int total = spearCount + slingCount;
if (total == 0) return true;
if (slingCount == 0) return true;
if (spearCount == 0) return false;
if (total == 0) return RESIDENT_WEAPON_SPEAR;
if (slingCount == 0) return RESIDENT_WEAPON_SPEAR;
if (spearCount == 0) return RESIDENT_WEAPON_SLING;
int roll = random(1, total);
return roll <= spearCount;
return (roll <= spearCount) ? RESIDENT_WEAPON_SPEAR : RESIDENT_WEAPON_SLING;
}
int perform_resident_defense() {
if (!can_residents_defend()) return 0;
// Choose weapon type randomly weighted by availability
bool useSpear = choose_defense_weapon();
// Choose weapon type (bows preferred, otherwise weighted by availability)
int weapon_type = choose_defense_weapon_type();
int damage = 0;
if (useSpear && get_storage_count(ITEM_SPEARS) > 0) {
if (weapon_type == RESIDENT_WEAPON_BOW && get_storage_count(ITEM_BOWS) > 0 && get_storage_count(ITEM_ARROWS) > 0) {
damage = apply_resident_damage_bonus(random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX));
add_storage_count(ITEM_ARROWS, -1);
play_1d_with_volume_step("sounds/weapons/bow_fire.ogg", x, BASE_END + 1, false, RESIDENT_DEFENSE_VOLUME_STEP);
} else if (weapon_type == RESIDENT_WEAPON_SPEAR && get_storage_count(ITEM_SPEARS) > 0) {
damage = apply_resident_damage_bonus(RESIDENT_SPEAR_DAMAGE);
// Weapons don't get consumed on use - they break via daily breakage check
// Just play the sound
play_1d_with_volume_step("sounds/weapons/spear_swing.ogg", x, BASE_END + 1, false, RESIDENT_DEFENSE_VOLUME_STEP);
} else if (get_storage_count(ITEM_SLINGS) > 0 && get_storage_count(ITEM_STONES) > 0) {
} else if (weapon_type == RESIDENT_WEAPON_SLING && get_storage_count(ITEM_SLINGS) > 0 && get_storage_count(ITEM_STONES) > 0) {
damage = apply_resident_damage_bonus(random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX));
// Slings use stones as ammo, so consume a stone
add_storage_count(ITEM_STONES, -1);
@@ -248,28 +265,31 @@ int perform_resident_defense() {
return damage;
}
// Proactive resident sling defense
timer resident_sling_timer;
// Proactive resident ranged defense
timer resident_ranged_timer;
void attempt_resident_sling_defense() {
// Only if residents exist and have slings with stones
void attempt_resident_ranged_defense() {
// Only if residents exist and have ranged weapons
if (residents_count <= 0) return;
if (get_storage_count(ITEM_SLINGS) <= 0 || get_storage_count(ITEM_STONES) <= 0) return;
bool has_bow = (get_storage_count(ITEM_BOWS) > 0 && get_storage_count(ITEM_ARROWS) > 0);
bool has_sling = (get_storage_count(ITEM_SLINGS) > 0 && get_storage_count(ITEM_STONES) > 0);
if (!has_bow && !has_sling) return;
// Cooldown between shots
if (resident_sling_timer.elapsed < get_resident_cooldown(RESIDENT_SLING_COOLDOWN)) return;
if (resident_ranged_timer.elapsed < get_resident_cooldown(RESIDENT_SLING_COOLDOWN)) return;
// Find nearest enemy within sling range
int nearestDistance = SLING_RANGE + 1;
int range = has_bow ? BOW_RANGE : SLING_RANGE;
// Find nearest enemy within range
int nearestDistance = range + 1;
int targetPos = -1;
bool targetIsBandit = false;
int sling_origin = BASE_END;
int ranged_origin = BASE_END;
// Check zombies
for (uint i = 0; i < zombies.length(); i++) {
int dist = abs(zombies[i].position - sling_origin);
if (dist > 0 && dist <= SLING_RANGE && dist < nearestDistance) {
int dist = abs(zombies[i].position - ranged_origin);
if (dist > 0 && dist <= range && dist < nearestDistance) {
nearestDistance = dist;
targetPos = zombies[i].position;
targetIsBandit = false;
@@ -278,8 +298,8 @@ void attempt_resident_sling_defense() {
// Check bandits
for (uint i = 0; i < bandits.length(); i++) {
int dist = abs(bandits[i].position - sling_origin);
if (dist > 0 && dist <= SLING_RANGE && dist < nearestDistance) {
int dist = abs(bandits[i].position - ranged_origin);
if (dist > 0 && dist <= range && dist < nearestDistance) {
nearestDistance = dist;
targetPos = bandits[i].position;
targetIsBandit = true;
@@ -290,11 +310,16 @@ void attempt_resident_sling_defense() {
if (targetPos == -1) return;
// Shoot!
resident_sling_timer.restart();
add_storage_count(ITEM_STONES, -1);
resident_ranged_timer.restart();
int damage = apply_resident_damage_bonus(random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX));
play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", x, targetPos, false, RESIDENT_DEFENSE_VOLUME_STEP);
if (has_bow) {
add_storage_count(ITEM_ARROWS, -1);
play_1d_with_volume_step("sounds/weapons/bow_fire.ogg", x, BASE_END + 1, false, RESIDENT_DEFENSE_VOLUME_STEP);
play_1d_with_volume_step("sounds/weapons/arrow_hit.ogg", x, targetPos, false, RESIDENT_DEFENSE_VOLUME_STEP);
} else {
add_storage_count(ITEM_STONES, -1);
play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", x, targetPos, false, RESIDENT_DEFENSE_VOLUME_STEP);
}
if (targetIsBandit) {
damage_bandit_at(targetPos, damage);
@@ -313,7 +338,7 @@ void attempt_resident_sling_defense() {
void process_daily_weapon_breakage() {
if (residents_count <= 0) return;
int totalWeapons = get_storage_count(ITEM_SPEARS) + get_storage_count(ITEM_SLINGS);
int totalWeapons = get_storage_count(ITEM_SPEARS) + get_storage_count(ITEM_SLINGS) + get_storage_count(ITEM_BOWS);
if (totalWeapons == 0) return;
// Number of breakage checks = min(residents, weapons)
@@ -322,17 +347,21 @@ void process_daily_weapon_breakage() {
// Distribute checks among available weapons
int spearChecks = 0;
int slingChecks = 0;
int bowChecks = 0;
for (int i = 0; i < checksToPerform; i++) {
int remainingSpears = get_storage_count(ITEM_SPEARS) - spearChecks;
int remainingSlings = get_storage_count(ITEM_SLINGS) - slingChecks;
int remaining = remainingSpears + remainingSlings;
int remainingBows = get_storage_count(ITEM_BOWS) - bowChecks;
int remaining = remainingSpears + remainingSlings + remainingBows;
if (remaining <= 0) break;
int roll = random(1, remaining);
if (roll <= remainingSpears && remainingSpears > 0) {
spearChecks++;
} else if (roll <= remainingSpears + remainingBows && remainingBows > 0) {
bowChecks++;
} else if (remainingSlings > 0) {
slingChecks++;
}
@@ -341,6 +370,7 @@ void process_daily_weapon_breakage() {
// Perform breakage checks
int spearsBroken = 0;
int slingsBroken = 0;
int bowsBroken = 0;
int break_chance = get_resident_break_chance(RESIDENT_WEAPON_BREAK_CHANCE);
for (int i = 0; i < spearChecks; i++) {
@@ -355,6 +385,12 @@ void process_daily_weapon_breakage() {
}
}
for (int i = 0; i < bowChecks; i++) {
if (random(1, 100) <= break_chance) {
bowsBroken++;
}
}
// Apply breakage
if (spearsBroken > 0) {
add_storage_count(ITEM_SPEARS, -spearsBroken);
@@ -373,6 +409,15 @@ void process_daily_weapon_breakage() {
: slingsBroken + " slings broke from wear.";
notify(msg);
}
if (bowsBroken > 0) {
add_storage_count(ITEM_BOWS, -bowsBroken);
if (get_storage_count(ITEM_BOWS) < 0) set_storage_count(ITEM_BOWS, 0);
string msg = (bowsBroken == 1)
? "A resident's bow broke from wear."
: bowsBroken + " bows broke from wear.";
notify(msg);
}
}
// Resident snare retrieval

View File

@@ -14,6 +14,11 @@ void perform_attack(int current_x) {
timer ammoBlockTimer;
string lastAmmoBlockMessage = "";
const int AMMO_BLOCK_COOLDOWN_MS = 3000;
const int BOW_HIT_NONE = 0;
const int BOW_HIT_BANDIT = 1;
const int BOW_HIT_BOAR = 2;
const int BOW_HIT_ZOMBIE = 3;
const int BOW_HIT_FLYING = 4;
void speak_ammo_blocked(string message) {
if (message == lastAmmoBlockMessage && ammoBlockTimer.elapsed < AMMO_BLOCK_COOLDOWN_MS) {
@@ -25,6 +30,44 @@ void speak_ammo_blocked(string message) {
speak_with_history(message, true);
}
int find_ranged_enemy(int player_x, int range, int direction, bool allow_flying, bool &out hit_bandit, bool &out hit_boar, bool &out hit_flying_creature) {
hit_bandit = false;
hit_boar = false;
hit_flying_creature = false;
for (int dist = 1; dist <= range; dist++) {
int check_x = player_x + (dist * direction);
if (check_x < 0 || check_x >= MAP_SIZE) break;
Bandit@ bandit = get_bandit_at(check_x);
if (bandit != null) {
hit_bandit = true;
return check_x;
}
GroundGame@ boar = get_boar_at(check_x);
if (boar != null) {
hit_boar = true;
return check_x;
}
Undead@ undead = get_zombie_at(check_x);
if (undead != null) {
return check_x;
}
if (allow_flying) {
FlyingCreature@ creature = get_flying_creature_at(check_x);
if (creature != null && creature.state == "flying") {
hit_flying_creature = true;
return check_x;
}
}
}
return -1;
}
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)
@@ -137,6 +180,145 @@ void update_sling_charge() {
}
}
int get_bow_draw_damage(int elapsed_ms) {
int clamped = elapsed_ms;
if (clamped < 0) clamped = 0;
if (clamped > BOW_DRAW_TIME_MS) clamped = BOW_DRAW_TIME_MS;
float ratio = float(clamped) / float(BOW_DRAW_TIME_MS);
int damage = int(ratio * BOW_DAMAGE_MAX);
if (damage < BOW_DAMAGE_MIN) damage = BOW_DAMAGE_MIN;
if (damage > BOW_DAMAGE_MAX) damage = BOW_DAMAGE_MAX;
return damage;
}
void stop_bow_shot_audio() {
safe_destroy_sound(bow_shot_sound_handle);
bow_shot_active = false;
bow_shot_duration_ms = 0;
bow_shot_hit_x = -1;
bow_shot_hit_type = BOW_HIT_NONE;
}
void start_bow_shot_audio(int start_x, int end_x, int hit_x, int hit_type, 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_hit_type = hit_type;
bow_shot_duration_ms = duration_ms;
bow_shot_drop_pending = false;
bow_shot_drop_pos = -1;
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",
x,
bow_shot_start_x,
false,
PLAYER_WEAPON_SOUND_VOLUME_STEP
);
}
void update_bow_shot() {
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;
int hit_type = bow_shot_hit_type;
bool drop_pending = bow_shot_drop_pending;
int drop_pos = bow_shot_drop_pos;
stop_bow_shot_audio();
if (hit_x >= 0) {
play_1d_with_volume_step(
"sounds/weapons/arrow_hit.ogg",
x,
hit_x,
false,
PLAYER_WEAPON_SOUND_VOLUME_STEP
);
if (hit_type == BOW_HIT_BANDIT) {
play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", x, hit_x, BANDIT_SOUND_VOLUME_STEP);
} else if (hit_type == BOW_HIT_BOAR) {
play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", x, hit_x, BOAR_SOUND_VOLUME_STEP);
} else if (hit_type == BOW_HIT_ZOMBIE) {
play_creature_hit_sound("sounds/enemies/zombie_hit.ogg", x, hit_x, ZOMBIE_SOUND_VOLUME_STEP);
}
}
if (drop_pending && drop_pos >= 0) {
if (get_drop_at(drop_pos) == null) {
add_world_drop(drop_pos, "arrow");
}
}
}
}
void release_bow_attack(int player_x) {
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 = (facing == 1) ? 1 : -1;
bool hit_bandit = false;
bool hit_flying_creature = false;
bool hit_boar = false;
int target_x = find_ranged_enemy(player_x, BOW_RANGE, search_direction, true, hit_bandit, hit_boar, hit_flying_creature);
int hit_type = BOW_HIT_NONE;
if (target_x != -1) {
if (hit_bandit) {
damage_bandit_at(target_x, damage);
hit_type = BOW_HIT_BANDIT;
} else if (hit_boar) {
damage_boar_at(target_x, damage);
hit_type = BOW_HIT_BOAR;
} else if (hit_flying_creature) {
damage_flying_creature_at(target_x, damage);
hit_type = BOW_HIT_FLYING;
} else {
damage_zombie_at(target_x, damage);
hit_type = BOW_HIT_ZOMBIE;
}
}
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;
}
int hit_x = (target_x != -1) ? target_x : -1;
start_bow_shot_audio(player_x, end_x, hit_x, hit_type, duration_ms);
if (random(1, 100) <= 25) {
bow_shot_drop_pending = true;
bow_shot_drop_pos = (target_x != -1) ? target_x : end_x;
}
}
void release_sling_attack(int player_x) {
// Consume stone
add_personal_count(ITEM_STONES, -1);
@@ -158,44 +340,7 @@ void release_sling_attack(int player_x) {
bool hit_bandit = false;
bool hit_flying_creature = false;
bool hit_boar = false;
// Priority: Find nearest enemy (bandit or zombie) first
for (int dist = 1; dist <= SLING_RANGE; 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 boar
GroundGame@ boar = get_boar_at(check_x);
if (boar != null) {
target_x = check_x;
hit_boar = true;
break;
}
// Then check for undead
Undead@ undead = get_zombie_at(check_x);
if (undead != null) {
target_x = check_x;
hit_bandit = false;
break;
}
// Then check for flying creature (only if flying)
FlyingCreature@ creature = get_flying_creature_at(check_x);
if (creature != null && creature.state == "flying") {
target_x = check_x;
hit_flying_creature = true;
break;
}
}
target_x = find_ranged_enemy(player_x, SLING_RANGE, search_direction, true, hit_bandit, hit_boar, hit_flying_creature);
// If no enemy found, check for trees (but don't damage them)
if (target_x == -1) {

View File

@@ -37,9 +37,12 @@ const int SLING_DAMAGE_MAX = 8;
const int SLING_RANGE = 8;
// Bow settings
const int BOW_DAMAGE_MIN = 6;
const int BOW_DAMAGE_MAX = 9;
const int BOW_DAMAGE_MIN = 0;
const int BOW_DAMAGE_MAX = 10;
const int BOW_RANGE = 12;
const int BOW_DRAW_TIME_MS = 1000;
const int ARROW_FLIES_DURATION_MS = 230;
const int BOW_MISS_EXTRA_TILES = 3;
const int ARROWS_PER_CRAFT = 12;
const int ARROW_CAPACITY_PER_QUIVER = 12;

View File

@@ -7,6 +7,7 @@ void run_materials_menu() {
"Butcher Game [Requires Game, Knife, Fire nearby]",
"Smoke Fish (1 Fish, 1 Stick) [Requires Fire nearby]",
"Arrows (2 Sticks, 4 Feathers, 2 Stones) [Requires Quiver]",
"Bowstring (3 Sinew) [Requires Fire nearby]",
"Incense (6 Sticks, 2 Vines, 1 Reed) [Requires Altar]"
};
@@ -34,7 +35,8 @@ void run_materials_menu() {
if (selection == 0) butcher_small_game();
else if (selection == 1) craft_smoke_fish();
else if (selection == 2) craft_arrows();
else if (selection == 3) craft_incense();
else if (selection == 3) craft_bowstring();
else if (selection == 4) craft_incense();
break;
}
@@ -42,7 +44,8 @@ void run_materials_menu() {
if (selection == 0) butcher_small_game_max();
else if (selection == 1) craft_smoke_fish_max();
else if (selection == 2) craft_arrows_max();
else if (selection == 3) craft_incense_max();
else if (selection == 3) craft_bowstring_max();
else if (selection == 4) craft_incense_max();
break;
}
}
@@ -119,6 +122,61 @@ void craft_arrows_max() {
speak_with_history("Crafted " + (ARROWS_PER_CRAFT * maxCraft) + " arrows.", true);
}
void craft_bowstring() {
WorldFire@ fire = get_fire_within_range(x, 3);
if (fire == null) {
speak_with_history("You need a fire within 3 tiles to make bowstring.", true);
return;
}
string missing = "";
if (get_personal_count(ITEM_SINEW) < 3) missing += "3 sinew ";
if (missing == "") {
if (get_personal_count(ITEM_BOWSTRINGS) >= get_personal_stack_limit()) {
speak_with_history("You can't carry any more bowstrings.", true);
return;
}
simulate_crafting(3);
add_personal_count(ITEM_SINEW, -3);
add_personal_count(ITEM_BOWSTRINGS, 1);
speak_with_history("Crafted a bowstring.", true);
} else {
speak_with_history("Missing: " + missing, true);
}
}
void craft_bowstring_max() {
WorldFire@ fire = get_fire_within_range(x, 3);
if (fire == null) {
speak_with_history("You need a fire within 3 tiles to make bowstring.", true);
return;
}
if (get_personal_count(ITEM_BOWSTRINGS) >= get_personal_stack_limit()) {
speak_with_history("You can't carry any more bowstrings.", true);
return;
}
int max_by_sinew = get_personal_count(ITEM_SINEW) / 3;
int max_craft = max_by_sinew;
int space = get_personal_stack_limit() - get_personal_count(ITEM_BOWSTRINGS);
if (max_craft > space) max_craft = space;
if (max_craft <= 0) {
speak_with_history("Missing: 3 sinew", true);
return;
}
int total_cost = max_craft * 3; // 3 sinew per bowstring
int craft_time = (total_cost < max_craft * 4) ? max_craft * 4 : total_cost;
simulate_crafting(craft_time);
add_personal_count(ITEM_SINEW, -(max_craft * 3));
add_personal_count(ITEM_BOWSTRINGS, max_craft);
speak_with_history("Crafted " + max_craft + " Bowstrings.", true);
}
void craft_incense() {
if (world_altars.length() == 0) {
speak_with_history("You need an altar to craft incense.", true);

View File

@@ -5,7 +5,8 @@ void run_weapons_menu() {
int selection = 0;
string[] options = {
"Spear (1 Stick, 1 Vine, 1 Stone) [Requires Knife]",
"Sling (1 Skin, 2 Vines)"
"Sling (1 Skin, 2 Vines)",
"Bow (1 Stick, 1 Bowstring)"
};
while(true) {
@@ -31,12 +32,14 @@ void run_weapons_menu() {
if (key_pressed(KEY_RETURN)) {
if (selection == 0) craft_spear();
else if (selection == 1) craft_sling();
else if (selection == 2) craft_bow();
break;
}
if (key_pressed(KEY_TAB)) {
if (selection == 0) craft_spear_max();
else if (selection == 1) craft_sling_max();
else if (selection == 2) craft_bow_max();
break;
}
}
@@ -155,6 +158,57 @@ void craft_sling_max() {
speak_with_history("Crafted " + max_craft + " Slings.", true);
}
void craft_bow() {
string missing = "";
if (get_personal_count(ITEM_STICKS) < 1) missing += "1 stick ";
if (get_personal_count(ITEM_BOWSTRINGS) < 1) missing += "1 bowstring ";
if (missing == "") {
if (get_personal_count(ITEM_BOWS) >= get_personal_stack_limit()) {
speak_with_history("You can't carry any more bows.", true);
return;
}
simulate_crafting(3);
add_personal_count(ITEM_STICKS, -1);
add_personal_count(ITEM_BOWSTRINGS, -1);
add_personal_count(ITEM_BOWS, 1);
speak_with_history("Crafted a Bow.", true);
} else {
speak_with_history("Missing: " + missing, true);
}
}
void craft_bow_max() {
if (get_personal_count(ITEM_BOWS) >= get_personal_stack_limit()) {
speak_with_history("You can't carry any more bows.", true);
return;
}
int max_by_sticks = get_personal_count(ITEM_STICKS);
int max_by_bowstrings = get_personal_count(ITEM_BOWSTRINGS);
int max_craft = max_by_sticks;
if (max_by_bowstrings < max_craft) max_craft = max_by_bowstrings;
int space = get_personal_stack_limit() - get_personal_count(ITEM_BOWS);
if (max_craft > space) max_craft = space;
if (max_craft <= 0) {
string missing = "";
if (get_personal_count(ITEM_STICKS) < 1) missing += "1 stick ";
if (get_personal_count(ITEM_BOWSTRINGS) < 1) missing += "1 bowstring ";
speak_with_history("Missing: " + missing, true);
return;
}
int total_cost = max_craft * 2; // 1 stick + 1 bowstring per bow
int craft_time = (total_cost < max_craft * 4) ? max_craft * 4 : total_cost;
simulate_crafting(craft_time);
add_personal_count(ITEM_STICKS, -max_craft);
add_personal_count(ITEM_BOWSTRINGS, -max_craft);
add_personal_count(ITEM_BOWS, max_craft);
speak_with_history("Crafted " + max_craft + " Bows.", true);
}
void craft_axe() {
string missing = "";
if (get_personal_count(ITEM_KNIVES) < 1) missing += "Stone Knife ";

View File

@@ -239,6 +239,9 @@ bool damage_undead_at(int pos, int damage) {
if (undeads[i].position == pos) {
undeads[i].health -= damage;
if (undeads[i].health <= 0) {
if (world_altars.length() > 0) {
favor += 0.2;
}
if (undeads[i].sound_handle != -1) {
p.destroy_sound(undeads[i].sound_handle);
undeads[i].sound_handle = -1;

View File

@@ -35,6 +35,20 @@ timer sling_charge_timer;
int sling_sound_handle = -1;
int last_sling_stage = -1; // Track which stage we're in to avoid duplicate sounds
// Bow state
bool bow_drawing = false;
timer bow_draw_timer;
bool bow_shot_active = false;
timer bow_shot_timer;
int bow_shot_start_x = 0;
int bow_shot_end_x = 0;
int bow_shot_hit_x = -1;
int bow_shot_hit_type = 0;
int bow_shot_sound_handle = -1;
int bow_shot_duration_ms = 0;
bool bow_shot_drop_pending = false;
int bow_shot_drop_pos = -1;
// Fishing state
bool is_casting = false; // Holding control, cast_strength playing
bool line_in_water = false; // Cast successful, waiting for fish
@@ -88,6 +102,8 @@ void restart_all_timers() {
fire_damage_timer.restart();
healing_timer.restart();
sling_charge_timer.restart();
bow_draw_timer.restart();
bow_shot_timer.restart();
// Fire fuel timers
for (uint i = 0; i < world_fires.length(); i++) {

View File

@@ -110,6 +110,10 @@ void stop_active_sounds() {
p.destroy_sound(sling_sound_handle);
sling_sound_handle = -1;
}
if (bow_shot_sound_handle != -1) {
p.destroy_sound(bow_shot_sound_handle);
bow_shot_sound_handle = -1;
}
stop_all_weather_sounds();
}
@@ -164,6 +168,16 @@ void reset_game_state() {
climb_target_y = 0;
fall_start_y = 0;
sling_charging = false;
bow_drawing = false;
bow_shot_active = false;
bow_shot_start_x = 0;
bow_shot_end_x = 0;
bow_shot_hit_x = -1;
bow_shot_hit_type = 0;
bow_shot_duration_ms = 0;
bow_shot_sound_handle = -1;
bow_shot_drop_pending = false;
bow_shot_drop_pos = -1;
searching = false;
rope_climbing = false;
rope_climb_up = true;
@@ -256,6 +270,8 @@ void reset_game_state() {
fire_damage_timer.restart();
healing_timer.restart();
sling_charge_timer.restart();
bow_draw_timer.restart();
bow_shot_timer.restart();
fall_timer.restart();
climb_timer.restart();
}

View File

@@ -284,7 +284,7 @@ void attempt_resident_recruitment() {
return;
}
int added = random(1, 3);
int added = random(1, 2);
// Don't exceed cap
if (residents_count + added > MAX_RESIDENTS) {
added = MAX_RESIDENTS - residents_count;
@@ -483,8 +483,8 @@ void update_time() {
ensure_ambience_running();
// Proactive resident defense with slings
attempt_resident_sling_defense();
// Proactive resident ranged defense
attempt_resident_ranged_defense();
// Manage invasion enemies during active invasion
manage_bandits_during_invasion();

View File

@@ -104,6 +104,20 @@ bool try_pickup_world_drop(WorldDrop@ drop) {
if (get_flying_creature_config_by_drop_type(drop.type) !is null) {
return try_pickup_small_game(drop.type);
}
if (drop.type == "arrow") {
int max_arrows = get_arrow_limit();
if (max_arrows <= 0) {
speak_with_history("You need a quiver to carry arrows.", true);
return false;
}
if (get_personal_count(ITEM_ARROWS) >= max_arrows) {
speak_with_history("You can't carry any more arrows.", true);
return false;
}
add_personal_count(ITEM_ARROWS, 1);
speak_with_history("Picked up arrow.", true);
return true;
}
if (drop.type == "boar carcass") {
if (get_personal_count(ITEM_BOAR_CARCASSES) >= get_personal_stack_limit()) {
speak_with_history("You can't carry any more boar carcasses.", true);