diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..186bd39 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,15 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: codefaux # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: # Replace with a single Polar username +buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +thanks_dev: # Replace with a single thanks.dev username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.gitignore b/.gitignore index 600d2d3..fbe68f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -.vscode \ No newline at end of file +persistence.pdn +persistence.code-workspace +files/inspect.lua +steam-update-persistence.sh diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f0a83f5 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Noita with debugger", + "type": "lua-local", + "request": "launch", + "program": { + "command": "C:/Abandon/Noita/noita_dev.exe", + "communication": "stdio" + }, + // "args": ["-debug_lua", "-no-console", "-luadebugger"], + "stopOnEntry": true, + "cwd": "C:/Abandon/Noita/", + "env": { + "LOCAL_LUA_DEBUGGER_VSCODE": "1", + "DESTINATION_PATH": "mods/noita-vscode-debugger" + }, + "verbose": true, + "pullBreakpointsSupport": true, + "scriptFiles": ["C:/Abandon/Noita/mods/noita-vscode-debugger/**/*.lua"], + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..916e5e5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,48 @@ +{ + "Lua.diagnostics.disable": [ + "lowercase-global" + ], + "Lua.diagnostics.globals": [ + "mod_dir", + "persistence_active", + "_nid", + "small_text_scale", + "gui", + "__layer", + "actions", + "actions_by_id", + "ACTION_TYPE_PROJECTILE", + "ACTION_TYPE_STATIC_PROJECTILE", + "Key_RETURN", + "ACTION_TYPE_MODIFIER", + "ACTION_TYPE_DRAW_MANY", + "ACTION_TYPE_MATERIAL", + "ACTION_TYPE_OTHER", + "ACTION_TYPE_UTILITY", + "ACTION_TYPE_PASSIVE", + "Key_LSHIFT", + "Key_GRAVE", + "Key_LCTRL", + "Mouse_wheel_down", + "Mouse_wheel_up", + "mod_setting_get_id", + "Key_ESCAPE", + "Key_TAB", + "reset_modifiers", + "ConfigGunShotEffects_Init", + "mod_setting_tooltip", + "mod_setting_handle_change_callback", + "AddGunActionPermanent", + "SetWandSprite", + "mod_setting_group_x_offset", + "MOD_SETTING_SCOPE_NEW_GAME", + "MOD_SETTING_SCOPE_RUNTIME", + "MOD_SETTING_SCOPE_RUNTIME_RESTART", + "mod_settings_get_version", + "mod_settings_update", + "mod_settings_gui", + "mod_settings_gui_count", + "loaded_profile_id", + "data_store_everyframe" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 59f9945..3a4117b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,67 @@ -# persistence -a mod for Noita which adds persistence +Add some Lite to your Rogue + +Credit to the original author; https://steamcommunity.com/sharedfiles/filedetails/?id=2144130266 +The mod has undergone a near full rewrite, but without their work this wouldn't have existed. +The original author wishes to thank the great modding community on the official Noita Discord server https://discord.com/invite/SZtrP2r. +Without them they would not been able to create this mod. +I consider Discord an ephemeral information source, which is wasteful of the human resources supporting it. I used the wiki, github, and mods (example and otherwise) to acquire my knowledge. +Informational efforts would be better suited on a wiki or other quasi-permanent platform than a user-required chat which by necessity fades to history and is fully lost. + +I'm happy to share any and all of my modding knowledge to anyone who asks. + + +Finally, my github repo can be found at https://github.com/codefaux/persistence + +Now, on with the details: + +Persistence has configurable options in the Mod Options menu. Browse this menu to tune difficulty to your tastes, but defaults are a good starting point. + +There are five player profile slots. (Slot 5 is only accessible via Mod Options menu) + +'Persistence areas' have been added. Persistence areas count as the player's spawn location and Holy Mountains. The Persistence features described below will only work inside a Persistence Area. + +'Amnesiac Mode' has been added. This makes Persistence forget all changes upon the death of your character, to give a less Rogue-Lite experience, or to allow expensive testing/experimentation. + +Money: +- An ingame money stash has been added. Each profile stores a separate money stash. +- Mod Options can lock the stash, only allow deposits, or allow deposits and withdrawals. +- Mod Options can assign a money payout from stash upon New Game, and upon reaching a Holy Mountain. +- Mod Options configure how much money is kept on death. Default is 25%. +- Persistence tracks your last known gold; polymorphed deaths still bank money properly. +- Transfers into stash or player exceeding a specific balance will be skipped. This is intentional, to avoid overflows. + +Spells: +- Spells can be researched using gold, to be permanently added to your profile's spell list. +- Researched spells can be purchased for gold. Partially used spells with limited uses cannot be researched. +- Spells can be recycled if not needed anymore. Recycled spells are simply deleted, there is no cost or benefit. +- Mod Options can be used to scale spell research and purchase costs. +- Spell 'Loadouts' can be stored from existing wands in your inventory, or cleared. Loadouts are shared across all profiles. +- Loadouts must be named. Names must be 1-32 characters. Names do not need to be unique. +- Stored Loadouts can be purchased. Purchased loadouts will fill your inventory. +- KNOWN ISSUE: Due to an in-game bug, it requires -extensive- checking to avoid spells overlapping occasionally in inventory. Occasionally, spells will overlap until moved. Nothing will be lost. +- KNOWN ISSUE: Due to limitations in string length, and how I must store spells, Spell Loadouts have a limited size which is difficult to predict. + +Wands: +- Wands can be researched, storing the best stats you've researched in your profile. +- Spells on a wand are destroyed when the wand is researched. Unresearched spells will not be automatically researched. +- Wands can be created using researched stats from your profile. +- Wands can be recycled if not needed. Recycled wands are simply deleted, there is no cost or benefit. +- Unique wands cannot be fully researched. Their Type (special shape, name) will not be usable. Their stats will. This is intentional. +- Wand Templates can be stored or updated during wand creation, or cleared. +- Wand Templates can be purchased. Purchased wands drop at your location. +- Wands created using Persistence can be modified. +- KNOWN ISSUE: Sometimes when researching/recycling/purchasing a wand, the Inventory screen will show spells on the wrong wand, or on nothing at all. Close and re-open the Inventory window. + + +A scanner runs while you explore the world. The scanner is a quality of life feature for Persistence, which can be disabled in Mod Options. +- If you're standing on or immediately beside a spell or wand in the world, Persistence will indicate if it is unresearched. +- Wands will indicate wether researched or not. +- Spells will indicate wether researched or not. +- Spells ON wands will only show if NOT researched. +- Wands which only provide a new Type (graphic) will indicate separately. + + + +Many thanks to everyone using the mod, and doubly so to anyone who reports an issue, suggests a feature, or just says thanks. +I'd like to thank people directly, but I don't want to violate anyone's privacy. +If you've made a suggestion I've implemented or reported an issue and would like credit, contact me and tell me how to address you. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..2516382 --- /dev/null +++ b/TODO.md @@ -0,0 +1,108 @@ +-- ============ +-- BACKBURNER +-- ============ +-- persistence tokens? +-- Wand Icons don't line up consistently (best-effort, needs better fix) +-- Mod order fixes? +-- function EntityGetInRadius(pos_x, pos_y, radius) to auto close menu? -- EntityGetHerdRelationSafe( entity_a:int, entity_b:int ) +-- Spell per-purchase cost ramp options; per-spell, per-copy +-- Mod setting for "allow research spells inside wands"? +-- Translation table for translation mods, translation helper Mod Options setting for tooltips indicating strings and meanings +-- Config option: Replace starter wand w/ templates 1, 2? +-- Apply loadouts to replacement wands? +-- Z order w/ blinking cursor, spell tooltip re: wand stats +-- stash overflows; split into two variables, store w/ data_store into big_money, bigger_money? + + +-- ===================== +-- BUGS / TOP PRIORITY +-- ===================== +-- Options to disable persistent wand stat memory, spell memory, allowing per-run "fresh profile" with stashed money +-- PRUNE DATASTORE/HELPER/WAND_SPELL_HELPER (second wave done) +-- Enable in parallel worlds -- look for controls_mouse and controls_wasd for new "lobby" area? +-- Flesh out and implement meta.lua rigorously +-- Sort order drop-down +-- Unresearched spells on wands not indicated in Research Wand menu + +-- ==================== +-- REQUESTS / IDEAS +-- ==================== +-- "Builds" - Match template to Loadout +-- check fit, can cast, etc +-- auto-load spells onto wand when purhcased +-- "Training" - Character stat buffs +-- PlayerEntity; +-- CharacterDataComponent +-- fly_time_max=3 -- adjusts as expected +-- flying_in_air_wait_frames==24 +-- CharacterPlatformingComponent +-- velocity_min x, y=-57, -200 +-- velocity_max x, y=57, 350 +-- fly_velocity_x = ? +-- fly_speed_max_up = ? +-- swim_drag = 0.95 +-- swim_extra_horizontal_drag = 0.9 +-- pixel_gravity = +-- DamageModelComponent +-- damage_multipliers +-- air_in_lungs_max +-- IngestionComponent +-- ingestion_capacity +-- ingestion_reduce_every_n_frame +-- KickComponent +-- kick_radius +-- player_kickforce +-- kick_damage +-- kick_knockback +-- LightComponent +-- radius +-- MaterialSuckerComponent +-- barrel_size +-- num_cells_sucked_per_frame + + +-- ==================== +-- DONE? NEEDS VERIFY +-- ==================== + + +-- ============== +-- KNOWN ISSUES +-- ============== +-- Spell overlap due to game item placement bug re: overfilled inventory. Worth fixing? Drop purchases on ground? +-- Inventory spells on wands visibly offset due to wand changes. Seems ingame bug? Unlock during manipulation? +-- Potential wand cost overlap in thoroughly broken gamestage. Clamp? +-- Too easy to research "ultimate tier wand." Weighted research? One stat at a time? + + +-- ============== +-- for archival +-- ============== +-- function extract_action_stats(action) +-- function draw_actions(_, _) end +-- function add_projectile(x) proc_projectiles(x); end +-- function add_projectile_trigger_hit_world(x, _) proc_projectiles(x); end +-- function add_projectile_trigger_timer(x, _, _) proc_projectiles(x); end +-- function add_projectile_trigger_death(x, _) proc_projectiles(x); end +-- function check_recursion(_, x) return x or 0; end +-- function move_discarded_to_deck() end +-- function order_deck() end +-- function StartReload() end +-- function EntityGetWithTag(_) return {} end +-- function GameGetFrameNum() return 5431289; end +-- function SetRandomSeed() end +-- function Random() return 1; end +-- function GetUpdatedEntityID() return 0; end +-- function EntityGetComponent(_) return {}; end +-- function EntityGetFirstComponent(_, _) return {}; end +-- function ComponentGetValue2(_) return 0; end +-- function EntityGetTransform(_) return {}; end +-- function EntityGetAllChildren(_) return {}; end +-- function EntityGetInRadiusWithTag(_, _) return {}; end +-- function GlobalsGetValue(_) return 0; end +-- function GlobalsSetValue(_) end +-- function find_the_wand_held() return nil; end +-- function EntityGetFirstComponentIncludingDisabled(_) end + +-- local biome = BiomeMapGetName( pos_x, pos_y ) +-- if ( string.find( biome, "holymountain" ) == nil ) and ( string.find( biome, "victoryroom" ) == nil ) then diff --git a/compatibility.xml b/compatibility.xml index 8f5619d..f92e75e 100644 --- a/compatibility.xml +++ b/compatibility.xml @@ -1,2 +1,2 @@ - + diff --git a/config.lua b/config.lua deleted file mode 100644 index 9039d47..0000000 --- a/config.lua +++ /dev/null @@ -1,31 +0,0 @@ -mod_config = { - money_to_keep_on_death = 0.25, - research_wand_price_multiplier = 1, - research_spell_price_multiplier = 10, - buy_wand_price_multiplier = 1, - buy_spell_price_multiplier = 1, - enable_edit_wands_in_lobby = true, - enable_teleport_back_up = true, - enable_menu_in_holy_mountain = false, - reuseable_holy_mountain = false, - spawn_location_as_lobby_location = false, -- false -> the lobby location will be relative to the mouse controlls stone; true -> the lobby location will be on the spawn location - always_choose_save_id = -1, -- -1 -> choose manualy; 0 -> choose to not use the mod; 1-5 -> choose saveslot - default_wands = { - { - name = "Handgun", - file = "data/items_gfx/handgun.png", - grip_x = 4, - grip_y = 4, - tip_x = 12, - tip_y = 4 - }, - { - name = "Bomb wand", - file = "data/items_gfx/bomb_wand.png", - grip_x = 4, - grip_y = 4, - tip_x = 12, - tip_y = 4 - } - } -}; \ No newline at end of file diff --git a/files/actions_by_id.lua b/files/actions_by_id.lua new file mode 100644 index 0000000..83badb0 --- /dev/null +++ b/files/actions_by_id.lua @@ -0,0 +1,573 @@ +---how to use for your own mod: add a credit to me (@codefaux on github) in your credits file or UI credits screen. drop this file into files\ and call it with dofile() -- NOT dofile_once() -- in init.lua OnWorldPostUpdate() + + +---only run function one time, but must init variable +if actions_by_id__init_done~=true then + ---global fill-in for missing core function + actions_by_id_loaded=false; + math.sign = math.sign or function (x) if x<0 then return -1; else return 1; end; end + + -- actions = actions or {}; + + ---variables and functions run/register/initialize only once + actions_by_id__notify_when_finished = true; + + ---kill file cache, force reload + __loaded = {}; + + dofile_once( "data/scripts/gun/gun.lua" ); + dofile_once( "data/scripts/lib/utilities.lua" ); + + ---init but don't overwrite public veriables + action_count = action_count or 0; + actions_by_id = actions_by_id or {}; + + ---returns nothing, directly acts on actions_by_id array + ---@param action_id string|table id of action to run OR entire action + function get_action_metadata(action_id) + if type(action_id)=="table" and action_id.id~=nil then action_id = action_id.id; end ---if passed action, switch to action_id string instead + if type(action_id)~="string" then return; end ---if still not string, exit + + if actions_by_id[action_id].metadata~=nil and actions_by_id[action_id].c~=nil then return; end ---ensure action has not been simulated prior + + reflecting = true; -- This is how we tell the game not to do the things, this redirects many of the action's calls to Reflection_RegisterProjectile() which allows us to extract data + metadata = {}; + + ---override Reflection_RegisterProjectile(xml) -in this context only- + Reflection_RegisterProjectile = function( projectile_xml ) + local skip_or_modify_hash = + { ---list of member names we don't care about, or need to explicitly change + config = true, + config_explosion = true, + damage_critical = true, + damage_by_type = true, + mStartingLifetime = true, + mTriggers = true, + do_moveto_update = true, + angular_velocity = true, + penetrate_world_velocity_coeff = true, + ground_penetration_coeff = true, + -- mInitialSpeed = true, + on_death_emit_particle = true, + ground_collision_fx = true, + die_on_low_velocity = true, + on_collision_spawn_entity = true, + penetrate_entities = true, + velocity_sets_scale = true, + go_through_this_material = true, + die_on_liquid_collision = true, + bounce_at_any_angle = true, + -- damage_melee = true, + -- damage_explosion = true, + mDamagedEntities = true, + on_death_emit_particle_count = true, + -- never_hit_player = true, + camera_shake_when_shot = true, + -- bounce_always = true, + velocity_sets_y_flip = true, + -- damage_physics_hit = true, + -- collide_with_world = true, + -- penetrate_world = true, + physics_impulse_coeff = true, + mShooterHerdId = true, + lifetime_randomness = true, + on_death_particle_check_concrete = true, + spawn_entity_is_projectile = true, + -- explosion_dont_damage_shooter = true, + -- damage_ice = true, + muzzle_flash_file = true, + shoot_light_flash_g = true, + -- damage_radioactive = true, + shoot_light_flash_b = true, + collide_with_entities = true, + damage_scale_max_speed = true, + ragdoll_fx_on_collision = true, + bounces_left = true, + lifetime = true, + velocity_updates_animation = true, + -- damage_curse = true, + damage_every_x_frames = true, + friction = true, + on_death_duplicate_remaining = true, + -- damage_fire = true, + -- damage_projectile = true, + shoot_light_flash_radius = true, + damage_overeating = true, + -- projectiles = true, + die_on_low_velocity_limit = true, + shell_casing_material = true, + lob_min = true, + on_collision_die = true, + -- damage_drill = true, + -- on_death_explode = true, + lob_max = true, + on_collision_remove_projectile = true, + -- friendly_fire = true, + -- damage_scaled_by_speed = true, + -- damage_healing = true, + spawn_entity = true, + velocity_sets_rotation = true, + on_death_item_pickable_radius = true, + on_lifetime_out_explode = true, + play_damage_sounds = true, + mWhoShotEntityTypeID = true, + -- projectile_type = true, + -- damage_poison = true, + damage_game_effect_entities = true, + attach_to_parent_trigger = true, + speed_min = true, + speed_max = true, + on_death_gfx_leave_sprite = true, + ground_penetration_max_durability_to_destroy = true, + mLastFrameDamaged = true, + shoot_light_flash_r = true, + shell_casing_offset = true, + -- bounce_energy = true, + collide_with_tag = true, + ragdoll_force_multiplier = true, + damage = true, + collide_with_shooter_frames = true, + -- knockback_force = true, + velocity_sets_scale_coeff = true, + dont_collide_with_tag = true, + hit_particle_force_multiplier = true, + create_shell_casing = true, + bounce_fx_file = true, + collect_materials_to_shooter = true, + blood_count_multiplier = true, + mEntityThatShot = true, + mWhoShot = true, + -- damage_electricity = true, + on_death_emit_particle_type = true, + -- damage_slice = true, + -- damage_holy = true, + -- direction_nonrandom_rad = true, + direction_random_rad = true + }; + -- local xml_entity_id = EntityLoadEndGameItem(projectile_xml, -2000000, -2000000); + + -- local xml_entity_id = EntityCreateNew(); ---create new entity (not using EntityLoad intentionally) + -- EntityApplyTransform(xml_entity_id, -20000, -20000); ---move new entity really far away + -- EntityLoadToEntity(projectile_xml, xml_entity_id); ---load projectile entity with EntityLoadToEntity because "Does not load tags and other stuff." is better for smooth gameplay + + -- if xml_entity_id~=nil and xml_entity_id~=0 then ---ensure loaded entity is 'valid' + -- local xml_component_pool = EntityGetAllComponents(xml_entity_id); ---gather components into pool for inspection + -- if xml_component_pool~=nil then ---if pool is valid + -- for _, xml_comp_id in pairs(xml_component_pool) do ---iterate pool for component ids + -- if xml_comp_id~=nil and xml_comp_id~=0 then ---ensure component is valid + -- EntitySetComponentIsEnabled(xml_entity_id, xml_comp_id, false); ---disable component for smoother gameplay + -- local xml_comp_name = ComponentGetTypeName(xml_comp_id); ---store component name, partially for reflection/debugging + -- if xml_comp_name=="ProjectileComponent" or xml_comp_name=="LightningComponent" or xml_comp_name=="ExplodeOnDamageComponent" or xml_comp_name=="ExplosionComponent" then ---only iterate on relevant components + -- local xml_comp_field_pool = ComponentGetMembers(xml_comp_id); ---gather components to pool for inspection + -- if xml_comp_field_pool~=nil then ---if pool is valid + -- metadata[projectile_xml] = metadata[projectile_xml] or {}; ---create empty table for incoming data if doesn't exist + -- metadata[projectile_xml][xml_comp_name] = metadata[projectile_xml][xml_comp_name] or {}; ---create empty table for incoming data if doesn't exist + -- for xml_comp_field, _ in pairs(xml_comp_field_pool) do ---iterate thru component members if they exist + -- if skip_or_modify_hash[xml_comp_field]~=true then ---only directly store members which aren't tagged + -- metadata[projectile_xml][xml_comp_name][xml_comp_field] = ComponentGetValue2(xml_comp_id, xml_comp_field); ---store member to structure + -- elseif xml_comp_field=="damage" then ---specific processing for "damage" + -- metadata[projectile_xml][xml_comp_name]["damage_basic"] = 25 * (ComponentGetValue2(xml_comp_id, xml_comp_field) or 0); + -- elseif xml_comp_field=="damage_by_type" then ---specific processing for "damage_by_type" + -- local dmg_type_pool = ComponentObjectGetMembers(xml_comp_id, xml_comp_field); ---gather damage_by_type members to pool for inspection + -- if dmg_type_pool~=nil then ---if pool is valid + -- for dmg_type, _ in pairs(dmg_type_pool) do ---break open damage_by_type table + -- metadata[projectile_xml][xml_comp_name]["damage_" .. dmg_type] = 25 * (ComponentObjectGetValue2(xml_comp_id, xml_comp_field, dmg_type) or 0); ---store in singles + -- end ---for dmg_type in damage_by_type + -- end--if damage_type_pool~-nil + -- elseif xml_comp_field=="config_explosion" then ---specific processing for "config_explosion" + -- metadata[projectile_xml][xml_comp_name]["damage_explosion"] = 25 * (ComponentObjectGetValue2(xml_comp_id, xml_comp_field, "damage") or 0); ---stored separately, standardise + -- metadata[projectile_xml][xml_comp_name]["explosion_radius"] = 25 * (ComponentObjectGetValue2(xml_comp_id, xml_comp_field, "explosion_radius") or 0); ---stored separately, standardise + -- elseif xml_comp_field=="mStartingLifetime" then ---specific processing for "mStartingLifetime" + -- metadata[projectile_xml][xml_comp_name]["lifetime"] = ComponentGetValue2(xml_comp_id, xml_comp_field); ---save with more typical name + -- elseif xml_comp_field=="direction_random_rad" then ---specific processing for "direction_random_rad" + -- metadata[projectile_xml][xml_comp_name]["spread_degrees"] = math.deg(ComponentGetValue2(xml_comp_id, xml_comp_field)); + -- elseif xml_comp_field=="speed_min" then ---specific processing for "speed_min" + -- local in_speed = ComponentGetValue2(xml_comp_id, xml_comp_field); ---read speed_min value + -- metadata[projectile_xml][xml_comp_name]["speed_min"] = in_speed; ---save speed_min value + -- metadata[projectile_xml][xml_comp_name]["speed"] = (metadata[projectile_xml][xml_comp_name]["speed"]==nil) and in_speed or ((metadata[projectile_xml][xml_comp_name]["speed"] + in_speed) / 2); ---if speed exists, average against it. Else, store speed_min as speed. + -- elseif xml_comp_field=="speed_max" then ---specific processing for "speed_max" + -- local in_speed = ComponentGetValue2(xml_comp_id, xml_comp_field); -- read speed_max value + -- metadata[projectile_xml][xml_comp_name]["speed_max"] = in_speed; -- save speed_max value + -- metadata[projectile_xml][xml_comp_name]["speed"] = (metadata[projectile_xml][xml_comp_name]["speed"]==nil) and in_speed or ((metadata[projectile_xml][xml_comp_name]["speed"] + in_speed) / 2); ---if speed exists, average against it. Else, store speed_max as speed. + -- end ---if skip_or_modify_hash[proj_member] block + -- end ---for proj_member in ComponentGetMembers(proj_comp); ---iterate thru component members if they exist + -- metadata[projectile_xml][xml_comp_name].projectiles = 1; ---start with one projectile + -- end ---if xml_comp_field_pool~=nil + -- end ---if xml_comp_name=={relevant values} + -- EntityRemoveComponent(xml_entity_id, xml_comp_id); ---remove the projectile component before we kill it to avoid issues + -- end ---if xml_comp_id!=nil|0 + -- end ---for xml_comp_id in xml_component_pool[] + -- end ---if xml_component_pool~=nil + -- EntityKill(xml_entity_id); ---kill the projectile entity since we're done with it + -- end ---if xml_entity_id~=nil|0 + end -- function override Reflection_RegisterProjectile(); + + ---KEEP for later stubbing if required; + -- function draw_actions(_, _) end + -- function add_projectile(x) Reflection_RegisterProjectile(x); end + -- function add_projectile_trigger_hit_world(x, _) Reflection_RegisterProjectile(x); end + -- function add_projectile_trigger_timer(x, _, _) Reflection_RegisterProjectile(x); end + -- function add_projectile_trigger_death(x, _) Reflection_RegisterProjectile(x); end + -- function check_recursion(_, x) return x or 0; end + -- function move_discarded_to_deck() end + -- function order_deck() end + -- function StartReload() end + -- -- function EntityGetWithTag(_) return {} end + -- function GameGetFrameNum() return 5431289; end + -- function SetRandomSeed() end + -- function Random() return 1; end + -- -- function GetUpdatedEntityID() return 0; end + -- -- function EntityGetComponent(_) return {}; end + -- -- function EntityGetFirstComponent(_, _) return {}; end + -- -- function ComponentGetValue2(_) return 0; end + -- -- function EntityGetTransform(_) return {}; end + -- -- function EntityGetAllChildren(_) return {}; end + -- -- function EntityGetInRadiusWithTag(_, _) return {}; end + -- -- function GlobalsGetValue(_) return 0; end + -- function GlobalsSetValue(_) end + -- function find_the_wand_held() return nil; end + -- -- function EntityGetFirstComponentIncludingDisabled(_) end + + ---strip values from c which we don't care about, modify others inline, return updated c table + ---@param in_c table incoming c table + ---@return table stripped c table + local function parse_c(in_c) + local skip_or_modify_hash = + { ---table of values to remove, modify here if add'l data required + -- pattern_degrees = true, + -- bounces = true, + -- action_spawn_level = true, + -- lightning_count = true, + state_destroyed_action = true, + -- action_ai_never_uses = true, + -- action_name = true, + -- recoil = true, + -- action_max_uses = true, + fire_rate_wait = true, + sprite = true, + -- explosion_damage_to_materials = true, + physics_impulse_coeff = true, + -- trail_material = true, + -- child_speed_multiplier = true, + -- action_draw_many_count = true, + -- damage_critical_chance = true, + gore_particles = true, + action_sprite_filename = true, + -- action_type = true, + game_effect_entities = true, + screenshake = true, + -- material = true, + extra_entities = true, + -- action_never_unlimited = true, + -- friendly_fire = true, + sound_loop_tag = true, + -- action_spawn_probability = true, + reload_time = true, + projectile_file = true, + state_shuffled = true, + -- explosion_radius = true, + custom_xml_file = true, + action_mana_drain = true, ---this value seems to be a lie + ragdoll_fx = true, + -- light = true, + -- action_is_dangerous_blast = true, + -- spread_degrees = true, + state_discarded_action = true, + action_unidentified_sprite_filename = true, + -- action_description = true, + -- speed_multiplier = true, + -- dampening = true, + -- damage_null_all = true, + -- knockback_force = true, + -- action_spawn_requires_flag = true, + -- blood_count_multiplier = true, + -- trail_material_amount = true, + -- damage_critical_multiplier = true, + state_cards_drawn = true, + action_spawn_manual_unlock = true, + -- material_amount = true, + -- action_id = true, + -- gravity = true, + -- lifetime_add = true, + -- damage_slice_add = true, + -- damage_ice_add = true, + -- damage_curse_add = true, + -- damage_healing_add = true, + -- damage_drill_add = true, + damage_fire_add = true, + damage_melee_add = true, + damage_electricity_add = true, + damage_explosion_add = true, + damage_projectile_add = true, + direction_random_rad = true, + }; + local out_c = {}; ---create out_c table + for membername, _ in pairs(in_c) do ---iterate through members in incoming table + if skip_or_modify_hash[membername]~=true then ---check against skip_hash to only store desired info + out_c[membername] = in_c[membername]; ---copy member to output table + elseif membername=="damage_electricity_add" or ---specific processing for "damage_[...]_add" + membername=="damage_melee_add" or + membername=="damage_explosion_add" or + membername=="damage_projectile_add" or + membername=="damage_fire_add" then + out_c[membername] = in_c[membername] * 25; ---engine shows value*25 for damages/health + elseif membername=="reload_time" or ---specific processing for "reload_time" | "fire_rate_wait" + membername=="fire_rate_wait" then + out_c[membername] = in_c[membername] / 60; ---engine shows times in 60th of second + elseif membername=="direction_random_rad" then ---specific processing for "direction_random_rad" + out_c["spread_degrees"] = math.deg(in_c[membername]); ---engine stores angles in radians + end ---if skip_hash[membername]; + end ---for membername in in_ic + return out_c; ---return data table + end ---function strip_c(in_c); ---returns stripped c structure + + -- local _draw_actions = draw_actions; + local draws = 0; -- start at 0 additional draws + draw_actions = function( x ) draws = draws + x; end -- another local override for action() to count draw_action calls + + local _c = c; -- capture the global c context + c = {}; -- clear c context so we only get one action() + shot_effects = {}; -- clear shot_effects context so we only get one action() + current_reload_time = 0; -- clear current_reload_time so we only get one action() + reset_modifiers( c ); -- prepare c table structure and initialize for relative operations + ConfigGunShotEffects_Init( shot_effects ); -- prepare shot_effects table structure and initialize for relative operations + actions_by_id[action_id].action(); -- call the action() function + actions_by_id[action_id] = actions_by_id[action_id] or {}; -- new table if not already present + actions_by_id[action_id].c = parse_c(c); -- strip c before storing its data + actions_by_id[action_id].c.draw_actions = draws; -- add a few flags + actions_by_id[action_id].c.reload_time = current_reload_time; + actions_by_id[action_id].c.recoil_knockback = shot_effects.recoil_knockback; + actions_by_id[action_id].metadata = metadata; + c = _c; -- restore the global c context + reflecting = false; -- Return to normal. Likely not necessary but.... + end -- function get_action_metadata(action_id) + + ---intended to be run when new actions are found, typically by code below -- feeds information directly into actions_by_id + ---@param max_new_actions_this_pass number + function collect_action_data(max_new_actions_this_pass) + local target_cnt = action_count + max_new_actions_this_pass; ---track how many actions we're targeting as our max + local player = EntityGetWithTag("player_unit")[1]; + + if player then EntityRemoveTag(player, "player_unit"); end ---remove player_unit tag, this protects us from some actions + for _,curr_action in pairs(actions) do ---iterate thru actions until finished or stopped + if actions_by_id[curr_action.id]==nil and curr_action.id~=nil then ---only process non-nil entries + action_count = (action_count or 0) + 1; ---add to the action count, start at zero if un-initialized + actions_by_id[curr_action.id] = curr_action; ---store current action basic data + get_action_metadata(curr_action.id); ---process action to add metadata + end ---if actions_by_id[curr_action.id]==nil and curr_action.id~=nil; + if action_count >= target_cnt then break; end + end ---for curr_action in actions; + if player then EntityAddTag(player, "player_unit"); end ---re-add player_unit tag to stored player entity + end -- function collect_action_data(max_new_actions_this_pass) + + ---debugging function + function table_dump(o) + if type(o) == 'table' then + local s = '{ ' + for k,v in pairs(o) do + if type(k) ~= 'number' then k = '"'..k..'"' end + s = s .. '['..k..'] = ' .. table_dump(v) .. ',' + end + return s .. '} ' + else + return tostring(o) + end + end ---function table_dump(0); + + ---convert numeric action type to translatable string + ---@param action_type number + ---@return string + function action_type_to_string(action_type) + if action_type == ACTION_TYPE_PROJECTILE then + return "$inventory_actiontype_projectile"; + end + if action_type == ACTION_TYPE_STATIC_PROJECTILE then + return "$inventory_actiontype_staticprojectile"; + end + if action_type == ACTION_TYPE_MODIFIER then + return "$inventory_actiontype_modifier"; + end + if action_type == ACTION_TYPE_DRAW_MANY then + return "$inventory_actiontype_drawmany"; + end + if action_type == ACTION_TYPE_MATERIAL then + return "$inventory_actiontype_material"; + end + if action_type == ACTION_TYPE_OTHER then + return "$inventory_actiontype_other"; + end + if action_type == ACTION_TYPE_UTILITY then + return "$inventory_actiontype_utility"; + end + if action_type == ACTION_TYPE_PASSIVE then + return "$inventory_actiontype_passive"; + end + return ""; + end + + ---convert numeric action type to slot sprite + ---@param action_type number + ---@return string + function action_type_to_slot_sprite(action_type) + if action_type == ACTION_TYPE_DRAW_MANY then + return "data/ui_gfx/inventory/item_bg_draw_many.png"; + end + if action_type == ACTION_TYPE_MATERIAL then + return "data/ui_gfx/inventory/item_bg_material.png"; + end + if action_type == ACTION_TYPE_MODIFIER then + return "data/ui_gfx/inventory/item_bg_modifier.png"; + end + if action_type == ACTION_TYPE_OTHER then + return "data/ui_gfx/inventory/item_bg_other.png"; + end + if action_type == ACTION_TYPE_PASSIVE then + return "data/ui_gfx/inventory/item_bg_passive.png"; + end + if action_type == ACTION_TYPE_PROJECTILE then + return "data/ui_gfx/inventory/item_bg_projectile.png"; + end + if action_type == ACTION_TYPE_STATIC_PROJECTILE then + return "data/ui_gfx/inventory/item_bg_static_projectile.png"; + end + if action_type == ACTION_TYPE_UTILITY then + return "data/ui_gfx/inventory/item_bg_utility.png"; + end + return "data/ui_gfx/inventory/hover_info_empty_slot.png"; + end + + ---these are for column 3, show_condition + __show_always = function(_) return true; end ---always show datum + __show_nz = function(value) return (value~=nil and value~=0) and true or false; end --show non-zero datum + __show_gz = function(value) return (value~=nil and value>0) and true or false; end --show greater than zero datum + __show_many = function(value) return (value~=nil and value>1) and true or false; end --show daum more than one + + __val = function(value) return value; end ---return value unmodified + __trans = function(value) return GameTextGetTranslatedOrNot(value); end ---pass value to game translation table + __yesno = function(value) return value and "$menu_yes" or "$menu_no"; end ---treat value as boolean, return translatable yes/no + __time = function(value) return GameTextGet("$inventory_seconds", string.format("%1.2f", value)); end ---format value as locale-translated time + __deg = function(value) return GameTextGet("$inventory_degrees", string.format("%d", value)); end ---format value as locale-translated "degrees" + __pct = function(value) return GameTextGet("$menu_slider_percentage", value); end ---format value as locale-translated "percentage" + __round = function(value) return math.sign(value) * math.floor(math.abs(value) + 0.49999999999999994); end ---round value via math shortcut, supporting float impercision + __type = function(value) return action_type_to_string(value); end ---return action type as string, ie "Static Projectile" + + ---TODO: Learn how to properly annotate this + local spell_tooltip_stats_format = + { ---membername_pattern = { icon, gametext, show_cond(value), show_value_gametext(value), } -- expand w/ "is_extra" for "extra-data" tooltips/sorting/etc + name = { nil, nil, __show_always, __trans }, + description = { nil, nil, __show_always, __trans }, + sprite = { __val, nil, __show_always, nil }, + type = { "data/ui_gfx/inventory/icon_action_type.png", "$inventory_actiontype", __show_always, __type }, + draw_actions = { "data/ui_gfx/inventory/icon_gun_actions_per_round.png", "$inventory_actiontype_drawmany", __show_many, __val }, + max_uses = { "data/ui_gfx/inventory/icon_action_max_uses.png", "$inventory_usesremaining", __show_nz, __val }, + mana = { "data/ui_gfx/inventory/icon_mana_drain.png", "$inventory_manadrain", __show_nz, __val }, + fire_rate_wait = { "data/ui_gfx/inventory/icon_gun_reload_time.png", "$inventory_castdelay", __show_always, __time }, + reload_time = { "data/ui_gfx/inventory/icon_reload_time.png", "$inventory_rechargetime", __show_nz, __time }, + damage_projectile_add = { "data/ui_gfx/inventory/icon_damage_projectile.png", "$inventory_damage", __show_nz, __round }, + damage_basic = { "data/ui_gfx/inventory/icon_damage_projectile.png", "$inventory_mod_damage", __show_nz, __round }, + damage_slice = { "data/ui_gfx/inventory/icon_damage_slice.png", "$inventory_dmg_slice", __show_nz, __round }, + damage_melee_add = { "data/ui_gfx/inventory/icon_damage_melee.png", "$inventory_dmg_melee", __show_nz, __round }, + damage_melee = { "data/ui_gfx/inventory/icon_damage_melee.png", "$inventory_dmg_melee", __show_nz, __round }, + damage_electricity_add = { "data/ui_gfx/inventory/icon_damage_electricity.png", "$inventory_mod_damage_electric", __show_nz, __round }, + damage_electricity = { "data/ui_gfx/inventory/icon_damage_electricity.png", "$inventory_mod_damage_electric", __show_nz, __round }, + damage_fire_add = { "data/ui_gfx/inventory/icon_damage_fire.png", "$inventory_dmg_fire", __show_nz, __round }, + damage_fire = { "data/ui_gfx/inventory/icon_damage_fire.png", "$inventory_dmg_fire", __show_nz, __round }, + damage_explosion_add = { "data/ui_gfx/inventory/icon_damage_explosion.png", "$inventory_dmg_explosion", __show_nz, __round }, + explosion_radius = { "data/ui_gfx/inventory/icon_explosion_radius.png", "$inventory_explosion_radius", __show_nz, __val }, + damage_explosion = { "data/ui_gfx/inventory/icon_damage_explosion.png", "$inventory_dmg_explosion", __show_nz, __round }, + damage_curse = { "data/ui_gfx/inventory/icon_damage_curse.png", "$inventory_dmg_curse", __show_nz, __round }, + damage_ice = { "data/ui_gfx/inventory/icon_damage_ice.png" , "$inventory_dmg_ice", __show_nz, __round }, + damage_drill = { "data/ui_gfx/inventory/icon_damage_drill.png", "$inventory_dmg_drill", __show_nz, __round }, + damage_poison = { "data/ui_gfx/inventory/icon_damage_curse.png", "$inventory_dmg_poison", __show_nz, __round }, + damage_healing = { "data/ui_gfx/inventory/icon_damage_healing.png", "$inventory_dmg_healing", __show_nz, __round }, + damage_radioactive = { "data/ui_gfx/inventory/icon_damage_curse.png", "$inventory_dmg_radioactive", __show_nz, __round }, + -- speed_multiplier = { "data/ui_gfx/inventory/icon_speed_multiplier.png", "$inventory_speed", __show_many, __val }, + speed = { "data/ui_gfx/inventory/icon_speed_multiplier.png", "$inventory_speed", __show_nz, __round }, + damage_critical_chance = { "data/ui_gfx/inventory/icon_damage_critical_chance.png", "$inventory_mod_critchance", __show_nz, __pct }, + projectiles = { "data/ui_gfx/inventory/icon_gun_actions_per_round.png", "$inventory_type_projectile", __show_many, __val }, + spread_degrees = { "data/ui_gfx/inventory/icon_spread_degrees.png", "$inventory_spread", __show_nz, __deg }, + -- speed_min = { "data/ui_gfx/inventory/icon_speed_multiplier.png", "$inventory_speed", __show_nz, __round }, + -- speed_max = { "data/ui_gfx/inventory/icon_speed_multiplier.png", "$inventory_speed", __show_nz, __round }, + } + + ---return formatted action structure for parsing in gui routines + ---@param in_action table|string action or action_id to be processed + ---@return table? + function get_action_struct(in_action) + if type(in_action)=="string" and actions_by_id[in_action]~=nil then in_action = actions_by_id[in_action]; end ---if passed action, switch to action_id string instead + if type(in_action)~="table" then return {}; end ---if still not string, exit + + if actions_by_id[in_action]~=nil and actions_by_id[in_action].metadata~=nil and actions_by_id[in_action].c~=nil then return nil; end ---ensure action has not been simulated prior + + get_action_metadata(in_action.id); + + local struct_data = {}; + local struct_index = 0; + local source_pool = {in_action, in_action.c }; + for _, projectile_component_pool in pairs(in_action.metadata) do + for _, projectile_component_member_pool in pairs(projectile_component_pool) do + source_pool[#source_pool+1] = projectile_component_member_pool; + end + end + + for _, member_pool in ipairs(source_pool) do + for member_name, member_value in pairs(member_pool) do + for datum_name, datum_formatting in pairs(spell_tooltip_stats_format) do + if member_name==datum_name and (datum_formatting[3](member_value)==true) then + local thismember_data = {} + if datum_formatting[1]~=nil then + if type(datum_formatting[1])=="function" then + thismember_data.icon = datum_formatting[1](member_value); + else + thismember_data.icon = datum_formatting[1]; + thismember_data.label = GameTextGetTranslatedOrNot(datum_formatting[2]); + end + end + if datum_formatting[4]~=nil then + thismember_data.value = datum_formatting[4](member_value); + end + if thismember_data["icon"]~=nil or thismember_data["value"]~=nil then + struct_index = struct_index + 1; + thismember_data.name = member_name; + struct_data[struct_index] = thismember_data; + end + end + end + end + end + return struct_data; + end + + function debug_print_action(debug_action) + local action_struct_pool = get_action_struct(debug_action); + if action_struct_pool==nil then return; end + + for action_member, action_struct in pairs(action_struct_pool) do + print("member: " .. action_member); + print("- icon: " .. (action_struct.icon or " ")); + print("- label: " .. (action_struct.label or " ")); + print("- value: " .. (action_struct.value or " ")); + print(" "); + end + end -- function debug_print_action(); + + actions_by_id__init_done = true; +end -- if initialized + +---intended to be run every world update w/ minimal impact +if action_count<#actions then + print("actions_by_id: capturing new actions, group " .. action_count .. " to (at most) " .. action_count + 100); + collect_action_data(100); + actions_by_id__notify_when_finished = true; + if action_count==#actions then actions_by_id_loaded = true; end +elseif actions_by_id__notify_when_finished==true then + actions_by_id__notify_when_finished = false; + actions_by_id_loaded = true; + print("actions_by_id: scan done, storing " .. action_count .. " actions") + local debug_action = actions_by_id["RUBBER_BALL"]; + + print("table = " .. table_dump(debug_action)); + print("action: " .. GameTextGetTranslatedOrNot(debug_action.name)); + + -- debug_print_action(debug_action); +end diff --git a/files/cost.lua b/files/cost.lua new file mode 100644 index 0000000..0c7a4bc --- /dev/null +++ b/files/cost.lua @@ -0,0 +1,102 @@ + + +function __cost_func_wand_type(_) + return 200; +end + +function __cost_func_shuffle(_shuffle) + return _shuffle and 0 or 100; +end + +function __cost_func_spells_per_cast(a_p_c) + return math.ceil(math.max(a_p_c-1,0)*500); +end + +function __cost_func_cast_delay(_castdelay) + return math.ceil((0.01 ^ ((_castdelay/60) - 1.8) + 200) * 0.1); +end + +function __cost_func_recharge_time(_rechargetime) + return math.ceil((0.01 ^ ((_rechargetime/60) - 1.8) + 200) * 0.1); +end + +function __cost_func_mana_max(_manamax) + return math.ceil(_manamax); +end + +function __cost_func_mana_charge_speed(_manachargespeed) + return math.ceil(_manachargespeed * 2); +end + +function __cost_func_capacity(_capacity) + return math.ceil((math.max(_capacity - 1, 0)) * 50); +end + +function __cost_func_spread(_spread) + return math.ceil(math.abs(10 - _spread) * 5); +end + +function __cost_func_always_cast_spells(_alwayscasts) + local _val = 0; for _, _a_c_id in ipairs(_alwayscasts) do if (_a_c_id~=nil and actions_by_id[_a_c_id]~=nil and actions_by_id[_a_c_id].price~=nil) then _val = _val + __get_ac_raw_cost(_a_c_id); end; end; return math.ceil(_val); +end + +function __cost_func_always_cast_spell(_alwayscast) + return math.ceil(__get_ac_raw_cost(_alwayscast)); +end + +function __cost_func_always_cast_count(_alwayscasts) + return math.ceil((_alwayscasts ^ 2) * 100); +end + +function __get_ac_raw_cost(_a_c_id) + if _a_c_id==nil then return 0; end + return math.ceil(actions_by_id[_a_c_id].price * 5); +end + +function fill_wand_stat_cost(wand_data) + wand_data.cost = {}; + wand_data.cost.wand_type = math.ceil(__cost_func_wand_type(wand_data["wand_type"]) * mod_setting.buy_wand_price_multiplier); + wand_data.cost.shuffle = math.ceil(__cost_func_shuffle(wand_data["shuffle"]) * mod_setting.buy_wand_price_multiplier); + wand_data.cost.spells_per_cast = math.ceil(__cost_func_spells_per_cast(wand_data["spells_per_cast"]) * mod_setting.buy_wand_price_multiplier); + wand_data.cost.cast_delay = math.ceil(__cost_func_cast_delay(wand_data["cast_delay"]) * mod_setting.buy_wand_price_multiplier); + wand_data.cost.recharge_time = math.ceil(__cost_func_recharge_time(wand_data["recharge_time"]) * mod_setting.buy_wand_price_multiplier); + wand_data.cost.mana_max = math.ceil(__cost_func_mana_max(wand_data["mana_max"]) * mod_setting.buy_wand_price_multiplier); + wand_data.cost.mana_charge_speed = math.ceil(__cost_func_mana_charge_speed(wand_data["mana_charge_speed"]) * mod_setting.buy_wand_price_multiplier); + wand_data.cost.capacity = math.ceil(__cost_func_capacity(wand_data["capacity"]) * mod_setting.buy_wand_price_multiplier); + wand_data.cost.spread = math.ceil(__cost_func_spread(wand_data["spread"]) * mod_setting.buy_wand_price_multiplier); + wand_data.cost.always_cast_count = math.ceil(__cost_func_always_cast_count(wand_data["always_cast_count"]) * mod_setting.buy_wand_price_multiplier); + local _sum = 0; + for _, _cost in pairs(wand_data["cost"]) do + _sum = _sum + _cost; + end + wand_data.cost.stat_sum = _sum; +end + +function get_wand_buy_price(wand_data) + local price = 0; + fill_wand_stat_cost(wand_data); + price = wand_data.cost.stat_sum; + price = price + math.ceil(__cost_func_always_cast_spells(wand_data["always_cast_spells"]) * mod_setting.buy_spell_price_multiplier); + + return math.ceil(price); +end + +function get_spell_purchase_price(action_id) + if action_id==nil or type(action_id)~="string" then return nil; end + + return math.ceil(actions_by_id[action_id].price * mod_setting.buy_spell_price_multiplier); +end + +function get_spell_research_price(action_id) + if action_id==nil or type(action_id)~="string" then return nil; end + + return math.ceil(actions_by_id[action_id].price * mod_setting.research_spell_price_multiplier); +end + +function get_spell_entity_research_price(entity_id) + local action_id = get_spell_entity_action_id(entity_id); + if action_id == nil then + return nil; + end + return get_spell_research_price(action_id); +end diff --git a/files/data_store.lua b/files/data_store.lua index f895877..9c313ee 100644 --- a/files/data_store.lua +++ b/files/data_store.lua @@ -1,840 +1,925 @@ -dofile_once("mods/persistence/config.lua"); -dofile_once("data/scripts/gun/gun_actions.lua"); -dofile_once("data/scripts/gun/procedural/wands.lua"); -dofile_once("mods/persistence/files/wand_spell_helper.lua"); - -spells_per_cast_min = 0; -mana_max_min = 0; -mana_charge_speed_min = 0; -capacity_min = 0; - -local data_store = {}; -local flag_prefix = "persistence"; -local selected_save_id; - -function get_save_count() - return 5; -end - -function get_template_count() - return 5; -end - -local function number_to_hex(number) - if number == nil then - return nil; - end - local positive = math.abs(number); - return (positive == number and "" or "-") .. string.format("%x", positive); -end - -local function hex_to_number(hex) - if hex == nil then - return nil; - end - if string.sub(hex, 1, 1) == "-" then - return tonumber(string.sub(hex, 2), 16) * -1; - else - return tonumber(hex, 16); - end -end - -local hex_chars = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "-" } -local function save_hex(name, hex) - if hex == nil then - for j = 1, #hex_chars do - RemoveFlagPersistent(flag_prefix .. "_" .. name .. "_" .. 1 .. "_" .. hex_chars[j]); - end - return; - end - for i = 1, #hex do - for j = 1, #hex_chars do - RemoveFlagPersistent(flag_prefix .. "_" .. name .. "_" .. i .. "_" .. hex_chars[j]); - end - AddFlagPersistent(flag_prefix .. "_" .. name .. "_" .. i .. "_" .. string.sub(hex, i, i)); - end - for j = 1, #hex_chars do - RemoveFlagPersistent(flag_prefix .. "_" .. name .. "_" .. #hex + 1 .. "_" .. hex_chars[j]); - end -end - -local function load_hex(name) - local output = ""; - local i = 1; - repeat - local hex_found = false; - for j = 1, #hex_chars do - if HasFlagPersistent(flag_prefix .. "_" .. name .. "_" .. i .. "_" .. hex_chars[j]) then - output = output .. hex_chars[j]; - hex_found = true; - break; - end - end - i = i + 1; - until not hex_found - return (output == "" and nil or output); -end - -function load_save_ids() - for i = 1, get_save_count() do - if HasFlagPersistent(flag_prefix .. "_" .. tostring(i)) then - if data_store[i] == nil then - data_store[i] = {}; - end - end - end - return get_save_ids(); -end - -function set_run_created_with_mod() - GameAddFlagRun(flag_prefix .. "_using_mod"); -end - -function get_run_created_with_mod() - return GameHasFlagRun(flag_prefix .. "_using_mod"); -end - -function get_save_ids() - local output = {}; - for i = 1, get_save_count() do - if data_store[i] ~= nil then - output[i] = true; - end - end - return output; -end - -function get_selected_save_id() - if selected_save_id == nil then - for i = 0, get_save_count() do - if GameHasFlagRun(flag_prefix .. "_selected_save_" .. tostring(i)) then - selected_save_id = i; - return i; - end - end - return nil; - else - return selected_save_id; - end -end - -function set_selected_save_id(id) - for i = 0, get_save_count() do - GameRemoveFlagRun(flag_prefix .. "_selected_save_" .. tostring(i)); - end - GameAddFlagRun(flag_prefix .. "_selected_save_" .. tostring(id)); -end - -function create_new_save(save_id) - delete_save(save_id); - load(save_id); - AddFlagPersistent(flag_prefix .. "_" .. tostring(save_id)); -end - -function delete_save(save_id) - local save_id_string = tostring(save_id); - - save_hex(save_id_string .. "_spells_per_cast", nil); - save_hex(save_id_string .. "_cast_delay_min", nil); - save_hex(save_id_string .. "_cast_delay_max", nil); - save_hex(save_id_string .. "_recharge_time_min", nil); - save_hex(save_id_string .. "_recharge_time_max", nil); - save_hex(save_id_string .. "_mana_max", nil); - save_hex(save_id_string .. "_capacity", nil); - save_hex(save_id_string .. "_spread_min", nil); - save_hex(save_id_string .. "_spread_max", nil); - save_hex(save_id_string .. "_money", nil); - for i = 1, #actions do - RemoveFlagPersistent(flag_prefix .. "_" .. save_id_string .. "_spell_" .. string.lower(actions[i].id)); - RemoveFlagPersistent(flag_prefix .. "_" .. save_id_string .. "_always_cast_spell_" .. string.lower(actions[i].id)); - end - for i = 1, #wands do - RemoveFlagPersistent(flag_prefix .. "_" .. save_id_string .. "_wand_type_" .. sprite_file_to_wand_type(wands[i].file)); - end - - for i = 1, get_template_count() do - delete_template(save_id, i); - end - - RemoveFlagPersistent(flag_prefix .. "_" .. save_id_string); - data_store[save_id] = nil; -end - -function get_player_money() - local money = tonumber(ComponentGetValue(get_wallet(), "money")); - return money == nil and 0 or money; -end - -function set_player_money(value) - ComponentSetValue(get_wallet(), "money", value); -end - -function load(save_id) - local save_id_string = tostring(save_id); - - data_store[save_id] = {}; - data_store[save_id]["spells_per_cast"] = hex_to_number(load_hex(save_id_string .. "_spells_per_cast")); - data_store[save_id]["cast_delay_min"] = hex_to_number(load_hex(save_id_string .. "_cast_delay_min")); - data_store[save_id]["cast_delay_max"] = hex_to_number(load_hex(save_id_string .. "_cast_delay_max")); - data_store[save_id]["recharge_time_min"] = hex_to_number(load_hex(save_id_string .. "_recharge_time_min")); - data_store[save_id]["recharge_time_max"] = hex_to_number(load_hex(save_id_string .. "_recharge_time_max")); - data_store[save_id]["mana_max"] = hex_to_number(load_hex(save_id_string .. "_mana_max")); - data_store[save_id]["mana_charge_speed"] = hex_to_number(load_hex(save_id_string .. "_mana_charge_speed")); - data_store[save_id]["capacity"] = hex_to_number(load_hex(save_id_string .. "_capacity")); - local spread_min = hex_to_number(load_hex(save_id_string .. "_spread_min")); - if spread_min ~= nil then - spread_min = spread_min / 10; - end - data_store[save_id]["spread_min"] = spread_min; - local spread_max = hex_to_number(load_hex(save_id_string .. "_spread_max")); - if spread_max ~= nil then - spread_max = spread_max / 10; - end - data_store[save_id]["spread_max"] = spread_max; - data_store[save_id]["money"] = hex_to_number(load_hex(save_id_string .. "_money")); - - data_store[save_id]["spells"] = {}; - data_store[save_id]["always_cast_spells"] = {}; - for i = 1, #actions do - if HasFlagPersistent(flag_prefix .. "_" .. save_id_string .. "_spell_" .. string.lower(actions[i].id)) then - data_store[save_id]["spells"][actions[i].id] = true; - end - if HasFlagPersistent(flag_prefix .. "_" .. save_id_string .. "_always_cast_spell_" .. string.lower(actions[i].id)) then - data_store[save_id]["always_cast_spells"][actions[i].id] = true; - end - end - - data_store[save_id]["wand_types"] = {}; - for i = 1, #mod_config.default_wands do - data_store[save_id]["wand_types"]["default_" .. tostring(i)] = true; - end - for i = 1, #wands do - local wand_type = sprite_file_to_wand_type(wands[i].file); - if HasFlagPersistent(flag_prefix .. "_" .. save_id_string .. "_wand_type_" .. string.lower(wand_type)) then - data_store[save_id]["wand_types"][wand_type] = true; - end - end - - data_store[save_id]["templates"] = {}; - for i = 1, get_template_count() do - if HasFlagPersistent(flag_prefix .. "_" .. save_id_string .. "_template_" .. tostring(i)) then - data_store[save_id]["templates"][i] = {}; - if HasFlagPersistent(flag_prefix .. "_" .. save_id_string .. "_template_" .. tostring(i) .. "_shuffle") then - data_store[save_id]["templates"][i]["shuffle"] = true; - else - data_store[save_id]["templates"][i]["shuffle"] = false; +if persistence_data_store_loaded~=true then + -- On load only + wands=wands or {}; + + dofile_once(mod_dir .. "files/helper.lua"); + dofile_once(mod_dir .. "files/encoder.lua") + + local data_store = {}; + + ---@type integer + default_profile_id = tonumber(mod_setting.always_choose_save_id) or 0; + ---@type integer + selected_profile_id = 0; + ---@type integer + DISABLE_PROFILE_ID = -999; + ---@type integer + loaded_profile_id = 0; + + function get_profile_count() return 4; end + function get_template_count() return 5; end + + + ---- ================= + ---- PRIVATE FUNCTIONS + ---- ================= + + + local function _do_startup_paycheck_check() + if not GameHasFlagRun("persistence_startup_paid") then + GameAddFlagRun("persistence_startup_paid"); + if mod_setting.start_with_money>0 then + ---@diagnostic disable-next-line: param-type-mismatch + local _withdraw = math.min(mod_setting.start_with_money, get_stash_money()); + GamePrint(string.format("Persistence: Run start, $ %i from Stash", _withdraw)); + transfer_money_stash_to_player(_withdraw); + end + end + end + + local function _do_holy_mountain_paycheck_check() + if mod_setting.holy_mountain_money==0 and mod_setting.holy_mountain_reward==0 then return; end + + local _workshop_e_id = tonumber(GlobalsGetValue("workshop_e_id", "0")) or 0; + if not EntityHasTag(_workshop_e_id, "persistence_visited") then return; end + if not EntityHasTag(_workshop_e_id, "persistence_unpaid") then return; end + if EntityHasTag(_workshop_e_id, "persistence_paid") then return; end + + ---@diagnostic disable-next-line: param-type-mismatch + local _withdraw = math.min(get_stash_money(), mod_setting.holy_mountain_money); + if _withdraw > 0 then + GamePrint(string.format("Persistence: Holy Mountain paycheck, $ %i from Stash", _withdraw)); + transfer_money_stash_to_player(_withdraw); + end + + local _reward = mod_setting.holy_mountain_reward; + if _reward > 0 then + GamePrint(string.format("Persistence: Holy Mountain paycheck, $ %i", _reward)); + increment_player_money(_reward); + end + + local _ent_x, _ent_y = EntityGetTransform(_workshop_e_id); + local _workshops_here = EntityGetInRadiusWithTag(_ent_x, _ent_y, 500, "persistence_workshop"); + for _, _test_e_id in ipairs(_workshops_here) do + EntityRemoveTag(_test_e_id, "persistence_unpaid"); + EntityAddTag(_test_e_id, "persistence_paid"); + EntityAddTag(_test_e_id, "persistence_visited"); + end + end + + + ---return wand bounds for profile + ---@param profile_id integer + ---@return wand_bounds_data + local function _get_wand_bounds(profile_id) + return { + wand_types = data_store[profile_id]["wand_types"], + always_casts = data_store[profile_id]["always_cast_spells"], + always_cast_count = data_store[profile_id]["always_cast_count"], + spells_per_cast = { 1, + data_store[profile_id]["spells_per_cast"] or 1 }, + mana_max = { 1, + data_store[profile_id]["mana_max"] or 50}, + mana_charge_speed = { 1, + data_store[profile_id]["mana_charge_speed"] or 10}, + capacity = { 1, + data_store[profile_id]["capacity"] or 1}, + cast_delay = { data_store[profile_id]["cast_delay_min"]==nil and 15 or data_store[profile_id]["cast_delay_min"], + data_store[profile_id]["cast_delay_max"]==nil and 15 or data_store[profile_id]["cast_delay_max"]}, + recharge_time = { data_store[profile_id]["recharge_time_min"]==nil and 15 or data_store[profile_id]["recharge_time_min"], + data_store[profile_id]["recharge_time_max"]==nil and 15 or data_store[profile_id]["recharge_time_max"]}, + spread = { data_store[profile_id]["spread_min"]==nil and 5 or data_store[profile_id]["spread_min"], + data_store[profile_id]["spread_max"]==nil and 5 or data_store[profile_id]["spread_max"]} + }; + end + + ---return in-memory {spell_id = true, ... }, spells_known for profile + ---@param profile_id integer + ---@return table spells {spell_id = true, ...} + ---@return integer count known spells + local function _get_profile_spells(profile_id) + return data_store[profile_id]["spells"], data_store[profile_id]["spells_known"]~=nil and data_store[profile_id]["spells_known"] or 0; + end + + ---update quantity of known spells, saved to datastore and disk + ---@param profile_id integer + ---@return integer count known spells + local function _update_spells_known_count(profile_id) + local spells_known = 0; + for _, known in pairs(data_store[profile_id]["spells"]) do + if known then spells_known = spells_known + 1; end + end + data_store[profile_id]["spells_known"] = spells_known; + encoder_write_integer(profile_id .. "_spells_known", spells_known); + return spells_known; + end + + ---add spells and update known spells count, saved to datastore and disk + ---@param profile_id integer + ---@param spells table {1=spell_id, 2=spell_id, ... } + local function _add_spells(profile_id, spells) + if spells == nil or #spells == 0 then return; end + for _, spell_id in ipairs(spells) do + data_store[profile_id]["spells"][spell_id] = true; + encoder_add_flag(profile_id .. "_spell_" .. string.lower(spell_id)); + end + _update_spells_known_count(profile_id); + end + + ---return in-memory {spell_id = true, ... }, always_cast_spells_known for profile + ---@param profile_id integer + ---@return table spells {spell_id = true, ...} + ---@return integer count known always_cast spells + local function _get_always_cast_spells(profile_id) + return data_store[profile_id]["always_cast_spells"], data_store[profile_id]["always_cast_spells_known"] or 0; + end + + ---update quantity of known always_cast spells, saved to datastore and disk + ---@param profile_id integer + ---@return integer count known always_cast spells + local function _update_always_cast_spells_known_count(profile_id) + local always_casts_known = 0; + for _, known in pairs(data_store[profile_id]["always_cast_spells"]) do + if known then always_casts_known = always_casts_known + 1; end + end + data_store[profile_id]["always_cast_spells_known"]=always_casts_known; + encoder_write_integer(profile_id .. "_always_cast_spells_known", always_casts_known); + return always_casts_known; + end + + ---add always_cast spells and update known always_cast spells count, saved to datastore and disk + ---@param profile_id integer + ---@param ac_spells table {1=spell_id, 2=spell_id, ... } + local function _add_always_cast_spells(profile_id, ac_spells) + if ac_spells == nil or #ac_spells == 0 then return; end + for _, curr_ac_spell_id in ipairs(ac_spells) do + data_store[profile_id]["always_cast_spells"][curr_ac_spell_id] = true; + encoder_add_flag(profile_id .. "_always_cast_spell_" .. string.lower(curr_ac_spell_id)); + end + _update_always_cast_spells_known_count(profile_id); + end + + ---load spells, always_cast spells, and known quantities from disk + ---@param profile_id integer + local function _load_profile_spells(profile_id) + if data_store ~= nil and data_store[profile_id] ~= nil then + local prefixed_string = profile_id; + data_store[profile_id]["always_cast_spells"] = {}; + data_store[profile_id]["spells"] = {}; + for curr_action_id, _ in pairs(actions_by_id) do + if encoder_has_flag(prefixed_string .. "_spell_" .. string.lower(curr_action_id)) then data_store[profile_id]["spells"][curr_action_id] = true; end + if encoder_has_flag(prefixed_string .. "_always_cast_spell_" .. string.lower(curr_action_id)) then data_store[profile_id]["always_cast_spells"][curr_action_id] = true; end + end + data_store[profile_id]["spells_known"] = _update_spells_known_count(profile_id); + data_store[profile_id]["always_cast_spells_known"] = _update_always_cast_spells_known_count(profile_id); + end + end + + ---update quantity of known wand types, saved to datastore and disk + ---@param profile_id integer + ---@return integer count known wand types + local function _update_wand_types_known_count(profile_id) + local wand_types_known = 0; + for _, known in pairs(data_store[profile_id]["wand_types"]) do + if known then wand_types_known = wand_types_known + 1; end + end + data_store[profile_id]["wand_types_known"]=wand_types_known; + encoder_write_integer(profile_id .. "_wand_types_known", wand_types_known); + return wand_types_known; + end + + ---add wand types and update known wand types count, saved to datastore and disk + ---@param profile_id integer + ---@param wand_types table {1=wand_type, 2=wand_type, ... } + local function _add_wand_types(profile_id, wand_types) + for _, curr_wand_type in ipairs(wand_types) do + if string.sub(curr_wand_type, 1, #"default") ~= "default" then + data_store[profile_id]["wand_types"][curr_wand_type] = true; + encoder_add_flag(profile_id .. "_wand_type_" .. string.lower(curr_wand_type)); + end + end + _update_wand_types_known_count(profile_id); + end + + ---load wand types and known quantity from disk + ---@param profile_id integer + local function _load_wand_types(profile_id) + local idx = 0; + data_store[profile_id]["wand_types"] = {}; + data_store[profile_id]["wand_types_idx"] = {}; + ---TODO: is this actually necessary? Debug and check + local prefixed_string = profile_id; + for i, _ in ipairs(default_wands) do + idx = idx + 1; + data_store[profile_id]["wand_types"]["default_" .. i] = true; + data_store[profile_id]["wand_types_idx"][idx] = "default_" .. i; + end + for _, scan_wand in ipairs(wands) do + local scan_wand_type = sprite_file_to_wand_type(scan_wand.file); + if encoder_has_flag(prefixed_string .. "_wand_type_" .. string.lower(scan_wand_type)) then + idx = idx + 1; + data_store[profile_id]["wand_types"][scan_wand_type] = true; + data_store[profile_id]["wand_types_idx"][idx] = scan_wand_type; + end + end + data_store[profile_id]["wand_types_known"] = _update_wand_types_known_count(profile_id); + end + + local function _set_spells_per_cast(profile_id, value) + data_store[profile_id]["spells_per_cast"] = value; + encoder_write_integer(profile_id .. "_spells_per_cast", data_store[profile_id]["spells_per_cast"]); + end + + local function _set_cast_delay_min(profile_id, value) + data_store[profile_id]["cast_delay_min"] = value; + encoder_write_integer(profile_id .. "_cast_delay_min", data_store[profile_id]["cast_delay_min"]); + end + + local function _set_cast_delay_max(profile_id, value) + data_store[profile_id]["cast_delay_max"] = value; + encoder_write_integer(profile_id .. "_cast_delay_max", data_store[profile_id]["cast_delay_max"]); + end + + local function _set_recharge_time_min(profile_id, value) + data_store[profile_id]["recharge_time_min"] = value; + encoder_write_integer(profile_id .. "_recharge_time_min", data_store[profile_id]["recharge_time_min"]); + end + + local function _set_recharge_time_max(profile_id, value) + data_store[profile_id]["recharge_time_max"] = value; + encoder_write_integer(profile_id .. "_recharge_time_max", data_store[profile_id]["recharge_time_max"]); + end + + local function _set_mana_max(profile_id, value) + data_store[profile_id]["mana_max"] = value; + encoder_write_integer(profile_id .. "_mana_max", data_store[profile_id]["mana_max"]); + end + + local function _set_mana_charge_speed(profile_id, value) + data_store[profile_id]["mana_charge_speed"] = value; + encoder_write_integer(profile_id .. "_mana_charge_speed", data_store[profile_id]["mana_charge_speed"]); + end + + local function _set_capacity(profile_id, value) + data_store[profile_id]["capacity"] = value; + encoder_write_integer(profile_id .. "_capacity", data_store[profile_id]["capacity"]); + end + + local function _set_spread_min(profile_id, value) + data_store[profile_id]["spread_min"] = value; + encoder_write_integer(profile_id .. "_spread_min", data_store[profile_id]["spread_min"] == nil and nil or math.floor(data_store[profile_id]["spread_min"] * 10)); + end + + local function _set_spread_max(profile_id, value) + data_store[profile_id]["spread_max"] = value; + encoder_write_integer(profile_id .. "_spread_max", data_store[profile_id]["spread_max"] == nil and nil or math.ceil(data_store[profile_id]["spread_max"] * 10)); + end + + -- money + local function _get_stash_money(profile_id) + return data_store[profile_id]["money"] or 0; + end + + local function _set_stash_money(profile_id, value) + data_store[profile_id]["money"] = value; + encoder_write_integer(profile_id .. "_money", data_store[profile_id]["money"]); + end + + ---increments stash money, returns success + ---@param profile_id integer profile + ---@param amount integer money + ---@return boolean success + local function _increment_stash_money(profile_id, amount) + local _stash = data_store[profile_id]["money"] or 0; + local _target = _stash + amount; + if _stash > _target then return false; end + + data_store[profile_id]["money"] = _target; + encoder_write_integer(profile_id .. "_money", data_store[profile_id]["money"]); + return true; + end + + ---decrements stash money, returns success + ---@param profile_id integer profile + ---@param amount integer money + ---@return boolean success + local function _decrement_stash_money(profile_id, amount) + local _stash = data_store[profile_id]["money"] or 0; + local _target = _stash - amount; + if _stash < _target then return false; end + + data_store[profile_id]["money"] = _target; + encoder_write_integer(profile_id .. "_money", data_store[profile_id]["money"]); + return true; + end + + -- always_cast_count + local function _get_always_cast_count(profile_id) + return data_store[profile_id]["always_cast_count"] or 0; + end + + local function _set_always_cast_count(profile_id, value) + data_store[profile_id]["always_cast_count"] = value; + encoder_write_integer(tostring(profile_id) .. "_always_cast_count", data_store[profile_id]["always_cast_count"]); + end + + + local function _transfer_money_player_to_stash(profile_id, amount) + if get_player_money() < amount then + return false; + end + + if _increment_stash_money(profile_id, amount) then + decrement_player_money(amount); + return true; + end + return false; + end + + local function _transfer_money_stash_to_player(profile_id, amount) + local _money = get_player_money(); + if _money >= (2^29) then return false; end + if _money + amount > (2^29) then return false; end + if _get_stash_money(profile_id) < amount then return false; end + + local _newtarget = math.min(_money + amount, (2^29)); + local _difference = _newtarget - _money; + + if set_player_money( _newtarget ) then + return _decrement_stash_money(profile_id, _difference); + else + return false; + end + end + + ---Load a profile quick-view, return values and populate datastore + ---@param profile_id integer profile id + ---@return integer|nil money stashed money + ---@return integer|nil always_cast quantity known always cast spells + ---@return integer|nil spells quantity known spells + ---@return integer|nil wand_types quantity known wand types + local function _load_profile_quick(profile_id) + local money = encoder_load_integer(profile_id .. "_money") or 0; + local always_cast = encoder_load_integer(profile_id .. "_always_cast_spells_known") or 0; + local spells = encoder_load_integer(profile_id .. "_spells_known") or 0; + local wand_types = encoder_load_integer(profile_id .. "_wand_types_known") or 0; + + if always_cast==0 or spells==0 or wand_types==0 then + _load_profile_spells(profile_id); + _load_wand_types(profile_id); + always_cast = _update_always_cast_spells_known_count(profile_id) or 0; + spells = _update_spells_known_count(profile_id) or 0; + wand_types = _update_wand_types_known_count(profile_id) or 0; + end + data_store[profile_id]["quickloaded"] = true; + data_store[profile_id]["money"] = money; + data_store[profile_id]["always_cast_spells_known"] = always_cast; + data_store[profile_id]["spells_known"] = spells; + data_store[profile_id]["wand_types_known"] = wand_types; + return money, always_cast, spells, wand_types; + end + + -- templates + local function _load_template(profile_id, template_id) + local template_id_string = template_id; + + local _template = {}; + if encoder_has_flag(profile_id .. "_template_" .. template_id_string) then + if encoder_has_flag(profile_id .. "_template_" .. template_id_string .. "_shuffle") then + _template["shuffle"] = true; + else + _template["shuffle"] = false; + end + _template["spells_per_cast"] = encoder_load_integer(profile_id .. "_template_" .. template_id_string .. "_spells_per_cast"); + _template["cast_delay"] = encoder_load_integer(profile_id .. "_template_" .. template_id_string .. "_cast_delay"); + _template["recharge_time"] = encoder_load_integer(profile_id .. "_template_" .. template_id_string .. "_recharge_time"); + _template["mana_max"] = encoder_load_integer(profile_id .. "_template_" .. template_id_string .. "_mana_max"); + _template["mana_charge_speed"] = encoder_load_integer(profile_id .. "_template_" .. template_id_string .. "_mana_charge_speed"); + _template["capacity"] = encoder_load_integer(profile_id .. "_template_" .. template_id_string .. "_capacity"); + _template["spread"] = encoder_load_integer(profile_id .. "_template_" .. template_id_string .. "_spread") / 10; + + _template["always_cast_spells"] = {}; + for key, _ in pairs(data_store[profile_id]["always_cast_spells"]) do + if encoder_has_flag(profile_id .. "_template_" .. template_id_string .. "_always_cast_spell_" .. string.lower(key)) then + table.insert(_template["always_cast_spells"], key); + end + end + _template["always_cast_count"] = #_template["always_cast_spells"]; + + for key, _ in pairs(data_store[profile_id]["wand_types"]) do + if encoder_has_flag(profile_id .. "_template_" .. template_id_string .. "_wand_type_" .. string.lower(key)) then + _template["wand_type"] = key; + break; + end + end + _template.price = get_wand_buy_price(_template); + end + return _template; + end + + local function _load_templates(profile_id) + local _templates = {}; + for _idx = 1, get_template_count() do + table.insert(_templates, _load_template(profile_id, _idx)); + end + data_store[profile_id]["template"] = _templates; + end + + local function _get_template(profile_id, template_id) + return data_store[profile_id]["template"][template_id]; + end + + local function _get_templates(profile_id) + return data_store[profile_id]["template"]; + end + + local function _delete_template(profile_id, template_id) + local template_prefix = profile_id .. "_template_" .. template_id; + encoder_clear_flag(template_prefix .. "_shuffle"); + encoder_clear_integer(template_prefix .. "_spells_per_cast"); + encoder_clear_integer(template_prefix .. "_cast_delay"); + encoder_clear_integer(template_prefix .. "_recharge_time"); + encoder_clear_integer(template_prefix .. "_mana_max"); + encoder_clear_integer(template_prefix .. "_mana_charge_speed"); + encoder_clear_integer(template_prefix .. "_capacity"); + encoder_clear_integer(template_prefix .. "_spread"); + + for scan_action_id, _ in pairs(actions_by_id) do + encoder_clear_flag(template_prefix .. "_always_cast_spell_" .. string.lower(scan_action_id)); + end + + for def_idx, _ in ipairs(default_wands) do + encoder_clear_flag(template_prefix .. "_wand_type_default_" .. def_idx); + end + for _, scan_wand in ipairs(wands) do + encoder_clear_flag(template_prefix .. "_wand_type_" .. string.lower(sprite_file_to_wand_type(scan_wand.file))); + end + + encoder_clear_flag(template_prefix); + + if type(data_store[profile_id]) == "table" then + if type(data_store[profile_id]["template"]) == "table" then + data_store[profile_id]["template"][template_id] = {}; + else + data_store[profile_id]["template"] = {}; + end + end + end + + local function _set_template(profile_id, template_id, wand_data) + _delete_template(profile_id, template_id); + if wand_data == nil then + return; + end + local template_prefix = profile_id .. "_template_" .. template_id; + if wand_data["shuffle"] then + encoder_add_flag(template_prefix .. "_shuffle"); + end + encoder_write_integer(template_prefix .. "_spells_per_cast", wand_data["spells_per_cast"]); + encoder_write_integer(template_prefix .. "_cast_delay", wand_data["cast_delay"]); + encoder_write_integer(template_prefix .. "_recharge_time", wand_data["recharge_time"]); + encoder_write_integer(template_prefix .. "_mana_max", wand_data["mana_max"]); + encoder_write_integer(template_prefix .. "_mana_charge_speed", wand_data["mana_charge_speed"]); + encoder_write_integer(template_prefix .. "_capacity", wand_data["capacity"]); + encoder_write_integer(template_prefix .. "_spread", math.floor(wand_data["spread"] * 10 + 0.5)); + + for _, spell in ipairs(wand_data["always_cast_spells"]) do + encoder_add_flag(template_prefix .. "_always_cast_spell_" .. string.lower(spell)); + end + + encoder_add_flag(template_prefix .. "_wand_type_" .. string.lower(wand_data["wand_type"])); + + encoder_add_flag(template_prefix); + + data_store[profile_id]["template"][template_id] = wand_data or {}; + end + + --- Check if and how a wand entity is new research for profile + ---@param profile_id integer + ---@param entity_id integer + ---@return table research_flags {is_new, improves_spells_per_cast, improves_cast_delay_min, improves_cast_delay_max, improves_recharge_time_min, improves_recharge_time_max, improves_mana_max, improves_mana_charge_speed, improves_capacity, improves_spread_min, improves_spread_max, improves_always_cast_spells, improves_wand_types, count_new_always_cast_spells} + ---@return table cost_data {_sum, wand_type, always_casts, always_cast_count, shuffle, spells_per_cast, cast_delay, recharge_time, mana_max, mana_charge_speed, capacity, spread } + ---@return table wand_data wand data + local function _get_wand_entity_research(profile_id, entity_id) + local _research = { is_new = false, + b_new_is_only_type = false, + b_spells_per_cast = false, + b_cast_delay = false, + b_cast_delay_min = false, + b_cast_delay_max = false, + b_recharge_time = false, + b_recharge_time_min = false, + b_recharge_time_max = false, + b_mana_max = false, + b_mana_charge_speed = false, + b_capacity = false, + b_spread = false, + b_spread_min = false, + b_spread_max = false, + b_always_cast_spells = false, + i_always_cast_spells = 0, + b_spells = false, + i_spells = 0, + b_always_cast_count = false, + b_wand_types = false }; + local _cost = { wand_type = 0, + always_cast_spells = 0, + always_cast_count = 0, + shuffle = 0, + spells_per_cast = 0, + cast_delay = 0, + recharge_time = 0, + mana_max = 0, + mana_charge_speed = 0, + capacity = 0, + spread = 0 }; + local _profile_known_spells = _get_profile_spells(profile_id); + local _in_wand_data = {}; + + if entity_id~=nil and entity_id~=0 then + local _wand_bounds = _get_wand_bounds(profile_id); + _in_wand_data = read_wand_entity(entity_id); + + if _in_wand_data["spells_per_cast"] > _wand_bounds.spells_per_cast[2] then + _research.b_spells_per_cast = true; + _research.is_new = true; + _cost.spells_per_cast = math.ceil(__cost_func_spells_per_cast(_in_wand_data["spells_per_cast"])); + end + if _wand_bounds.cast_delay[1] == nil or _wand_bounds.cast_delay[2] == nil then + _research.b_cast_delay = true; + _research.b_cast_delay_min = true; + _research.b_cast_delay_max = true; + _research.is_new = true; + _cost.cast_delay = math.ceil(__cost_func_cast_delay(_in_wand_data["cast_delay"])); + else + if _in_wand_data["cast_delay"] < _wand_bounds.cast_delay[1] then + _research.b_cast_delay = true; + _research.b_cast_delay_min = true; + _research.is_new = true; + _cost.cast_delay = math.ceil(__cost_func_cast_delay(_in_wand_data["cast_delay"])); + elseif _in_wand_data["cast_delay"] > _wand_bounds.cast_delay[2] then + _research.b_cast_delay = true; + _research.b_cast_delay_max = true; + _research.is_new = true; + _cost.cast_delay = math.ceil(__cost_func_cast_delay(_in_wand_data["cast_delay"])); + end + end + if _wand_bounds.recharge_time[1] == nil or _wand_bounds.recharge_time[2] == nil then + _research.b_recharge_time = true; + _research.b_recharge_time_min = true; + _research.b_recharge_time_max = true; + _research.is_new = true; + _cost.recharge_time = math.ceil(__cost_func_recharge_time(_in_wand_data["recharge_time"])); + else + if _in_wand_data["recharge_time"] < _wand_bounds.recharge_time[1] then + _research.b_recharge_time = true; + _research.b_recharge_time_min = true; + _research.is_new = true; + _cost.recharge_time = math.ceil(__cost_func_recharge_time(_in_wand_data["recharge_time"])); + end + if _in_wand_data["recharge_time"] > _wand_bounds.recharge_time[2] then + _research.b_recharge_time = true; + _research.b_recharge_time_max = true; + _research.is_new = true; + _cost.recharge_time = math.ceil(__cost_func_recharge_time(_in_wand_data["recharge_time"])); + end + end + if _in_wand_data["mana_max"] > _wand_bounds.mana_max[2] then + _research.b_mana_max = true; + _research.is_new = true; + _cost.mana_max = math.ceil(__cost_func_mana_max(_in_wand_data["mana_max"])); + end + if _in_wand_data["mana_charge_speed"] > _wand_bounds.mana_charge_speed[2] then + _research.b_mana_charge_speed = true; + _research.is_new = true; + _cost.mana_charge_speed = math.ceil(__cost_func_mana_charge_speed(_in_wand_data["mana_charge_speed"])); + end + if _in_wand_data["capacity"] > _wand_bounds.capacity[2] then + _cost.capacity = math.ceil(__cost_func_capacity(_in_wand_data["capacity"])); + _research.b_capacity = true; + _research.is_new = true; + end + if _wand_bounds.spread[1] == nil or _wand_bounds.spread[2] == nil then + _research.b_spread = true; + _research.b_spread_min = true; + _research.b_spread_max = true; + _research.is_new = true; + _cost.spread = math.ceil(__cost_func_spread(_in_wand_data["spread"])); + else + if _in_wand_data["spread"] < _wand_bounds.spread[1] then + _research.b_spread = true; + _research.b_spread_min = true; + _research.is_new = true; + _cost.spread = math.ceil(__cost_func_spread(_in_wand_data["spread"])); + end + if _in_wand_data["spread"] > _wand_bounds.spread[2] then + _research.b_spread = true; + _research.b_spread_max = true; + _research.is_new = true; + _cost.spread = math.ceil(__cost_func_spread(_in_wand_data["spread"])); + end + end + + if _in_wand_data["always_cast_spells"] ~= nil and #_in_wand_data["always_cast_spells"] > 0 then + for _, _always_cast_id in pairs(_in_wand_data["always_cast_spells"]) do + if actions_by_id[_always_cast_id] ~= nil and (_wand_bounds.always_casts == nil or _wand_bounds.always_casts[_always_cast_id] == nil) then + _research.i_always_cast_spells = _research.i_always_cast_spells + 1; + _research.b_always_cast_spells = true; + _research.is_new = true; + _cost.always_cast_spells = _cost.always_cast_spells + math.ceil(__cost_func_always_cast_spell(_always_cast_id)); + end + end + if (#_in_wand_data["always_cast_spells"] or 0) > (_wand_bounds.always_cast_count or 0) then + _research.b_always_cast_count = true; + _research.is_new = true; + _cost.always_cast_count = math.ceil(__cost_func_always_cast_count(#_in_wand_data["always_cast_spells"])); + end + end + + if _in_wand_data["spells"] ~= nil and #_in_wand_data["spells"] > 0 then + for _, _spell_id in pairs(_in_wand_data["spells"]) do + if actions_by_id[_spell_id] ~= nil and (_profile_known_spells == nil or _profile_known_spells[_spell_id] ~= true) then + _research.i_spells = _research.i_spells + 1; + _research.b_spells = true; + end + end + end + + if _wand_bounds.wand_types == nil or _wand_bounds.wand_types[_in_wand_data["wand_type"]] == nil then + if wand_type_to_base_wand(_in_wand_data["wand_type"]) ~= nil then + _research.b_new_is_only_type = not _research.is_new; + _research.b_wand_types = true; + _research.is_new = true; + _cost.wand_type = math.ceil(__cost_func_wand_type(_in_wand_data["wand_type"])); + end + end + end -- if entity_id~=nil|0 + + local _sum = 0; + for _member_name, _member_cost in pairs(_cost) do + if _member_name=="always_cast_spells" then + _sum = _sum + math.ceil(_member_cost * mod_setting.research_spell_price_multiplier); + else + _sum = _sum + math.ceil(_member_cost * mod_setting.research_wand_price_multiplier); + end + end + _cost._sum = _sum; + + return _research, _cost, _in_wand_data; + end + + local function _research_wand(profile_id, entity_id) + local _research, _cost, _wand_data = _get_wand_entity_research(profile_id, entity_id); + + if get_player_money() < _cost._sum then + return false; + end + + if _research.b_spells_per_cast then _set_spells_per_cast(profile_id, _wand_data["spells_per_cast"]); end + if _research.b_cast_delay_min then _set_cast_delay_min(profile_id, _wand_data["cast_delay"]); end + if _research.b_cast_delay_max then _set_cast_delay_max(profile_id, _wand_data["cast_delay"]); end + if _research.b_recharge_time_min then _set_recharge_time_min(profile_id, _wand_data["recharge_time"]); end + if _research.b_recharge_time_max then _set_recharge_time_max(profile_id, _wand_data["recharge_time"]); end + if _research.b_mana_max then _set_mana_max(profile_id, _wand_data["mana_max"]); end + if _research.b_mana_charge_speed then _set_mana_charge_speed(profile_id, _wand_data["mana_charge_speed"]); end + if _research.b_capacity then _set_capacity(profile_id, _wand_data["capacity"]); end + if _research.b_spread_min then _set_spread_min(profile_id, _wand_data["spread"]); end + if _research.b_spread_max then _set_spread_max(profile_id, _wand_data["spread"]); end + if _research.b_always_cast_spells then _add_always_cast_spells(profile_id, _wand_data["always_cast_spells"]); end + if _research.b_always_cast_count then _set_always_cast_count(profile_id, #_wand_data["always_cast_spells"]); end + if _research.b_wand_types then _add_wand_types(profile_id, { _wand_data["wand_type"] }); end + + delete_wand_entity(entity_id); + decrement_player_money(_cost._sum); + return true; + end + + local function _research_spell_entity(profile_id, entity_id) + local price = get_spell_entity_research_price(entity_id); + if (price == nil) or (get_player_money() < price) then + return false; + end + + _add_spells(profile_id, { get_spell_entity_action_id(entity_id) }); + + delete_spell_entity(entity_id); + decrement_player_money(price); + return true; + end + + ---- =========================== + ---- PRIVATE TO PUBLIC PROMOTION + ---- =========================== + + function get_stash_money() return _get_stash_money(loaded_profile_id); end + function set_stash_money(value) return _set_stash_money(loaded_profile_id, value); end + ---increments stash money, returns success + ---@param amount integer money + ---@return boolean success + function increment_stash_money(amount) return _increment_stash_money(loaded_profile_id, amount); end + ---increments stash money, returns success + ---@param amount integer money + ---@return boolean success + function decrement_stash_money(amount) return _decrement_stash_money(loaded_profile_id, amount); end + function transfer_money_player_to_stash(value) return _transfer_money_player_to_stash(loaded_profile_id, value); end + function transfer_money_stash_to_player(value) return _transfer_money_stash_to_player(loaded_profile_id, value); end + function get_profile_spells() return _get_profile_spells(loaded_profile_id); end + function research_spell_entity(entity_id) return _research_spell_entity(loaded_profile_id, entity_id); end + function research_wand(entity_id) return _research_wand(loaded_profile_id, entity_id); end + function get_templates() return _get_templates(loaded_profile_id); end + function get_template(template_id) return _get_template(loaded_profile_id, template_id); end + function load_templates() return _load_templates(loaded_profile_id); end + function set_template(template_id, wand_data) _set_template(loaded_profile_id, template_id, wand_data); end + function delete_template(template_id) _delete_template(loaded_profile_id, template_id); end + function get_always_cast_spells() return _get_always_cast_spells(loaded_profile_id); end + function get_always_cast_count() return _get_always_cast_count(loaded_profile_id); end + function get_wand_bounds() return _get_wand_bounds(loaded_profile_id); end + function get_wand_entity_research(entity_id) return _get_wand_entity_research(loaded_profile_id, entity_id); end + + ---- ================ + ---- PUBLIC FUNCTIONS + ---- ================ + + function does_profile_know_spell(action_id) return data_store[loaded_profile_id]["spells"][action_id] or false; end + + function get_modify_wand_table(entity_id) + local _members = {"spells_per_cast", "cast_delay", "recharge_time", "mana_max", "mana_charge_speed", "spread", "capacity"}; + + local _template = get_template(1); + local _table = { origin_entity = entity_id, bounds = get_wand_bounds() }; + if _template.capacity~=nil and (entity_id==nil or entity_id==0) then + _table.wand = {}; + for _member, _data in pairs(_template) do + _table.wand[_member] = _data; + end + else + _table.wand = read_wand_entity(entity_id); + _table.price = get_wand_buy_price(_table.wand); + _table.origin_e_id = entity_id; + end + if _table.wand.wand_type==-1 then + _table.wand.wand_type = data_store[loaded_profile_id]["wand_types_idx"][1]; + _table.wand.sprite = wand_type_to_sprite_file(_table.wand.wand_type); + _table.wand.shuffle = false; + for _, _member in ipairs(_members) do + _table.wand[_member] = math.floor( (_table.bounds[_member][1] + _table.bounds[_member][2]) / 2 ); + end + end + _table.ac_limit = _table.bounds.always_cast_count; + _table.ac_spells = get_always_cast_spell_purchase_table(); + return _table; + end + + -- PROFILES + + ---erase profile slot and write new, clear in-memory datastore for profile, tag profile as selected + ---@param profile_id integer + function create_new_profile(profile_id, _force) + _force = _force==true or false; + if data_store[profile_id]~=nil and data_store[profile_id].quickloaded==true and not _force then return; end + + delete_profile(profile_id); + load_profile(profile_id); + encoder_add_flag(tostring(profile_id)); + selected_profile_id=profile_id; + end + + ---erase profile slot and mark empty, clear in-memory datastore for profile + ---@param profile_id integer + function delete_profile(profile_id) + encoder_clear_integer(profile_id .. "_spells_per_cast"); + encoder_clear_integer(profile_id .. "_cast_delay_min"); + encoder_clear_integer(profile_id .. "_cast_delay_max"); + encoder_clear_integer(profile_id .. "_recharge_time_min"); + encoder_clear_integer(profile_id .. "_recharge_time_max"); + encoder_clear_integer(profile_id .. "_mana_max"); + encoder_clear_integer(profile_id .. "_capacity"); + encoder_clear_integer(profile_id .. "_always_cast_count"); + encoder_clear_integer(profile_id .. "_spread_min"); + encoder_clear_integer(profile_id .. "_spread_max"); + encoder_clear_integer(profile_id .. "_money"); + encoder_clear_integer(profile_id .. "_always_cast_spells_known"); + encoder_clear_integer(profile_id .. "_spells_known"); + encoder_clear_integer(profile_id .. "_wand_types_known"); + for _, curr_action in ipairs(actions_by_id) do + encoder_clear_flag(profile_id .. "_spell_" .. string.lower(curr_action.id)); + encoder_clear_flag(profile_id .. "_always_cast_spell_" .. string.lower(curr_action.id)); + end + for _, curr_wand in ipairs(wands) do + encoder_clear_flag(profile_id .. "_wand_type_" .. sprite_file_to_wand_type(curr_wand.file)); + end + + for i = 1, get_template_count() do + _delete_template(profile_id, i); + end + + encoder_clear_flag(tostring(profile_id)); -- remove profile exists tag + data_store[profile_id] = nil; + end + + ---return byval copy of datastore for quickloaded profiles + ---@return table {quickloaded, money, always_cast_spells_known, wand_types_known} + function get_quick_profiles() + clean_store = {}; + for profile_id = 1, get_profile_count() do + clean_store[profile_id] = {}; + if data_store[profile_id]~=nil and data_store[profile_id]["quickloaded"]==true then + clean_store[profile_id]["quickloaded"] = data_store[profile_id]["quickloaded"]; + clean_store[profile_id]["money"] = data_store[profile_id]["money"]; + clean_store[profile_id]["always_cast_spells_known"] = data_store[profile_id]["always_cast_spells_known"]; + clean_store[profile_id]["spells_known"] = data_store[profile_id]["spells_known"]; + clean_store[profile_id]["wand_types_known"] = data_store[profile_id]["wand_types_known"]; + else + clean_store[profile_id]["quickloaded"] = nil; + end + end + return clean_store; + end + + ---load profile_id from disk to data_store + ---@param profile_id number + function load_profile(profile_id) + for to_clear = 1, get_profile_count() do + data_store[to_clear] = {} + end + + _load_profile_quick(profile_id); + + data_store[profile_id]["spells_per_cast"] = encoder_load_integer(profile_id .. "_spells_per_cast"); + data_store[profile_id]["cast_delay_min"] = encoder_load_integer(profile_id .. "_cast_delay_min"); + data_store[profile_id]["cast_delay_max"] = encoder_load_integer(profile_id .. "_cast_delay_max"); + data_store[profile_id]["recharge_time_min"] = encoder_load_integer(profile_id .. "_recharge_time_min"); + data_store[profile_id]["recharge_time_max"] = encoder_load_integer(profile_id .. "_recharge_time_max"); + data_store[profile_id]["mana_max"] = encoder_load_integer(profile_id .. "_mana_max"); + data_store[profile_id]["mana_charge_speed"] = encoder_load_integer(profile_id .. "_mana_charge_speed"); + data_store[profile_id]["always_cast_count"] = encoder_load_integer(profile_id .. "_always_cast_count"); + data_store[profile_id]["capacity"] = encoder_load_integer(profile_id .. "_capacity"); + local spread_min = encoder_load_integer(profile_id .. "_spread_min"); + if spread_min ~= nil then + spread_min = spread_min / 10; + end + data_store[profile_id]["spread_min"] = spread_min; + local spread_max = encoder_load_integer(profile_id .. "_spread_max"); + if spread_max ~= nil then + spread_max = spread_max / 10; + end + data_store[profile_id]["spread_max"] = spread_max; + + + _load_profile_spells(profile_id); + _load_wand_types(profile_id); + _load_templates(profile_id); + + data_store[profile_id]["loaded"] = true; + + print("========================="); + print("persistence: Loaded profile " .. profile_id); + print("money: " .. data_store[profile_id]["money"]); + print("always_cast_spells_known: " .. data_store[profile_id]["always_cast_spells_known"]); + print("spells_known: " .. data_store[profile_id]["spells_known"]); + print("wand_types_known: " .. data_store[profile_id]["wand_types_known"]); + loaded_profile_id = profile_id; + GlobalsSetValue("persistence_profile", tostring(profile_id)); + end + + function data_store_everyframe() + if selected_profile_id>0 and loaded_profile_id~=selected_profile_id then load_profile(selected_profile_id); end + + if loaded_profile_id==0 then return; end + + local _game_frame = GameGetFrameNum(); + + if _game_frame%60 then + _do_startup_paycheck_check(); + end + + if _game_frame%30==0 and _in_workshop then + _do_holy_mountain_paycheck_check(); + end + end + ---end function declarations, first-run code here; + + for profile_idx = 1, get_profile_count() do + data_store[profile_idx]=data_store[profile_idx] or {}; + if encoder_has_flag(tostring(profile_idx)) then + _load_profile_quick(profile_idx) + end + end + + if GlobalsGetValue("persistence_profile", "0")~="0" then + selected_profile_id = tonumber(GlobalsGetValue("persistence_profile", "0")); + elseif GameHasFlagRun("persistence_using_mod") then + for i = 1, get_profile_count() do + if GameHasFlagRun("persistence_selected_profile_" .. tostring(i)) then + selected_profile_id = i; + break; end - data_store[save_id]["templates"][i]["spells_per_cast"] = hex_to_number(load_hex(save_id_string .. "_template_" .. tostring(i) .. "_spells_per_cast")); - data_store[save_id]["templates"][i]["cast_delay"] = hex_to_number(load_hex(save_id_string .. "_template_" .. tostring(i) .. "_cast_delay")); - data_store[save_id]["templates"][i]["recharge_time"] = hex_to_number(load_hex(save_id_string .. "_template_" .. tostring(i) .. "_recharge_time")); - data_store[save_id]["templates"][i]["mana_max"] = hex_to_number(load_hex(save_id_string .. "_template_" .. tostring(i) .. "_mana_max")); - data_store[save_id]["templates"][i]["mana_charge_speed"] = hex_to_number(load_hex(save_id_string .. "_template_" .. tostring(i) .. "_mana_charge_speed")); - data_store[save_id]["templates"][i]["capacity"] = hex_to_number(load_hex(save_id_string .. "_template_" .. tostring(i) .. "_capacity")); - data_store[save_id]["templates"][i]["spread"] = hex_to_number(load_hex(save_id_string .. "_template_" .. tostring(i) .. "_spread")) / 10; - - data_store[save_id]["templates"][i]["always_cast_spells"] = {}; - for key, _ in pairs(data_store[save_id]["always_cast_spells"]) do - if HasFlagPersistent(flag_prefix .. "_" .. save_id_string .. "_template_" .. tostring(i) .. "_always_cast_spell_" .. string.lower(key)) then - table.insert(data_store[save_id]["templates"][i]["always_cast_spells"], key); - break; - end - end - - for key, _ in pairs(data_store[save_id]["wand_types"]) do - if HasFlagPersistent(flag_prefix .. "_" .. save_id_string .. "_template_" .. tostring(i) .. "_wand_type_" .. string.lower(key)) then - data_store[save_id]["templates"][i]["wand_type"] = key; - break; - end - end - end - end -end - --- spells per cast -function get_spells_per_cast(save_id) - if data_store[save_id] == nil then - return nil; - end - return data_store[save_id]["spells_per_cast"] == nil and spells_per_cast_min or data_store[save_id]["spells_per_cast"]; -end - -local function set_spells_per_cast(save_id, value) - if data_store[save_id] == nil then - return; - end - data_store[save_id]["spells_per_cast"] = value; - save_hex(tostring(save_id) .. "_spells_per_cast", number_to_hex(data_store[save_id]["spells_per_cast"])); -end - --- cast delay min -function get_cast_delay_min(save_id) - if data_store[save_id] == nil then - return nil; - end - return data_store[save_id]["cast_delay_min"]; -end - -local function set_cast_delay_min(save_id, value) - if data_store[save_id] == nil then - return; - end - data_store[save_id]["cast_delay_min"] = value; - save_hex(tostring(save_id) .. "_cast_delay_min", number_to_hex(data_store[save_id]["cast_delay_min"])); -end - --- cast delay max -function get_cast_delay_max(save_id) - if data_store[save_id] == nil then - return nil; - end - return data_store[save_id]["cast_delay_max"]; -end - -local function set_cast_delay_max(save_id, value) - if data_store[save_id] == nil then - return; - end - data_store[save_id]["cast_delay_max"] = value; - save_hex(tostring(save_id) .. "_cast_delay_max", number_to_hex(data_store[save_id]["cast_delay_max"])); -end - --- recharge time min -function get_recharge_time_min(save_id) - if data_store[save_id] == nil then - return nil; - end - return data_store[save_id]["recharge_time_min"]; -end - -local function set_recharge_time_min(save_id, value) - if data_store[save_id] == nil then - return; - end - data_store[save_id]["recharge_time_min"] = value; - save_hex(tostring(save_id) .. "_recharge_time_min", number_to_hex(data_store[save_id]["recharge_time_min"])); -end - --- recharge time max -function get_recharge_time_max(save_id) - if data_store[save_id] == nil then - return nil; - end - return data_store[save_id]["recharge_time_max"]; -end - -local function set_recharge_time_max(save_id, value) - if data_store[save_id] == nil then - return; - end - data_store[save_id]["recharge_time_max"] = value; - save_hex(tostring(save_id) .. "_recharge_time_max", number_to_hex(data_store[save_id]["recharge_time_max"])); -end - --- mana max -function get_mana_max(save_id) - if data_store[save_id] == nil then - return nil; - end - return data_store[save_id]["mana_max"] == nil and mana_max_min or data_store[save_id]["mana_max"]; -end - -local function set_mana_max(save_id, value) - if data_store[save_id] == nil then - return; - end - data_store[save_id]["mana_max"] = value; - save_hex(tostring(save_id) .. "_mana_max", number_to_hex(data_store[save_id]["mana_max"])); -end - --- mana charge speed -function get_mana_charge_speed(save_id) - if data_store[save_id] == nil then - return nil; - end - return data_store[save_id]["mana_charge_speed"] == nil and mana_charge_speed_min or data_store[save_id]["mana_charge_speed"]; -end - -local function set_mana_charge_speed(save_id, value) - if data_store[save_id] == nil then - return; - end - data_store[save_id]["mana_charge_speed"] = value; - save_hex(tostring(save_id) .. "_mana_charge_speed", number_to_hex(data_store[save_id]["mana_charge_speed"])); -end - --- capacity -function get_capacity(save_id) - if data_store[save_id] == nil then - return nil; - end - return data_store[save_id]["capacity"] == nil and capacity_min or data_store[save_id]["capacity"]; -end - -local function set_capacity(save_id, value) - if data_store[save_id] == nil then - return; - end - data_store[save_id]["capacity"] = value; - save_hex(tostring(save_id) .. "_capacity", number_to_hex(data_store[save_id]["capacity"])); -end - --- spread min -function get_spread_min(save_id) - if data_store[save_id] == nil then - return nil; - end - return data_store[save_id]["spread_min"]; -end - -local function set_spread_min(save_id, value) - if data_store[save_id] == nil then - return; - end - data_store[save_id]["spread_min"] = value; - save_hex(tostring(save_id) .. "_spread_min", number_to_hex(data_store[save_id]["spread_min"] == nil and nil or math.floor(data_store[save_id]["spread_min"] * 10))); -end - --- spread max -function get_spread_max(save_id) - if data_store[save_id] == nil then - return nil; - end - return data_store[save_id]["spread_max"]; -end - -local function set_spread_max(save_id, value) - if data_store[save_id] == nil then - return; - end - data_store[save_id]["spread_max"] = value; - save_hex(tostring(save_id) .. "_spread_max", number_to_hex(data_store[save_id]["spread_max"] == nil and nil or math.ceil(data_store[save_id]["spread_max"] * 10))); -end - --- money -function get_safe_money(save_id) - if data_store[save_id] == nil then - return nil; - end - return data_store[save_id]["money"] == nil and 0 or data_store[save_id]["money"]; -end - -function set_safe_money(save_id, value) - if data_store[save_id] == nil then - return; - end - data_store[save_id]["money"] = value; - save_hex(tostring(save_id) .. "_money", number_to_hex(data_store[save_id]["money"])); -end - --- spells -function get_spells(save_id) - if data_store[save_id] == nil then - return nil; - end - if data_store[save_id]["spells"] == nil then - return {}; - end - return data_store[save_id]["spells"]; -end - -local function add_spells(save_id, spells) - if data_store[save_id] == nil or spells == nil or #spells == 0 then - return; - end - if data_store[save_id]["spells"] == nil then - data_store[save_id]["spells"] = {}; - end - for i = 1, #spells do - data_store[save_id]["spells"][spells[i]] = true; - AddFlagPersistent(flag_prefix .. "_" .. tostring(save_id) .. "_spell_" .. string.lower(spells[i])); - end -end - --- always cast spells -function get_always_cast_spells(save_id) - if data_store[save_id] == nil then - return nil; - end - if data_store[save_id]["always_cast_spells"] == nil then - return {}; - end - return data_store[save_id]["always_cast_spells"]; -end - -local function add_always_cast_spells(save_id, spells) - if data_store[save_id] == nil or spells == nil or #spells == 0 then - return; - end - if data_store[save_id]["always_cast_spells"] == nil then - data_store[save_id]["always_cast_spells"] = {}; - end - for i = 1, #spells do - data_store[save_id]["always_cast_spells"][spells[i]] = true; - AddFlagPersistent(flag_prefix .. "_" .. tostring(save_id) .. "_always_cast_spell_" .. string.lower(spells[i])); - end -end - --- wand types -function get_wand_types(save_id) - if data_store[save_id] == nil then - return nil; - end - if data_store[save_id]["wand_types"] == nil then - return {}; - end - return data_store[save_id]["wand_types"]; -end - -local function add_wand_types(save_id, wand_types) - if data_store[save_id] == nil or wand_types == nil or #wand_types == 0 then - return; - end - if data_store[save_id]["wand_types"] == nil then - data_store[save_id]["wand_types"] = {}; - end - for i = 1, #wand_types do - if string.sub(wand_types[i], 1, #"default") ~= "default" then - data_store[save_id]["wand_types"][wand_types[i]] = true; - AddFlagPersistent(flag_prefix .. "_" .. tostring(save_id) .. "_wand_type_" .. string.lower(wand_types[i])); - end - end -end - --- templates -function get_template(save_id, template_id) - if data_store[save_id] == nil or data_store[save_id]["templates"] == nil then - return nil; - end - return data_store[save_id]["templates"][template_id]; -end - -function set_template(save_id, template_id, wand_data) - if data_store[save_id] == nil or data_store[save_id]["templates"] == nil then - return; - end - delete_template(save_id, template_id); - if wand_data == nil then - return; - end - local template_prefix = tostring(save_id) .. "_template_" .. tostring(template_id); - local template_flag_prefix = flag_prefix .. "_" .. template_prefix; - if wand_data["shuffle"] then - AddFlagPersistent(template_flag_prefix .. "_shuffle"); - end - save_hex(template_prefix .. "_spells_per_cast", number_to_hex(wand_data["spells_per_cast"])); - save_hex(template_prefix .. "_cast_delay", number_to_hex(wand_data["cast_delay"])); - save_hex(template_prefix .. "_recharge_time", number_to_hex(wand_data["recharge_time"])); - save_hex(template_prefix .. "_mana_max", number_to_hex(wand_data["mana_max"])); - save_hex(template_prefix .. "_mana_charge_speed", number_to_hex(wand_data["mana_charge_speed"])); - save_hex(template_prefix .. "_capacity", number_to_hex(wand_data["capacity"])); - save_hex(template_prefix .. "_spread", number_to_hex(math.floor(wand_data["spread"] * 10 + 0.5))); - - for _, spell in ipairs(wand_data["always_cast_spells"]) do - AddFlagPersistent(template_flag_prefix .. "_always_cast_spell_" .. string.lower(spell)); - end - - AddFlagPersistent(template_flag_prefix .. "_wand_type_" .. string.lower(wand_data["wand_type"])); - - AddFlagPersistent(template_flag_prefix); - data_store[save_id]["templates"][template_id] = wand_data; -end - -function delete_template(save_id, template_id) - if data_store[save_id] == nil then - return; - end - local template_prefix = tostring(save_id) .. "_template_" .. tostring(template_id); - local template_flag_prefix = flag_prefix .. "_" .. template_prefix; - RemoveFlagPersistent(template_flag_prefix .. "_shuffle"); - save_hex(template_prefix .. "_spells_per_cast", nil); - save_hex(template_prefix .. "_cast_delay", nil); - save_hex(template_prefix .. "_recharge_time", nil); - save_hex(template_prefix .. "_mana_max", nil); - save_hex(template_prefix .. "_mana_charge_speed", nil); - save_hex(template_prefix .. "_capacity", nil); - save_hex(template_prefix .. "_spread", nil); - - for i = 1, #actions do - RemoveFlagPersistent(template_flag_prefix .. "_always_cast_spell_" .. string.lower(actions[i].id)); - end - - for i = 1, #mod_config.default_wands do - RemoveFlagPersistent(template_flag_prefix .. "_wand_type_default_" .. tostring(i)); - end - for i = 1, #wands do - RemoveFlagPersistent(template_flag_prefix .. "_wand_type_" .. string.lower(sprite_file_to_wand_type(wands[i].file))); - end - - RemoveFlagPersistent(template_flag_prefix); - if data_store[save_id]["templates"] ~= nil then - data_store[save_id]["templates"][template_id] = nil; - end -end - -function can_create_wand(save_id) - return get_cast_delay_min(save_id) ~= nil and get_cast_delay_max(save_id) ~= nil and get_recharge_time_min(save_id) ~= nil and get_recharge_time_max(save_id) ~= nil and get_spread_min(save_id) ~= nil and get_spread_max(save_id) ~= nil; -end - -function research_wand_is_new(save_id, entity_id) - local wand_data = read_wand(entity_id); - local spells_per_cast = get_spells_per_cast(save_id); - local cast_delay_min = get_cast_delay_min(save_id); - local cast_delay_max = get_cast_delay_max(save_id); - local recharge_time_min = get_recharge_time_min(save_id); - local recharge_time_max = get_recharge_time_max(save_id); - local mana_max = get_mana_max(save_id); - local mana_charge_speed = get_mana_charge_speed(save_id); - local capacity = get_capacity(save_id); - local spread_min = get_spread_min(save_id); - local spread_max = get_spread_max(save_id); - local always_cast_spells = get_always_cast_spells(save_id); - local wand_types = get_wand_types(save_id); - - if wand_data["spells_per_cast"] > spells_per_cast then - return true; - end - if cast_delay_min == nil or cast_delay_max == nil then - return true; - else - if wand_data["cast_delay"] < cast_delay_min then - return true; - end - if wand_data["cast_delay"] > cast_delay_max then - return true; - end - end - if recharge_time_min == nil or recharge_time_max == nil then - return true; - else - if wand_data["recharge_time"] < recharge_time_min then - return true; - end - if wand_data["recharge_time"] > recharge_time_max then - return true; - end - end - if wand_data["mana_max"] > mana_max then - return true; - end - if wand_data["mana_charge_speed"] > mana_charge_speed then - return true; - end - if wand_data["capacity"] > capacity then - return true; - end - if spread_min == nil or spread_max == nil then - return true; - else - if wand_data["spread"] < spread_min then - return true; - end - if wand_data["spread"] > spread_max then - return true; - end - end - if wand_data["always_cast_spells"] ~= nil and #wand_data["always_cast_spells"] > 0 then - for i = 1, #wand_data["always_cast_spells"] do - if always_cast_spells[wand_data["always_cast_spells"][i]] == nil then - for j = 1, #actions do - if actions[j].id == wand_data["always_cast_spells"][i] then - return true; - end - end - end - end - end - if wand_types[wand_data["wand_type"]] == nil then - if wand_type_to_wand(wand_data["wand_type"]) ~= nil then - return true; - end - end - - return false; -end - -function research_wand_price(save_id, entity_id) - local wand_data = read_wand(entity_id); - local spells_per_cast = get_spells_per_cast(save_id); - local cast_delay_min = get_cast_delay_min(save_id); - local cast_delay_max = get_cast_delay_max(save_id); - local recharge_time_min = get_recharge_time_min(save_id); - local recharge_time_max = get_recharge_time_max(save_id); - local mana_max = get_mana_max(save_id); - local mana_charge_speed = get_mana_charge_speed(save_id); - local capacity = get_capacity(save_id); - local spread_min = get_spread_min(save_id); - local spread_max = get_spread_max(save_id); - local always_cast_spells = get_always_cast_spells(save_id); - local wand_types = get_wand_types(save_id); - local price = 0; - - if wand_data["spells_per_cast"] > spells_per_cast then - price = price + (wand_data["spells_per_cast"] - spells_per_cast) * 1000; - end - if cast_delay_min == nil or cast_delay_max == nil then - price = price + 0.01 ^ (wand_data["cast_delay"] / 60 - 1.8) + 200; - else - if wand_data["cast_delay"] < cast_delay_min then - price = price + (0.01 ^ (wand_data["cast_delay"] / 60 - 1.8) + 200) - (0.01 ^ (cast_delay_min / 60 - 1.8) + 200); - end - if wand_data["cast_delay"] > cast_delay_max then - price = price + (wand_data["cast_delay"] / 60 - cast_delay_max / 60) * 100; - end - end - if recharge_time_min == nil or recharge_time_max == nil then - price = price + 0.01 ^ (wand_data["recharge_time"] / 60 - 1.8) + 200; - else - if wand_data["recharge_time"] < recharge_time_min then - price = price + (0.01 ^ (wand_data["recharge_time"] / 60 - 1.8) + 200) - (0.01 ^ (recharge_time_min / 60 - 1.8) + 200); - end - if wand_data["recharge_time"] > recharge_time_max then - price = price + (wand_data["recharge_time"] / 60 - recharge_time_max / 60) * 100; - end - end - if wand_data["mana_max"] > mana_max then - price = price + (wand_data["mana_max"] - mana_max) * 10; - end - if wand_data["mana_charge_speed"] > mana_charge_speed then - price = price + (wand_data["mana_charge_speed"] - mana_charge_speed) * 20; - end - if wand_data["capacity"] > capacity then - price = price + (wand_data["capacity"] - capacity) * 1000; - end - if spread_min == nil or spread_max == nil then - price = price + math.abs(5 - wand_data["spread"]) * 10; - else - if wand_data["spread"] < spread_min then - price = price + (spread_min - wand_data["spread"]) * 10; - end - if wand_data["spread"] > spread_max then - price = price + (wand_data["spread"] - spread_max) * 10; - end - end - if wand_data["always_cast_spells"] ~= nil and #wand_data["always_cast_spells"] > 0 then - for i = 1, #wand_data["always_cast_spells"] do - if always_cast_spells[wand_data["always_cast_spells"][i]] == nil then - for j = 1, #actions do - if actions[j].id == wand_data["always_cast_spells"][i] then - price = price + actions[j].price * 20; - break; - end - end - end - end - end - if wand_types[wand_data["wand_type"]] == nil then - if wand_type_to_wand(wand_data["wand_type"]) ~= nil then - price = math.max(100, price); - end - end - - return math.ceil(price * mod_config.research_wand_price_multiplier); -end - -function research_wand(save_id, entity_id) - local wand_data = read_wand(entity_id); - local spells_per_cast = get_spells_per_cast(save_id); - local cast_delay_min = get_cast_delay_min(save_id); - local cast_delay_max = get_cast_delay_max(save_id); - local recharge_time_min = get_recharge_time_min(save_id); - local recharge_time_max = get_recharge_time_max(save_id); - local mana_max = get_mana_max(save_id); - local mana_charge_speed = get_mana_charge_speed(save_id); - local capacity = get_capacity(save_id); - local spread_min = get_spread_min(save_id); - local spread_max = get_spread_max(save_id); - - local price = research_wand_price(save_id, entity_id); - if get_player_money() < price then - return false; - end - - if wand_data["spells_per_cast"] > spells_per_cast then - set_spells_per_cast(save_id, wand_data["spells_per_cast"]); - end - if cast_delay_min == nil or wand_data["cast_delay"] < cast_delay_min then - set_cast_delay_min(save_id, wand_data["cast_delay"]); - end - if cast_delay_max == nil or wand_data["cast_delay"] > cast_delay_max then - set_cast_delay_max(save_id, wand_data["cast_delay"]); - end - if recharge_time_min == nil or wand_data["recharge_time"] < recharge_time_min then - set_recharge_time_min(save_id, wand_data["recharge_time"]); - end - if recharge_time_max == nil or wand_data["recharge_time"] > recharge_time_max then - set_recharge_time_max(save_id, wand_data["recharge_time"]); - end - if wand_data["mana_max"] > mana_max then - set_mana_max(save_id, wand_data["mana_max"]); - end - if wand_data["mana_charge_speed"] > mana_charge_speed then - set_mana_charge_speed(save_id, wand_data["mana_charge_speed"]); - end - if wand_data["capacity"] > capacity then - set_capacity(save_id, wand_data["capacity"]); - end - if spread_min == nil or wand_data["spread"] < spread_min then - set_spread_min(save_id, wand_data["spread"]); - end - if spread_max == nil or wand_data["spread"] > spread_max then - set_spread_max(save_id, wand_data["spread"]); - end - if wand_data["always_cast_spells"] ~= nil and #wand_data["always_cast_spells"] > 0 then - add_always_cast_spells(save_id, wand_data["always_cast_spells"]); - end - if wand_type_to_wand(wand_data["wand_type"]) ~= nil then - add_wand_types(save_id, { wand_data["wand_type"] }); - end - - delete_wand(entity_id); - set_player_money(get_player_money() - price); - return true; -end - -function research_spell_price(entity_id) - local action_id = read_spell(entity_id); - if action_id == nil then - return nil - end - for i = 1, #actions do - if actions[i].id == action_id then - return math.ceil(actions[i].price * mod_config.research_spell_price_multiplier); end - end -end - -function research_spell(save_id, entity_id) - local price = research_spell_price(entity_id); - if price == nil then - return false; - end - if get_player_money() < price then - return false; - end - - add_spells(save_id, { read_spell(entity_id) }); - - delete_spell(entity_id); - set_player_money(get_player_money() - price); - return true; -end - -function transfer_money_to_safe(save_id, amount) - if get_player_money() < amount then - return false; - end - - set_safe_money(save_id, get_safe_money(save_id) + amount); - set_player_money(get_player_money() - amount); - return true; -end - -function transfer_money_to_player(save_id, amount) - if get_safe_money(save_id) < amount then - return false; - end - - set_player_money(get_player_money() + amount); - set_safe_money(save_id, get_safe_money(save_id) - amount); - return true; -end \ No newline at end of file + elseif default_profile_id>0 then + selected_profile_id = default_profile_id; + end + + print("========================="); + print("persistence: Datastore loaded."); + persistence_data_store_loaded = true; +end -- if persistence_data_store_loaded==false; \ No newline at end of file diff --git a/files/debug.lua b/files/debug.lua deleted file mode 100644 index 09e1c9c..0000000 --- a/files/debug.lua +++ /dev/null @@ -1,64 +0,0 @@ -function str(var) - if type(var) == 'table' then - local s = '{\n' - for k,v in pairs(var) do - if type(k) ~= 'number' then k = '"'..k..'"' end - s = s .. '\t['..k..'] = ' .. string.gsub(str(v), "\n", "\n\t") .. ',\n' - end - return s .. '}' - end - return tostring(var) -end - - -function debug_entity(e) - local parent = EntityGetParent(e) - local children = EntityGetAllChildren(e) - local comps = EntityGetAllComponents(e) - - print("--- ENTITY DATA ---") - print("Parent: ["..parent.."] " .. (EntityGetName(parent) or "nil")) - - print(" Entity: ["..tostring(e).."] " .. (EntityGetName(e) or "nil")) - print(" Tags: " .. (EntityGetTags(e) or "nil")) - if (comps ~= nil) then - for _, comp in ipairs(comps) do - print(" Comp: ["..comp.."] " .. (ComponentGetTypeName(comp) or "nil")) - if comp ~= nil then - for member_key, member_val in pairs(ComponentGetMembers(comp)) do - --[[if member_val == "" then - print(" [" .. member_key .. "]:") - for object_key, object_val in pairs(ComponentObjectGetMembers(comp, member_key)) do - print(" [" .. object_key .. "]: " .. (object_val or "nil")) - end - else]]-- - print(" [" .. member_key .. "]: " .. (member_val or "nil")) - --end - end - end - end - end - - if children == nil then return end - - for _, child in ipairs(children) do - local comps = EntityGetAllComponents(child) - print(" Child: ["..child.."] " .. EntityGetName(child)) - for _, comp in ipairs(comps) do - print(" Comp: ["..comp.."] " .. (ComponentGetTypeName(comp) or "nil")) - if comp ~= nil then - for member_key, member_val in pairs(ComponentGetMembers(comp)) do - --[[if member_val == "" then - print(" [" .. member_key .. "]:") - for object_key, object_val in pairs(ComponentObjectGetMembers(comp, member_key)) do - print(" [" .. object_key .. "]: " .. (object_val or "nil")) - end - else]]-- - print(" [" .. member_key .. "]: " .. (member_val or "nil")) - --end - end - end - end - end - print("--- END ENTITY DATA ---") -end \ No newline at end of file diff --git a/files/edit_wands_in_lobby.xml b/files/edit_wands_in_lobby.xml deleted file mode 100644 index e56c417..0000000 --- a/files/edit_wands_in_lobby.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/files/encoder.lua b/files/encoder.lua new file mode 100644 index 0000000..78248e0 --- /dev/null +++ b/files/encoder.lua @@ -0,0 +1,146 @@ +-- On load only; +if persistence_encoder_loaded~=true then + encoder_read_only = false; + + ---encode number to hex string for saving + ---@param number integer + ---@return string hex_data + local function number_to_hex(number) + if number == nil then return ""; end + + local positive = math.abs(number); + return (positive == number and "" or "-") .. string.format("%x", positive); + end + + ---decode hex string to number + ---@param hex string hex_data + ---@return number + local function hex_to_number(hex) + if hex == nil then return 0; end + + if string.sub(hex, 1, 1) == "-" then + return tonumber(string.sub(hex, 2), 16) * -1; + else + return tonumber(hex, 16); + end + end + + local hex_chars = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "-" } + ---encode hex value and write to disk under named tag (mod_flag_name will be prepended) + ---@param name string name of psuedo-variable + ---@param hex string|nil + local function write_encode_hex(name, hex) + -- print(string.format("write_encode '%s' '%s'", name, hex)) + if encoder_read_only then + -- print("suppressed"); + return; + end + + if hex == nil then + local i=1; + repeat + local hex_found = false; + for j = 1, #hex_chars do + if HasFlagPersistent(mod_flag_name .. "_" .. name .. "_" .. i .. "_" .. hex_chars[j]) then + RemoveFlagPersistent(mod_flag_name .. "_" .. name .. "_" .. i .. "_" .. hex_chars[j]); + hex_found = true; + end + end + i = i + 1; + until not hex_found + return; + end + + for i = 1, #hex do + for j = 1, #hex_chars do + RemoveFlagPersistent(mod_flag_name .. "_" .. name .. "_" .. i .. "_" .. hex_chars[j]); + end + AddFlagPersistent(mod_flag_name .. "_" .. name .. "_" .. i .. "_" .. string.sub(hex, i, i)); + end + for j = 1, #hex_chars do + RemoveFlagPersistent(mod_flag_name .. "_" .. name .. "_" .. #hex + 1 .. "_" .. hex_chars[j]); + end + end + + ---decode hex value of named tag from disk (mod_flag_name will be prepended) + ---@param name string name of pseudo-variable + ---@return string + local function load_decode_hex(name) + -- print(string.format("load_decode '%s'", name)) + local output = ""; + local i = 1; + repeat + local hex_found = false; + for j = 1, #hex_chars do + if HasFlagPersistent(mod_flag_name .. "_" .. name .. "_" .. i .. "_" .. hex_chars[j]) then + output = output .. hex_chars[j]; + hex_found = true; + break; + end + end + i = i + 1; + until not hex_found + return (output == "" and nil or output); + end + -- end local funcions, begin public; + + + + ---clear named flag from disk + ---@param name string name of flag + function encoder_clear_flag(name) + -- print(string.format("encoder_clear '%s'", name)) + if encoder_read_only then + -- print("suppressed"); + return; + end + + RemoveFlagPersistent(mod_flag_name .. "_" .. name); + end + + ---add named flag to disk + ---@param name string name of flag + function encoder_add_flag(name) + -- print(string.format("encoder_add '%s'", name)) + if encoder_read_only then + -- print("suppressed"); + return; + end + + AddFlagPersistent(mod_flag_name .. "_" .. name); + end + + ---check named flag from disk + ---@param name string name of flag + function encoder_has_flag(name) + -- print(string.format("encoder_has '%s'", name)) + return HasFlagPersistent(mod_flag_name .. "_" .. name); + end + + ---decode number value of named tag from disk + ---@param name string name of pseudo-variable (mod_flag_name will be prepended) + ---@return number + function encoder_load_integer(name) + return hex_to_number(load_decode_hex(name)); + end + + ---write encoded number value of named tag to disk + ---@param name string name of pseudo-variable (mod_flag_name will be prepended) + ---@param value integer value of pseudo-variable + function encoder_write_integer(name, value) + write_encode_hex(name, number_to_hex(value)); + end + + ---clear encoded number value of named tag from disk + ---@param name string name of pseudo-variable (mod_flag_name will be prepended) + function encoder_clear_integer(name) + write_encode_hex(name, nil); + end + ---end function declarations, run code here; + + + + print("========================="); + print("persistence: Encoder loaded."); + persistence_encoder_loaded=true; +end diff --git a/files/entity/lobby.xml b/files/entity/lobby.xml new file mode 100644 index 0000000..eb9eeac --- /dev/null +++ b/files/entity/lobby.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/files/entity/lobby_collider.lua b/files/entity/lobby_collider.lua new file mode 100644 index 0000000..4991fec --- /dev/null +++ b/files/entity/lobby_collider.lua @@ -0,0 +1,10 @@ +function collision_trigger(colliding_entity_id) + + GlobalsSetValue("lobby_collider_triggered", "13"); --- triggers every 10 frames, plus leeway + + local _e_id = GetUpdatedEntityID(); + GlobalsSetValue("lobby_e_id", tostring(_e_id)); + if not EntityHasTag(_e_id, "persistence_visited") then + EntityAddTag(_e_id, "persistence_visited"); + end +end diff --git a/files/entity/lobby_effect.lua b/files/entity/lobby_effect.lua new file mode 100644 index 0000000..f7f4dc0 --- /dev/null +++ b/files/entity/lobby_effect.lua @@ -0,0 +1,10 @@ + +if GlobalsGetValue("persistence_active", "false")=="false" then return; end + +local _e_id = GetUpdatedEntityID(); +if _e_id==0 then return; end + +local _c_id = EntityGetFirstComponentIncludingDisabled(_e_id, "GameEffectComponent"); +if _c_id==nil or _c_id==0 then return; end + +EntitySetComponentIsEnabled(_e_id, _c_id, GlobalsGetValue("lobby_collider_triggered", "0")~="0" or GlobalsGetValue("workshop_collider_triggered", "0")~="0"); diff --git a/files/entity/persistence_workshop.xml b/files/entity/persistence_workshop.xml new file mode 100644 index 0000000..a331508 --- /dev/null +++ b/files/entity/persistence_workshop.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/files/wand_empty.xml b/files/entity/wand_empty.xml similarity index 100% rename from files/wand_empty.xml rename to files/entity/wand_empty.xml diff --git a/files/entity/workshop_collider.lua b/files/entity/workshop_collider.lua new file mode 100644 index 0000000..ebe0a70 --- /dev/null +++ b/files/entity/workshop_collider.lua @@ -0,0 +1,11 @@ +function collision_trigger(colliding_entity_id) + + GlobalsSetValue("workshop_collider_triggered", "13"); + + local _e_id = GetUpdatedEntityID(); + GlobalsSetValue("workshop_e_id", tostring(_e_id)); + if not EntityHasTag(_e_id, "persistence_visited") then + EntityAddTag(_e_id, "persistence_visited"); + EntityAddTag(_e_id, "persistence_unpaid"); + end +end diff --git a/files/entity_mgr.lua b/files/entity_mgr.lua new file mode 100644 index 0000000..0dfeae2 --- /dev/null +++ b/files/entity_mgr.lua @@ -0,0 +1,128 @@ +if entity_mgr_loaded==nil then entity_mgr_loaded=false; end +if entity_mgr_loaded==false then +-- one time init + known_workshops = {}; + lobby_e_id = 0; + local _lobby_entity_frame_skip=30; + + local function _do_lobby_effect_check() + if mod_setting.enable_edit_wands_in_lobby==true then + local _lobby_effect_pool = EntityGetWithName("persistence_lobby_effect_entity"); + if _lobby_effect_pool==nil or _lobby_effect_pool==0 then + if type(_lobby_effect_pool)=="table" then + for _, _e_id in ipairs(_lobby_effect_pool) do + EntityKill(_e_id); + end + end + create_lobby_effect_entity(); + end + end + end + + ---close out frame by disabling triggers + function OnModEndFrame() + if mod_disabled then return; end + + local _lobby_frames = tonumber(GlobalsGetValue("lobby_collider_triggered", "0")); + local _workshop_frames = tonumber(GlobalsGetValue("workshop_collider_triggered", "0")); + local _game_frame = GameGetFrameNum(); + + if _lobby_frames>0 then + GlobalsSetValue("lobby_collider_triggered", tostring(_lobby_frames-1)); + end ---decrement trigger per frame, re-enabled by collider entity + + if _workshop_frames>0 then + GlobalsSetValue("workshop_collider_triggered", tostring(_workshop_frames-1)); + end ---decrement trigger per frame, re-enabled by collider entity + + if _game_frame%_lobby_entity_frame_skip==0 and _lobby_frames>1 then + _do_lobby_effect_check(); + end + end + + function LockPlayer() + if player_e_id==0 or not EntityGetIsAlive(player_e_id) then + local _e_id = EntityGetWithTag("player_unit")[1]; + if _e_id~=nil and _e_id~=0 then + player_e_id = _e_id; + else + return; + end + end + EntitySetComponentIsEnabled(player_e_id, EntityGetFirstComponentIncludingDisabled(player_e_id, "PlatformShooterPlayerComponent") or 0, false); --- Recenters camera + EntitySetComponentIsEnabled(player_e_id, EntityGetFirstComponentIncludingDisabled(player_e_id, "CharacterDataComponent") or 0, false); --- Stops movement + EntitySetComponentIsEnabled(player_e_id, EntityGetFirstComponentIncludingDisabled(player_e_id, "DamageModelComponent") or 0, false); --- Prevents damage + EntitySetComponentIsEnabled(player_e_id, EntityGetFirstComponentIncludingDisabled(player_e_id, "InventoryGuiComponent") or 0, false); --- Removes inventory GUI + -- EntitySetComponentIsEnabled(player_e_id, EntityGetFirstComponentIncludingDisabled(player_e_id, "Inventory2Component") or 0, false); --- Disables player inventory + -- EntitySetComponentIsEnabled(player_e_id, EntityGetFirstComponentIncludingDisabled(player_e_id, "ControlsComponent") or 0, false); --- Disables player controls + end + + function UnlockPlayer() + if player_e_id==0 or not EntityGetIsAlive(player_e_id) then + local _e_id = EntityGetWithTag("player_unit")[1]; + if _e_id~=nil and _e_id~=0 then + player_e_id = _e_id; + else + return; + end + end + EntitySetComponentIsEnabled(player_e_id, EntityGetFirstComponentIncludingDisabled(player_e_id, "PlatformShooterPlayerComponent") or 0, true); + EntitySetComponentIsEnabled(player_e_id, EntityGetFirstComponentIncludingDisabled(player_e_id, "CharacterDataComponent") or 0, true); + EntitySetComponentIsEnabled(player_e_id, EntityGetFirstComponentIncludingDisabled(player_e_id, "DamageModelComponent") or 0, true); + EntitySetComponentIsEnabled(player_e_id, EntityGetFirstComponentIncludingDisabled(player_e_id, "InventoryGuiComponent") or 0, true); + -- EntitySetComponentIsEnabled(player_e_id, EntityGetFirstComponentIncludingDisabled(player_e_id, "Inventory2Component") or 0, true); + -- EntitySetComponentIsEnabled(player_e_id, EntityGetFirstComponentIncludingDisabled(player_e_id, "ControlsComponent") or 0, true); + end + + function isLocked() + if player_e_id==0 or not EntityGetIsAlive(player_e_id) then return false; end + return not (ComponentGetIsEnabled(EntityGetFirstComponentIncludingDisabled(player_e_id, "PlatformShooterPlayerComponent") or 0) and + ComponentGetIsEnabled(EntityGetFirstComponentIncludingDisabled(player_e_id, "CharacterDataComponent") or 0) and + ComponentGetIsEnabled(EntityGetFirstComponentIncludingDisabled(player_e_id, "DamageModelComponent") or 0) and + ComponentGetIsEnabled(EntityGetFirstComponentIncludingDisabled(player_e_id, "InventoryGuiComponent") or 0) ); + end + + ---end function declarations, run code here; + + + +end + + +-- TODO : Remove Move lobby to spawn mod setting? + + +-- every frame +local _frame_skip=10; +if GameGetFrameNum()%_frame_skip==0 then -- every ten frames, for performance + if lobby_e_id==0 and player_e_id~=0 then + lobby_e_id=EntityGetWithName("persistence_lobby"); + if lobby_e_id==0 then + local x_loc = GlobalsGetValue("first_spawn_x", "x"); + local y_loc = GlobalsGetValue("first_spawn_y", "x"); + lobby_e_id = EntityLoad(mod_dir .. "files/entity/lobby.xml", x_loc, y_loc); + end + end + + local workshop_pool = EntityGetWithTag("workshop"); + for _, workshop_e_id in ipairs(workshop_pool) do + if not EntityHasTag(workshop_e_id, "persistence_cloned") then + local workshop_hitbox_comp = EntityGetComponent(workshop_e_id, "HitboxComponent")[1]; + if workshop_hitbox_comp~=nil then + local workshop_hitbox = ComponentGetMembers(workshop_hitbox_comp); + if workshop_hitbox~=nil then + print("persistence: entity_mgr.lua: cloned workshop " .. workshop_e_id); + local _width = workshop_hitbox["aabb_max_x"] - workshop_hitbox["aabb_min_x"]; + local _height = workshop_hitbox["aabb_max_y"] - workshop_hitbox["aabb_min_y"]; + local _xloc, _yloc = EntityGetFirstHitboxCenter(workshop_e_id); + local new_workshop_id = EntityLoad(mod_dir .. "files/entity/persistence_workshop.xml", _xloc, _yloc); + EntityAddComponent2(new_workshop_id, "CollisionTriggerComponent", {width=_width, height=_height, radius=1000, destroy_this_entity_when_triggered=false, required_tag="player_unit", _enabled=true}); + EntityAddTag(workshop_e_id, "persistence_cloned"); + if mod_setting.reusable_holy_mountain~=true then + EntityAddChild(workshop_e_id, new_workshop_id); + end + end + end + end + end +end \ No newline at end of file diff --git a/files/gui.lua b/files/gui.lua index b5a992c..31277a0 100644 --- a/files/gui.lua +++ b/files/gui.lua @@ -1,1114 +1,126 @@ -dofile_once("mods/persistence/config.lua"); -dofile_once("mods/persistence/files/data_store.lua"); -dofile_once("mods/persistence/files/helper.lua"); -dofile_once("data/scripts/gun/procedural/wands.lua"); -dofile_once("mods/persistence/files/wand_spell_helper.lua"); - -local gui = GuiCreate(); -local active_windows = {}; - -local function gui_sprite(x, y, file_path) - local cx, cy = GameGetCameraPos(); - local size_x, size_y = get_screen_size(); - GameCreateSpriteForXFrames(file_path, cx - size_x / 2 + size_x * (x / 100), cy - size_y / 2 + size_y * (y / 100), false); -end - -function show_save_selector_gui() - local delete_save_confirmation = 0; - active_windows["save_selector"] = { true, function (get_next_id) - GuiLayoutBeginVertical(gui, 1, 20); - for i = 1, get_save_count() do - GuiText(gui, 0, 0, "Save slot " .. pad_number(i, #tostring(get_save_count())) .. ":"); - if get_save_ids()[i] == nil then - if GuiButton(gui, 20, 0, "Create new save", get_next_id()) then - set_selected_save_id(i); - create_new_save(i); - hide_save_selector_gui(); - OnSaveAvailable(i); - enable_controlls(); - end - else - if GuiButton(gui, 20, 0, "Load save", get_next_id()) then - set_selected_save_id(i); - load(i); - hide_save_selector_gui(); - OnSaveAvailable(i); - enable_controlls(); - end - if delete_save_confirmation == i then - if GuiButton(gui, 20, 0, "Press again to delete", get_next_id()) then - delete_save_confirmation = 0; - delete_save(i); - end - else - if GuiButton(gui, 20, 0, "Delete save", get_next_id()) then - delete_save_confirmation = i; - end - end - end - end - if GuiButton(gui, 0, 20, "Play without this mod", get_next_id()) then - set_selected_save_id(0); - hide_save_selector_gui(); - enable_controlls(); - end - GuiLayoutEnd(gui); - end }; -end - -function hide_save_selector_gui() - active_windows["save_selector"] = nil; -end - -function show_money_gui() - active_windows["money"] = { false, function(get_next_id) - local save_id = get_selected_save_id(); - local safe_money = get_safe_money(save_id); - local player_money = get_player_money(); - - GuiLayoutBeginHorizontal(gui, 85, 15); - GuiLayoutBeginVertical(gui, 0, 0); - if safe_money < 1 then - GuiText(gui, 0, 0, "^ 1$"); - else - if GuiButton(gui, 0, 0, "^ 1$", get_next_id()) then - transfer_money_to_player(save_id, 1); - end - end - if safe_money < 10 then - GuiText(gui, 0, 0, "^ 10$"); - else - if GuiButton(gui, 0, 0, "^ 10$", get_next_id()) then - transfer_money_to_player(save_id, 10); - end - end - if safe_money < 100 then - GuiText(gui, 0, 0, "^ 100$"); - else - if GuiButton(gui, 0, 0, "^ 100$", get_next_id()) then - transfer_money_to_player(save_id, 100); - end - end - if safe_money < 1000 then - GuiText(gui, 0, 0, "^ 1000$"); - else - if GuiButton(gui, 0, 0, "^ 1000$", get_next_id()) then - transfer_money_to_player(save_id, 1000); - end - end - if GuiButton(gui, 0, 0, "^ ALL", get_next_id()) then - transfer_money_to_player(save_id, safe_money); - end - GuiLayoutEnd(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutBeginVertical(gui, 0, 0); - if GuiButton(gui, 0, 0, "v ALL", get_next_id()) then - transfer_money_to_safe(save_id, player_money); - end - if player_money < 1000 then - GuiText(gui, 0, 0, "v 1000$"); - else - if GuiButton(gui, 0, 0, "v 1000$", get_next_id()) then - transfer_money_to_safe(save_id, 1000); - end - end - if player_money < 100 then - GuiText(gui, 0, 0, "v 100$"); - else - if GuiButton(gui, 0, 0, "v 100$", get_next_id()) then - transfer_money_to_safe(save_id, 100); - end - end - if player_money < 10 then - GuiText(gui, 0, 0, "v 10$"); - else - if GuiButton(gui, 0, 0, "v 10$", get_next_id()) then - transfer_money_to_safe(save_id, 10); - end - end - if player_money < 1 then - GuiText(gui, 0, 0, "v 1$"); - else - if GuiButton(gui, 0, 0, "v 1$", get_next_id()) then - transfer_money_to_safe(save_id, 1); - end - end - GuiLayoutEnd(gui); - GuiLayoutEnd(gui); - GuiLayoutBeginHorizontal(gui, 86, 31); - GuiText(gui, 0, 0, " $" .. tostring(safe_money)); - GuiLayoutEnd(gui); - end }; -end - -function hide_money_gui() - active_windows["money"] = nil; -end - -function show_teleport_gui() - local teleport_confirmation = false; - active_windows["teleport"] = { false, function(get_next_id) - GuiLayoutBeginHorizontal(gui, 45, 1); - if teleport_confirmation then - if GuiButton(gui, 0, 0, "Press again to teleport", get_next_id()) then - teleport_back_to_lobby(); - end - else - if GuiButton(gui, 0, 0, "Teleport back up", get_next_id()) then - teleport_confirmation = true; - end - end - GuiLayoutEnd(gui); - end }; -end - -function hide_teleport_gui() - active_windows["teleport"] = nil; +if persistence_active==false then return; end +if persistence_gui_loaded~=true then + -- once, on load + dofile_once("data/scripts/debug/keycodes.lua"); + dofile_once(mod_dir .. "files/data_store.lua"); + dofile_once(mod_dir .. "files/gui_subfunc.lua"); + + gui = GuiCreate(); + active_windows = {}; + fourslot_table = {}; + fourslot_confirmation = 0; + spell_list_confirmation = 0; + small_text_scale = 0.9; + spell_tooltip_id = ""; + _right_panel_id = 0; + + window_open=false; + + function __nil(...) return; end + function __layer(n) return 1000 - ((n+1) * 50); end + + dofile_once(mod_dir .. "files/gui/fourslot.lua"); + dofile_once(mod_dir .. "files/gui/modify_wand.lua"); + dofile_once(mod_dir .. "files/gui/money.lua"); + dofile_once(mod_dir .. "files/gui/persistence.lua"); + dofile_once(mod_dir .. "files/gui/scan_nearby_entities.lua"); + dofile_once(mod_dir .. "files/gui/spell_list.lua"); + dofile_once(mod_dir .. "files/gui/spell_loadout.lua"); + dofile_once(mod_dir .. "files/gui/spell_tooltip.lua"); + dofile_once(mod_dir .. "files/gui/teleport.lua"); + dofile_once(mod_dir .. "files/gui/wand_template.lua"); + + ---Close all open windows + function close_open_windows() + -- close_profile_select(); + close_wands(); + close_money(); + close_wands(); + close_purchase_spells(); + close_inventory_spells(); + close_modify_wand(); + -- close_wand_template(); + close_scan_nearby_entities(); + -- close_spell_loadouts(); + right_panel_picker(0); + -- close_persistence_menu(); + -- close_spell_tooltip(); + end + ---end function declarations, run code here; + + print("========================="); + print("persistence: GUI loaded."); + persistence_gui_loaded=true; +end +-- every frame; +if selected_profile_id~=DISABLE_PROFILE_ID then + _in_lobby = GlobalsGetValue("lobby_collider_triggered", "0")~="0"; + _in_workshop = GlobalsGetValue("workshop_collider_triggered", "0")~="0"; + _allow_teleport = _in_workshop and mod_setting.enable_teleport_back_up==true; + _allow_workshop = _in_workshop and mod_setting.enable_menu_in_holy_mountain==true; + _in_persistence_area = _in_lobby or _allow_workshop; + _persistence_available = _in_persistence_area or mod_setting.global_persistence==true; + + data_store_everyframe(); + if spell_tooltip_id=="" then close_spell_tooltip(); end + spell_tooltip_id=""; + + if loaded_profile_id>0 then + ---profile loaded, proceed as normal + if profile_open then close_profile_select(); end + + if isLocked() then UnlockPlayer(); end + + present_scan_nearby_entities(); + + if InputIsKeyJustDown(Key_ESCAPE) then + close_open_windows(); + end + + if _allow_teleport then + present_teleport(); + else + close_teleport(); + end + if _persistence_available then + present_persistence_menu(); + else + close_persistence_menu(); + end + elseif selected_profile_id==DISABLE_PROFILE_ID then + if isLocked() then UnlockPlayer(); end + close_open_windows(); + close_profile_select(); + else + if not isLocked() then LockPlayer(); end + present_profile_select(); + end +else + if isLocked() then UnlockPlayer(); end + close_open_windows(); + close_profile_select(); +end + +window_open = profile_open or money_open or wands_open or inventory_spells_open or modify_wand_open; +-- if window_open and spell_tooltip_id~="" then show_spell_tooltip_gui(); end + +if gui~=nil and active_windows~=nil and EntityGetIsAlive(player_e_id) then + GuiStartFrame(gui); + local start_gui_id = 319585; + if window_open then + if not isLocked() then LockPlayer(); end + GuiZSetForNextWidget(gui, 1000); + GuiImage(gui, start_gui_id - 1, 0, 0, mod_dir .. "files/img/gui_darken.png", 1, 1, 1, 0); + end + for name, window in pairs(active_windows) do + if window~=nil then + local gui_id = start_gui_id + simple_string_hash(name); + window(function() + gui_id = gui_id + 1; + return gui_id; + end); + end + end +else + if isLocked() then UnlockPlayer(); end end - -local research_wands_open = false; -function show_research_wands_gui() - research_wands_open = true; - local wand_entity_ids = get_all_wands(); - local wands = {}; - - for pos, entity_id in pairs(wand_entity_ids) do - wands[pos] = { - ["entity_id"] = entity_id, - ["wand_data"] = read_wand(entity_id) - }; - end - - active_windows["research_wands"] = { true, function(get_next_id) - local player_money = get_player_money(); - GuiLayoutBeginHorizontal(gui, 30, 30); - GuiLayoutBeginVertical(gui, 0, 0); - for i = 1, 4 do - GuiText(gui, 0, 0, "Inventory Slot " .. tostring(i) .. ":"); - end - GuiLayoutEnd(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutBeginVertical(gui, 0, 0); - for i = 0, 3 do - if wands[i] ~= nil then - local price = research_wand_price(get_selected_save_id(), wands[i].entity_id); - local is_new = research_wand_is_new(get_selected_save_id(), wands[i].entity_id); - if is_new then - if price > player_money then - GuiText(gui, 0, 0, tostring(price) .. "$"); - else - if #wands[i].wand_data.spells > 0 then - if GuiButton(gui, 0, 0, tostring(price) .. "$", get_next_id()) then - research_wand(get_selected_save_id(), wands[i].entity_id); - wands[i] = nil; - end - else - if GuiButton(gui, 0, 0, tostring(price) .. "$", get_next_id()) then - research_wand(get_selected_save_id(), wands[i].entity_id); - wands[i] = nil; - end - end - end - else - GuiText(gui, 0, 0, "0$"); - end - else - GuiText(gui, 0, 0, " "); - end - end - GuiLayoutEnd(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutBeginVertical(gui, 0, 0); - for i = 0, 3 do - if wands[i] ~= nil then - local price = research_wand_price(get_selected_save_id(), wands[i].entity_id); - local is_new = research_wand_is_new(get_selected_save_id(), wands[i].entity_id); - if is_new then - if price > player_money then - GuiText(gui, 0, 0, "You can't afford that"); - else - if #wands[i].wand_data.spells > 0 then - GuiText(gui, 0, 0, "WARNING: The spells on this wand will be lost"); - else - GuiText(gui, 0, 0, " "); - end - end - else - GuiText(gui, 0, 0, "This wand does not have anything new to research"); - end - else - GuiText(gui, 0, 0, " "); - end - end - GuiLayoutEnd(gui); - GuiLayoutEnd(gui); - end }; -end - -function hide_research_wands_gui() - research_wands_open = false; - active_windows["research_wands"] = nil; -end - -local research_spells_open = false; -function show_research_spells_gui() - research_spells_open = true; - local spell_entity_ids = get_all_spells(); - local researched_spells = get_spells(get_selected_save_id()); - local spell_data_temp = {}; - local spell_data = {}; - - for i = 1, #spell_entity_ids do - local action_id = read_spell(spell_entity_ids[i]); - if action_id ~= nil then - if researched_spells[action_id] == nil then - spell_data_temp[action_id] = spell_entity_ids[i]; - end - end - end - for i = 1, #actions do - local entity_id = spell_data_temp[actions[i].id]; - if entity_id ~= nil then - table.insert(spell_data, { - ["entity_id"] = entity_id, - ["id"] = actions[i].id, - ["name"] = GameTextGetTranslatedOrNot(actions[i].name), - ["price"] = research_spell_price(entity_id) - }); - end - end - table.sort(spell_data, function(a, b) return a.name < b.name end); - - active_windows["research_spells"] = { true, function(get_next_id) - if #spell_data > 0 then - local player_money = get_player_money(); - GuiLayoutBeginHorizontal(gui, 40, 15); - GuiLayoutBeginVertical(gui, 0, 0); - for _, value in ipairs(spell_data) do - if player_money < value.price then - GuiText(gui, 0, 0, tostring(value.price) .. "$"); - else - if GuiButton(gui, 0, 0, tostring(value.price) .. "$", get_next_id()) then - research_spell(get_selected_save_id(), value.entity_id); - hide_research_spells_gui(); - show_research_spells_gui(); - end - end - end - GuiLayoutEnd(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutBeginVertical(gui, 0, 0); - for _, value in ipairs(spell_data) do - GuiText(gui, 0, 0, value.name); - end - GuiLayoutEnd(gui); - GuiLayoutEnd(gui); - else - GuiLayoutBeginHorizontal(gui, 40, 30); - GuiText(gui, 0, 0, "No new spells to research"); - GuiLayoutEnd(gui); - end - end }; -end - -function hide_research_spells_gui() - research_spells_open = false; - active_windows["research_spells"] = nil; -end - -local buy_wands_open = false; -function show_buy_wands_gui() - buy_wands_open = true; - local save_id = get_selected_save_id(); - if can_create_wand(save_id) then - local window_nr = 0; - local spells_per_cast = get_spells_per_cast(save_id); - local cast_delay_min = get_cast_delay_min(save_id); - local cast_delay_max = get_cast_delay_max(save_id); - local recharge_time_min = get_recharge_time_min(save_id); - local recharge_time_max = get_recharge_time_max(save_id); - local mana_max = get_mana_max(save_id); - local mana_charge_speed = get_mana_charge_speed(save_id); - local capacity = get_capacity(save_id); - local spread_min = get_spread_min(save_id); - local spread_max = get_spread_max(save_id); - local wand_types = get_wand_types(save_id); - local always_cast_spells = get_always_cast_spells(save_id); - local wand_data_selected = { - ["shuffle"] = true, - ["spells_per_cast"] = spells_per_cast_min, - ["cast_delay"] = math.floor((cast_delay_min + cast_delay_max)/2), - ["recharge_time"] = math.floor((recharge_time_min + recharge_time_max)/2), - ["mana_max"] = mana_max_min, - ["mana_charge_speed"] = mana_charge_speed_min, - ["capacity"] = capacity_min, - ["spread"] = spread_min, - ["always_cast_spells"] = {}, - ["wand_type"] = "default_1"; - }; - local delete_template_confirmation = 0; - - local spells_page_number = 1; - local spell_data = {}; - - for i = 1, #actions do - if always_cast_spells[actions[i].id] ~= nil then - table.insert(spell_data, { - ["id"] = actions[i].id, - ["name"] = GameTextGetTranslatedOrNot(actions[i].name), - ["selected"] = false - }); - end - end - - local function toggle_select_spell(action_id) - local selected = false; - for i = 1, #spell_data do - if spell_data[i].id == action_id then - selected = not spell_data[i].selected; - spell_data[i].selected = selected; - break; - end - end - for i = 1, #wand_data_selected["always_cast_spells"] do - if wand_data_selected["always_cast_spells"][i] == action_id then - table.remove(wand_data_selected["always_cast_spells"], i); - break; - end - end - if selected then - table.insert(wand_data_selected["always_cast_spells"], action_id); - end - end - - table.sort(spell_data, function(a, b) return a.name < b.name end); - local spell_columns = split_array(spell_data, 20); - - local wand_types_page_number = 1; - local wand_type_list = {}; - - for wand_type, _ in pairs(wand_types) do - table.insert(wand_type_list, { - ["wand_type"] = wand_type, - ["sprite_file"] = wand_type_to_sprite_file(wand_type) - }); - end - - table.sort(wand_type_list, function(a, b) return a.wand_type < b.wand_type end); - local wand_type_columns = split_array(wand_type_list, 5); - - active_windows["buy_wands"] = { true, function(get_next_id) - local player_money = get_player_money(); - local price = create_wand_price(wand_data_selected); - if window_nr == 0 then - GuiLayoutBeginHorizontal(gui, 20, 15); - GuiLayoutBeginVertical(gui, 0, 0); - GuiText(gui, 0, 0, "$inventory_shuffle"); - GuiText(gui, 0, 0, "$inventory_actionspercast"); - GuiText(gui, 0, 0, "$inventory_castdelay"); - GuiText(gui, 0, 0, "$inventory_rechargetime"); - GuiText(gui, 0, 0, "$inventory_manamax"); - GuiText(gui, 0, 0, "$inventory_manachargespeed"); - GuiText(gui, 0, 0, "$inventory_capacity"); - GuiText(gui, 0, 0, "$inventory_spread"); - GuiText(gui, 0, 0, "$inventory_alwayscasts"); - GuiText(gui, 0, 0, "Wand design"); - GuiLayoutEnd(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutBeginVertical(gui, 0, 0); - GuiText(gui, 0, 0, " "); - GuiText(gui, 0, 0, " "); - if wand_data_selected["cast_delay"] - 60 >= cast_delay_min then - if GuiButton(gui, 0, 0, "<<<", get_next_id()) then - wand_data_selected["cast_delay"] = wand_data_selected["cast_delay"] - 60; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["recharge_time"] - 60 >= recharge_time_min then - if GuiButton(gui, 0, 0, "<<<", get_next_id()) then - wand_data_selected["recharge_time"] = wand_data_selected["recharge_time"] - 60; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["mana_max"] - 100 >= mana_max_min then - if GuiButton(gui, 0, 0, "<<<", get_next_id()) then - wand_data_selected["mana_max"] = wand_data_selected["mana_max"] - 100; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["mana_charge_speed"] - 100 >= mana_charge_speed_min then - if GuiButton(gui, 0, 0, "<<<", get_next_id()) then - wand_data_selected["mana_charge_speed"] = wand_data_selected["mana_charge_speed"] - 100; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - GuiText(gui, 0, 0, " "); - if wand_data_selected["spread"] - 10 >= spread_min then - if GuiButton(gui, 0, 0, "<<<<", get_next_id()) then - wand_data_selected["spread"] = wand_data_selected["spread"] - 10; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - GuiText(gui, 0, 0, " "); - GuiLayoutEnd(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutBeginVertical(gui, 0, 0); - GuiText(gui, 0, 0, " "); - GuiText(gui, 0, 0, " "); - if wand_data_selected["cast_delay"] - 6 >= cast_delay_min then - if GuiButton(gui, 0, 0, "<<", get_next_id()) then - wand_data_selected["cast_delay"] = wand_data_selected["cast_delay"] - 6; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["recharge_time"] - 6 >= recharge_time_min then - if GuiButton(gui, 0, 0, "<<", get_next_id()) then - wand_data_selected["recharge_time"] = wand_data_selected["recharge_time"] - 6; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["mana_max"] - 10 >= mana_max_min then - if GuiButton(gui, 0, 0, "<<", get_next_id()) then - wand_data_selected["mana_max"] = wand_data_selected["mana_max"] - 10; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["mana_charge_speed"] - 10 >= mana_charge_speed_min then - if GuiButton(gui, 0, 0, "<<", get_next_id()) then - wand_data_selected["mana_charge_speed"] = wand_data_selected["mana_charge_speed"] - 10; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["capacity"] - 10 >= capacity_min then - if GuiButton(gui, 0, 0, "<<", get_next_id()) then - wand_data_selected["capacity"] = wand_data_selected["capacity"] - 10; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["spread"] - 1 >= spread_min then - if GuiButton(gui, 0, 0, "<<", get_next_id()) then - wand_data_selected["spread"] = wand_data_selected["spread"] - 1; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - GuiText(gui, 0, 0, " "); - GuiLayoutEnd(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutBeginVertical(gui, 0, 0); - if wand_data_selected["shuffle"] == true then - if GuiButton(gui, 0, 0, "<", get_next_id()) then - wand_data_selected["shuffle"] = false; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["spells_per_cast"] > spells_per_cast_min then - if GuiButton(gui, 0, 0, "<", get_next_id()) then - wand_data_selected["spells_per_cast"] = wand_data_selected["spells_per_cast"] - 1; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["cast_delay"] > cast_delay_min then - if GuiButton(gui, 0, 0, "<", get_next_id()) then - wand_data_selected["cast_delay"] = wand_data_selected["cast_delay"] - 1; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["recharge_time"] > recharge_time_min then - if GuiButton(gui, 0, 0, "<", get_next_id()) then - wand_data_selected["recharge_time"] = wand_data_selected["recharge_time"] - 1; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["mana_max"] > mana_max_min then - if GuiButton(gui, 0, 0, "<", get_next_id()) then - wand_data_selected["mana_max"] = wand_data_selected["mana_max"] - 1; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["mana_charge_speed"] > mana_charge_speed_min then - if GuiButton(gui, 0, 0, "<", get_next_id()) then - wand_data_selected["mana_charge_speed"] = wand_data_selected["mana_charge_speed"] - 1; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["capacity"] > capacity_min then - if GuiButton(gui, 0, 0, "<", get_next_id()) then - wand_data_selected["capacity"] = wand_data_selected["capacity"] - 1; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["spread"] - 0.1 >= spread_min then - if GuiButton(gui, 0, 0, "<", get_next_id()) then - wand_data_selected["spread"] = wand_data_selected["spread"] - 0.1; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - GuiText(gui, 0, 0, " "); - GuiLayoutEnd(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutBeginVertical(gui, 0, 0); - GuiText(gui, 0, 0, wand_data_selected["shuffle"] and "$menu_yes" or "$menu_no"); - GuiText(gui, 0, 0, tostring(wand_data_selected["spells_per_cast"])); - GuiText(gui, 0, 0, tostring(math.floor((wand_data_selected["cast_delay"] / 60) * 100 + 0.5) / 100)); - GuiText(gui, 0, 0, tostring(math.floor((wand_data_selected["recharge_time"] / 60) * 100 + 0.5) / 100)); - GuiText(gui, 0, 0, tostring(wand_data_selected["mana_max"])); - GuiText(gui, 0, 0, tostring(wand_data_selected["mana_charge_speed"])); - GuiText(gui, 0, 0, tostring(wand_data_selected["capacity"])); - GuiText(gui, 0, 0, tostring(math.floor(wand_data_selected["spread"] * 10 + 0.5) / 10)); - if GuiButton(gui, 0, 0, "Select", get_next_id()) then - window_nr = 1; - end - if GuiButton(gui, 0, 0, "Select", get_next_id()) then - window_nr = 2; - end - GuiLayoutEnd(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutBeginVertical(gui, 0, 0); - if wand_data_selected["shuffle"] == false then - if GuiButton(gui, 0, 0, ">", get_next_id()) then - wand_data_selected["shuffle"] = true; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["spells_per_cast"] < spells_per_cast then - if GuiButton(gui, 0, 0, ">", get_next_id()) then - wand_data_selected["spells_per_cast"] = wand_data_selected["spells_per_cast"] + 1; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["cast_delay"] < cast_delay_max then - if GuiButton(gui, 0, 0, ">", get_next_id()) then - wand_data_selected["cast_delay"] = wand_data_selected["cast_delay"] + 1; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["recharge_time"] < recharge_time_max then - if GuiButton(gui, 0, 0, ">", get_next_id()) then - wand_data_selected["recharge_time"] = wand_data_selected["recharge_time"] + 1; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["mana_max"] < mana_max then - if GuiButton(gui, 0, 0, ">", get_next_id()) then - wand_data_selected["mana_max"] = wand_data_selected["mana_max"] + 1; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["mana_charge_speed"] < mana_charge_speed then - if GuiButton(gui, 0, 0, ">", get_next_id()) then - wand_data_selected["mana_charge_speed"] = wand_data_selected["mana_charge_speed"] + 1; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["capacity"] < capacity then - if GuiButton(gui, 0, 0, ">", get_next_id()) then - wand_data_selected["capacity"] = wand_data_selected["capacity"] + 1; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["spread"] + 0.1 <= spread_max then - if GuiButton(gui, 0, 0, ">", get_next_id()) then - wand_data_selected["spread"] = wand_data_selected["spread"] + 0.1; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - GuiText(gui, 0, 0, " "); - GuiLayoutEnd(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutBeginVertical(gui, 0, 0); - GuiText(gui, 0, 0, " "); - GuiText(gui, 0, 0, " "); - if wand_data_selected["cast_delay"] + 6 <= cast_delay_max then - if GuiButton(gui, 0, 0, ">>", get_next_id()) then - wand_data_selected["cast_delay"] = wand_data_selected["cast_delay"] + 6; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["recharge_time"] + 6 <= recharge_time_max then - if GuiButton(gui, 0, 0, ">>", get_next_id()) then - wand_data_selected["recharge_time"] = wand_data_selected["recharge_time"] + 6; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["mana_max"] + 10 <= mana_max then - if GuiButton(gui, 0, 0, ">>", get_next_id()) then - wand_data_selected["mana_max"] = wand_data_selected["mana_max"] + 10; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["mana_charge_speed"] + 10 <= mana_charge_speed then - if GuiButton(gui, 0, 0, ">>", get_next_id()) then - wand_data_selected["mana_charge_speed"] = wand_data_selected["mana_charge_speed"] + 10; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["capacity"] + 10 <= capacity then - if GuiButton(gui, 0, 0, ">>", get_next_id()) then - wand_data_selected["capacity"] = wand_data_selected["capacity"] + 10; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["spread"] + 1 <= spread_max then - if GuiButton(gui, 0, 0, ">>", get_next_id()) then - wand_data_selected["spread"] = wand_data_selected["spread"] + 1; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - GuiText(gui, 0, 0, " "); - GuiLayoutEnd(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutBeginVertical(gui, 0, 0); - GuiText(gui, 0, 0, " "); - GuiText(gui, 0, 0, " "); - if wand_data_selected["cast_delay"] + 60 <= cast_delay_max then - if GuiButton(gui, 0, 0, ">>>", get_next_id()) then - wand_data_selected["cast_delay"] = wand_data_selected["cast_delay"] + 60; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["recharge_time"] + 60 <= recharge_time_max then - if GuiButton(gui, 0, 0, ">>>", get_next_id()) then - wand_data_selected["recharge_time"] = wand_data_selected["recharge_time"] + 60; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["mana_max"] + 100 <= mana_max then - if GuiButton(gui, 0, 0, ">>>", get_next_id()) then - wand_data_selected["mana_max"] = wand_data_selected["mana_max"] + 100; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - if wand_data_selected["mana_charge_speed"] + 100 <= mana_charge_speed then - if GuiButton(gui, 0, 0, ">>>", get_next_id()) then - wand_data_selected["mana_charge_speed"] = wand_data_selected["mana_charge_speed"] + 100; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - GuiText(gui, 0, 0, " "); - if wand_data_selected["spread"] + 10 <= spread_max then - if GuiButton(gui, 0, 0, ">>>", get_next_id()) then - wand_data_selected["spread"] = wand_data_selected["spread"] + 10; - end - else - GuiButton(gui, 0, 0, " ", get_next_id()); - end - GuiText(gui, 0, 0, " "); - GuiLayoutEnd(gui); - GuiLayoutEnd(gui); - gui_sprite(22, 48, wand_type_to_sprite_file(wand_data_selected["wand_type"])); - - GuiLayoutBeginVertical(gui, 80, 50); - for i = 1, get_template_count() do - GuiText(gui, 0, 0, "Wand template slot " .. pad_number(i, #tostring(get_template_count())) .. ":"); - if get_template(save_id, i) == nil then - if GuiButton(gui, 40, 0, "Save template", get_next_id()) then - set_template(save_id, i, wand_data_selected); - end - else - if GuiButton(gui, 40, 0, "Load template", get_next_id()) then - wand_data_selected = get_template(save_id, i); - end - if delete_template_confirmation == i then - if GuiButton(gui, 40, 0, "Press again to delete", get_next_id()) then - delete_template_confirmation = 0; - delete_template(save_id, i); - end - else - if GuiButton(gui, 40, 0, "Delete template", get_next_id()) then - delete_template_confirmation = i; - end - end - end - end - GuiLayoutEnd(gui); - elseif window_nr == 1 then - if spell_columns[spells_page_number * 2 - 1] ~= nil then - GuiLayoutBeginHorizontal(gui, 30, 15); - GuiLayoutBeginVertical(gui, 0, 0); - for _, value in ipairs(spell_columns[spells_page_number * 2 - 1]) do - if GuiButton(gui, 0, 0, "[" .. (value.selected and "x" or " ") .. "]", get_next_id()) then - toggle_select_spell(value.id); - end - end - GuiLayoutEnd(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutBeginVertical(gui, 0, 0); - for _, value in ipairs(spell_columns[spells_page_number * 2 - 1]) do - GuiText(gui, 0, 0, value.name); - end - GuiLayoutEnd(gui); - GuiLayoutEnd(gui); - end - if spell_columns[spells_page_number * 2] ~= nil then - GuiLayoutBeginHorizontal(gui, 60, 15); - GuiLayoutBeginVertical(gui, 0, 0); - for _, value in ipairs(spell_columns[spells_page_number * 2]) do - if GuiButton(gui, 0, 0, "[" .. (value.selected and "x" or " ") .. "]", get_next_id()) then - toggle_select_spell(value.id); - end - end - GuiLayoutEnd(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutBeginVertical(gui, 0, 0); - for _, value in ipairs(spell_columns[spells_page_number * 2]) do - GuiText(gui, 0, 0, value.name); - end - GuiLayoutEnd(gui); - GuiLayoutEnd(gui); - end - if spells_page_number > 1 then - GuiLayoutBeginHorizontal(gui, 48, 95); - if GuiButton(gui, 0, 0, "<<", get_next_id()) then - spells_page_number = spells_page_number - 1; - end - GuiLayoutEnd(gui); - end - GuiLayoutBeginHorizontal(gui, 50, 95); - GuiText(gui, 0, 0, tostring(spells_page_number)); - GuiLayoutEnd(gui); - if spells_page_number < math.ceil(#spell_columns / 2) then - GuiLayoutBeginHorizontal(gui, 52, 95); - if GuiButton(gui, 0, 0, ">>", get_next_id()) then - spells_page_number = spells_page_number + 1; - end - GuiLayoutEnd(gui); - end - elseif window_nr == 2 then - if wand_type_columns[wand_types_page_number * 2 - 1] ~= nil then - for i, value in ipairs(wand_type_columns[wand_types_page_number * 2 - 1]) do - GuiLayoutBeginHorizontal(gui, 20, 16 + i * 10); - if GuiButton(gui, 0, 0, "Select", get_next_id()) then - wand_data_selected["wand_type"] = value.wand_type; - window_nr = 0; - wand_types_page_number = 1; - end - GuiLayoutEnd(gui); - end - for i, value in ipairs(wand_type_columns[wand_types_page_number * 2 - 1]) do - gui_sprite(25, 15 + i * 10, value.sprite_file); - end - end - if wand_type_columns[wand_types_page_number * 2] ~= nil then - for i, value in ipairs(wand_type_columns[wand_types_page_number * 2]) do - GuiLayoutBeginHorizontal(gui, 60, 16 + i * 10); - if GuiButton(gui, 0, 0, "Select", get_next_id()) then - wand_data_selected["wand_type"] = value.wand_type; - window_nr = 0; - wand_types_page_number = 1; - end - GuiLayoutEnd(gui); - end - for i, value in ipairs(wand_type_columns[wand_types_page_number * 2]) do - gui_sprite(65, 15 + i * 10, value.sprite_file); - end - end - if wand_types_page_number > 1 then - GuiLayoutBeginHorizontal(gui, 48, 95); - if GuiButton(gui, 0, 0, "<<", get_next_id()) then - wand_types_page_number = wand_types_page_number - 1; - end - GuiLayoutEnd(gui); - end - GuiLayoutBeginHorizontal(gui, 50, 95); - GuiText(gui, 0, 0, tostring(wand_types_page_number)); - GuiLayoutEnd(gui); - if wand_types_page_number < math.ceil(#wand_type_columns / 2) then - GuiLayoutBeginHorizontal(gui, 52, 95); - if GuiButton(gui, 0, 0, ">>", get_next_id()) then - wand_types_page_number = wand_types_page_number + 1; - end - GuiLayoutEnd(gui); - end - end - if window_nr ~= 0 then - GuiLayoutBeginHorizontal(gui, 15, 15); - if GuiButton(gui, 0, 0, "$menu_return", get_next_id()) then - window_nr = 0; - spells_page_number = 1; - wand_types_page_number = 1; - end - GuiLayoutEnd(gui); - end - - GuiLayoutBeginHorizontal(gui, 20, 95); - if player_money < price then - GuiText(gui, 0, 0, tostring(price) .. "$ You can't afford that"); - else - if GuiButton(gui, 0, 0, tostring(price) .. "$ Buy", get_next_id()) then - create_wand(wand_data_selected); - end - end - GuiLayoutEnd(gui); - end }; - else - active_windows["buy_wands"] = { true, function(get_next_id) - GuiLayoutBeginHorizontal(gui, 40, 30); - GuiText(gui, 0, 0, "You don't have enough research to create a wand"); - GuiLayoutEnd(gui); - end }; - end -end - -function hide_buy_wands_gui() - buy_wands_open = false; - active_windows["buy_wands"] = nil; -end - -local buy_spells_open = false; -function show_buy_spells_gui() - buy_spells_open = true; - local page_number = 1; - local spells = get_spells(get_selected_save_id()); - local spell_data = {}; - - for i = 1, #actions do - if spells[actions[i].id] ~= nil then - table.insert(spell_data, { - ["id"] = actions[i].id, - ["name"] = GameTextGetTranslatedOrNot(actions[i].name), - ["price"] = create_spell_price(actions[i].id) - }); - end - end - - table.sort(spell_data, function(a, b) return a.name < b.name end); - local columns = split_array(spell_data, 20); - - active_windows["buy_spells"] = { true, function(get_next_id) - local player_money = get_player_money(); - if columns[page_number * 2 - 1] ~= nil then - GuiLayoutBeginHorizontal(gui, 30, 15); - GuiLayoutBeginVertical(gui, 0, 0); - for _, value in ipairs(columns[page_number * 2 - 1]) do - if player_money < value.price then - GuiText(gui, 0, 0, tostring(value.price) .. "$"); - else - if GuiButton(gui, 0, 0, tostring(value.price) .. "$", get_next_id()) then - create_spell(value.id); - end - end - end - GuiLayoutEnd(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutBeginVertical(gui, 0, 0); - for _, value in ipairs(columns[page_number * 2 - 1]) do - GuiText(gui, 0, 0, value.name); - end - GuiLayoutEnd(gui); - GuiLayoutEnd(gui); - end - if columns[page_number * 2] ~= nil then - GuiLayoutBeginHorizontal(gui, 60, 15); - GuiLayoutBeginVertical(gui, 0, 0); - for _, value in ipairs(columns[page_number * 2]) do - if player_money < value.price then - GuiText(gui, 0, 0, tostring(value.price) .. "$"); - else - if GuiButton(gui, 0, 0, tostring(value.price) .. "$", get_next_id()) then - create_spell(value.id); - end - end - end - GuiLayoutEnd(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutAddHorizontalSpacing(gui); - GuiLayoutBeginVertical(gui, 0, 0); - for _, value in ipairs(columns[page_number * 2]) do - GuiText(gui, 0, 0, value.name); - end - GuiLayoutEnd(gui); - GuiLayoutEnd(gui); - end - if page_number > 1 then - GuiLayoutBeginHorizontal(gui, 48, 95); - if GuiButton(gui, 0, 0, "<<", get_next_id()) then - page_number = page_number - 1; - end - GuiLayoutEnd(gui); - end - GuiLayoutBeginHorizontal(gui, 50, 95); - GuiText(gui, 0, 0, tostring(page_number)); - GuiLayoutEnd(gui); - if page_number < math.ceil(#columns / 2) then - GuiLayoutBeginHorizontal(gui, 52, 95); - if GuiButton(gui, 0, 0, ">>", get_next_id()) then - page_number = page_number + 1; - end - GuiLayoutEnd(gui); - end - end }; -end - -function hide_buy_spells_gui() - buy_spells_open = false; - active_windows["buy_spells"] = nil; -end - -function show_menu_gui() - hide_research_wands_gui(); - hide_research_spells_gui(); - hide_buy_wands_gui(); - hide_buy_spells_gui(); - active_windows["menu"] = { false, function(get_next_id) - GuiLayoutBeginVertical(gui, 1, 30); - if GuiButton(gui, research_wands_open and 10 or 0, 0, "Research Wands", get_next_id()) then - hide_research_spells_gui(); - hide_buy_wands_gui(); - hide_buy_spells_gui(); - if research_wands_open then - hide_research_wands_gui(); - else - show_research_wands_gui(); - end - end - if GuiButton(gui, research_spells_open and 10 or 0, 0, "Research Spells", get_next_id()) then - hide_research_wands_gui(); - hide_buy_wands_gui(); - hide_buy_spells_gui(); - if research_spells_open then - hide_research_spells_gui(); - else - show_research_spells_gui(); - end - end - if GuiButton(gui, buy_wands_open and 10 or 0, 0, "Buy Wands", get_next_id()) then - hide_research_wands_gui(); - hide_research_spells_gui(); - hide_buy_spells_gui(); - if buy_wands_open then - hide_buy_wands_gui(); - else - show_buy_wands_gui(); - end - end - if GuiButton(gui, buy_spells_open and 10 or 0, 0, "Buy Spells", get_next_id()) then - hide_research_wands_gui(); - hide_research_spells_gui(); - hide_buy_wands_gui(); - if buy_spells_open then - hide_buy_spells_gui(); - else - show_buy_spells_gui(); - end - end - GuiLayoutEnd(gui); - end }; -end - -function hide_menu_gui() - hide_research_wands_gui(); - hide_research_spells_gui(); - hide_buy_wands_gui(); - hide_buy_spells_gui(); - active_windows["menu"] = nil; -end - -function show_lobby_gui() - show_menu_gui(); - show_money_gui(); -end - -function hide_lobby_gui() - hide_menu_gui(); - hide_money_gui(); -end - -function hide_all_gui() - active_windows = {}; -end - -function gui_update() - if gui ~= nil then - if active_windows ~= nil then - local is_dark_background = false; - GuiStartFrame(gui); - for _, window in pairs(active_windows) do - if window[1] then - is_dark_background = true; - end - end - if is_dark_background then - local cx, cy = GameGetCameraPos(); - GameCreateSpriteForXFrames("mods/persistence/files/gui_darken.png", cx, cy); - end - local start_gui_id = 14796823; - for name, window in pairs(active_windows) do - local gui_id = start_gui_id + simple_string_hash(name); - window[2](function() - gui_id = gui_id + 1; - return gui_id; - end); - end - end - end -end \ No newline at end of file diff --git a/files/gui/fourslot.lua b/files/gui/fourslot.lua new file mode 100644 index 0000000..da7fbfb --- /dev/null +++ b/files/gui/fourslot.lua @@ -0,0 +1,302 @@ +if fourslot_loaded~=true then + profile_open=false; + wands_open=false; + + profile_fourslot = { + id = "profile_select", + centertext = "SELECT A PROFILE", + greentext = "See Auto-Load in Mod Options", + redtext = "THERE IS NO UNDO", + slot_title = "Profile slot %i:", + render_below_footer = function (x_base, y_base, width, height, fourslot_table, _nid) + GuiBeginAutoBox(gui); + + GuiZSetForNextWidget(gui, __layer(5)); + GuiColorNextWidgetBool(gui, not encoder_read_only); + GuiOptionsAddForNextWidget(gui, GUI_OPTION.Align_HorizontalCenter); + if GuiButton(gui, _nid(), x_base + (width/2), y_base + height + 30, "Play with Persistence") then + encoder_read_only = false; + end + GuiGuideTip(gui, "(Normal) Persistence is enabled, profile saving is automatic.", ""); + + GuiZSetForNextWidget(gui, __layer(5)); + GuiColorNextWidgetBool(gui, encoder_read_only); + GuiOptionsAddForNextWidget(gui, GUI_OPTION.Align_HorizontalCenter); + if GuiButton(gui, _nid(), x_base + (width/2), y_base + height + 40, "Play in Amnesiac Mode") then + encoder_read_only = true; + end + GuiGuideTip(gui, "Amnesiac Mode will allow players to LOAD a profile (or not)", "but CHANGES WILL NOT BE SAVED."); + + GuiZSetForNextWidget(gui, __layer(5)); + GuiOptionsAddForNextWidget(gui, GUI_OPTION.Align_HorizontalCenter); + if GuiButton(gui, _nid(), x_base + (width/2), y_base + height + 60, "Start without Persistence") then + selected_profile_id = DISABLE_PROFILE_ID; + end + GuiGuideTip(gui, "(Disable) Persistence is disabled. Profiles will not be saved or loaded.", "Click to play."); + + GuiZSetForNextWidget(gui, __layer(4)); + GuiEndAutoBoxNinePiece(gui, 10); + end, + render_header_func = function (x_base, y_base, margin, panel_width, panel_height, layer, slot_data, _nid) + local row1_y_offset = 4; + local row2_y_offset = 14; + local x_offset = 16; + + GuiZSetForNextWidget(gui, __layer(layer)); + GuiColorNextWidgetEnum(gui, COLORS.Green); + if slot_data.quickloaded~=nil and slot_data.quickloaded==true then + if GuiButton(gui, _nid(), x_base + x_offset, y_base + row1_y_offset, "- Load Profile") then + selected_profile_id = slot_data.id; + end + if fourslot_confirmation == slot_data.id then + GuiZSetForNextWidget(gui, __layer(layer)); + GuiColorNextWidgetEnum(gui, COLORS.Yellow); + if GuiButton(gui, _nid(), x_base + x_offset, y_base + row2_y_offset, "- Press again to delete") then + fourslot_confirmation = 0; + delete_profile(slot_data.id); + slot_data.quickloaded = nil; + end + else + GuiZSetForNextWidget(gui, __layer(layer)); + GuiColorNextWidgetEnum(gui, COLORS.Yellow); + if GuiButton(gui, _nid(), x_base + x_offset, y_base + row2_y_offset, "- Delete profile") then + fourslot_confirmation = slot_data.id; + end + end + else + GuiZSetForNextWidget(gui, __layer(layer)); + if GuiButton(gui, _nid(), x_base + x_offset, y_base + row1_y_offset, "- Create new profile") then + create_new_profile(slot_data.id) + end + end + end, + datum_translation = { + _index = {[0] = 4, [1] = "money", [2] = "spells_known", [3] = "wand_types_known", [4] = "always_cast_spells_known" }, + money = {"Stash:", __val, 9, __render_money_stat }, + spells_known = {"Spells:", __val, 9, __render_gen_stat }, + wand_types_known = {"Wand Types:", __val, 9, __render_gen_stat }, + always_cast_spells_known = {"Always Casts:", __val, 9, __render_gen_stat }, + }, + datum_exists_member = "quickloaded", + slots_func = get_quick_profiles, + slots_data = {}; + }; + + wands_fourslot = { + id = "wand_select", + centertext = "WANDS ARE DESTROYED WHEN RESEARCHED", + greentext = "Green stats improve your research", + redtext = "Red stats don't improve research", + slot_title = "Wand slot %i:", + render_slot_func = __render_wand_slot, + render_header_func = function (x_base, y_base, margin, panel_width, panel_height, layer, slot_data, _nid) + local row1_y_offset = 2; + local row2_y_offset = 11; + local row3_y_offset = 18; + local x_offset = 6; + + if slot_data.e_id~=nil then + if slot_data.research.is_new then + GuiZSetForNextWidget(gui, __layer(layer)); + GuiColorNextWidgetBool(gui, last_known_money >= slot_data.cost._sum); + if GuiButton(gui, _nid(), x_base + x_offset, y_base + row1_y_offset, string.format("- Research for $ %1.0f", slot_data.cost._sum)) and slot_data.cost._sum < last_known_money then + research_wand(slot_data.e_id); + slot_data = {}; + GamePrintImportant("Wand Researched"); + return true; + end + else + if fourslot_confirmation~=slot_data.id and EntityHasTag(slot_data.e_id, "persistence") then + GuiZSetForNextWidget(gui, __layer(layer)); + GuiColorNextWidgetEnum(gui, COLORS.Green); + if GuiButton(gui, _nid(), x_base + x_offset, y_base + row1_y_offset, "- Modify wand") then + GamePrint("Modify Wand"); + present_modify_wand(slot_data.e_id or 0, slot_data.id); + end + GuiGuideTip(gui, "Click to modify Persistence-created Wand", "Right-click to Recycle (Requires confirmation)"); + if select(2, GuiGetPreviousWidgetInfo(gui))==true then + fourslot_confirmation = slot_data.id; + end + else + if fourslot_confirmation == slot_data.id then + GuiZSetForNextWidget(gui, __layer(layer)); + GuiColorNextWidgetEnum(gui, COLORS.Yellow); + if GuiButton(gui, _nid(), x_base + x_offset, y_base + row1_y_offset, "- Click to recycle") then + fourslot_confirmation = 0; + delete_wand_entity(slot_data.e_id); + slot_data = {}; + GamePrintImportant("Wand Recycled"); + return true; + end + else + GuiZSetForNextWidget(gui, __layer(layer)); + GuiColorNextWidgetEnum(gui, COLORS.Dim); + if GuiButton(gui, _nid(), x_base + x_offset, y_base + row1_y_offset, "No improved stats") then + fourslot_confirmation = slot_data.id; + end + GuiGuideTip(gui, "Click to recycle wand. No cost. No gain.", "(Requires confirmation)"); + end + end + end + if #slot_data.wand["spells"]>0 then + if slot_data.research.b_spells then + GuiColorNextWidgetEnum(gui, COLORS.Red); + GuiZSetForNextWidget(gui, __layer(layer)); + GuiText(gui, x_base + 0 + x_offset, y_base + row2_y_offset, "WAND CONTAINS", small_text_scale); + GuiColorNextWidgetEnum(gui, COLORS.Red); + GuiZSetForNextWidget(gui, __layer(layer)); + GuiText(gui, x_base + 2 + x_offset, y_base + row3_y_offset, "UNRESEARCHED SPELLS", small_text_scale); + GuiGuideTip(gui, "Spells on wands are destroyed", "At least one spell is unresearched"); + elseif (not EntityHasTag(slot_data.e_id, "persistence")) then + GuiColorNextWidgetEnum(gui, COLORS.Yellow); + GuiZSetForNextWidget(gui, __layer(layer)); + GuiText(gui, x_base + 0 + x_offset, y_base + row2_y_offset, "Wand contains spells which", small_text_scale); + GuiColorNextWidgetEnum(gui, COLORS.Yellow); + GuiZSetForNextWidget(gui, __layer(layer)); + GuiText(gui, x_base + 2 + x_offset, y_base + row3_y_offset, "will be lost on research", small_text_scale); + GuiGuideTip(gui, "Spells on wands are destroyed", "These spells are all researched"); + end + end + else + GuiZSetForNextWidget(gui, __layer(layer)); + GuiColorNextWidgetEnum(gui, COLORS.Green); + if GuiButton(gui, _nid(), x_base + x_offset, y_base + row1_y_offset, "- Create new wand") then + GamePrint("Create Wand"); + present_modify_wand(slot_data.e_id or 0, slot_data.id); + ---TODO: CREATE WAND + end + GuiGuideTip(gui, "Create a new wand from researched stats", "Wand Templates can be saved and loaded") + end + end, + datum_submember = "wand", + datum_translation = { + _index = {[0] = 11, [1] = "sprite", [2] = "spells", [3] = "shuffle", [4] = "spells_per_cast", [5] = "cast_delay", [6] = "recharge_time", [7] = "mana_max", [8] = "mana_charge_speed", [9] = "capacity", [10] = "spread", [11] = "always_cast_spells"}, + sprite = {"", __val, 0, __render_wand_sprite, "b_wand_types" }, + spells = {"", __val, 34, __render_wand_spells, "b_capacity" }, + shuffle = {"$inventory_shuffle", __yesno, 9, __render_gen_stat, "b_shuffle" }, + spells_per_cast = {"$inventory_actionspercast", __val, 9, __render_gen_stat, "b_spells_per_cast" }, + cast_delay = {"$inventory_castdelay", __ctime, 9, __render_gen_stat, "b_cast_delay" }, + recharge_time = {"$inventory_rechargetime", __ctime, 9, __render_gen_stat, "b_recharge_time" }, + mana_max = {"$inventory_manamax", __round, 9, __render_gen_stat, "b_mana_max" }, + mana_charge_speed = {"$inventory_manachargespeed", __round, 9, __render_gen_stat, "b_mana_charge_speed" }, + capacity = {"$inventory_capacity", __val, 9, __render_gen_stat, "b_capacity" }, + spread = {"$inventory_spread", __deg, 9, __render_gen_stat, "b_spread" }, + always_cast_spells = {"$inventory_alwayscasts", __val, 9, __render_wand_spells, "b_always_cast_spells" }, + }, + datum_exists_member = "wand", + slots_func = get_player_wands, + slots_data = {}; + }; + + local function draw_fourslot_ui(fourslot_table) + local _reload_data=true; + fourslot_confirmation=0; + + active_windows[fourslot_table.id] = function (_nid) + local function _gui_nop(x_base, y_base, margin, panel_width, panel_height, layer, slot_data, _nid) return; end + + if _reload_data~=false then fourslot_table.slots_data = fourslot_table.slots_func(); _reload_data=false; end + + if fourslot_table.slots_data == nil then return; end + + local x_base = 30; + local y_base = 20; + local margin = 4; + local width = 448; + local height = 200; + local x_offset = x_base + margin; + local y_offset = y_base + margin; + local panel_width = 104; + local panel_height = height - (margin * 2); + GuiZSet(gui, __layer(0)); ---gui frame + GuiImageNinePiece(gui, _nid(), x_base, y_base, width, height); + + for _panel_id = 1, 4 do + fourslot_table.slots_data[_panel_id].id = _panel_id; + local panel_x_offset = x_offset + ((_panel_id-1) * (panel_width + (margin*2))); + GuiZSet(gui, __layer(1)); ---per-panel border + GuiImageNinePiece(gui, _nid(), panel_x_offset, y_offset, panel_width, panel_height); + + local panel_sub_width = panel_width - (margin*2); + local panel_sub_height = panel_height - (margin*2); + panel_x_offset = panel_x_offset; + panel_y_offset = y_offset; + + local header_x_pos = panel_x_offset; + local header_y_pos = panel_y_offset; + GuiOptionsAddForNextWidget(gui, GUI_OPTION.GamepadDefaultWidget); + if (fourslot_table.render_header_func or _gui_nop)(header_x_pos, header_y_pos, margin, panel_sub_width, panel_sub_height, 2, fourslot_table.slots_data[_panel_id], _nid) then + _reload_data = true; + end + + local label_x_pos = panel_x_offset + 23; + local label_y_pos = header_y_pos + 27; + GuiZSetForNextWidget(gui, __layer(2)); ---slot label + GuiText(gui, label_x_pos, label_y_pos, string.format(fourslot_table.slot_title, _panel_id), 1); + + local slot_x_pos = panel_x_offset + margin; + local slot_y_pos = label_y_pos + 12; + panel_sub_width = panel_width - (margin*2); + panel_sub_height = panel_sub_height - (slot_y_pos - y_offset - margin); + (fourslot_table.render_slot_func or _gui_nop)(slot_x_pos, slot_y_pos, margin, panel_sub_width, panel_sub_height, 2, fourslot_table.slots_data[_panel_id], _nid) + local datum_x_pos = panel_x_offset + margin; + local datum_y_pos = slot_y_pos + margin; + for _order_i = 1, fourslot_table.datum_translation._index[0] do + if fourslot_table.slots_data[_panel_id][fourslot_table.datum_exists_member]~=nil then + local _datum_name = fourslot_table.datum_translation._index[_order_i]; + local _datum_value = fourslot_table.datum_submember~=nil and fourslot_table.slots_data[_panel_id][fourslot_table.datum_submember][_datum_name] or fourslot_table.slots_data[_panel_id][_datum_name]; + local _trans_label = (fourslot_table.datum_translation[_datum_name][1] or ""); + local _value_func = (fourslot_table.datum_translation[_datum_name][2] or __val); + local _height = (fourslot_table.datum_translation[_datum_name][3] or 0); + local _render_func = (fourslot_table.datum_translation[_datum_name][4] or _gui_nop); + local _datum_table = fourslot_table.slots_data[_panel_id]; + if fourslot_table.slots_data[_panel_id].research~=nil then + local _improves_member = fourslot_table.datum_translation[_datum_name][5]; + local _improves_bool = fourslot_table.slots_data[_panel_id].research==nil and nil or fourslot_table.slots_data[_panel_id].research[_improves_member]; + _datum_table.color_val = _improves_bool; + end + + _datum_table.label = _trans_label; + _datum_table.value = _value_func(_datum_value); + _render_func(datum_x_pos, datum_y_pos, margin, panel_sub_width, panel_sub_height, 4, _datum_table, _nid); + datum_y_pos = datum_y_pos + _height; + end + end + end + __render_tricolor_footer(x_base, y_base, width, height, fourslot_table); + if fourslot_table.render_below_footer~=nil then fourslot_table.render_below_footer(x_base, y_base, width, height, fourslot_table, _nid); end + end + end + + function present_profile_select() + if profile_open==true then return; end + + draw_fourslot_ui(profile_fourslot); + profile_open = true; + end + + function close_profile_select() + if profile_open==false then return; end + + active_windows[profile_fourslot.id] = nil; + profile_open = false; + end + + function present_wands() + if wands_open==true then return; end + + draw_fourslot_ui(wands_fourslot); + wands_open = true; + end + + function close_wands() + if wands_open==false then return; end + + active_windows[wands_fourslot.id] = nil; + wands_open = false; + end + + print("========================="); + print("persistence: fourslot loaded."); + fourslot_loaded = true; +end \ No newline at end of file diff --git a/files/gui/modify_wand.lua b/files/gui/modify_wand.lua new file mode 100644 index 0000000..4e9c5c6 --- /dev/null +++ b/files/gui/modify_wand.lua @@ -0,0 +1,419 @@ +if modify_wand_loaded~=true then + modify_wand_open=false; + + modify_wand_table = { + id = "modify_wand", + centertext = "CREATE YOUR WAND", + greentext = "Stats are limited to researched values.", + redtext = "Better wands cost more. No refunds.", + slot_title = "Wand slot %i:", + render_slot_func = __render_wand_slot, + render_header_func = function (x_base, y_base, margin, panel_width, panel_height, layer, slot_data, _nid) + local row1_y_offset = 2; + local row2_y_offset = 11; + local row3_y_offset = 18; + local x_offset = 6; + slot_data.price = get_wand_buy_price(slot_data.wand); + + local _tmp_price = slot_data.price; + if slot_data.origin_e_id~=nil and slot_data.origin_e_id~=0 then + local _var_comp = EntityGetFirstComponentIncludingDisabled(slot_data.origin_e_id, "VariableStorageComponent", "persistence_wand_price") or 0; + local _origin_price = ComponentGetValue(_var_comp, "value_int"); + _tmp_price = _tmp_price - _origin_price; + GuiZSetForNextWidget(gui, __layer(layer)); + GuiColorNextWidgetEnum(gui, COLORS.Dim); + GuiText(gui, x_base + x_offset, y_base + row2_y_offset, string.format("Paid: $ %i", _origin_price, small_text_scale)); + end + + GuiZSetForNextWidget(gui, __layer(layer)); + GuiColorNextWidgetBool(gui, last_known_money >= _tmp_price); + if GuiButton(gui, _nid(), x_base + x_offset, y_base + row1_y_offset, string.format("Purchase: $ %1.0f", _tmp_price)) and _tmp_price <= get_player_money() then + if slot_data.origin_e_id~=nil and slot_data.origin_e_id~=0 then + modify_wand_entity(slot_data); + GamePrintImportant("Wand Modified"); + else + slot_data.wand.price = _tmp_price; + purchase_wand(slot_data.wand); + GamePrintImportant("Wand Purchased"); + end + close_open_windows(); + return true; + end + end, + datum_translation = { + -- = { + + + diff --git a/persistence.vdf b/persistence.vdf new file mode 100644 index 0000000..68ac8d9 --- /dev/null +++ b/persistence.vdf @@ -0,0 +1,76 @@ +"workshopitem" +{ + "appid" "881100" + "publishedfileid" "3253132683" + "contentfolder" "/home/danin/git/persistence" + "previewfile" "/home/danin/git/persistence/workshop_preview_image.png" + "title" "Persistence" + "description" "Add some Lite to your Rogue + +Credit to the original author; https://steamcommunity.com/sharedfiles/filedetails/?id=2144130266 +The mod has undergone a near full rewrite, but without their work this wouldn't have existed. +The original author wishes to thank the great modding community on the official Noita Discord server https://discord.com/invite/SZtrP2r. +Without them they would not been able to create this mod. +I consider Discord an ephemeral information source, which is wasteful of the human resources supporting it. I used the wiki, github, and mods (example and otherwise) to acquire my knowledge. +Informational efforts would be better suited on a wiki or other quasi-permanent platform than a user-required chat which by necessity fades to history and is fully lost. + +I'm happy to share any and all of my modding knowledge to anyone who asks. + + +Finally, my github repo can be found at https://github.com/codefaux/persistence + +Now, on with the details: + +Persistence has configurable options in the Mod Options menu. Browse this menu to tune difficulty to your tastes, but defaults are a good starting point. + +There are five player profile slots. (Slot 5 is only accessible via Mod Options menu) + +'Persistence areas' have been added. Persistence areas count as the player's spawn location and Holy Mountains. The Persistence features described below will only work inside a Persistence Area. + +'Amnesiac Mode' has been added. This makes Persistence forget all changes upon the death of your character, to give a less Rogue-Lite experience, or to allow expensive testing/experimentation. + +Money: +- An ingame money stash has been added. Each profile stores a separate money stash. +- Mod Options can lock the stash, only allow deposits, or allow deposits and withdrawals. +- Mod Options can assign a money payout from stash upon New Game, and upon reaching a Holy Mountain. +- Mod Options configure how much money is kept on death. Default is 25%. +- Persistence tracks your last known gold; polymorphed deaths still bank money properly. +- Transfers into stash or player exceeding a specific balance will be skipped. This is intentional, to avoid overflows. + +Spells: +- Spells can be researched using gold, to be permanently added to your profile's spell list. +- Researched spells can be purchased for gold. Partially used spells with limited uses cannot be researched. +- Spells can be recycled if not needed anymore. Recycled spells are simply deleted, there is no cost or benefit. +- Mod Options can be used to scale spell research and purchase costs. +- Spell 'Loadouts' can be stored from existing wands in your inventory, or cleared. Loadouts are shared across all profiles. +- Loadouts must be named. Names must be 1-32 characters. Names do not need to be unique. +- Stored Loadouts can be purchased. Purchased loadouts will fill your inventory. +- KNOWN ISSUE: Due to an in-game bug, it requires -extensive- checking to avoid spells overlapping occasionally in inventory. Occasionally, spells will overlap until moved. Nothing will be lost. +- KNOWN ISSUE: Due to limitations in string length, and how I must store spells, Spell Loadouts have a limited size which is difficult to predict. + +Wands: +- Wands can be researched, storing the best stats you've researched in your profile. +- Spells on a wand are destroyed when the wand is researched. Unresearched spells will not be automatically researched. +- Wands can be created using researched stats from your profile. +- Wands can be recycled if not needed. Recycled wands are simply deleted, there is no cost or benefit. +- Unique wands cannot be fully researched. Their Type (special shape, name) will not be usable. Their stats will. This is intentional. +- Wand Templates can be stored or updated during wand creation, or cleared. +- Wand Templates can be purchased. Purchased wands drop at your location. +- Wands created using Persistence can be modified. +- KNOWN ISSUE: Sometimes when researching/recycling/purchasing a wand, the Inventory screen will show spells on the wrong wand, or on nothing at all. Close and re-open the Inventory window. + + +A scanner runs while you explore the world. The scanner is a quality of life feature for Persistence, which can be disabled in Mod Options. +- If you're standing on or immediately beside a spell or wand in the world, Persistence will indicate if it is unresearched. +- Wands will indicate wether researched or not. +- Spells will indicate wether researched or not. +- Spells ON wands will only show if NOT researched. +- Wands which only provide a new Type (graphic) will indicate separately. + + + +Many thanks to everyone using the mod, and doubly so to anyone who reports an issue, suggests a feature, or just says thanks. +I'd like to thank people directly, but I don't want to violate anyone's privacy. +If you've made a suggestion I've implemented or reported an issue and would like credit, contact me and tell me how to address you." + "changenote" "- Fix for crashes related to attempting to preview empty wand template (proper)" +} diff --git a/settings.lua b/settings.lua new file mode 100644 index 0000000..30bb964 --- /dev/null +++ b/settings.lua @@ -0,0 +1,359 @@ +dofile_once("data/scripts/lib/mod_settings.lua"); +-- dofile_once("data/scripts/debug/keycodes.lua"); + +-- This file can't access other files from this or other mods in all circumstances. +-- Settings will be automatically saved. +-- Settings don't have access unsafe lua APIs. + +-- Use ModSettingGet() in the game to query settings. +-- For some settings (for example those that affect world generation) you might want to retain the current value until a certain point, even +-- if the player has changed the setting while playing. +-- To make it easy to define settings like that, each setting has a "scope" (e.g. MOD_SETTING_SCOPE_NEW_GAME) that will define when the changes +-- will actually become visible via ModSettingGet(). In the case of MOD_SETTING_SCOPE_NEW_GAME the value at the start of the run will be visible +-- until the player starts a new game. +-- ModSettingSetNextValue() will set the buffered value, that will later become visible via ModSettingGet(), unless the setting scope is MOD_SETTING_SCOPE_RUNTIME. + +function mod_setting_integer(mod_id, gui, in_main_menu, im_id, setting) + local value = ModSettingGetNextValue(mod_setting_get_id(mod_id,setting)); + if type(value) ~= "number" then value = setting.value_default or 0; end + + local value_new = GuiSlider(gui, im_id, mod_setting_group_x_offset, 0, setting.ui_name, value, setting.value_min, setting.value_max, setting.value_default, setting.value_display_multiplier or 1, setting.value_display_formatting or "", 64); + value_new = math.floor(value_new); + + if value ~= value_new then + ModSettingSetNextValue(mod_setting_get_id(mod_id,setting), value_new, false); + mod_setting_handle_change_callback(mod_id, gui, in_main_menu, setting, value, value_new); + end + + mod_setting_tooltip(mod_id, gui, in_main_menu, setting); +end + +function mod_setting_number_multiple(mod_id, gui, in_main_menu, im_id, setting) + local _round_to = setting.round_to or 1; + + local value = ModSettingGetNextValue(mod_setting_get_id(mod_id,setting)) or setting.value_default; + + local value_new = value; + value_new = math.floor( (GuiSlider(gui, im_id, mod_setting_group_x_offset, 0, setting.ui_name, value_new, setting.value_min, setting.value_max, setting.value_default, setting.value_display_multiplier or 1, setting.value_display_formatting or "", 64) / _round_to) + 0.5 ) * _round_to; + + if value ~= value_new then + ModSettingSetNextValue(mod_setting_get_id(mod_id,setting), value_new, false); + mod_setting_handle_change_callback(mod_id, gui, in_main_menu, setting, value, value_new); + end + + mod_setting_tooltip(mod_id, gui, in_main_menu, setting); +end + +mod_id = "persistence" -- This should match the name of your mod's folder. + +mod_settings_version = 2; -- This is a magic global that can be used to migrate settings to new mod versions. call mod_settings_get_version() before mod_settings_update() to get the old value. +mod_settings = +{ + { + category_id = "encoded_settings", + ui_name = "", + ui_description = "", + settings = { + { hidden = true, id = "loadout_1", ui_name = "1", ui_description = " ", text_max_length = 512, value_default = "", + allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789, ", scope = MOD_SETTING_SCOPE_RUNTIME, }, + { hidden = true, id = "loadout_2", ui_name = "2", ui_description = " ", text_max_length = 512, value_default = "", + allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789, ", scope = MOD_SETTING_SCOPE_RUNTIME, }, + { hidden = true, id = "loadout_3", ui_name = "3", ui_description = " ", text_max_length = 512, value_default = "", + allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789, ", scope = MOD_SETTING_SCOPE_RUNTIME, }, + { hidden = true, id = "loadout_4", ui_name = "4", ui_description = " ", text_max_length = 512, value_default = "", + allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789, ", scope = MOD_SETTING_SCOPE_RUNTIME, }, + { hidden = true, id = "loadout_5", ui_name = "5", ui_description = " ", text_max_length = 512, value_default = "", + allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789, ", scope = MOD_SETTING_SCOPE_RUNTIME, }, + { hidden = true, id = "loadout_6", ui_name = "6", ui_description = " ", text_max_length = 512, value_default = "", + allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789, ", scope = MOD_SETTING_SCOPE_RUNTIME, }, + { hidden = true, id = "loadout_7", ui_name = "7", ui_description = " ", text_max_length = 512, value_default = "", + allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789, ", scope = MOD_SETTING_SCOPE_RUNTIME, }, + { hidden = true, id = "loadout_8", ui_name = "8", ui_description = " ", text_max_length = 512, value_default = "", + allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789, ", scope = MOD_SETTING_SCOPE_RUNTIME, }, + { hidden = true, id = "loadout_9", ui_name = "9", ui_description = " ", text_max_length = 512, value_default = "", + allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789, ", scope = MOD_SETTING_SCOPE_RUNTIME, }, + { hidden = true, id = "loadout_10", ui_name = "10", ui_description = " ", text_max_length = 512, value_default = "", + allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789, ", scope = MOD_SETTING_SCOPE_RUNTIME, }, + { hidden = true, id = "loadout_11", ui_name = "11", ui_description = " ", text_max_length = 512, value_default = "", + allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789, ", scope = MOD_SETTING_SCOPE_RUNTIME, }, + { hidden = true, id = "loadout_12", ui_name = "12", ui_description = " ", text_max_length = 512, value_default = "", + allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789, ", scope = MOD_SETTING_SCOPE_RUNTIME, }, + { hidden = true, id = "loadout_13", ui_name = "13", ui_description = " ", text_max_length = 512, value_default = "", + allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789, ", scope = MOD_SETTING_SCOPE_RUNTIME, }, + { hidden = true, id = "loadout_14", ui_name = "14", ui_description = " ", text_max_length = 512, value_default = "", + allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789, ", scope = MOD_SETTING_SCOPE_RUNTIME, }, + { hidden = true, id = "loadout_15", ui_name = "15", ui_description = " ", text_max_length = 512, value_default = "", + allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789, ", scope = MOD_SETTING_SCOPE_RUNTIME, }, + { hidden = true, id = "loadout_16", ui_name = "16", ui_description = " ", text_max_length = 512, value_default = "", + allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789, ", scope = MOD_SETTING_SCOPE_RUNTIME, }, + { hidden = true, id = "loadout_17", ui_name = "17", ui_description = " ", text_max_length = 512, value_default = "", + allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789, ", scope = MOD_SETTING_SCOPE_RUNTIME, }, + { hidden = true, id = "loadout_18", ui_name = "18", ui_description = " ", text_max_length = 512, value_default = "", + allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789, ", scope = MOD_SETTING_SCOPE_RUNTIME, }, + { hidden = true, id = "loadout_19", ui_name = "19", ui_description = " ", text_max_length = 512, value_default = "", + allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789, ", scope = MOD_SETTING_SCOPE_RUNTIME, }, + { hidden = true, id = "loadout_20", ui_name = "20", ui_description = " ", text_max_length = 512, value_default = "", + allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789, ", scope = MOD_SETTING_SCOPE_RUNTIME, }, + }, + }, + { + id = "restart_warning", + ui_name = "-- NOTE: These settings will not apply until you start a New Game. --", + not_setting = true, + }, + { + category_id = "liteness_settings", + ui_name = "LITENESS", + ui_description = "'Lite-ness' settings (Keep money, etc)", + settings = { + { + id = "money_saved_on_death", + ui_name = "Money Saved on Death", + ui_description = "How much money persists after you do not", + ui_fn = mod_setting_number, + value_default = 25, + value_min = 0, + value_max = 100, + value_display_formatting = " $0 %", + scope = MOD_SETTING_SCOPE_NEW_GAME, + }, + { + id = "cap_money_saved_on_death", + ui_name = "MAX Money Saved on Death", + ui_description = "If you think you're earning too quickly (0 is no limit)", + ui_fn = mod_setting_number, + value_default = 0, + value_min = 0, + value_max = 500, + value_display_formatting = " $ $0 k", + scope = MOD_SETTING_SCOPE_NEW_GAME, + }, + }, + }, + { + category_id = "default_settings", + ui_name = "DEFAULTS", + ui_description = "Run defaults", + settings = { + { + id = "gamepad_menu_trigger", + ui_name = "Gamepad Menu Trigger", + ui_description = "Which key the mod listens to in order to trigger the Persistence menu", + value_default = "25", + values = { {"23","A"}, {"24", "B"}, {"25", "X"}, {"26", "Y"}, {"11", "DPAD_UP"}, {"12", "DPAD_DOWN"}, {"13", "DPAD_LEFT"}, {"14", "DPAD_RIGHT"} }, + -- value_default = JOY_BUTTON_X, + -- values = { {JOY_BUTTON_A,"A"}, {JOY_BUTTON_B , "B"}, {JOY_BUTTON_X , "X"}, {JOY_BUTTON_Y , "Y"}, {JOY_BUTTON_DPAD_UP , "DPAD_UP"}, {JOY_BUTTON_DPAD_DOWN , "DPAD_DOWN"}, {JOY_BUTTON_DPAD_LEFT , "DPAD_LEFT"}, {JOY_BUTTON_DPAD_RIGHT , "DPAD_RIGHT"} }, + scope = MOD_SETTING_SCOPE_RUNTIME, + }, + { + id = "always_choose_save_id", + ui_name = "Load-Save behavior", + ui_description = "Manually or automatically load a save (or not)", + value_default = "-1", + values = { {"-1","Manual"}, {"0","Disable mod"}, {"1","Use Slot 1"}, {"2","Use Slot 2"}, {"3","Use Slot 3"}, {"4","Use Slot 4"}, {"5","Use Slot 5"} }, + scope = MOD_SETTING_SCOPE_RUNTIME_RESTART, + }, + { + id = "allow_stash", + ui_name = "(Manual) Stash use", + ui_description = "Optionally disallow withdrawals, or manual use altogether. Automatic payouts still work.", + value_default = "1", + values = { {"1","Allow"}, {"0","Disable Entirely"}, {"-1","Deposit Only"} }, + scope = MOD_SETTING_SCOPE_NEW_GAME, + }, + { + id = "start_with_money", + ui_name = "Start with money", + ui_description = "Money to withdraw from your Stash at run start", + ui_fn = mod_setting_number_multiple, + round_to = 25, + value_default = 0, + value_min = 0, + value_max = 5000, + value_display_multiplier = 1, + value_display_formatting = " $ $0", + scope = MOD_SETTING_SCOPE_NEW_GAME, + }, + { + id = "holy_mountain_money", + ui_name = "Holy Mountain Reward (Stash)", + ui_description = "Money to withdraw from your Stash at each Holy Mountain, useful if Stash access is disabled as a challenge", + ui_fn = mod_setting_number_multiple, + round_to = 25, + value_default = 0, + value_min = 0, + value_max = 5000, + value_display_multiplier = 1, + value_display_formatting = " $ $0", + scope = MOD_SETTING_SCOPE_NEW_GAME, + }, + { + id = "holy_mountain_reward", + ui_name = "Holy Mountain Reward (Bonus)", + ui_description = "Money to reward player for reaching each Holy Mountain", + ui_fn = mod_setting_number_multiple, + round_to = 50, + value_default = 0, + value_min = 0, + value_max = 10000, + value_display_multiplier = 1, + value_display_formatting = " $ $0", + scope = MOD_SETTING_SCOPE_NEW_GAME, + } + }, + }, + { + category_id = "multiplier_settings", + ui_name = "MULTIPLIERS", + ui_description = "Cost multipliers", + settings = { + { + id = "research_wand_price_multiplier", + ui_name = "Wand Research Price Multiplier", + ui_description = "Price Multiplier to Research a Wand", + ui_fn = mod_setting_number_multiple, + round_to = 0.01, + value_default = 1, + value_min = .1, + value_max = 3, + value_display_multiplier = 100, + value_display_formatting = " $0 %", + scope = MOD_SETTING_SCOPE_NEW_GAME, + }, + { + id = "research_spell_price_multiplier", + ui_name = "Spell Research Price Multiplier", + ui_description = "Price Multiplier to Research a Spell", + ui_fn = mod_setting_number_multiple, + round_to = 0.1, + value_default = 10, + value_min = 1, + value_max = 30, + value_display_multiplier = 10, + value_display_formatting = " $0 %", + scope = MOD_SETTING_SCOPE_NEW_GAME, + }, + { + id = "buy_wand_price_multiplier", + ui_name = "Buy Wand Price Multiplier", + ui_description = "Price Multiplier to Buy a Wand", + ui_fn = mod_setting_number_multiple, + round_to = 0.01, + value_default = 1, + value_min = .1, + value_max = 3, + value_display_multiplier = 100, + value_display_formatting = " $0 %", + scope = MOD_SETTING_SCOPE_NEW_GAME, + }, + { + id = "buy_spell_price_multiplier", + ui_name = "Buy Spell Price Multiplier", + ui_description = "Price Multiplier to Buy a Spell", + ui_fn = mod_setting_number_multiple, + round_to = 0.01, + value_default = 1, + value_min = .1, + value_max = 3, + value_display_multiplier = 100, + value_display_formatting = " $0 %", + scope = MOD_SETTING_SCOPE_NEW_GAME, + }, + }, + }, + { + category_id = "toggle_settings", + ui_name = "TOGGLES", + ui_description = "Feature toggles", + settings = { + { + id = "show_guide_tips", + ui_name = "Show Guide Tooltips in Persistence menus", + ui_description = "Hotkeys, feature explanations, etc...", + value_default = true, + scope = MOD_SETTING_SCOPE_RUNTIME, + }, + { + id = "allow_scanner", + ui_name = "Entity Scanner", + ui_description = "The Entity Scanner watches for nearby spells and wands, and indicates wether or not they have been researched.", + value_default = true, + scope = MOD_SETTING_SCOPE_RUNTIME, + }, + { + hidden = true, -- Unused, keep for future need? + id = "move_lobby_to_spawn", + ui_name = "Move Lobby to Spawn location", + ui_description = "Lobby is normally Starting Cave. Move Lobby to Spawn instead. Meant for Random runs.", + value_default = false, + scope = MOD_SETTING_SCOPE_NEW_GAME, + }, + { + id = "enable_edit_wands_in_lobby", + ui_name = "Allow editing Wands in Lobby", + ui_description = "Only allowed in Holy Mountain / with perk normally", + value_default = false, + scope = MOD_SETTING_SCOPE_NEW_GAME, + }, + { + id = "enable_teleport_back_up", + ui_name = "Allow Teleport to Lobby from within Holy Mountain", + ui_description = "Note: There is no return teleport!", + value_default = true, + scope = MOD_SETTING_SCOPE_NEW_GAME, + }, + { + id = "enable_menu_in_holy_mountain", + ui_name = "Allow Persistence menu in Holy Mountain", + ui_description = "Allow access to Persistence menu (for stash deposit/withdraw, research, buy) in Holy Mountain", + value_default = true, + scope = MOD_SETTING_SCOPE_NEW_GAME, + }, + { + id = "reusable_holy_mountain", + ui_name = "Allow Holy Mountain to be reused", + ui_description = "Definitely a cheat.", + value_default = false, + scope = MOD_SETTING_SCOPE_NEW_GAME, + }, + { + id = "global_persistence", + ui_name = "Always allow Persistence menu access", + ui_description = "Because why not?", + value_default = false, + scope = MOD_SETTING_SCOPE_NEW_GAME, + }, + }, + }, +} + +-- This function is called to ensure the correct setting values are visible to the game via ModSettingGet(). your mod's settings don't work if you don't have a function like this defined in settings.lua. +-- This function is called: +-- - when entering the mod settings menu (init_scope will be MOD_SETTINGS_SCOPE_ONLY_SET_DEFAULT) +-- - before mod initialization when starting a new game (init_scope will be MOD_SETTING_SCOPE_NEW_GAME) +-- - when entering the game after a restart (init_scope will be MOD_SETTING_SCOPE_RESTART) +-- - at the end of an update when mod settings have been changed via ModSettingsSetNextValue() and the game is unpaused (init_scope will be MOD_SETTINGS_SCOPE_RUNTIME) +function ModSettingsUpdate(init_scope) + local old_version = mod_settings_get_version(mod_id) -- This can be used to migrate some settings between mod versions. + if old_version==1 then + -- need to update money_saved_on_death * 100 + money_saved_on_death = ModSettingGet("persistence.money_saved_on_death"); + ModSettingSet("persistence.money_saved_on_death", money_saved_on_death * 100, money_saved_on_death==25); + end + mod_settings_update(mod_id, mod_settings, init_scope) +end + +-- This function should return the number of visible setting UI elements. +-- Your mod's settings wont be visible in the mod settings menu if this function isn't defined correctly. +-- If your mod changes the displayed settings dynamically, you might need to implement custom logic. +-- The value will be used to determine whether or not to display various UI elements that link to mod settings. +-- At the moment it is fine to simply return 0 or 1 in a custom implementation, but we don't guarantee that will be the case in the future. +-- This function is called every frame when in the settings menu. +function ModSettingsGuiCount() + return mod_settings_gui_count(mod_id, mod_settings) +end + +-- This function is called to display the settings UI for this mod. Your mod's settings wont be visible in the mod settings menu if this function isn't defined correctly. +function ModSettingsGui(gui, in_main_menu) + mod_settings_gui(mod_id, mod_settings, gui, in_main_menu) +end diff --git a/workshop.xml b/workshop.xml index 5ff49de..7d3a1a0 100644 --- a/workshop.xml +++ b/workshop.xml @@ -1,7 +1,74 @@ + dont_upload_folders=".vscode|.git|.github" + dont_upload_files=".gitignore|README.md|files/debug.lua|persistence.pdn|persistence.vdf|persistence.code-workspace|TODO.md|README.md"> \ No newline at end of file diff --git a/workshop_id.txt b/workshop_id.txt index 234ae44..88e5138 100644 --- a/workshop_id.txt +++ b/workshop_id.txt @@ -1 +1 @@ -2144130266 \ No newline at end of file +3253132683 \ No newline at end of file diff --git a/workshop_preview_image.png b/workshop_preview_image.png index e4b968d..b80d82a 100644 Binary files a/workshop_preview_image.png and b/workshop_preview_image.png differ