Documentation sort of updated, food consumption also updated for residents.

This commit is contained in:
Storm Dragon
2026-01-23 23:39:12 -05:00
parent 32a6894401
commit 78e8d434a3
8 changed files with 307 additions and 89 deletions
+104 -24
View File
@@ -1,31 +1,111 @@
# Draugnorak
A survival audio game built with NVGT.
A survival audio game built with NVGT. Explore, gather, craft, and defend your base as day and night cycle on.
## Quick Start
- Move with Left/Right, jump with Up.
- Hold Shift for 1 second to search the current area.
- Craft basic tools in the base with C.
- Set snares and build fires to survive.
## Controls
- **Arrow Keys**: Move (Left/Right) and Jump (Up).
- **Shift**: Search the current area for resources.
- **Control**: Attack with equipped weapon.
- **C**: Open Crafting Menu (Base area only).
- **E**: Open Equipment Menu.
- **A**: Open Action Menu (Place objects, feed fire).
- **I**: Check Inventory.
- **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.
- **A**: Action menu (place snare, feed fire, burn incense).
- **C**: Crafting menu (base only).
- **I**: Inventory.
- **E**: Equipment.
- **P**: Character info.
- **B**: Base info (base only).
- **Q**: Quests menu (base only).
- **H**: Check Health.
- **T**: Check Time.
- **X**: Check Coordinates.
- **B**: Base info menu (base only).
- **Escape**: Exit game.
- **S**: Altar menu (base only).
- **Tab**: Adventure menu (outside base, once per day).
- **H**: Health.
- **T**: Time.
- **X**: Coordinates + terrain.
- **1-0**: Quick slots (equip bound items).
- **Backspace**: Pause/Resume.
- **Escape**: Exit game (or close menus).
## Gameplay
- **Gathering**: Search in grass for sticks and vines. Search in gravel for stones.
- **Crafting**: Create tools like Spears, Axes, Snares, and Firepits (base only).
- **Combat**: Defend yourself or hunt.
- **Survival**: Build fires and set snares to survive.
- **Fire Management**:
- Build a firepit (9 stones) in the base area - it's crafted in place
- Build fires (2 sticks + 1 log) only on tiles with a firepit
- Fires burn out over time and need fuel (vines, sticks, or logs)
- Stand next to a fire and press A to feed it
- Standing on a fire damages your health!
### History Shortcuts
- **\\**: Repeat most recent notification.
- **[ / ]**: Previous/next notification.
- **, / .**: Previous/next speech history message.
## Menus
- **Up/Down**: Move selection.
- **Enter**: Confirm.
- **Escape**: Close.
- **Tab (in most menus)**: Perform “max” actions (craft all, deposit all, etc.).
## Gathering and Resources
- **Grass/Forest/Deep Forest**: Sticks and vines (search).
- **Gravel/Stone**: Stones (search).
- **Streams/Water edges**: Reeds and clay (search).
- **World drops**: Found on the ground after hunting; search picks them up.
- **Trees**:
- Search near trees for sticks/vines.
- Use an axe to chop down a tree for logs (and extra sticks/vines).
- Trees regrow over time.
- You can climb trees (Up) and climb down (Down).
## Crafting (Base Only)
Crafting is only available in the base (C).
### Categories
- **Weapons**: Spear, Sling
- **Tools**: Knife, Snare, Stone Axe, Fishing Pole, Rope, Quiver, Reed Basket, Clay Pot
- **Materials**: Arrows, Butcher Game, Incense
- **Clothing**: Skin gear, Moccasins, Pouch, Backpack
- **Buildings**: Firepit, Fire, Herb Garden, Storage, Pasture, Stable, Altar
- **Barricade**: Reinforcement options
- **Runes**: Unlocks later
## Combat and Hunting
- Equip weapons in the Equipment menu (E) or bind them to quick slots.
- **Spear**: 1-tile range.
- **Axe**: Melee; also chops trees.
- **Sling**: Ranged (uses stones). Hold Control to charge; release on the “in-range” audio cue.
- Enemies include zombies (night), bandits (invasions), and boars (daytime).
- Bow combat is planned; arrows are stockpiled for future bow use.
## Base and Survival
- **Passive healing** in the base when below max health.
- **Herb Garden** increases healing speed (base only, one per base).
- **Fires** require a nearby firepit and need fuel (sticks, vines, logs).
- **Barricade** starts at 100 health, capped at 500; reinforces via Crafting > Barricade.
- **Storage** enables base inventory management and helps attract residents.
- **Residents** (if recruited) consume meat, help defend, repair the barricade, and collect resources.
## Time, Weather, and Events
- **1 real minute = 1 in-game hour**.
- Day/night ambience crossfades around sunrise/sunset.
- Weather transitions between clear, windy, rainy, and stormy.
- **Invasions** can begin after day 2 and expand the map eastward.
- Expansions can be normal biomes or mountain ranges.
## Mountains and Rope Climbing
- Mountains have elevation; steep slopes require a rope.
- When prompted, press Up to climb up or Down to climb down.
- Moving left/right during a rope climb cancels and causes a fall.
- Falling from height can cause damage.
## Quests, Adventures, and Runes
- **Quests** become available in the base once you have an altar and enough favor.
- **Adventures** are accessed with Tab outside the base (once per day).
- The mountain adventure features the **Unicorn boss**.
- Defeating the Unicorn unlocks the **Rune of Swiftness**.
- Runes are engraved in the base (Crafting > Runes) and require a knife, clay, and favor.
## Inventory and Storage
- 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.
- Storage is separate and accessed in the base (Inventory menu when storage exists).
## Saving and Loading
- The game auto-saves daily at 6 AM (in-game time).
- Load from the main menu. Starting a new game prompts if a save exists.
+1 -1
View File
@@ -1,7 +1,7 @@
// Base automation helpers
int get_daily_food_requirement() {
if (residents_count <= 0) return 0;
return (residents_count + 1) / 2;
return residents_count * 2;
}
void consume_food_for_residents() {
+76 -2
View File
@@ -5,6 +5,7 @@ void run_materials_menu() {
int selection = 0;
string[] options = {
"Butcher Game [Requires Game, Knife, Fire nearby]",
"Arrows (2 Sticks, 4 Feathers, 2 Stones) [Requires Quiver]",
"Incense (6 Sticks, 2 Vines, 1 Reed) [Requires Altar]"
};
@@ -30,18 +31,91 @@ void run_materials_menu() {
if (key_pressed(KEY_RETURN)) {
if (selection == 0) butcher_small_game();
else if (selection == 1) craft_incense();
else if (selection == 1) craft_arrows();
else if (selection == 2) craft_incense();
break;
}
if (key_pressed(KEY_TAB)) {
if (selection == 0) butcher_small_game_max();
else if (selection == 1) craft_incense_max();
else if (selection == 1) craft_arrows_max();
else if (selection == 2) craft_incense_max();
break;
}
}
}
void craft_arrows() {
if (get_personal_count(ITEM_QUIVERS) <= 0) {
speak_with_history("You need a quiver to craft arrows.", true);
return;
}
int currentArrows = get_personal_count(ITEM_ARROWS);
int capacity = get_arrow_limit() - currentArrows;
if (capacity < ARROWS_PER_CRAFT) {
speak_with_history("Not enough quiver capacity for arrows.", true);
return;
}
string missing = "";
if (get_personal_count(ITEM_STICKS) < 2) missing += "2 sticks ";
if (get_personal_count(ITEM_FEATHERS) < 4) missing += "4 feathers ";
if (get_personal_count(ITEM_STONES) < 2) missing += "2 stones ";
if (missing == "") {
simulate_crafting(8);
add_personal_count(ITEM_STICKS, -2);
add_personal_count(ITEM_FEATHERS, -4);
add_personal_count(ITEM_STONES, -2);
add_personal_count(ITEM_ARROWS, ARROWS_PER_CRAFT);
speak_with_history("Crafted " + ARROWS_PER_CRAFT + " arrows.", true);
} else {
speak_with_history("Missing: " + missing, true);
}
}
void craft_arrows_max() {
if (get_personal_count(ITEM_QUIVERS) <= 0) {
speak_with_history("You need a quiver to craft arrows.", true);
return;
}
int currentArrows = get_personal_count(ITEM_ARROWS);
int capacity = get_arrow_limit() - currentArrows;
int maxByCapacity = capacity / ARROWS_PER_CRAFT;
if (maxByCapacity <= 0) {
speak_with_history("Not enough quiver capacity for arrows.", true);
return;
}
int maxBySticks = get_personal_count(ITEM_STICKS) / 2;
int maxByFeathers = get_personal_count(ITEM_FEATHERS) / 4;
int maxByStones = get_personal_count(ITEM_STONES) / 2;
int maxCraft = maxBySticks;
if (maxByFeathers < maxCraft) maxCraft = maxByFeathers;
if (maxByStones < maxCraft) maxCraft = maxByStones;
if (maxByCapacity < maxCraft) maxCraft = maxByCapacity;
if (maxCraft <= 0) {
string missing = "";
if (get_personal_count(ITEM_STICKS) < 2) missing += "2 sticks ";
if (get_personal_count(ITEM_FEATHERS) < 4) missing += "4 feathers ";
if (get_personal_count(ITEM_STONES) < 2) missing += "2 stones ";
speak_with_history("Missing: " + missing, true);
return;
}
int totalCost = maxCraft * 8; // 2 sticks + 4 feathers + 2 stones per bundle
int craftTime = (totalCost < maxCraft * 4) ? maxCraft * 4 : totalCost;
simulate_crafting(craftTime);
add_personal_count(ITEM_STICKS, -(maxCraft * 2));
add_personal_count(ITEM_FEATHERS, -(maxCraft * 4));
add_personal_count(ITEM_STONES, -(maxCraft * 2));
add_personal_count(ITEM_ARROWS, ARROWS_PER_CRAFT * maxCraft);
speak_with_history("Crafted " + (ARROWS_PER_CRAFT * maxCraft) + " arrows.", true);
}
void craft_incense() {
if (world_altars.length() == 0) {
speak_with_history("You need an altar to craft incense.", true);
+58 -4
View File
@@ -9,6 +9,7 @@ void run_tools_menu() {
"Stone Axe (1 Stick, 1 Vine, 2 Stones) [Requires Knife]",
"Fishing Pole (1 Stick, 2 Vines)",
"Rope (3 Vines)",
"Quiver (2 Skins, 2 Vines)",
"Reed Basket (3 Reeds)",
"Clay Pot (3 Clay)"
};
@@ -39,8 +40,9 @@ void run_tools_menu() {
else if (selection == 2) craft_axe();
else if (selection == 3) craft_fishing_pole();
else if (selection == 4) craft_rope();
else if (selection == 5) craft_reed_basket();
else if (selection == 6) craft_clay_pot();
else if (selection == 5) craft_quiver();
else if (selection == 6) craft_reed_basket();
else if (selection == 7) craft_clay_pot();
break;
}
@@ -50,8 +52,9 @@ void run_tools_menu() {
else if (selection == 2) craft_axe_max();
else if (selection == 3) craft_fishing_pole_max();
else if (selection == 4) craft_rope_max();
else if (selection == 5) craft_reed_basket_max();
else if (selection == 6) craft_clay_pot_max();
else if (selection == 5) craft_quiver_max();
else if (selection == 6) craft_reed_basket_max();
else if (selection == 7) craft_clay_pot_max();
break;
}
}
@@ -242,6 +245,57 @@ void craft_rope_max() {
speak_with_history("Crafted " + max_craft + " Rope.", true);
}
void craft_quiver() {
string missing = "";
if (get_personal_count(ITEM_SKINS) < 2) missing += "2 skins ";
if (get_personal_count(ITEM_VINES) < 2) missing += "2 vines ";
if (missing == "") {
if (get_personal_count(ITEM_QUIVERS) >= get_personal_stack_limit()) {
speak_with_history("You can't carry any more quivers.", true);
return;
}
simulate_crafting(4);
add_personal_count(ITEM_SKINS, -2);
add_personal_count(ITEM_VINES, -2);
add_personal_count(ITEM_QUIVERS, 1);
speak_with_history("Crafted a Quiver.", true);
} else {
speak_with_history("Missing: " + missing, true);
}
}
void craft_quiver_max() {
if (get_personal_count(ITEM_QUIVERS) >= get_personal_stack_limit()) {
speak_with_history("You can't carry any more quivers.", true);
return;
}
int maxBySkins = get_personal_count(ITEM_SKINS) / 2;
int maxByVines = get_personal_count(ITEM_VINES) / 2;
int maxCraft = maxBySkins;
if (maxByVines < maxCraft) maxCraft = maxByVines;
int space = get_personal_stack_limit() - get_personal_count(ITEM_QUIVERS);
if (maxCraft > space) maxCraft = space;
if (maxCraft <= 0) {
string missing = "";
if (get_personal_count(ITEM_SKINS) < 2) missing += "2 skins ";
if (get_personal_count(ITEM_VINES) < 2) missing += "2 vines ";
speak_with_history("Missing: " + missing, true);
return;
}
int totalCost = maxCraft * 4; // 2 skins + 2 vines per quiver
int craftTime = (totalCost < maxCraft * 4) ? maxCraft * 4 : totalCost;
simulate_crafting(craftTime);
add_personal_count(ITEM_SKINS, -(maxCraft * 2));
add_personal_count(ITEM_VINES, -(maxCraft * 2));
add_personal_count(ITEM_QUIVERS, maxCraft);
speak_with_history("Crafted " + maxCraft + " Quivers.", true);
}
void craft_reed_basket() {
string missing = "";
if (get_personal_count(ITEM_REEDS) < 3) missing += "3 reeds ";
+14
View File
@@ -63,6 +63,19 @@ int get_arrow_limit() {
return quivers * ARROW_CAPACITY_PER_QUIVER;
}
void clamp_arrows_to_quiver_limit() {
int maxArrows = get_arrow_limit();
int currentArrows = get_personal_count(ITEM_ARROWS);
if (currentArrows <= maxArrows) return;
set_personal_count(ITEM_ARROWS, maxArrows);
if (maxArrows == 0) {
speak_with_history("You need a quiver to carry arrows.", true);
} else {
speak_with_history("You can only carry " + maxArrows + " arrows with your current quivers.", true);
}
}
string get_equipment_name(int equip_type) {
if (equip_type == EQUIP_SPEAR) return "Spear";
if (equip_type == EQUIP_AXE) return "Stone Axe";
@@ -274,5 +287,6 @@ void cleanup_equipment_after_inventory_change() {
if (get_personal_count(ITEM_MOCCASINS) <= 0) equipped_feet = EQUIP_NONE;
if (get_personal_count(ITEM_SKIN_POUCHES) <= 0 && equipped_arms == EQUIP_POUCH) equipped_arms = EQUIP_NONE;
if (get_personal_count(ITEM_BACKPACKS) <= 0 && equipped_arms == EQUIP_BACKPACK) equipped_arms = EQUIP_NONE;
clamp_arrows_to_quiver_limit();
update_max_health_from_equipment();
}
+8 -54
View File
@@ -30,38 +30,15 @@ void sacrifice_item(int item_type) {
return;
}
if (item_type == ITEM_STICKS) add_personal_count(ITEM_STICKS, -1);
else if (item_type == ITEM_VINES) add_personal_count(ITEM_VINES, -1);
else if (item_type == ITEM_REEDS) add_personal_count(ITEM_REEDS, -1);
else if (item_type == ITEM_STONES) add_personal_count(ITEM_STONES, -1);
else if (item_type == ITEM_LOGS) add_personal_count(ITEM_LOGS, -1);
else if (item_type == ITEM_CLAY) add_personal_count(ITEM_CLAY, -1);
else if (item_type == ITEM_SMALL_GAME) {
if (item_type == ITEM_SMALL_GAME) {
add_personal_count(ITEM_SMALL_GAME, -1);
if (personal_small_game_types.length() > 0) {
personal_small_game_types.remove_at(0);
}
}
else if (item_type == ITEM_MEAT) add_personal_count(ITEM_MEAT, -1);
else if (item_type == ITEM_SKINS) add_personal_count(ITEM_SKINS, -1);
else if (item_type == ITEM_FEATHERS) add_personal_count(ITEM_FEATHERS, -1);
else if (item_type == ITEM_DOWN) add_personal_count(ITEM_DOWN, -1);
else if (item_type == ITEM_INCENSE) add_personal_count(ITEM_INCENSE, -1);
else if (item_type == ITEM_SPEARS) add_personal_count(ITEM_SPEARS, -1);
else if (item_type == ITEM_SLINGS) add_personal_count(ITEM_SLINGS, -1);
else if (item_type == ITEM_AXES) add_personal_count(ITEM_AXES, -1);
else if (item_type == ITEM_SNARES) add_personal_count(ITEM_SNARES, -1);
else if (item_type == ITEM_KNIVES) add_personal_count(ITEM_KNIVES, -1);
else if (item_type == ITEM_FISHING_POLES) add_personal_count(ITEM_FISHING_POLES, -1);
else if (item_type == ITEM_ROPES) add_personal_count(ITEM_ROPES, -1);
else if (item_type == ITEM_REED_BASKETS) add_personal_count(ITEM_REED_BASKETS, -1);
else if (item_type == ITEM_CLAY_POTS) add_personal_count(ITEM_CLAY_POTS, -1);
else if (item_type == ITEM_SKIN_HATS) add_personal_count(ITEM_SKIN_HATS, -1);
else if (item_type == ITEM_SKIN_GLOVES) add_personal_count(ITEM_SKIN_GLOVES, -1);
else if (item_type == ITEM_SKIN_PANTS) add_personal_count(ITEM_SKIN_PANTS, -1);
else if (item_type == ITEM_SKIN_TUNICS) add_personal_count(ITEM_SKIN_TUNICS, -1);
else if (item_type == ITEM_MOCCASINS) add_personal_count(ITEM_MOCCASINS, -1);
else if (item_type == ITEM_SKIN_POUCHES) add_personal_count(ITEM_SKIN_POUCHES, -1);
else {
add_personal_count(item_type, -1);
}
cleanup_equipment_after_inventory_change();
double favor_gain = get_item_favor_value(item_type);
@@ -82,36 +59,13 @@ void sacrifice_item_max(int item_type) {
double favor_per_item = get_item_favor_value(item_type);
if (item_type == ITEM_STICKS) { set_personal_count(ITEM_STICKS, 0); }
else if (item_type == ITEM_VINES) { set_personal_count(ITEM_VINES, 0); }
else if (item_type == ITEM_REEDS) { set_personal_count(ITEM_REEDS, 0); }
else if (item_type == ITEM_STONES) { set_personal_count(ITEM_STONES, 0); }
else if (item_type == ITEM_LOGS) { set_personal_count(ITEM_LOGS, 0); }
else if (item_type == ITEM_CLAY) { set_personal_count(ITEM_CLAY, 0); }
else if (item_type == ITEM_SMALL_GAME) {
if (item_type == ITEM_SMALL_GAME) {
set_personal_count(ITEM_SMALL_GAME, 0);
personal_small_game_types.resize(0);
}
else if (item_type == ITEM_MEAT) { set_personal_count(ITEM_MEAT, 0); }
else if (item_type == ITEM_SKINS) { set_personal_count(ITEM_SKINS, 0); }
else if (item_type == ITEM_FEATHERS) { set_personal_count(ITEM_FEATHERS, 0); }
else if (item_type == ITEM_DOWN) { set_personal_count(ITEM_DOWN, 0); }
else if (item_type == ITEM_INCENSE) { set_personal_count(ITEM_INCENSE, 0); }
else if (item_type == ITEM_SPEARS) { set_personal_count(ITEM_SPEARS, 0); }
else if (item_type == ITEM_SLINGS) { set_personal_count(ITEM_SLINGS, 0); }
else if (item_type == ITEM_AXES) { set_personal_count(ITEM_AXES, 0); }
else if (item_type == ITEM_SNARES) { set_personal_count(ITEM_SNARES, 0); }
else if (item_type == ITEM_KNIVES) { set_personal_count(ITEM_KNIVES, 0); }
else if (item_type == ITEM_FISHING_POLES) { set_personal_count(ITEM_FISHING_POLES, 0); }
else if (item_type == ITEM_ROPES) { set_personal_count(ITEM_ROPES, 0); }
else if (item_type == ITEM_REED_BASKETS) { set_personal_count(ITEM_REED_BASKETS, 0); }
else if (item_type == ITEM_CLAY_POTS) { set_personal_count(ITEM_CLAY_POTS, 0); }
else if (item_type == ITEM_SKIN_HATS) { set_personal_count(ITEM_SKIN_HATS, 0); }
else if (item_type == ITEM_SKIN_GLOVES) { set_personal_count(ITEM_SKIN_GLOVES, 0); }
else if (item_type == ITEM_SKIN_PANTS) { set_personal_count(ITEM_SKIN_PANTS, 0); }
else if (item_type == ITEM_SKIN_TUNICS) { set_personal_count(ITEM_SKIN_TUNICS, 0); }
else if (item_type == ITEM_MOCCASINS) { set_personal_count(ITEM_MOCCASINS, 0); }
else if (item_type == ITEM_SKIN_POUCHES) { set_personal_count(ITEM_SKIN_POUCHES, 0); }
else {
set_personal_count(item_type, 0);
}
cleanup_equipment_after_inventory_change();
double total_favor = favor_per_item * available;
+24 -4
View File
@@ -166,7 +166,16 @@ void withdraw_item(int item_type) {
speak_with_history("Nothing to withdraw.", true);
return;
}
int capacity = get_personal_stack_limit() - get_personal_count(item_type);
int capacity = 0;
if (item_type == ITEM_ARROWS) {
capacity = get_arrow_limit() - get_personal_count(ITEM_ARROWS);
if (capacity <= 0) {
speak_with_history("You can't carry any more arrows.", true);
return;
}
} else {
capacity = get_personal_stack_limit() - get_personal_count(item_type);
}
if (capacity <= 0) {
speak_with_history("You can't carry any more " + get_item_label(item_type) + ".", true);
return;
@@ -212,9 +221,20 @@ void withdraw_item_max(int item_type) {
return;
}
int personal_limit = get_personal_stack_limit();
int current_personal = get_personal_count(item_type);
int space = personal_limit - current_personal;
int personalLimit = 0;
int currentPersonal = 0;
if (item_type == ITEM_ARROWS) {
personalLimit = get_arrow_limit();
currentPersonal = get_personal_count(ITEM_ARROWS);
if (personalLimit <= 0) {
speak_with_history("You need a quiver to carry arrows.", true);
return;
}
} else {
personalLimit = get_personal_stack_limit();
currentPersonal = get_personal_count(item_type);
}
int space = personalLimit - currentPersonal;
if (space <= 0) {
speak_with_history("Can't carry any more.", true);
+22
View File
@@ -406,6 +406,8 @@ void update_time() {
attempt_resident_collection();
}
ensure_ambience_running();
// Proactive resident defense with slings
attempt_resident_sling_defense();
@@ -533,6 +535,9 @@ void complete_crossfade() {
}
if (night_sound_handle != -1) p.update_sound_start_values(night_sound_handle, 0.0, 0.0, 1.0);
is_daytime = false;
if (night_sound_handle == -1 || !p.sound_is_active(night_sound_handle)) {
update_ambience(false);
}
} else {
// Destroy night sound, ensure day is at full volume
if (night_sound_handle != -1) {
@@ -541,6 +546,23 @@ void complete_crossfade() {
}
if (day_sound_handle != -1) p.update_sound_start_values(day_sound_handle, 0.0, 0.0, 1.0);
is_daytime = true;
if (day_sound_handle == -1 || !p.sound_is_active(day_sound_handle)) {
update_ambience(false);
}
}
}
void ensure_ambience_running() {
if (crossfade_active) return;
if (is_daytime) {
if (day_sound_handle == -1 || !p.sound_is_active(day_sound_handle)) {
update_ambience(false);
}
} else {
if (night_sound_handle == -1 || !p.sound_is_active(night_sound_handle)) {
update_ambience(false);
}
}
}