// Save system const string SAVE_FILE_PATH = "save.dat"; const string SAVE_ENCRYPTION_KEY = "draugnorak_save_v1"; const int SAVE_VERSION = 1; bool has_save_game() { return file_exists(SAVE_FILE_PATH); } string encrypt_save_data(const string&in rawData) { return string_aes_encrypt(rawData, SAVE_ENCRYPTION_KEY); } string decrypt_save_data(const string&in encryptedData) { return string_aes_decrypt(encryptedData, SAVE_ENCRYPTION_KEY); } bool save_data(const string&in filename, const string&in data) { if (data.length() == 0) { return false; } file tmp; if (!tmp.open(filename, "wb")) { return false; } if (tmp.write(data) < data.length()) { tmp.close(); return false; } tmp.close(); return true; } bool read_file_string(const string&in filename, string&out data) { file tmp; if (!tmp.open(filename, "rb")) { return false; } data = tmp.read(); tmp.close(); return data.length() > 0; } double get_number(dictionary@ data, const string&in key, double defaultValue) { double value; if (@data == null) return defaultValue; if (!data.get(key, value)) return defaultValue; return value; } bool get_bool(dictionary@ data, const string&in key, bool defaultValue) { bool value; if (@data == null) return defaultValue; if (!data.get(key, value)) return defaultValue; return value; } string[] get_string_list(dictionary@ data, const string&in key) { string[] result; if (@data == null) return result; if (!data.get(key, result)) return result; return result; } void stop_active_sounds() { if (day_sound_handle != -1) { p.destroy_sound(day_sound_handle); day_sound_handle = -1; } if (night_sound_handle != -1) { p.destroy_sound(night_sound_handle); night_sound_handle = -1; } if (fall_sound_handle != -1) { p.destroy_sound(fall_sound_handle); fall_sound_handle = -1; } if (sling_sound_handle != -1) { p.destroy_sound(sling_sound_handle); sling_sound_handle = -1; } } void clear_world_objects() { for (uint i = 0; i < world_snares.length(); i++) { world_snares[i].destroy(); } world_snares.resize(0); for (uint i = 0; i < world_fires.length(); i++) { world_fires[i].destroy(); } world_fires.resize(0); for (uint i = 0; i < world_streams.length(); i++) { world_streams[i].destroy(); } world_streams.resize(0); world_firepits.resize(0); world_herb_gardens.resize(0); for (uint i = 0; i < trees.length(); i++) { if (trees[i].sound_handle != -1) { p.destroy_sound(trees[i].sound_handle); trees[i].sound_handle = -1; } } trees.resize(0); clear_zombies(); clear_bandits(); } void reset_game_state() { stop_active_sounds(); clear_world_objects(); x = 0; y = 0; facing = 1; jumping = false; climbing = false; falling = false; climb_target_y = 0; fall_start_y = 0; sling_charging = false; searching = false; player_health = 10; max_health = 10; inv_stones = 0; inv_sticks = 0; inv_vines = 0; inv_logs = 0; inv_clay = 0; inv_small_game = 0; inv_small_game_types.resize(0); inv_meat = 0; inv_skins = 0; inv_spears = 0; inv_snares = 0; inv_axes = 0; inv_knives = 0; inv_fishing_poles = 0; inv_slings = 0; spear_equipped = false; axe_equipped = false; sling_equipped = false; MAP_SIZE = 35; expanded_area_start = -1; expanded_area_end = -1; expanded_terrain_types.resize(0); barricade_health = 0; barricade_initialized = false; current_hour = 8; current_day = 1; is_daytime = true; sun_setting_warned = false; sunrise_warned = false; area_expanded_today = false; invasion_active = false; invasion_start_hour = -1; walktimer.restart(); jumptimer.restart(); search_timer.restart(); search_delay_timer.restart(); attack_timer.restart(); hour_timer.restart(); fire_damage_timer.restart(); healing_timer.restart(); sling_charge_timer.restart(); fall_timer.restart(); climb_timer.restart(); } void start_new_game() { reset_game_state(); spawn_trees(5, 19); init_barricade(); init_time(); } string serialize_bool(bool value) { return value ? "1" : "0"; } string serialize_tree(Tree@ tree) { return tree.position + "|" + tree.sticks + "|" + tree.vines + "|" + tree.health + "|" + tree.height + "|" + serialize_bool(tree.depleted) + "|" + serialize_bool(tree.is_chopped) + "|" + tree.minutes_since_depletion; } string serialize_snare(WorldSnare@ snare) { return snare.position + "|" + serialize_bool(snare.has_catch) + "|" + snare.catch_type + "|" + snare.catch_chance + "|" + snare.escape_chance + "|" + serialize_bool(snare.active); } string serialize_fire(WorldFire@ fire) { return fire.position + "|" + fire.fuel_remaining + "|" + serialize_bool(fire.low_fuel_warned); } string serialize_stream(WorldStream@ stream) { return stream.start_position + "|" + stream.get_width(); } bool save_game_state() { dictionary saveData; saveData.set("version", SAVE_VERSION); saveData.set("player_x", x); saveData.set("player_y", y); saveData.set("player_facing", facing); saveData.set("player_health", player_health); saveData.set("player_max_health", max_health); saveData.set("inventory_stones", inv_stones); saveData.set("inventory_sticks", inv_sticks); saveData.set("inventory_vines", inv_vines); saveData.set("inventory_logs", inv_logs); saveData.set("inventory_clay", inv_clay); saveData.set("inventory_small_game", inv_small_game); saveData.set("inventory_meat", inv_meat); saveData.set("inventory_skins", inv_skins); saveData.set("inventory_spears", inv_spears); saveData.set("inventory_snares", inv_snares); saveData.set("inventory_axes", inv_axes); saveData.set("inventory_knives", inv_knives); saveData.set("inventory_fishing_poles", inv_fishing_poles); saveData.set("inventory_slings", inv_slings); saveData.set("inventory_small_game_types", inv_small_game_types); saveData.set("equipment_spear_equipped", spear_equipped); saveData.set("equipment_axe_equipped", axe_equipped); saveData.set("equipment_sling_equipped", sling_equipped); saveData.set("time_current_hour", current_hour); saveData.set("time_current_day", current_day); saveData.set("time_is_daytime", is_daytime); saveData.set("time_sun_setting_warned", sun_setting_warned); saveData.set("time_sunrise_warned", sunrise_warned); saveData.set("time_area_expanded_today", area_expanded_today); saveData.set("time_invasion_active", invasion_active); saveData.set("time_invasion_start_hour", invasion_start_hour); saveData.set("world_map_size", MAP_SIZE); saveData.set("world_expanded_area_start", expanded_area_start); saveData.set("world_expanded_area_end", expanded_area_end); saveData.set("world_barricade_initialized", barricade_initialized); saveData.set("world_barricade_health", barricade_health); saveData.set("world_expanded_terrain_types", expanded_terrain_types); string[] treeData; for (uint i = 0; i < trees.length(); i++) { treeData.insert_last(serialize_tree(trees[i])); } saveData.set("trees_data", treeData); string[] snareData; for (uint i = 0; i < world_snares.length(); i++) { snareData.insert_last(serialize_snare(world_snares[i])); } saveData.set("snares_data", snareData); string[] fireData; for (uint i = 0; i < world_fires.length(); i++) { fireData.insert_last(serialize_fire(world_fires[i])); } saveData.set("fires_data", fireData); string[] firepitPositions; for (uint i = 0; i < world_firepits.length(); i++) { firepitPositions.insert_last("" + world_firepits[i].position); } saveData.set("firepits_positions", firepitPositions); string[] herbPositions; for (uint i = 0; i < world_herb_gardens.length(); i++) { herbPositions.insert_last("" + world_herb_gardens[i].position); } saveData.set("herb_gardens_positions", herbPositions); string[] streamData; for (uint i = 0; i < world_streams.length(); i++) { streamData.insert_last(serialize_stream(world_streams[i])); } saveData.set("streams_data", streamData); string rawData = saveData.serialize(); string encryptedData = encrypt_save_data(rawData); return save_data(SAVE_FILE_PATH, encryptedData); } bool load_game_state() { if (!file_exists(SAVE_FILE_PATH)) { return false; } string encryptedData; if (!read_file_string(SAVE_FILE_PATH, encryptedData)) { return false; } string rawData = decrypt_save_data(encryptedData); if (rawData.length() == 0) { return false; } dictionary@ saveData = deserialize(rawData); if (@saveData == null) { return false; } reset_game_state(); MAP_SIZE = int(get_number(saveData, "world_map_size", 35)); expanded_area_start = int(get_number(saveData, "world_expanded_area_start", -1)); expanded_area_end = int(get_number(saveData, "world_expanded_area_end", -1)); string[] loadedTerrain = get_string_list(saveData, "world_expanded_terrain_types"); expanded_terrain_types.resize(0); for (uint i = 0; i < loadedTerrain.length(); i++) { expanded_terrain_types.insert_last(loadedTerrain[i]); } barricade_initialized = get_bool(saveData, "world_barricade_initialized", true); barricade_health = int(get_number(saveData, "world_barricade_health", BARRICADE_BASE_HEALTH)); if (!barricade_initialized) { init_barricade(); } else { if (barricade_health < 0) barricade_health = 0; if (barricade_health > BARRICADE_MAX_HEALTH) barricade_health = BARRICADE_MAX_HEALTH; } x = int(get_number(saveData, "player_x", 0)); y = int(get_number(saveData, "player_y", 0)); facing = int(get_number(saveData, "player_facing", 1)); player_health = int(get_number(saveData, "player_health", 10)); max_health = int(get_number(saveData, "player_max_health", 10)); if (x < 0) x = 0; if (x >= MAP_SIZE) x = MAP_SIZE - 1; if (y < 0) y = 0; if (facing != 0 && facing != 1) facing = 1; inv_stones = int(get_number(saveData, "inventory_stones", 0)); inv_sticks = int(get_number(saveData, "inventory_sticks", 0)); inv_vines = int(get_number(saveData, "inventory_vines", 0)); inv_logs = int(get_number(saveData, "inventory_logs", 0)); inv_clay = int(get_number(saveData, "inventory_clay", 0)); inv_small_game = int(get_number(saveData, "inventory_small_game", 0)); inv_meat = int(get_number(saveData, "inventory_meat", 0)); inv_skins = int(get_number(saveData, "inventory_skins", 0)); inv_spears = int(get_number(saveData, "inventory_spears", 0)); inv_snares = int(get_number(saveData, "inventory_snares", 0)); inv_axes = int(get_number(saveData, "inventory_axes", 0)); inv_knives = int(get_number(saveData, "inventory_knives", 0)); inv_fishing_poles = int(get_number(saveData, "inventory_fishing_poles", 0)); inv_slings = int(get_number(saveData, "inventory_slings", 0)); string[] loadedSmallGameTypes = get_string_list(saveData, "inventory_small_game_types"); inv_small_game_types.resize(0); for (uint i = 0; i < loadedSmallGameTypes.length(); i++) { inv_small_game_types.insert_last(loadedSmallGameTypes[i]); } if (inv_small_game_types.length() == 0 && inv_small_game > 0) { for (int i = 0; i < inv_small_game; i++) { inv_small_game_types.insert_last("small game"); } } else { inv_small_game = inv_small_game_types.length(); } spear_equipped = get_bool(saveData, "equipment_spear_equipped", false); axe_equipped = get_bool(saveData, "equipment_axe_equipped", false); sling_equipped = get_bool(saveData, "equipment_sling_equipped", false); current_hour = int(get_number(saveData, "time_current_hour", 8)); current_day = int(get_number(saveData, "time_current_day", 1)); sun_setting_warned = get_bool(saveData, "time_sun_setting_warned", false); sunrise_warned = get_bool(saveData, "time_sunrise_warned", false); area_expanded_today = get_bool(saveData, "time_area_expanded_today", false); invasion_active = get_bool(saveData, "time_invasion_active", false); invasion_start_hour = int(get_number(saveData, "time_invasion_start_hour", -1)); if (current_hour < 0) current_hour = 0; if (current_hour > 23) current_hour = 23; if (current_day < 1) current_day = 1; is_daytime = (current_hour >= 6 && current_hour < 19); hour_timer.restart(); string[] treeData = get_string_list(saveData, "trees_data"); for (uint i = 0; i < treeData.length(); i++) { string[]@ parts = treeData[i].split("|"); if (parts.length() < 8) continue; int pos = parse_int(parts[0]); Tree@ tree = Tree(pos); tree.sticks = parse_int(parts[1]); tree.vines = parse_int(parts[2]); tree.health = parse_int(parts[3]); tree.height = parse_int(parts[4]); tree.depleted = (parse_int(parts[5]) == 1); tree.is_chopped = (parse_int(parts[6]) == 1); tree.minutes_since_depletion = parse_int(parts[7]); tree.regen_timer.restart(); trees.insert_last(tree); } string[] snareData = get_string_list(saveData, "snares_data"); for (uint i = 0; i < snareData.length(); i++) { string[]@ parts = snareData[i].split("|"); if (parts.length() < 6) continue; int pos = parse_int(parts[0]); WorldSnare@ snare = WorldSnare(pos); snare.has_catch = (parse_int(parts[1]) == 1); snare.catch_type = parts[2]; snare.catch_chance = parse_int(parts[3]); snare.escape_chance = parse_int(parts[4]); snare.active = (parse_int(parts[5]) == 1); snare.minute_timer.restart(); world_snares.insert_last(snare); } string[] fireData = get_string_list(saveData, "fires_data"); for (uint i = 0; i < fireData.length(); i++) { string[]@ parts = fireData[i].split("|"); if (parts.length() < 3) continue; int pos = parse_int(parts[0]); WorldFire@ fire = WorldFire(pos); fire.fuel_remaining = parse_int(parts[1]); fire.low_fuel_warned = (parse_int(parts[2]) == 1); fire.fuel_timer.restart(); world_fires.insert_last(fire); } string[] firepitPositions = get_string_list(saveData, "firepits_positions"); for (uint i = 0; i < firepitPositions.length(); i++) { add_world_firepit(parse_int(firepitPositions[i])); } string[] herbPositions = get_string_list(saveData, "herb_gardens_positions"); for (uint i = 0; i < herbPositions.length(); i++) { add_world_herb_garden(parse_int(herbPositions[i])); } string[] streamData = get_string_list(saveData, "streams_data"); for (uint i = 0; i < streamData.length(); i++) { string[]@ parts = streamData[i].split("|"); if (parts.length() < 2) continue; int startPos = parse_int(parts[0]); int width = parse_int(parts[1]); if (width < 1) width = 1; add_world_stream(startPos, width); } update_ambience(true); return true; }