diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..52b4a61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.pyc +*.swp +build/ +dist/* +.vscode/ +dev_tools/ +__pycache__ +.idea +random-seed-* \ No newline at end of file diff --git a/README.md b/README.md index 08a0fe7..be2fe85 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Dark Souls Item Randomizer -Instructions: +**Instructions:** * (PTDE Only) Unpack your Dark Souls archive files using UnpackDarkSoulsForModding, which can be found [here](https://github.com/HotPocketRemix/UnpackDarkSoulsForModding). * Download `DarkSoulsItemRandomizer.exe`, and place it in your Dark Souls directory, where DARKSOULS.exe (PTDE) or DarkSoulsRemastered.exe (DS1R) are. @@ -9,8 +9,15 @@ configuration to `GameParam.parambnd` or `GameParam.parambnd.dcx`, depending on A directory with the current date/time stamp will be created containing information about the item distribution, including a cheatsheet. * If you want to inspect a certain item distribution, but not modify the actual game files, you can instead input a seed and click the "Write Seed Info" button to generate the same type of folder as above, without modifying the current item distribution. Note that you *must* input a seed for this to work. +* Having 'Save Settings To Default' will write settings to INI file when Export button is pressed. +* To share settings and seeds with others, right click and choose 'Copy settings to Sync'. A seed/settings string will be generated and placed in your clipboard, use paste (CTRL-V) to paste that string in chat, Discord, e-mails, etc. +* To load settings shared with others, copy to the clipboard (CTRL+C) the seed/settings strings shared with you and then right click in the and coose 'Paste settings sync'. -To restore the default item distribution: +*To restore the default item distribution automatically:* + +* Press the 'Revert to vanilla' button + +*To restore the default item distribution manually:* * Check that `GameParam.parambnd[.dcx].bak` exists in `\param\GameParam`. * Delete `GameParam.parambnd[.dcx]` from `\param\GameParam`. @@ -56,10 +63,17 @@ Fashion Souls: If active, the many armor sets in game are split up, and each pie Laundromat Mixup: If active, most human NPCs will wear random armor instead of their usual sets. If Fashion Souls is not active, NPCs will wear randomly chosen full sets of armor. If Fashion Souls is active, NPCs will wear independently chosen random pieces of armor. -Senile Gwynevere: If active, the Lordvessel will be shuffled like other key items, and Gwynevere will give the player a different item in place of the Lordvessel. Not compatible with not shuffling keys. +Lordvessel Randomized: If active, the Lordvessel will be shuffled like other key items, and Gwynevere will give the player a different item in place of the Lordvessel. Not compatible with not shuffling keys. + +Lordvessel @ Firelink: If active, the Lordvessel will always be in Firelink Shrine. This can drastically cut down game time length. Not compatible with not shuffling keys. Senile Primordial Serpents: If active, the four Lord Souls will be shuffled like other key items. Not compatible with not shuffling keys. +Eager Smiths: If active, weapons capable of upgrading along the various ember paths (Chaos, Magic, Divine, etc.) have a 25% chance of dropping as a +0 version of one of these paths. A bit of an easier setting, but will allow for more diversity of viable weapons across playthroughs. + +No DLC: If active, key items will NOT be in Painted World or Artorias of the Abyss DLC locations. Also keys needed for those areas will not be placed outside of these areas, to prevent you from getting keys in sections that aren't needed. + +Seek Guidance Help: If active, useful hints replace the dev messages that appear when using the Seek Guidance miracle. Petrus at the Firelink Shrine has the Seek Guidance miracle and a talisman for sale. **Race Mode Possible Key Locations**: @@ -264,6 +278,8 @@ Senile Primordial Serpents: If active, the four Lord Souls will be shuffled like 1700050: The Duke's Archives Chest #2 (In the room with broken machinery; a fog gate nearby leads to the outside grounds and Crystal Cave. Normally holds Prism Stone x20.) + 1700200: The Duke's Archives - Big Hat Logan's cell + (Requires Archive Tower Giant Cell Key to open.) 1700210: The Duke's Archives - Archive Prison Extra Key Original Location (In a side cell in the Prison. The cell's door can be opened using the Archive Tower Cell Key, which is always dropped by the sleeping Serpent @@ -349,3 +365,56 @@ Senile Primordial Serpents: If active, the four Lord Souls will be shuffled like - Force Quit Wrong Warp is *not* generally required, but may be useful to escape from certain softlocks and avoid the need for some keys. + +**Seek Guidance help:** + +Don't forget that Petrus at the Firelink Shrine has the Seek Guidance miracle and a talisman for sale. + +*The hint rules are as follows:* + +take 2 of the big key hints and put them in twice (4 slots) +take 6 of the key hints, and put them in twice (12 slots, 16 total) +take 4 of the useful items, put them in (4 slots, 20 total) +take 6 of the useful locations, put them in (6 slots, 26 total) +take 3 trash hints, shuffle them in twice (6 slots, 32 total) + +*Hint Locations:* + +Area|Original Text|Location| +---|---|---| +Undead Asylum|"Exchange"|Near Snuggly, in the large bird nest to the right as you exit the Asylum.| +Firelink Shrine|"Path ahead"|Drop off point from the elevator, before the path to the egg-nest above the old shrine.| +Firelink Shrine|"Here!"|At the base of the flying buttress, reached by jumping/rolling, on the path to the egg-nest.| +Undead Burg|"Imminent Merchant"|Under the aqueduct, in front of Domhall of Zena once he relocates there from the Depths.| +Undead Burg|"Imminent drake..."|At the start of the Hellkite bridge, beside basement door.| +Undead Burg|"Jumping off ahead"|Before barrels where two Hollows hanging from railing climb up to ambush.| +Undead Burg|"Prisoner ahead"|In front of the house where Griggs of Vinheim is trapped in Lower Undead Burg.| +Undead Parish|"Prisoner ahead"|Inside Undead Church, in the upper doorway towards Lautrec's cell.| +Undead Parish|"Blacksmith ahead"|On the start of the bridge towards the old church, where Andre of Astora is located.| +Darkroot Garden|"Follow the shining flowers"|Right outside the exit from the old church, before entering the garden.| +Darkroot Garden|"Are the trees moving?"|Past the first living/killable tree on the right side of the Moonlight Butterfly area.| +Depths|"I can't take this"|Inside the bonfire chamber.| +Depths|"Try sliding down"|In Giant Rat chamber, on far left side next to top of waterfall.| +Depths|"Imminent shortcut"|Before the shortcut.| +Depths|"Weakness: Head"|In Gaping Dragon boss area, near pillars on left side.| +Blighttown|"Path Ahead"|Near first bonfire, on approach to nearby fire hounds.| +Quelaag's Domain|"Illusory Wall"|Just before illusory wall leading to Chaos Servant covenant, below Second Bell of Awakening.| +The Catacombs|"Try divine"|Right below the first ladder in the Catacombs.| +The Catacombs|"Treasure"|Near weak looking and breakable brick wall.| +The Catacombs|"Blacksmith ahead"|In the wheel skeleton pit, right outside the entrance to Vamos.| +The Catacombs|"Liar"|Near Patches at his bridge turning switch location.| +The Catacombs|"Up"|At the base of the ladder, along the path from the tomb where the Darkmoon Seance Ring is found.| +The Catacombs|"Shortcut ahead"|Right after Pinwheel, on the route to the drop leading back to the Catacombs| +New Londo Ruins|"Need Curses"|Right before the rickety bridge that leads to the old city.| +New Londo Ruins|"Imminent jumping off ..."|At bottom of broken stairs, outside building holding Sealer.| +New Londo Ruins|"Need Covenant"|Outcrop at bottom of staircase leading to Abyss/Four Kings.| +Sen's Fortress|"Prisoner ahead"|In front of breakable wall where Big Hat Logan is kept.| +Anor Londo|"Path ahead"|At the base of the flying buttress leading to the upper floor of the church with the painting.| +Anor Londo|"Try Projectile"|Right inside the door in the Chamber of the Princess.| +Valley of Drakes|"Ring Ahead"|Before the bridge with the wyverns, approaching from the Darkroot elevator.| +Demon Ruins|"Safe Zone Ahead"|In Centipede Demon room, to the right on approach to patch of ground out of the lava.| +Crystal Cave|"Path Ahead"|On the first invisible bridge.| + +**For Developers:** + +Generating executable: `pyinstaller randomizer_gui.py --onefile -i favicon.ico --add-data "favicon.gif;." --noconsole --name "DarkSoulsItemRandomizer"` \ No newline at end of file diff --git a/chr_init_param.py b/chr_init_param.py index efd98be..9906b63 100644 --- a/chr_init_param.py +++ b/chr_init_param.py @@ -1,6 +1,9 @@ import struct import sys +import chr_setup as chr_s +import item_lot_formatter as item_lf + def extract_shift_jisz(content, offset): extracted = b'' while content[offset:offset+1] != b'\x00': @@ -212,6 +215,25 @@ def find_chr_by_id(self, chr_id): if chr_init.chr_init_id == chr_id: return chr_init return None + + def return_cheatsheet(self): + ret = "" + attrs_to_print = ['spell_1', 'spell_2', 'spell_3', 'spell_4', 'spell_5', 'spell_6', 'spell_7', + 'arrow_1', 'arrow_2', 'bolt_1', 'bolt_2', + 'item_1', 'item_2', 'item_3', 'item_4', 'item_5', 'item_6', 'item_7', 'item_8', 'item_9', 'item_10', + 'ring_1', 'ring_2', 'ring_3', 'ring_4', 'ring_5'] + for class_name in chr_s.CLASS_TO_CHR_INIT: + (_, start_class_init_id) = chr_s.CLASS_TO_CHR_INIT[class_name] + ret += class_name.title() + " starting items:\n\t" + chr = self.find_chr_by_id(start_class_init_id) + for attr_to_print in attrs_to_print: + attr_id = getattr(chr, attr_to_print) + attr_value = "None" + if attr_id > 0: + attr_value = item_lf.ITEMS[attr_id] + ret += attr_to_print + ":'" + attr_value +"' " + ret += "\n" + return ret def export_as_binary(self): num_of_records = len(self.chr_inits) diff --git a/chr_setup.py b/chr_setup.py index c2379b7..d6c3130 100644 --- a/chr_setup.py +++ b/chr_setup.py @@ -3,6 +3,7 @@ import random from enum import Enum +from enum import IntEnum import math import chr_init_param as cip @@ -977,14 +978,20 @@ class CHR_INIT(Enum): CHR_INIT.SPELL_1: ("spell_1", "extra"), CHR_INIT.ITEM_1: ("item_1", "extra") } + + class POOL_CFG(IntEnum): + ORIGINAL = 0 + ORIGINAL_AND_NEW = 1 + NEW = 2 - def __init__(self, class_name, item_type, item, chr_init_fields, item_quantity = 1, should_pass_to_items = True): + def __init__(self, class_name, item_type, item, chr_init_fields, item_quantity = 1, should_pass_to_items = True, pool_cfg = POOL_CFG.ORIGINAL_AND_NEW): self.class_name = class_name self.item_type = item_type self.item = item self.chr_init_fields = chr_init_fields self.item_quantity = item_quantity self.should_pass_to_items = should_pass_to_items + self.pool_config = pool_cfg EXTRA_DATA = [ StartingClassData("hunter", item_s.ITEM_TYPE.WEAPON, 1200000, [StartingClassData.CHR_INIT.WEP_R2]), @@ -1002,40 +1009,59 @@ def __init__(self, class_name, item_type, item, chr_init_fields, item_quantity = StartingClassData("sorcerer", item_s.ITEM_TYPE.WEAPON, 1305000, [StartingClassData.CHR_INIT.WEP_R2]), StartingClassData("sorcerer", item_s.ITEM_TYPE.WEAPON, 1308000, [StartingClassData.CHR_INIT.WEP_R2]), StartingClassData("sorcerer", item_s.ITEM_TYPE.WEAPON, 9018000, [StartingClassData.CHR_INIT.WEP_R2]), - StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3000, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3010, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3020, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3100, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3300, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3400, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3410, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3500, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3510, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3520, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3530, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3540, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3550, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), + StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3000, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL_AND_NEW), #"Sorcery: Soul Arrow" INT:10 + StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3010, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL_AND_NEW), #"Sorcery: Great Soul Arrow" INT:14 + StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3020, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL_AND_NEW), #"Sorcery: Heavy Soul Arrow" INT:12 + StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3100, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL_AND_NEW), #"Magic Weapon" INT:10 + StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3300, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL_AND_NEW), #"Sorcery: Magic Shield" + StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3400, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL), #"Sorcery: Hidden Weapon" + StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3410, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL), #"Sorcery: Hidden Body" + StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3500, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL_AND_NEW), #"Cast Light" INT:14 + StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3510, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL), #"Sorcery: Hush" + StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3520, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL), #"Sorcery: Aural Decoy" + StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3530, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL), #"Sorcery: Repair" + StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3540, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL), #"Sorcery: Fall Control" + StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3550, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL), #"Sorcery: Chameleon" + StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3720, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.NEW), #"Dark Bead" INT:16 + StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3710, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.NEW), #"Dark Orb" INT:16 + StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3110, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.NEW), #"Great Magic Weapon" INT:15 + StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3030, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.NEW), #"Great Heavy Soul Arrow" INT:16 + StartingClassData("sorcerer", item_s.ITEM_TYPE.ITEM, 3040, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.NEW), #"Homing Soulmass" INT:18 StartingClassData("pyromancer", item_s.ITEM_TYPE.WEAPON, 1330000, [StartingClassData.CHR_INIT.WEP_R2]), StartingClassData("pyromancer", item_s.ITEM_TYPE.WEAPON, 1330100, [StartingClassData.CHR_INIT.WEP_R2]), StartingClassData("pyromancer", item_s.ITEM_TYPE.WEAPON, 1330200, [StartingClassData.CHR_INIT.WEP_R2]), - StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4000, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4010, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4100, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4200, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4210, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4300, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4310, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4360, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4400, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), + StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4000, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL_AND_NEW), #"Pyromancy: Fireball" + StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4010, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL_AND_NEW), #"Fire Orb" + StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4100, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL_AND_NEW), #"Pyromancy: Combustion" + StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4110, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.NEW), #"Pyromancy: Great Combustion" + StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4030, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.NEW), #"Pyromancy: Firestorm" + StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4040, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.NEW), #"Pyromancy: Fire Tempest" + StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4050, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.NEW), #"Pyromancy: Fire Surge" + StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4060, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.NEW), #"Pyromancy: Fire Whip" + StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4520, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.NEW), #"Pyromancy: Chaos Fire Whip" + StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4200, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL_AND_NEW), #"Pyromancy: Poison Mist" + StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4210, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL_AND_NEW), #"Pyromancy: Toxic Mist" + StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4300, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL), #"Iron Flesh" + StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4310, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL), #"Pyromancy: Flash Sweat" + StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4360, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL), #"Pyromancy: Undead Rapport" + StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4400, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL_AND_NEW), #"Pyromancy: Power Within" + StartingClassData("pyromancer", item_s.ITEM_TYPE.ITEM, 4530, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.NEW), #"Pyromancy: Black Flame" StartingClassData("cleric", item_s.ITEM_TYPE.WEAPON, 1360000, [StartingClassData.CHR_INIT.WEP_R2]), StartingClassData("cleric", item_s.ITEM_TYPE.WEAPON, 1361000, [StartingClassData.CHR_INIT.WEP_R2]), StartingClassData("cleric", item_s.ITEM_TYPE.WEAPON, 1362000, [StartingClassData.CHR_INIT.WEP_R2]), StartingClassData("cleric", item_s.ITEM_TYPE.WEAPON, 1365000, [StartingClassData.CHR_INIT.WEP_R2]), - StartingClassData("cleric", item_s.ITEM_TYPE.ITEM, 5000, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("cleric", item_s.ITEM_TYPE.ITEM, 5020, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("cleric", item_s.ITEM_TYPE.ITEM, 5300, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("cleric", item_s.ITEM_TYPE.ITEM, 5400, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False), - StartingClassData("cleric", item_s.ITEM_TYPE.ITEM, 5600, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False) + StartingClassData("cleric", item_s.ITEM_TYPE.ITEM, 5000, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL_AND_NEW), #"Miracle: Heal" Faith:12 + StartingClassData("cleric", item_s.ITEM_TYPE.ITEM, 5020, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL_AND_NEW), #"Great Heal Excerpt" Fatih:13 + StartingClassData("cleric", item_s.ITEM_TYPE.ITEM, 5300, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL_AND_NEW), #"Miracle: Force" Faith:12 + StartingClassData("cleric", item_s.ITEM_TYPE.ITEM, 5400, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL), #"Seek Guidance" Faith:12 + StartingClassData("cleric", item_s.ITEM_TYPE.ITEM, 5600, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.ORIGINAL), #"Miracle: Magic Barrier" Faith:14 + StartingClassData("cleric", item_s.ITEM_TYPE.ITEM, 5500, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.NEW), #"Miracle: Lightning Spear" Faith:20 + StartingClassData("cleric", item_s.ITEM_TYPE.ITEM, 5510, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.NEW), #"Miracle: Great Lightning Spear" Faith:30 + StartingClassData("cleric", item_s.ITEM_TYPE.ITEM, 5310, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.NEW), #"Miracle: Wrath of the Gods" Faith:28 + StartingClassData("cleric", item_s.ITEM_TYPE.ITEM, 5320, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.NEW), #"Miracle: Emit Force" Faith:18 + StartingClassData("cleric", item_s.ITEM_TYPE.ITEM, 5900, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.NEW), #"Miracle: Sunlight Blade" Faith:30 + StartingClassData("cleric", item_s.ITEM_TYPE.ITEM, 5100, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.NEW), #"Miracle: Gravelord Sword Dance" Faith:0 + StartingClassData("cleric", item_s.ITEM_TYPE.ITEM, 5110, [StartingClassData.CHR_INIT.SPELL_1, StartingClassData.CHR_INIT.ITEM_1], should_pass_to_items = False, pool_cfg=StartingClassData.POOL_CFG.NEW) #"Miracle: Gravelord Greatsword Dance" Faith:0 ] def create_starting_class_weapon_data(chr_init_param, rand_options): @@ -1061,7 +1087,10 @@ def create_starting_class_weapon_data(chr_init_param, rand_options): def randomize_starting_chr_weapons(chr_init_param, rand_options, random_source): starting_class_weapon_data = create_starting_class_weapon_data(chr_init_param, rand_options) - joined_data = starting_class_weapon_data + EXTRA_DATA + + extra_data = [item for item in EXTRA_DATA if ((rand_options.better_start_spells and (item.pool_config>=StartingClassData.POOL_CFG.ORIGINAL_AND_NEW)) or (not rand_options.better_start_spells and (item.pool_config<=StartingClassData.POOL_CFG.ORIGINAL_AND_NEW)))] + + joined_data = starting_class_weapon_data + extra_data data_passed_to_items = {} for class_name in CLASS_TO_CHR_INIT: @@ -1114,6 +1143,9 @@ def randomize_starting_chr_weapons(chr_init_param, rand_options, random_source): rng_opt.RandOptSoulItemsDifficulty.SHUFFLE, rng_opt.RandOptStartItemsDifficulty.SHIELD_AND_1H, rng_opt.RandOptGameVersion.PTDE, + False, + False, + False, False) random_source = random.Random() diff --git a/dist/DarkSoulsItemRandomizer.exe b/dist/DarkSoulsItemRandomizer.exe index c11fdb1..8d98079 100644 Binary files a/dist/DarkSoulsItemRandomizer.exe and b/dist/DarkSoulsItemRandomizer.exe differ diff --git a/dist/randomizer.ini b/dist/randomizer.ini new file mode 100644 index 0000000..0f5c124 --- /dev/null +++ b/dist/randomizer.ini @@ -0,0 +1,17 @@ +[DEFAULT] +difficulty = 0 +fashion_souls = True +key_placement = 2 +use_lordvessel = Firelink +use_lord_souls = False +soul_items_diff = 2 +start_items_diff = 1 +game_version = DARK SOULS: REMASTERED +randomize_npc_armor = True +ascend_weapons = True +keys_not_in_dlc = True +set_up_hints = True +no_online_items = True +better_start_spells = True +no_black_knight_weapons = True + diff --git a/fmg_handler.py b/fmg_handler.py new file mode 100644 index 0000000..d68b2f3 --- /dev/null +++ b/fmg_handler.py @@ -0,0 +1,108 @@ +import struct +import sys + +class Entry: + def __init__(self, id = None, text = None): + self.id = id + self.text = text + +class Group: + def __init__(self, offsetIndex = None, firstID = None, lastID = None): + self.offsetIndex = offsetIndex + self.firstID = firstID + self.lastID = lastID + +class FMGHandler: + def __init__(self, messages: list[Entry] = None): + if messages == None: + messages = [] + self.messages = messages + + @classmethod + def load_from_file_content(cls, file_content): + master_offset = 0 + + (unk00, bigEndian, version, unk03, fileSize, unk08, unk09, unk0A, unk0B, + groupCount, stringCount, stringOffsetsOffset) = struct.unpack_from(" - : " + hint_debug_text += ( + f"{location['area']} - {location['original_text']}: " + f"{single_line_hint}\n" + ) + + with open(filepath, 'w') as f: + f.write(hint_debug_text) \ No newline at end of file diff --git a/ini_parser.py b/ini_parser.py new file mode 100644 index 0000000..ba0f9f2 --- /dev/null +++ b/ini_parser.py @@ -0,0 +1,37 @@ +import configparser + +def get_values_from_ini(ini_file, section="DEFAULT"): + config = configparser.ConfigParser() + config.read(ini_file) + + if section in config: #check if section in ini + return config[section] + else: + return config["DEFAULT"] + +def save_ini(ini_file, options): + config = configparser.ConfigParser() + for k,v in vars(options).items(): + config["DEFAULT"][k] = str(v) #values must be strings to write + + with open(ini_file, 'w') as configfile: + config.write(configfile) + +def get_option_value(config, k): + """ + The following parses the values in the ini to produce the option list in the correct format + While I could create a separate list/dict to specify the typing of the data coming in + or by creating a section in the inifile for the typing + This would add extra work to the developer who adds new features which I think is far more annoying + So instead we get this ugly mess of try/excepts to get the typing we actually care about: int -> bool -> string + TODO: Allow type to be specified when calling (not really important at all) + """ + try: + option = config.getint(k, 0) + except ValueError: + try: + option = config.getboolean(k, False) + except ValueError: + option = config.get(k, "No Default Value") + + return option diff --git a/item_lot_formatter.py b/item_lot_formatter.py index bf852d8..10ff1c3 100644 --- a/item_lot_formatter.py +++ b/item_lot_formatter.py @@ -12723,304 +12723,304 @@ 53520000: "Bell Gargoyle #1 Tail Cut", 53530000: "Anor Londo Gargoyle #1 Tail Cut", 60001101: "Undead Merchant shop", - 60001102: None, - 60001103: None, - 60001104: None, - 60001107: None, - 60001108: None, - 60001110: None, - 60001111: None, - 60001112: None, - 60001113: None, - 60001114: None, - 60001115: None, - 60001116: None, - 60001117: None, - 60001118: None, - 60001119: None, - 60001120: None, - 60001121: None, - 60001122: None, - 60001123: None, - 60001124: None, - 60001125: None, - 60001126: None, - 60001127: None, - 60001128: None, - 60001129: None, - 60001130: None, - 60001131: None, - 60001132: None, + 60001102: "Undead Merchant shop", + 60001103: "Undead Merchant shop", + 60001104: "Undead Merchant shop", + 60001107: "Undead Merchant shop", + 60001108: "Undead Merchant shop", + 60001110: "Undead Merchant shop", + 60001111: "Undead Merchant shop", + 60001112: "Undead Merchant shop", + 60001113: "Undead Merchant shop", + 60001114: "Undead Merchant shop", + 60001115: "Undead Merchant shop", + 60001116: "Undead Merchant shop", + 60001117: "Undead Merchant shop", + 60001118: "Undead Merchant shop", + 60001119: "Undead Merchant shop", + 60001120: "Undead Merchant shop", + 60001121: "Undead Merchant shop", + 60001122: "Undead Merchant shop", + 60001123: "Undead Merchant shop", + 60001124: "Undead Merchant shop", + 60001125: "Undead Merchant shop", + 60001126: "Undead Merchant shop", + 60001127: "Undead Merchant shop", + 60001128: "Undead Merchant shop", + 60001129: "Undead Merchant shop", + 60001130: "Undead Merchant shop", + 60001131: "Undead Merchant shop", + 60001132: "Undead Merchant shop", 60001133: "Undead Merchant + Domnhall of Zena shared shop item", 60001200: "Female Undead Merchant shop", - 60001201: None, - 60001202: None, - 60001203: None, - 60001204: None, - 60001205: None, - 60001206: None, - 60001207: None, - 60001208: None, - 60001209: None, - 60001210: None, - 60001211: None, - 60001212: None, - 60001213: None, - 60001214: None, - 60001215: None, - 60001216: None, - 60001217: None, - 60001218: None, - 60001219: None, - 60001220: None, + 60001201: "Female Undead Merchant shop", + 60001202: "Female Undead Merchant shop", + 60001203: "Female Undead Merchant shop", + 60001204: "Female Undead Merchant shop", + 60001205: "Female Undead Merchant shop", + 60001206: "Female Undead Merchant shop", + 60001207: "Female Undead Merchant shop", + 60001208: "Female Undead Merchant shop", + 60001209: "Female Undead Merchant shop", + 60001210: "Female Undead Merchant shop", + 60001211: "Female Undead Merchant shop", + 60001212: "Female Undead Merchant shop", + 60001213: "Female Undead Merchant shop", + 60001214: "Female Undead Merchant shop", + 60001215: "Female Undead Merchant shop", + 60001216: "Female Undead Merchant shop", + 60001217: "Female Undead Merchant shop", + 60001218: "Female Undead Merchant shop", + 60001219: "Female Undead Merchant shop", + 60001220: "Female Undead Merchant shop", 60005300: "Crestfallen Merchant shop", - 60005301: None, - 60005302: None, - 60005303: None, - 60005304: None, - 60005306: None, - 60005307: None, - 60005308: None, - 60005309: None, - 60005310: None, - 60005311: None, - 60005312: None, - 60005313: None, - 60005314: None, - 60005315: None, - 60005316: None, - 60005317: None, - 60005318: None, - 60005319: None, - 60005320: None, - 60005321: None, - 60005322: None, - 60005323: None, - 60005324: None, - 60005325: None, - 60005326: None, - 60005327: None, - 60005328: None, - 60005329: None, + 60005301: "Crestfallen Merchant shop", + 60005302: "Crestfallen Merchant shop", + 60005303: "Crestfallen Merchant shop", + 60005304: "Crestfallen Merchant shop", + 60005306: "Crestfallen Merchant shop", + 60005307: "Crestfallen Merchant shop", + 60005308: "Crestfallen Merchant shop", + 60005309: "Crestfallen Merchant shop", + 60005310: "Crestfallen Merchant shop", + 60005311: "Crestfallen Merchant shop", + 60005312: "Crestfallen Merchant shop", + 60005313: "Crestfallen Merchant shop", + 60005314: "Crestfallen Merchant shop", + 60005315: "Crestfallen Merchant shop", + 60005316: "Crestfallen Merchant shop", + 60005317: "Crestfallen Merchant shop", + 60005318: "Crestfallen Merchant shop", + 60005319: "Crestfallen Merchant shop", + 60005320: "Crestfallen Merchant shop", + 60005321: "Crestfallen Merchant shop", + 60005322: "Crestfallen Merchant shop", + 60005323: "Crestfallen Merchant shop", + 60005324: "Crestfallen Merchant shop", + 60005325: "Crestfallen Merchant shop", + 60005326: "Crestfallen Merchant shop", + 60005327: "Crestfallen Merchant shop", + 60005328: "Crestfallen Merchant shop", + 60005329: "Crestfallen Merchant shop", 60001400: "Andre of Astora shop item", 60001402: "Andre of Astora + Giant Blacksmith + Blacksmith Vamos shared shop item", 60001403: "Andre of Astora + Giant Blacksmith + Blacksmith Vamos shared shop item", 60001404: "Andre of Astora + Giant Blacksmith + Blacksmith Vamos + Undead Merchant shared shop item", 60001405: "Andre of Astora shop", - 60001406: None, - 60001407: None, - 60001408: None, - 60001409: None, - 60001410: None, - 60001411: None, - 60001412: None, - 60001413: None, - 60001414: None, - 60001415: None, - 60001416: None, - 60001417: None, - 60001418: None, - 60001419: None, - 60001420: None, + 60001406: "Andre of Astora shop", + 60001407: "Andre of Astora shop", + 60001408: "Andre of Astora shop", + 60001409: "Andre of Astora shop", + 60001410: "Andre of Astora shop", + 60001411: "Andre of Astora shop", + 60001412: "Andre of Astora shop", + 60001413: "Andre of Astora shop", + 60001414: "Andre of Astora shop", + 60001415: "Andre of Astora shop", + 60001416: "Andre of Astora shop", + 60001417: "Andre of Astora shop", + 60001418: "Andre of Astora shop", + 60001419: "Andre of Astora shop", + 60001420: "Andre of Astora shop", 60001500: "Domnhall of Zena shop", - 60001502: None, - 60001503: None, - 60001504: None, - 60001505: None, - 60001506: None, - 60001507: None, - 60001508: None, - 60001509: None, - 60001510: None, - 60001511: None, - 60001512: None, - 60001513: None, - 60001514: None, - 60001550: "Domnhall of Zena shop items after defeating Iron Golem", - 60001551: None, - 60001552: None, - 60001553: None, - 60001554: "Domnhall of Zena shop items after defeating Super Smough", - 60001555: None, - 60001556: None, - 60001557: None, - 60001558: "Domnhall of Zena shop items after defeating Super Ornstein", - 60001559: None, - 60001560: None, - 60001561: None, - 60001562: "Domnhall of Zena shop items after defeating Dark Sun Gwyndolin", - 60001563: None, - 60001564: None, - 60001565: None, - 60001566: "Domnhall of Zena shop items after defeating Gwyn, Lord of Cinder (NG+)", - 60001567: None, - 60001568: None, - 60001569: None, + 60001502: "Domnhall of Zena shop", + 60001503: "Domnhall of Zena shop", + 60001504: "Domnhall of Zena shop", + 60001505: "Domnhall of Zena shop", + 60001506: "Domnhall of Zena shop", + 60001507: "Domnhall of Zena shop", + 60001508: "Domnhall of Zena shop", + 60001509: "Domnhall of Zena shop", + 60001510: "Domnhall of Zena shop", + 60001511: "Domnhall of Zena shop", + 60001512: "Domnhall of Zena shop", + 60001513: "Domnhall of Zena shop", + 60001514: "Domnhall of Zena shop", + 60001550: "Domnhall of Zena shop, after defeating Iron Golem", + 60001551: "Domnhall of Zena shop, after defeating Iron Golem", + 60001552: "Domnhall of Zena shop, after defeating Iron Golem", + 60001553: "Domnhall of Zena shop, after defeating Iron Golem", + 60001554: "Domnhall of Zena shop, after defeating Super Smough", + 60001555: "Domnhall of Zena shop, after defeating Super Smough", + 60001556: "Domnhall of Zena shop, after defeating Super Smough", + 60001557: "Domnhall of Zena shop, after defeating Super Smough", + 60001558: "Domnhall of Zena shop, after defeating Super Ornstein", + 60001559: "Domnhall of Zena shop, after defeating Super Ornstein", + 60001560: "Domnhall of Zena shop, after defeating Super Ornstein", + 60001561: "Domnhall of Zena shop, after defeating Super Ornstein", + 60001562: "Domnhall of Zena shop, after defeating Dark Sun Gwyndolin", + 60001563: "Domnhall of Zena shop, after defeating Dark Sun Gwyndolin", + 60001564: "Domnhall of Zena shop, after defeating Dark Sun Gwyndolin", + 60001565: "Domnhall of Zena shop, after defeating Dark Sun Gwyndolin", + 60001566: "Domnhall of Zena shop, after defeating Gwyn, Lord of Cinder (NG+)", + 60001567: "Domnhall of Zena shop, after defeating Gwyn, Lord of Cinder (NG+)", + 60001568: "Domnhall of Zena shop, after defeating Gwyn, Lord of Cinder (NG+)", + 60001569: "Domnhall of Zena shop, after defeating Gwyn, Lord of Cinder (NG+)", 60001580: "Domnhall of Zena shop item", - 60001581: "Domnhall of Zena shop items after defeating Knight Artorias", - 60001582: None, - 60001583: None, - 60001584: None, - 60001600: "Patches shop items", - 60001601: None, - 60001602: None, - 60001603: None, - 60001604: None, - 60001605: None, - 60001606: None, - 60001607: None, - 60001608: None, - 60001609: None, - 60001610: None, - 60001611: None, - 60001612: None, - 60001613: None, - 60001614: None, - 60001615: None, - 60001616: None, - 60001617: None, - 60001700: "Shiva of the East shop items", - 60001701: None, - 60001702: None, - 60001703: None, - 60001704: None, - 60001705: None, - 60001706: None, - 60001707: None, - 60001708: None, - 60001709: None, - 60002000: "Griggs of Vinheim shop items", - 60002001: None, - 60002002: None, - 60002003: None, - 60002004: None, - 60002005: None, - 60002006: None, - 60002007: None, - 60002008: None, - 60002009: None, - 60002010: None, + 60001581: "Domnhall of Zena shop, after defeating Knight Artorias", + 60001582: "Domnhall of Zena shop, after defeating Knight Artorias", + 60001583: "Domnhall of Zena shop, after defeating Knight Artorias", + 60001584: "Domnhall of Zena shop, after defeating Knight Artorias", + 60001600: "Patches shop", + 60001601: "Patches shop", + 60001602: "Patches shop", + 60001603: "Patches shop", + 60001604: "Patches shop", + 60001605: "Patches shop", + 60001606: "Patches shop", + 60001607: "Patches shop", + 60001608: "Patches shop", + 60001609: "Patches shop", + 60001610: "Patches shop", + 60001611: "Patches shop", + 60001612: "Patches shop", + 60001613: "Patches shop", + 60001614: "Patches shop", + 60001615: "Patches shop", + 60001616: "Patches shop", + 60001617: "Patches shop", + 60001700: "Shiva of the East shop", + 60001701: "Shiva of the East shop", + 60001702: "Shiva of the East shop", + 60001703: "Shiva of the East shop", + 60001704: "Shiva of the East shop", + 60001705: "Shiva of the East shop", + 60001706: "Shiva of the East shop", + 60001707: "Shiva of the East shop", + 60001708: "Shiva of the East shop", + 60001709: "Shiva of the East shop", + 60002000: "Griggs of Vinheim shop", + 60002001: "Griggs of Vinheim shop", + 60002002: "Griggs of Vinheim shop", + 60002003: "Griggs of Vinheim shop", + 60002004: "Griggs of Vinheim shop", + 60002005: "Griggs of Vinheim shop", + 60002006: "Griggs of Vinheim shop", + 60002007: "Griggs of Vinheim shop", + 60002008: "Griggs of Vinheim shop", + 60002009: "Griggs of Vinheim shop", + 60002010: "Griggs of Vinheim shop", 60002020: "Griggs of Vinheim additional shop items after Big Hat Logan departs", - 60002021: None, - 60002100: "Rickert of Vinheim shop items", - 60002101: None, - 60002102: None, + 60002021: "Griggs of Vinheim additional shop items after Big Hat Logan departs", + 60002100: "Rickert of Vinheim shop", + 60002101: "Rickert of Vinheim shop", + 60002102: "Rickert of Vinheim shop", 60002200: "Dusk of Oolacile + Elizabeth shared shop items", - 60002201: None, - 60002202: None, - 60002203: None, - 60002204: None, + 60002201: "Dusk of Oolacile + Elizabeth shared shop items", + 60002202: "Dusk of Oolacile + Elizabeth shared shop items", + 60002203: "Dusk of Oolacile + Elizabeth shared shop items", + 60002204: "Dusk of Oolacile + Elizabeth shared shop items", 60002205: "Dusk of Oolacile shop item", - 60002400: "Ingward shop items", - 60002401: None, - 60003000: "Laurentius shop items", - 60003001: None, - 60003002: None, - 60003003: None, - 60003004: None, - 60003210: "Eingyi shop items after infected with the Egg Parasite", - 60003211: None, - 60003212: None, - 60003400: "Quelana of Izalith shop items", - 60003401: None, - 60003402: None, - 60003403: None, - 60003404: None, - 60003405: None, - 60003406: None, - 60003407: None, - 60004000: "Petrus of Thorolund shop items", - 60004001: None, - 60004002: None, - 60004003: None, - 60004004: None, - 60004005: None, - 60004006: None, - 60004200: "Rhea of Thorolund shop items", - 60004201: None, - 60004205: None, - 60004202: None, - 60004203: None, - 60004204: None, - 60004206: None, - 60004207: None, - 60004208: None, - 60004209: None, - 60004401: "Oswald of Carim shop items", - 60004402: None, - 60004403: None, - 60004404: None, - 60004405: None, - 60004406: None, - 60004407: None, - 60004408: None, - 60005000: "Big Hat Logan shop items", - 60005001: None, - 60005002: None, - 60005003: None, - 60005004: None, - 60005005: None, - 60005006: None, - 60005007: None, - 60005020: "Big Hat Logan additional shop items after moving to Duke's Archives", - 60005021: None, - 60005022: None, + 60002400: "Ingward shop", + 60002401: "Ingward shop", + 60003000: "Laurentius shop", + 60003001: "Laurentius shop", + 60003002: "Laurentius shop", + 60003003: "Laurentius shop", + 60003004: "Laurentius shop", + 60003210: "Eingyi shop (requires Egg Parasite)", + 60003211: "Eingyi shop (requires Egg Parasite)", + 60003212: "Eingyi shop (requires Egg Parasite)", + 60003400: "Quelana of Izalith shop", + 60003401: "Quelana of Izalith shop", + 60003402: "Quelana of Izalith shop", + 60003403: "Quelana of Izalith shop", + 60003404: "Quelana of Izalith shop", + 60003405: "Quelana of Izalith shop", + 60003406: "Quelana of Izalith shop", + 60003407: "Quelana of Izalith shop", + 60004000: "Petrus of Thorolund shop", + 60004001: "Petrus of Thorolund shop", + 60004002: "Petrus of Thorolund shop", + 60004003: "Petrus of Thorolund shop", + 60004004: "Petrus of Thorolund shop", + 60004005: "Petrus of Thorolund shop", + 60004006: "Petrus of Thorolund shop", + 60004200: "Rhea of Thorolund shop", + 60004201: "Rhea of Thorolund shop", + 60004205: "Rhea of Thorolund shop", + 60004202: "Rhea of Thorolund shop", + 60004203: "Rhea of Thorolund shop", + 60004204: "Rhea of Thorolund shop", + 60004206: "Rhea of Thorolund shop", + 60004207: "Rhea of Thorolund shop", + 60004208: "Rhea of Thorolund shop", + 60004209: "Rhea of Thorolund shop", + 60004401: "Oswald of Carim shop", + 60004402: "Oswald of Carim shop", + 60004403: "Oswald of Carim shop", + 60004404: "Oswald of Carim shop", + 60004405: "Oswald of Carim shop", + 60004406: "Oswald of Carim shop", + 60004407: "Oswald of Carim shop", + 60004408: "Oswald of Carim shop", + 60005000: "Big Hat Logan shop (initial)", + 60005001: "Big Hat Logan shop (initial)", + 60005002: "Big Hat Logan shop (initial)", + 60005003: "Big Hat Logan shop (initial)", + 60005004: "Big Hat Logan shop (initial)", + 60005005: "Big Hat Logan shop (initial)", + 60005006: "Big Hat Logan shop (initial)", + 60005007: "Big Hat Logan shop (initial)", + 60005020: "Big Hat Logan after moving to Duke's Archives", + 60005021: "Big Hat Logan after moving to Duke's Archives", + 60005022: "Big Hat Logan after moving to Duke's Archives", 60006100: "Darkstalker Kaathe shop items", - 60006200: "Giant Blacksmith shop items", - 60006201: None, - 60006202: None, - 60006203: None, - 60006204: None, - 60006205: None, - 60006206: None, - 60006207: None, - 60006208: None, - 60006209: None, - 60006210: None, - 60006211: None, - 60006212: None, - 60006213: None, - 60006214: None, - 60006218: None, - 60006219: None, - 60006220: None, - 60006221: None, - 60006300: "Blacksmith Vamos shop items", - 60006301: None, - 60006305: None, - 60006306: None, - 60006307: None, - 60006308: None, - 60006309: None, - 60006310: None, - 60006410: "Marvellous Chester shop items", - 60006411: None, - 60006412: None, - 60006413: None, - 60006414: None, - 60006415: None, - 60006416: None, - 60006417: None, - 60006418: None, - 60006419: None, - 60006420: None, - 60006421: None, - 60006422: None, - 60006423: None, - 60006424: None, - 60006425: None, - 60006426: None, + 60006200: "Giant Blacksmith shop", + 60006201: "Giant Blacksmith shop", + 60006202: "Giant Blacksmith shop", + 60006203: "Giant Blacksmith shop", + 60006204: "Giant Blacksmith shop", + 60006205: "Giant Blacksmith shop", + 60006206: "Giant Blacksmith shop", + 60006207: "Giant Blacksmith shop", + 60006208: "Giant Blacksmith shop", + 60006209: "Giant Blacksmith shop", + 60006210: "Giant Blacksmith shop", + 60006211: "Giant Blacksmith shop", + 60006212: "Giant Blacksmith shop", + 60006213: "Giant Blacksmith shop", + 60006214: "Giant Blacksmith shop", + 60006218: "Giant Blacksmith shop", + 60006219: "Giant Blacksmith shop", + 60006220: "Giant Blacksmith shop", + 60006221: "Giant Blacksmith shop", + 60006300: "Blacksmith Vamos shop", + 60006301: "Blacksmith Vamos shop", + 60006305: "Blacksmith Vamos shop", + 60006306: "Blacksmith Vamos shop", + 60006307: "Blacksmith Vamos shop", + 60006308: "Blacksmith Vamos shop", + 60006309: "Blacksmith Vamos shop", + 60006310: "Blacksmith Vamos shop", + 60006410: "Marvellous Chester shop", + 60006411: "Marvellous Chester shop", + 60006412: "Marvellous Chester shop", + 60006413: "Marvellous Chester shop", + 60006414: "Marvellous Chester shop", + 60006415: "Marvellous Chester shop", + 60006416: "Marvellous Chester shop", + 60006417: "Marvellous Chester shop", + 60006418: "Marvellous Chester shop", + 60006419: "Marvellous Chester shop", + 60006420: "Marvellous Chester shop", + 60006421: "Marvellous Chester shop", + 60006422: "Marvellous Chester shop", + 60006423: "Marvellous Chester shop", + 60006424: "Marvellous Chester shop", + 60006425: "Marvellous Chester shop", + 60006426: "Marvellous Chester shop", 60006500: "Elizabeth shop items", - 60006507: None, - 60006600: "Hawkeye Gough shop items", - 60006601: None, - 60006602: None, - 60006603: None, - 60006604: None, - 60006608: None, - 60006609: None, - 60006610: None, - 60006611: None + 60006507: "Elizabeth shop items", + 60006600: "Hawkeye Gough shop", + 60006601: "Hawkeye Gough shop", + 60006602: "Hawkeye Gough shop", + 60006603: "Hawkeye Gough shop", + 60006604: "Hawkeye Gough shop", + 60006608: "Hawkeye Gough shop", + 60006609: "Hawkeye Gough shop", + 60006610: "Hawkeye Gough shop", + 60006611: "Hawkeye Gough shop" } def format_item_lot_entry_as_human_readable(entry, total_weight, suppress_percent = False): diff --git a/item_table.py b/item_table.py index 4d5691a..b664477 100644 --- a/item_table.py +++ b/item_table.py @@ -10,8 +10,10 @@ import itertools +import hint_setup + class ItemTable: - def __init__(self, location_dict = None, shop_dict = None): + def __init__(self, rand_options, location_dict = None, shop_dict = None): if location_dict == None: location_dict = {} if shop_dict == None: @@ -20,6 +22,8 @@ def __init__(self, location_dict = None, shop_dict = None): self.location_dict = location_dict self.shop_dict = shop_dict self.key_locs = {} + self.hint_builder = hint_setup.HintBuilder(rand_options) + self.rand_options = rand_options for loc_id in location_dict: if location_dict[loc_id].location_id != loc_id: @@ -40,6 +44,7 @@ def place_itemlotpart_at_location(self, itemlotpart, loc_id, item_list, price = " cannot accept proposed itemlotpart due to size.") else: self.table[loc_id] += [itemlotpart] + [item_list[i] for i in itemlotpart.follow_items] + self.hint_builder.AddItemOrLocationToHintBuilder(itemlotpart, loc_id) if price != None: for linkloc_id in [loc_id] + self.location_dict[loc_id].linked_locations: if linkloc_id in self.shop_dict: @@ -429,7 +434,7 @@ def build_hintsheet(self): "very_large_ember": "Very Large Ember", "large_magic_ember": "Large Magic Ember", "crystal_ember": "Crystal Ember", - "cast_light": "Cast Light", + "sunlight_maggot": "Sunlight Maggot", "lord_soul_shard_seath": "Bequeathed Lord Soul Shard (Seath)", "lord_soul_shard_four_kings": "Bequeathed Lord Soul Shard (Four Kings)", "lord_soul_bed_of_chaos": "Lord Soul (Bed of Chaos)", diff --git a/items_setup.py b/items_setup.py index 7148fad..1ef78d8 100644 --- a/items_setup.py +++ b/items_setup.py @@ -1473,7 +1473,7 @@ def get_max_effective_size(self): 34720010: ItemLotPart(ITEM_DIF.EASY, 2, [ItemLotEntry(ITEM_TYPE.WEAPON, 9019000)], flag = 51210940), 34720020: ItemLotPart(ITEM_DIF.EASY, 2, [ItemLotEntry(ITEM_TYPE.WEAPON, 9019000)], flag = 51210930), 34800000: ItemLotPart(ITEM_DIF.NPC_HARD, 2, [ItemLotEntry(ITEM_TYPE.NONE, 0, count = 0, rate = 965), ItemLotEntry(ITEM_TYPE.ITEM, 1060, rate = 5, luck = False), ItemLotEntry(ITEM_TYPE.ITEM, 375, rate = 30, luck = False)]), - 34800100: ItemLotPart(ITEM_DIF.HARD, 2, [ItemLotEntry(ITEM_TYPE.ARMOR, 190000)], flag = 51410990), + 34800100: ItemLotPart(ITEM_DIF.KEY, 2, [ItemLotEntry(ITEM_TYPE.ARMOR, 190000)], flag = 51410990, key_name = "sunlight_maggot"), 34900000: ItemLotPart(ITEM_DIF.NOT_IN_POOL, 2, [ItemLotEntry(ITEM_TYPE.NONE, 0, count = 0)]), 34900100: ItemLotPart(ITEM_DIF.NOT_IN_POOL, 2, [ItemLotEntry(ITEM_TYPE.NONE, 0, count = 0)]), 34900200: ItemLotPart(ITEM_DIF.NOT_IN_POOL, 2, [ItemLotEntry(ITEM_TYPE.NONE, 0, count = 0)]), @@ -1793,7 +1793,7 @@ def get_max_effective_size(self): 60002200: ItemLotPart(ITEM_DIF.EASY, 2, [ItemLotEntry(ITEM_TYPE.ITEM, 3400)], flag = 11207000), 60002201: ItemLotPart(ITEM_DIF.MEDIUM, 2, [ItemLotEntry(ITEM_TYPE.ITEM, 3410)], flag = 11207010), - 60002202: ItemLotPart(ITEM_DIF.KEY, 2, [ItemLotEntry(ITEM_TYPE.ITEM, 3500)], flag = 11207020, key_name = "cast_light"), + 60002202: ItemLotPart(ITEM_DIF.EASY, 2, [ItemLotEntry(ITEM_TYPE.ITEM, 3500)], flag = 11207020), 60002203: ItemLotPart(ITEM_DIF.EASY, 2, [ItemLotEntry(ITEM_TYPE.ITEM, 3530)], flag = 11207030), 60002204: ItemLotPart(ITEM_DIF.EASY, 2, [ItemLotEntry(ITEM_TYPE.ITEM, 3550)], flag = 11207040), 60002205: ItemLotPart(ITEM_DIF.HARD, 2, [ItemLotEntry(ITEM_TYPE.WEAPON, 1305000)]), @@ -1820,8 +1820,12 @@ def get_max_effective_size(self): 60004001: ItemLotPart(ITEM_DIF.MEDIUM, 2, [ItemLotEntry(ITEM_TYPE.ITEM, 5020)], flag = 11027270), 60004002: ItemLotPart(ITEM_DIF.MEDIUM, 2, [ItemLotEntry(ITEM_TYPE.ITEM, 5210)], flag = 11027280), 60004003: ItemLotPart(ITEM_DIF.MEDIUM, 2, [ItemLotEntry(ITEM_TYPE.ITEM, 5300)], flag = 11027290), - 60004004: ItemLotPart(ITEM_DIF.EASY, 2, [ItemLotEntry(ITEM_TYPE.ITEM, 5400)], flag = 11017180), - 60004005: ItemLotPart(ITEM_DIF.MEDIUM, 2, [ItemLotEntry(ITEM_TYPE.WEAPON, 1360000)]), + + # Leave the Seek Guidance and Talisman that Petrus sells alone, so that it can be always + # sold so that the player can utilize the dev hints for the hint option. + 60004004: ItemLotPart(ITEM_DIF.NOT_IN_POOL, 2, [ItemLotEntry(ITEM_TYPE.ITEM, 5400)], flag = 11017180), + 60004005: ItemLotPart(ITEM_DIF.NOT_IN_POOL, 2, [ItemLotEntry(ITEM_TYPE.WEAPON, 1360000)]), + 60004006: ItemLotPart(ITEM_DIF.MEDIUM, 2, [ItemLotEntry(ITEM_TYPE.WEAPON, 1362000)]), 60004200: ItemLotPart(ITEM_DIF.MEDIUM, 2, [ItemLotEntry(ITEM_TYPE.ITEM, 240)]), 60004201: ItemLotPart(ITEM_DIF.EASY, 2, [ItemLotEntry(ITEM_TYPE.ITEM, 5000)]), @@ -2002,6 +2006,29 @@ def get_max_effective_size(self): RANDOM_UPGRADE_RARE = [(1030, 2), (1040, 2), (1050, 2), (1060, 2), (1110, 1), (1120, 1), (1130, 1)] # Will have 20% drop chance. RANDOM_UPGRADE_ULTRARARE = [(1070, 1), (1080, 1), (1090, 1), (1100, 1), (1110, 2), (1120, 2), (1130, 2)] # Will have 15% drop chance. +ONLINE_USE_ITEMS = [ + 100, #White Sign Soapstone + 101, #Red Sign Soapstone + 102, #Red Eye Orb + 103, #Black Separation Crystal + 106, #Orange Guidance Soapstone + 108, #Book of the Guilty + 109, #Eye of Death + 111, #Cracked Red Eye Orb + 112, #Servant Roster + 113, #Blue Eye Orb + 114, #Dragon Eye + 118, #Purple Coward's Crystal + 373, #Indictment + 374, #Souvenir of Reprisal + 385 #Dried Finger +] + +ONLINE_USE_RINGS = [ + 102, #Darkmoon Blade Covenant Ring + 103 #Cat Covenant Ring +] + # (consumable_type, consumable_id, min_amount, max_amount) RANDOM_CONSUMABLES = [ (ITEM_TYPE.ITEM, 230, 1, 1), # Elizabeth's Mushroom @@ -2057,6 +2084,26 @@ def get_max_effective_size(self): (ITEM_TYPE.WEAPON, 2104000, 10, 50) # Lightning Bolt ] +# (consumable_type, consumable_id, min_amount, max_amount) +BLACK_KNIGHT_WEAPON_REPLACEMENTS = [ + (ITEM_TYPE.ITEM, 1030, 2, 3), # "Titanite Chunk" + (ITEM_TYPE.ITEM, 1040, 2, 3), # "Blue Titanite Chunk" + (ITEM_TYPE.ITEM, 1050, 2, 3), # "White Titanite Chunk" + (ITEM_TYPE.ITEM, 1060, 2, 3), # "Red Titanite Chunk" + (ITEM_TYPE.ITEM, 1070, 1, 1), # "Titanite Slab" + (ITEM_TYPE.ITEM, 1080, 1, 1), # "Blue Titanite Slab" + (ITEM_TYPE.ITEM, 1090, 1, 1), # "White Titanite Slab" + (ITEM_TYPE.ITEM, 1100, 1, 1), # "Red Titanite Slab" + (ITEM_TYPE.ITEM, 1120, 2, 3), # "Demon Titanite" +] + +BLACK_KNIGHT_WEAPONS = [ + 310000, #" Black Knight Sword" + 355000, #" Black Knight Greatsword" + 753000, # "Black Knight Greataxe" + 1105000 # "Black Knight Halberd", +] + def boss_weapon_list_helper(min_index, max_index): return [(ITEM_TYPE.WEAPON, i) for i in range(min_index, max_index+1, 100)] @@ -2074,3 +2121,114 @@ def boss_weapon_list_helper(min_index, max_index): 710: [boss_weapon_list_helper(9012000, 9012800) + boss_weapon_list_helper(9013000 , 9013700)], 711: [boss_weapon_list_helper(9017000, 9017500)], } + +ASCENDABLE_WEAPONS = { + 100000: ("Dagger", True), + 101000: ("Parrying Dagger", True), + 103000: ("Bandit's Knife", True), + 200000: ("Shortsword", True), + 201000: ("Longsword", True), + 202000: ("Broadsword", True), + 203000: ("Broken Straight Sword", True), + 204000: ("Balder Side Sword", True), + 206000: ("Sunlight Straight Sword", True), + 207000: ("Barbed Straight Sword", True), + 209000: ("Astora's Straight Sword", True), + 210000: ("Darksword", True), + 212000: ("Straight Sword Hilt", True), + 300000: ("Bastard Sword", True), + 302000: ("Man-serpent Greatsword", True), + 303000: ("Flamberge", True), + 350000: ("Zweihander", True), + 351000: ("Greatsword", True), + 352000: ("Demon Great Machete", True), + 400000: ("Scimitar", True), + 401000: ("Falchion", True), + 402000: ("Shotel", True), + 405000: ("Painting Guardian Sword", True), + 450000: ("Server", True), + 451000: ("Murakumo", True), + 500000: ("Uchigatana", True), + 501000: ("Washing Pole", True), + 502000: ("Iaito", True), + 600000: ("Mail Breaker", True), + 601000: ("Rapier", True), + 602000: ("Estoc", True), + 604000: ("Ricard's Rapier", True), + 700000: ("Hand Axe", True), + 701000: ("Battle Axe", True), + 702000: ("Crescent Axe", True), + 703000: ("Butcher Knife", True), + 705000: ("Gargoyle Tail Axe", True), + 750000: ("Greataxe", True), + 751000: ("Demon's Greataxe", True), + 800000: ("Club", True), + 801000: ("Mace", True), + 802000: ("Morning Star", True), + 803000: ("Warpick", True), + 804000: ("Pickaxe", True), + 809000: ("Reinforced Club", True), + 810000: ("Blacksmith Hammer", True), + 811000: ("Blacksmith Giant Hammer", True), + 812000: ("Hammer of Vamos", True), + 850000: ("Great Club", True), + 852000: ("Demon's Great Hammer", True), + 855000: ("Large Club", True), + 901000: ("Caestus", True), + 902000: ("Claw", True), + 1000000: ("Spear", True), + 1001000: ("Winged Spear", True), + 1002000: ("Partizan", True), + 1004000: ("Channeler's Trident", True), + 1050000: ("Pike", True), + 1100000: ("Halberd", True), + 1103000: ("Gargoyle's Halberd", True), + 1106000: ("Lucerne", True), + 1107000: ("Scythe", True), + 1150000: ("Great Scythe", True), + 1200000: ("Short Bow", True), + 1201000: ("Longbow", True), + 1202000: ("Black Bow of Pharis", True), + 1204000: ("Composite Bow", True), + 1250000: ("Light Crossbow", False), + 1251000: ("Heavy Crossbow", False), + 1252000: ("Avelyn", False), + 1253000: ("Sniper Crossbow", False), + 1400000: ("East-West Shield", False), + 1401000: ("Wooden Shield", False), + 1402000: ("Large Leather Shield", False), + 1403000: ("Small Leather Shield", False), + 1404000: ("Target Shield", False), + 1405000: ("Buckler", False), + 1406000: ("Cracked Round Shield", False), + 1408000: ("Leather Shield", False), + 1409000: ("Plank Shield", False), + 1410000: ("Caduceus Round Shield", False), + 1450000: ("Heater Shield", False), + 1451000: ("Knight Shield", False), + 1452000: ("Tower Kite Shield", False), + 1453000: ("Grass Crest Shield", False), + 1454000: ("Hollow Soldier Shield", False), + 1455000: ("Balder Shield", False), + 1460000: ("Warrior's Round Shield", False), + 1461000: ("Iron Round Shield", False), + 1462000: ("Spider Shield", False), + 1470000: ("Spiked Shield", False), + 1472000: ("Sunlight Shield", False), + 1475000: ("Pierce Shield", False), + 1476000: ("Red and White Round Shield", False), + 1477000: ("Caduceus Kite Shield", False), + 1478000: ("Gargoyle's Shield", False), + 1500000: ("Eagle Shield", False), + 1501000: ("Tower Shield", False), + 1502000: ("Giant Shield", False), + 1506000: ("Bonewheel Shield", False), + 1600000: ("Whip", True), + 1601000: ("Notched Whip", True), + 9000000: ("Effigy Shield", False), + 9001000: ("Sanctus", False), + 9002000: ("Bloodshield", False), + 9003000: ("Black Iron Greatshield", False), + 9016000: ("Four-pronged Plow", True), + 9019000: ("Guardian Tail", True) + } diff --git a/key_items_setup.py b/key_items_setup.py index f6eabf5..e51ef2d 100644 --- a/key_items_setup.py +++ b/key_items_setup.py @@ -17,7 +17,7 @@ "peculiar_doll", "broken_pendant", "crest_key", "crest_of_artorias", "residence_key", "dark_ember", "divine_ember", "enchanted_ember", "large_divine_ember", "large_flame_ember", "chaos_flame_ember", - "very_large_ember", "large_magic_ember", "crystal_ember", "cast_light", + "very_large_ember", "large_magic_ember", "crystal_ember", "sunlight_maggot", "lord_soul_shard_seath", "lord_soul_shard_four_kings", "lord_soul_bed_of_chaos", "lord_soul_nito" ] @@ -236,17 +236,18 @@ def get_connections(rand_options): AREA.OOLACILE_TOWNSHIP: { "none": [AREA.ROYAL_WOOD, AREA.CHASM_OF_THE_ABYSS], "crest_key": [AREA.KALAMEET_FIGHT], - "cast_light": [AREA.OOLACILE_HIDDEN]}, + "sunlight_maggot": [AREA.OOLACILE_HIDDEN]}, AREA.OOLACILE_HIDDEN: { - "cast_light": [AREA.OOLACILE_TOWNSHIP]}, + "sunlight_maggot": [AREA.OOLACILE_TOWNSHIP]}, AREA.KALAMEET_FIGHT: { "none": [AREA.ROYAL_WOOD]}, AREA.CHASM_OF_THE_ABYSS: { "none": [AREA.ROYAL_WOOD, AREA.OOLACILE_TOWNSHIP]}, AREA.CATACOMBS: { - "none": [AREA.FIRELINK, AREA.TOMB_OF_THE_GIANTS_PRE_LV]}, + "none": [AREA.FIRELINK], + "sunlight_maggot": [AREA.TOMB_OF_THE_GIANTS_PRE_LV]}, AREA.TOMB_OF_THE_GIANTS_PRE_LV: { - "none": [AREA.CATACOMBS], + "sunlight_maggot": [AREA.CATACOMBS], "lordvessel": [AREA.TOMB_OF_THE_GIANTS_POST_LV]}, AREA.TOMB_OF_THE_GIANTS_POST_LV: { "lordvessel": [AREA.TOMB_OF_THE_GIANTS_PRE_LV], diff --git a/locations_setup.py b/locations_setup.py index 3dfc06d..c006d17 100644 --- a/locations_setup.py +++ b/locations_setup.py @@ -108,6 +108,14 @@ def __init__(self, diff, area, max_size, is_transient = False, self.has_flag = has_flag self.has_cumul_flag = has_cumul_flag self.default_key = default_key + self.is_dlc = (area == AREA.KALAMEET_FIGHT or + area == AREA.OOLACILE_HIDDEN or + area == AREA.OOLACILE_SANCTUARY or + area == AREA.OOLACILE_TOWNSHIP or + area == AREA.PAINTED_WORLD or + area == AREA.PAINTED_WORLD_ANNEX or + area == AREA.CHASM_OF_THE_ABYSS or + area == AREA.ROYAL_WOOD) LOCATIONS = { 0: Location(LOC_DIF.IGNORE, AREA.NONE, 1), @@ -791,7 +799,7 @@ def __init__(self, diff, area, max_size, is_transient = False, 1700160: Location(LOC_DIF.HARD, AREA.CRYSTAL_CAVE, 9), 1700170: Location(LOC_DIF.MEDIUM, AREA.CRYSTAL_CAVE, 9), 1700180: Location(LOC_DIF.EASY, AREA.CRYSTAL_CAVE, 9), - 1700200: Location(LOC_DIF.HARD, AREA.DUKES_PRISON_GIANT_CELL, 9), + 1700200: Location(LOC_DIF.HARD, AREA.DUKES_PRISON_GIANT_CELL, 9, is_race_key_loc = True), 1700210: Location(LOC_DIF.HARD, AREA.DUKES_PRISON, 9, is_race_key_loc = True, default_key = "archive_prison_extra_key"), 1700510: Location(LOC_DIF.EASY, AREA.DUKES_ARCHIVES, 9, is_race_key_loc = True), 1700520: Location(LOC_DIF.MEDIUM, AREA.DUKES_ARCHIVES, 9, is_race_key_loc = True), @@ -1066,7 +1074,7 @@ def __init__(self, diff, area, max_size, is_transient = False, 34720010: Location(LOC_DIF.HARD, AREA.OOLACILE_SANCTUARY, 9, is_transient = True), 34720020: Location(LOC_DIF.HARD, AREA.OOLACILE_SANCTUARY, 9, is_transient = True), 34800000: Location(LOC_DIF.NPC_MEDIUM, AREA.NPC_RNG_DROP, 9, is_transient = True), - 34800100: Location(LOC_DIF.HARD, AREA.LOST_IZALITH, 9, is_transient = True), + 34800100: Location(LOC_DIF.HARD, AREA.LOST_IZALITH, 9, is_transient = True, default_key = "sunlight_maggot"), 34900000: Location(LOC_DIF.LEAVE_ALONE, AREA.MOVING_NPC, 9, is_transient = True), 34900100: Location(LOC_DIF.LEAVE_ALONE, AREA.MOVING_NPC, 9, is_transient = True), 34900200: Location(LOC_DIF.LEAVE_ALONE, AREA.MOVING_NPC, 9, is_transient = True), @@ -1357,7 +1365,7 @@ def __init__(self, diff, area, max_size, is_transient = False, 60002102: Location(LOC_DIF.LEAVE_ALONE, AREA.NEW_LONDO_PRE_SEAL, 1, is_transient = True), 60002200: Location(LOC_DIF.EASY, AREA.MOVING_NPC, 1, is_transient = True, linked_locations = [60006502]), 60002201: Location(LOC_DIF.EASY, AREA.MOVING_NPC, 1, is_transient = True, linked_locations = [60006503]), - 60002202: Location(LOC_DIF.EASY, AREA.MOVING_NPC, 1, is_transient = True, linked_locations = [60006504], default_key = "cast_light"), + 60002202: Location(LOC_DIF.EASY, AREA.MOVING_NPC, 1, is_transient = True, linked_locations = [60006504]), 60002203: Location(LOC_DIF.MEDIUM, AREA.MOVING_NPC, 1, is_transient = True, linked_locations = [60006505]), 60002204: Location(LOC_DIF.MEDIUM, AREA.MOVING_NPC, 1, is_transient = True, linked_locations = [60006506]), 60002205: Location(LOC_DIF.MEDIUM, AREA.MOVING_NPC, 1, is_transient = True), @@ -1384,8 +1392,8 @@ def __init__(self, diff, area, max_size, is_transient = False, 60004001: Location(LOC_DIF.EASY, AREA.FIRELINK, 1, is_transient = True), 60004002: Location(LOC_DIF.EASY, AREA.FIRELINK, 1, is_transient = True), 60004003: Location(LOC_DIF.EASY, AREA.FIRELINK, 1, is_transient = True), - 60004004: Location(LOC_DIF.EASY, AREA.FIRELINK, 1, is_transient = True), - 60004005: Location(LOC_DIF.MEDIUM, AREA.FIRELINK, 1, is_transient = True), + 60004004: Location(LOC_DIF.LEAVE_ALONE, AREA.FIRELINK, 1, is_transient = True), + 60004005: Location(LOC_DIF.LEAVE_ALONE, AREA.FIRELINK, 1, is_transient = True), 60004006: Location(LOC_DIF.MEDIUM, AREA.FIRELINK, 1, is_transient = True), 60004200: Location(LOC_DIF.EASY, AREA.UNDEAD_PARISH, 1, has_flag = 11017050, is_transient = True), 60004201: Location(LOC_DIF.EASY, AREA.UNDEAD_PARISH, 1, has_flag = 11017060, is_transient = True), diff --git a/randomize_item_table.py b/randomize_item_table.py index 0c6efbe..3e9621a 100644 --- a/randomize_item_table.py +++ b/randomize_item_table.py @@ -146,6 +146,20 @@ def transmute_itemlotpart_to_consumable(itemlotpart, random_source): log.debug("Transmuting itemlotpart into consumable (" + str(cons_type) + ", " + str(cons_id) + ", " + str(cons_count) + ")") return itemlotpart + +def transmute_itemlotparts_to_bk_replacement(itemlotpart, random_source): + if itemlotpart.items: + for itemlotentry in itemlotpart.items: + if (itemlotentry.item_type == item_s.ITEM_TYPE.WEAPON and + itemlotentry.item_id in item_s.BLACK_KNIGHT_WEAPONS): + (cons_type, cons_id, cons_count_min, cons_count_max) = random_source.choice(sorted(item_s.BLACK_KNIGHT_WEAPON_REPLACEMENTS)) + cons_count = random_source.randrange(cons_count_min, cons_count_max + 1) + itemlotentry.item_type = cons_type + itemlotentry.item_id = cons_id + itemlotentry.count = cons_count + log.debug("Transmuting itemlotpart into black knight replacement (" + str(cons_type) + ", " + + str(cons_id) + ")") + return itemlotpart def transmute_itemlotpart_to_boss_item(itemlotpart, random_source): if itemlotpart.items: @@ -160,6 +174,22 @@ def transmute_itemlotpart_to_boss_item(itemlotpart, random_source): log.debug("Transmuting itemlotpart into boss item (" + str(boss_item_type) + ", " + str(boss_item_id) + ")") return itemlotpart + +def ascend_itemlotpart_to_ascended_item(itemlotpart, random_source): + if itemlotpart.items: + for itemlotentry in itemlotpart.items: + if itemlotentry.item_type == item_s.ITEM_TYPE.WEAPON and \ + itemlotentry.item_id in item_s.ASCENDABLE_WEAPONS: + if random_source.random() < .25: + entry = item_s.ASCENDABLE_WEAPONS[itemlotentry.item_id] + if entry[1] == True: + add = random_source.choice([1,2,3,4,5,6,7,8,9]) + else: + add = random_source.choice([1,2,4,6,8]) + itemlotentry.item_id = itemlotentry.item_id + (add * 100) + log.debug("Ascending itemlotpart into ascended weapon (" + + str(itemlotentry.item_id) + ")") + return itemlotpart def place_ignored_items(table, item_list): log.info("Placing ignored items.") @@ -181,18 +211,22 @@ def place_upgrade_items(table, random_source, item_list): if loc.diff == loc_s.LOC_DIF.RANDOM_UPGRADE: itemlotpart = transmute_itemlotpart_to_rng_drop_upgrade(itemlotpart, random_source) table.place_itemlotpart_at_location(itemlotpart, loc_id, item_list) - + +def place_key_item_in_specific_location(table, key_name, current_key_locations, item_list, first_key_loc_id): + current_key_locations[key_name] = table.location_dict[first_key_loc_id] + key_item = find_key_item_by_name(key_name, item_list) + table.place_itemlotpart_at_location(key_item, first_key_loc_id, item_list) + def place_key_item_in_vanilla_location(table, key_name, current_key_locations, item_list): key_loc_ids = [loc_id for loc_id in table.location_dict if table.location_dict[loc_id].default_key == key_name] first_key_loc_id = key_loc_ids[0] - current_key_locations[key_name] = table.location_dict[first_key_loc_id] - key_item = find_key_item_by_name(key_name, item_list) - table.place_itemlotpart_at_location(key_item, first_key_loc_id, item_list) + place_key_item_in_specific_location(table, key_name, current_key_locations, item_list, first_key_loc_id) def place_key_items(table, rand_options, random_source, item_list): log.info("Placing key items.") current_key_locations = {} + dlc_keys = ["annex_key", "broken_pendant", "crest_key", "peculiar_doll"] if rand_options.key_placement == rng_opt.RandOptKeyDifficulty.LEAVE_ALONE: log.info("Placing key items in vanilla locations.") for key_name in key_items_s.KEY_NAMES: @@ -203,7 +237,10 @@ def place_key_items(table, rand_options, random_source, item_list): current_key_locations[key] = "cannot_place" #current_areas = set(["starting"]) - if not rand_options.use_lordvessel: + if rand_options.use_lordvessel == rng_opt.RandOptLordvesselLocation.FIRELINK: + log.info("Placing Lordvessel in Firelink shrine.") + place_key_item_in_specific_location(table, "lordvessel", current_key_locations, item_list, 1020000) + elif rand_options.use_lordvessel == rng_opt.RandOptLordvesselLocation.GWYNEVERE: log.info("Placing Lordvessel in vanilla location.") place_key_item_in_vanilla_location(table, "lordvessel", current_key_locations, item_list) if not rand_options.use_lord_souls: @@ -212,7 +249,10 @@ def place_key_items(table, rand_options, random_source, item_list): place_key_item_in_vanilla_location(table, "lord_soul_shard_four_kings", current_key_locations, item_list) place_key_item_in_vanilla_location(table, "lord_soul_bed_of_chaos", current_key_locations, item_list) place_key_item_in_vanilla_location(table, "lord_soul_nito", current_key_locations, item_list) - + if rand_options.keys_not_in_dlc: + log.info("Since DLC disabled, placing DLC keys in vanilla locations.") + place_key_item_in_vanilla_location(table, "annex_key", current_key_locations, item_list) + place_key_item_in_vanilla_location(table, "crest_key", current_key_locations, item_list) has_good_trial = False while not has_good_trial: has_good_trial = True @@ -232,7 +272,9 @@ def place_key_items(table, rand_options, random_source, item_list): trial_key_locations[key_name] = current_key_locations[key_name] else: trial_key_locations[key_name] = "to_place" - + + is_race_key_loc = (rand_options.key_placement == rng_opt.RandOptKeyDifficulty.RACE_MODE or + rand_options.key_placement == rng_opt.RandOptKeyDifficulty.SPEEDRUN_MODE) keys_to_place = sorted([key for key in trial_key_locations if trial_key_locations[key] == "to_place"]) used_loc_ids = set([]) for key_name in keys_to_place: @@ -247,9 +289,10 @@ def place_key_items(table, rand_options, random_source, item_list): plausible_loc_ids if (loc_id not in used_loc_ids and table.location_dict[loc_id].area in restricted_areas) and not table.location_dict[loc_id].is_transient] - if (rand_options.key_placement == rng_opt.RandOptKeyDifficulty.RACE_MODE or - rand_options.key_placement == rng_opt.RandOptKeyDifficulty.SPEEDRUN_MODE): - possible_loc_ids = [loc_id for loc_id in pre_possible_loc_ids if table.location_dict[loc_id].is_race_key_loc] + + keys_not_in_dlc = rand_options.keys_not_in_dlc + if (is_race_key_loc or keys_not_in_dlc) and ((key_name not in dlc_keys) or not keys_not_in_dlc): + possible_loc_ids = [loc_id for loc_id in pre_possible_loc_ids if (((not is_race_key_loc) or table.location_dict[loc_id].is_race_key_loc) and ((not keys_not_in_dlc) or (not table.location_dict[loc_id].is_dlc)))] else: possible_loc_ids = pre_possible_loc_ids @@ -314,13 +357,32 @@ def place_non_key_fixed_items(table, rand_options, random_source, item_list): item_s.ITEM_DIF.BOSS_SOUL] and not item_s.ITEMS[item_id].always_follow_items): item_list[item_id].follow_items = [] + + if rand_options.no_online_items: + # because Darkmoon Blade Covenant Ring follows Blue Eye Orb, we need to go thru list now to scrub online items. + online_loc_ids = [3000] # also populate the 'Snuggly the Crow trade for Cracked Red Eye Orb' location with the 1st online item we see. + for item_id in item_ids_to_place: + item = item_list[item_id] + for itemlot in item.items: + if (itemlot.item_type == item_s.ITEM_TYPE.ITEM and \ + itemlot.item_id in item_s.ONLINE_USE_ITEMS) or \ + (itemlot.item_type == item_s.ITEM_TYPE.RING and \ + itemlot.item_id in item_s.ONLINE_USE_RINGS): + item = transmute_itemlotpart_to_consumable(item, random_source) + item.diff = item_s.ITEM_DIF.EASY # change it to easy; important to fix one of the cracked red orb's as it's salable and we want to remove that" + item_list[item_id] = item + if (len(online_loc_ids) > 0) and not item.needs_flag: + online_loc_id = online_loc_ids.pop() + table.place_itemlotpart_at_location(item, online_loc_id, item_list) + item_ids_to_place.pop(item_ids_to_place.index(item_id)) + # Remove following items from items_to_place. following_items = set(f_item_id for item_id in item_ids_to_place for f_item_id in item_list[item_id].follow_items) item_ids_to_place = [item_id for item_id in item_ids_to_place if item_id not in following_items] item_ids_to_place.sort(key = lambda item_id: item_list[item_id].get_max_effective_size(), reverse = True) - + for item_id in item_ids_to_place: item = item_list[item_id] @@ -336,6 +398,14 @@ def place_non_key_fixed_items(table, rand_options, random_source, item_list): if item.diff in [item_s.ITEM_DIF.SALABLE_EASY, item_s.ITEM_DIF.SALABLE_MEDIUM, item_s.ITEM_DIF.SALABLE_HARD]: item.items[0].count = -1 + + # Optionally randomly ascend weapons. + if rand_options.ascend_weapons == True: + item = ascend_itemlotpart_to_ascended_item(item, random_source) + + # Remove Black Knight weapons + if rand_options.no_black_knight_weapons: + item = transmute_itemlotparts_to_bk_replacement(item, random_source) # Place item. possible_loc_ids = create_random_placement_list(table, @@ -364,7 +434,7 @@ def place_starting_equipment(table, data_passed_from_chr_init, item_list): def build_table(rand_options, random_source, chr_init_data): # Create a deep copy of the list of items to be modified for this table. item_list = copy.deepcopy(item_s.ITEMS) - + # Deal with chr_init_data if chr_init_data == None: chr_inits = [chr_s.VANILLA_CHRS[chr_id].to_chr_init(chr_id , "") for chr_id in chr_s.VANILLA_CHRS] @@ -377,7 +447,7 @@ def build_table(rand_options, random_source, chr_init_data): for chr_init in given_cip.chr_inits: print(chr_init.to_string()) - table = item_t.ItemTable(copy.deepcopy(loc_s.LOCATIONS), copy.deepcopy(shop_s.DEFAULT_SHOP_DATA)) + table = item_t.ItemTable(rand_options, copy.deepcopy(loc_s.LOCATIONS), copy.deepcopy(shop_s.DEFAULT_SHOP_DATA)) place_ignored_items(table, item_list) place_upgrade_items(table, random_source, item_list) place_key_items(table, rand_options, random_source, item_list) @@ -407,6 +477,9 @@ def build_table(rand_options, random_source, chr_init_data): rng_opt.RandOptSoulItemsDifficulty.SHUFFLE, rng_opt.RandOptStartItemsDifficulty.COMBINED_POOL_AND_2H, rng_opt.RandOptGameVersion.PTDE, + False, + False, + False, False) rng = random.Random() diff --git a/randomizer.ini b/randomizer.ini new file mode 100644 index 0000000..0f5c124 --- /dev/null +++ b/randomizer.ini @@ -0,0 +1,17 @@ +[DEFAULT] +difficulty = 0 +fashion_souls = True +key_placement = 2 +use_lordvessel = Firelink +use_lord_souls = False +soul_items_diff = 2 +start_items_diff = 1 +game_version = DARK SOULS: REMASTERED +randomize_npc_armor = True +ascend_weapons = True +keys_not_in_dlc = True +set_up_hints = True +no_online_items = True +better_start_spells = True +no_black_knight_weapons = True + diff --git a/randomizer_gui.py b/randomizer_gui.py index f8ce27d..eb1f76c 100644 --- a/randomizer_gui.py +++ b/randomizer_gui.py @@ -1,3 +1,6 @@ +# TODO: is lightning spear, great lightening spear and sunlight spear being shuffled in? +# TODO: generate cheat sheet doesn't do seek guidance hints or show sync-num + import tkinter as tk import tkinter.ttk as ttk from tkinter import messagebox as tkMB @@ -15,16 +18,34 @@ import randomize_item_table import bnd_rebuilder import dcx_handler +from fmg_handler import FMGHandler import logging + +from settings_string_io import SettingsStringIO, SettingsVariable, IncompatibleVersionError, InvalidValueError + log = logging.getLogger(__name__) +import ini_parser +INI_FILE = "randomizer.ini" + MAX_SEED_LENGTH = 64 -VERSION_NUM = "0.3" +VERSION_NUM = "0.8.1" +# only add versions compatible RNG-wise, IE when fixing GUI stuff +COMPATIBLE_VERSIONS = [VERSION_NUM, ] + +#PTDE_GAMEPARAM_PATH_LIST = ["./GameParam.parambnd", "./param/GameParam/GameParam.parambnd", "C:\Program Files (x86)\Steam\steamapps\common\Dark Souls Prepare to Die Edition\DATA\param\GameParam\GameParam.parambnd", "C:\Programs\Steam\steamapps\common\Dark Souls Prepare to Die Edition\DATA\param\GameParam\GameParam.parambnd"] +#DS1R_GAMEPARAM_PATH_LIST = ["./GameParam.parambnd.dcx", "./param/GameParam/GameParam.parambnd.dcx", "D:\SteamLibrary\steamapps\common\DARK SOULS REMASTERED\param\GameParam\GameParam.parambnd.dcx", "D:\Program Files (x86)\Steam\steamapps\common\DARK SOULS REMASTERED\param\GameParam\GameParam.parambnd.dcx", "C:\programs\Steam\steamapps\common\DARK SOULS REMASTERED\param\GameParam\GameParam.parambnd.dcx"] -PTDE_GAMEPARAM_PATH_LIST = ["./GameParam.parambnd", "./param/GameParam/GameParam.parambnd"] -DS1R_GAMEPARAM_PATH_LIST = ["./GameParam.parambnd.dcx", "./param/GameParam/GameParam.parambnd.dcx"] +#PTDE_ENGMENU_PATH_LIST = ["./msg/ENGLISH/menu.msgbnd", "C:\Program Files (x86)\Steam\steamapps\common\Dark Souls Prepare to Die Edition\DATA\msg\ENGLISH\menu.msgbnd", "C:\Programs\Steam\steamapps\common\Dark Souls Prepare to Die Edition\DATA\msg\ENGLISH\menu.msgbnd"] +#DS1R_ENGMENU_PATH_LIST = ["./msg/ENGLISH/menu.msgbnd.dcx", "D:\SteamLibrary\steamapps\common\DARK SOULS REMASTERED\msg\ENGLISH\menu.msgbnd.dcx", "D:\Program Files (x86)\Steam\steamapps\common\DARK SOULS REMASTERED\msg\ENGLISH\menu.msgbnd.dcx", "C:\Programs\Steam\steamapps\common\DARK SOULS REMASTERED\msg\ENGLISH\menu.msgbnd.dcx"] + +PTDE_GAMEPARAM_PATH_LIST = ["./GameParam.parambnd", "./param/GameParam/GameParam.parambnd", "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Dark Souls Prepare to Die Edition\\DATA\\param\\GameParam\\GameParam.parambnd", "C:\\Programs\\Steam\\steamapps\\common\\Dark Souls Prepare to Die Edition\\DATA\\param\\GameParam\\GameParam.parambnd"] +DS1R_GAMEPARAM_PATH_LIST = ["./GameParam.parambnd.dcx", "./param/GameParam/GameParam.parambnd.dcx", "D:\\SteamLibrary\\steamapps\\common\\DARK SOULS REMASTERED\\param\\GameParam\\GameParam.parambnd.dcx", "D:\\Program Files (x86)\\Steam\\steamapps\\common\\DARK SOULS REMASTERED\\param\\GameParam\\GameParam.parambnd.dcx", "C:\\programs\\Steam\\steamapps\\common\\DARK SOULS REMASTERED\\param\\GameParam\\GameParam.parambnd.dcx"] + +PTDE_ENGMENU_PATH_LIST = ["./msg/ENGLISH/menu.msgbnd", "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Dark Souls Prepare to Die Edition\\DATA\\msg\\ENGLISH\\menu.msgbnd", "C:\\Programs\\Steam\\steamapps\\common\\Dark Souls Prepare to Die Edition\\DATA\\msg\\ENGLISH\\menu.msgbnd"] +DS1R_ENGMENU_PATH_LIST = ["./msg/ENGLISH/menu.msgbnd.dcx", "D:\\SteamLibrary\\steamapps\\common\\DARK SOULS REMASTERED\\msg\\ENGLISH\\menu.msgbnd.dcx", "D:\\Program Files (x86)\\Steam\\steamapps\\common\\DARK SOULS REMASTERED\\msg\\ENGLISH\\menu.msgbnd.dcx", "C:\\Programs\\Steam\\steamapps\\common\\DARK SOULS REMASTERED\\msg\\ENGLISH\\menu.msgbnd.dcx"] DESC_DICT = { "diff": {rngopts.RandOptDifficulty.EASY: "* Perfectly fair. Items have an equal chance to be placed anywhere.\n", @@ -32,38 +53,46 @@ rngopts.RandOptDifficulty.HARD: "* Heavily biased. Desirable items are hidden, and are rarely in early areas.\n"}, "key_diff": {rngopts.RandOptKeyDifficulty.LEAVE_ALONE: ("* Key items are placed in their usual locations.\n" + " For a player who wants random items without needing to hunt for keys.\n" + - " Some key locations may contain additional items in some seeds.\n\n"), + " Some key locations may contain additional items in some seeds.\n"), rngopts.RandOptKeyDifficulty.RANDOMIZE: ("* Key items are shuffled into the item pool and placed in random locations.\n" + " For a player who wishes to explore all of Lordran. Average run is ~10hrs.\n" + - " Players will typically need to pick up many items in search of keys.\n\n"), + " Players will typically need to pick up many items in search of keys.\n"), rngopts.RandOptKeyDifficulty.RACE_MODE: ( "* Key items are shuffled but can be placed only in certain locations.\n" + " Good for races / short runs. Average run is ~4hrs. See README for list of \n" + - " locations to check. Read this list ahead of time to know where to check.\n\n"), + " locations to check. Read this list ahead of time to know where to check.\n"), rngopts.RandOptKeyDifficulty.SPEEDRUN_MODE: ( "* Key items are shuffled but can be placed only in certain locations.\n" + " May require skips and glitches to complete. SOFTLOCKING IS POSSIBLE.\n" + - " See README for list of locations to check. Read this list ahead of time.\n\n")}, - "souls_diff": {rngopts.RandOptSoulItemsDifficulty.SHUFFLE: "* Soul items are shuffled into the item pool like other items.\n\n", - rngopts.RandOptSoulItemsDifficulty.CONSUMABLE: "* Lesser soul items are replaced with a random consumable before shuffling.\n\n", - rngopts.RandOptSoulItemsDifficulty.TRANSPOSE: "* Boss souls have a 75% chance to be transposed to one of their boss items.\n\n"}, + " See README for list of locations to check. Read this list ahead of time.\n")}, + "souls_diff": {rngopts.RandOptSoulItemsDifficulty.SHUFFLE: "* Soul items are shuffled into the item pool like other items.\n", + rngopts.RandOptSoulItemsDifficulty.CONSUMABLE: "* Lesser soul items are replaced with a random consumable before shuffling.\n", + rngopts.RandOptSoulItemsDifficulty.TRANSPOSE: "* Boss souls have a 75% chance to be transposed to one of their boss items.\n"}, "start_items": {rngopts.RandOptStartItemsDifficulty.SHIELD_AND_1H: ("* Player starts with random class-usable (L) shield & (R) weapon.\n" + - " The weapon is usable one-handed with base stats.\n\n"), + " The weapon is usable one-handed with base stats.\n"), rngopts.RandOptStartItemsDifficulty.SHIELD_AND_2H: ("* Player starts with random class-usable (L) shield & (R) weapon.\n" + - " The weapon may need to be two-handed to be usable with base stats.\n\n"), + " The weapon may need to be two-handed to be usable with base stats.\n"), rngopts.RandOptStartItemsDifficulty.COMBINED_POOL_AND_2H: ("* Player starts with random class-usable (L) shield OR weapon & (R) weapon.\n" + - " The weapon(s) may need to be two-handed to be usable with base stats.\n\n")}, + " The weapon(s) may need to be two-handed to be usable with base stats.\n")}, "fashion": {True: "* Armor sets ARE NOT kept together during shuffling.\n Players will typically need to mix-and-match armor pieces.\n", False: "* Armor sets ARE kept together during shuffling.\n Players will be able to find full sets of armor at once.\n"}, - "npc_armor": {True: "* NPCs wear randomly chosen armor instead of their normal sets.\n If Fashion Souls is on, NPCs will also mix-and-match their armor.\n\n", - False: "* NPCs will wear their normal sets of armor.\n NPCs have their familiar look, weight class and defense stats.\n\n"}, - "use_lv": {True: "* The Lordvessel IS included in the randomized keys.\n Difficulty ranges from much easier (in Firelink) to harder (in TotG).\n", - False: "* The Lordvessel IS NOT included in the randomized keys.\n Difficulty is standard. Lordvessel is given by Gwynevere in Anor Londo.\n"}, - "use_lord_souls": {True: "* The 4 Lord Souls ARE included in the randomized keys.\n Difficulty ranges from much easier to much harder.", - False: "* The 4 Lord Souls ARE NOT included in the randomized keys.\n Difficulty is standard. Lord Souls are dropped by their normal bosses."} + "npc_armor": {True: "* NPCs wear randomly chosen armor instead of their normal sets.\n If Fashion Souls is on, NPCs will also mix-and-match their armor.\n", + False: "* NPCs will wear their normal sets of armor.\n NPCs have their familiar look, weight class and defense stats.\n"}, + "use_lv": {rngopts.RandOptLordvesselLocation.RANDOMIZED: "* The Lordvessel IS included in the randomized keys.\n Difficulty ranges from much easier (in Firelink) to harder (in TotG).\n", + rngopts.RandOptLordvesselLocation.GWYNEVERE: "* The Lordvessel IS NOT included in the randomized keys.\n Difficulty is standard. Lordvessel is given by Gwynevere in Anor Londo.\n", + rngopts.RandOptLordvesselLocation.FIRELINK: "* The Lordvessel is at Firelink Shrine.\n Difficulty is easy.\n"}, + "use_lord_souls": {True: "* The 4 Lord Souls ARE included in the randomized keys.\n Difficulty ranges from much easier to much harder.\n", + False: "* The 4 Lord Souls ARE NOT included in the randomized keys.\n Difficulty is standard. Lord Souls are dropped by their normal bosses.\n"}, + "ascend_weapons": {True: "* Normal weapons have a 25% chance to be ascended with a random ember.\n", + False: "* Normal weapons drop as expected.\n"}, + "set_up_hints": {True: "* The dev messages visibile with Seek Guidance will have \n hints automatically added in.\n", + False: "* There are no hints that are added to the seed via Seek Guidance.\n\n"}, + "keys_not_in_dlc": {True: "* Key items will NOT be in DLC (Painted World, Artorias of the Abyss).\n", + False: "* Key items can be in DLC (Painted World, Artorias of the Abyss).\n"}, + "no_black_knight_weapons": {True: "* Black Knight weapons replaced by Titanite Chunks and Slabs.\n", + False: "* Black Knight weapons are available for use.\n"} } -DESC_ORDER = ["diff", "key_diff", "souls_diff", "start_items", "fashion", "npc_armor", "use_lv", "use_lord_souls"] - +DESC_ORDER = ["diff", "key_diff", "souls_diff", "keys_not_in_dlc", "start_items", "fashion", "npc_armor", "use_lv", "use_lord_souls", "ascend_weapons", "set_up_hints", "no_black_knight_weapons"] def resource_path(rel_path): @@ -73,10 +102,12 @@ def resource_path(rel_path): base_path = os.path.abspath(".") return os.path.join(base_path, rel_path) + class Placeholder_State(object): - __slots__ = 'normal_color', 'normal_font', 'placeholder_text', 'placeholder_color', 'placeholder_font', 'with_placeholder' - -class DescriptionState(): + __slots__ = 'normal_color', 'normal_font', 'placeholder_text', 'placeholder_color', 'placeholder_font', 'with_placeholder' + + +class DescriptionState: NORMAL = "normal" DEEMPH = "deemph" EMPH = "emph" @@ -100,6 +131,7 @@ def write_state_to_area(self): class MainGUI: def __init__(self): + init_options = ini_parser.get_values_from_ini(INI_FILE, section="DEFAULT") #load ini file self.seed_rng = random.Random() self.has_hovered_desc = False self.root = tk.Tk() @@ -116,13 +148,16 @@ def __init__(self): self.seed_entry = tk.Entry(self.root, font="TkFixedFont", textvariable=self.seed_string, width=70) self.entry_state = self.add_placeholder_to(self.seed_entry, 'Type a seed (or leave blank for a random seed)') self.seed_entry.grid(row=0, column=1, columnspan=2, ipady=2, ipadx=1, padx=2, sticky='EW') - tk.Label(self.root, text="Made by HotPocketRemix ").grid(row=0, column=3, columnspan=2, sticky='E', padx=2) - self.sellout_button = tk.Button(self.root, text="$", bg="pale goldenrod", + self.sellout_button = tk.Button(self.root, text="?", bg="pale goldenrod", padx=2, pady=2, command=self.lift_sellout_area) self.sellout_button.grid(row=0, column=4, padx=2, sticky='E') + self.normalize_button = tk.Button(self.root, text="Revert to vanilla", + padx=2, pady=2, command=self.normalize_game) + self.normalize_button.grid(row=0, column=3, padx=2, sticky='E') tk.Label(self.root, text="Dark Souls Game Version:").grid(row=1, column=0, columnspan=2, sticky='W', padx=2, ipady=1) self.game_version = tk.StringVar() + self.game_version.trace('w', lambda name, index, mode: self.update()) self.game_version_menu = ttk.Combobox(self.root, textvariable=self.game_version, state="readonly", values=[rngopts.RandOptGameVersion.PTDE, rngopts.RandOptGameVersion.REMASTERED], style="GameVersion.TCombobox") @@ -134,159 +169,214 @@ def __init__(self): self.game_version_menu.config(width=30) self.game_version_menu.grid(row=1, column=2, sticky='EW', padx=2) - self.msg_area = tk.Text(self.root, width=76, height=19, state="disabled", background=self.root.cget('background'), wrap="word") - self.msg_area.grid(row=2, column=0, columnspan=3, rowspan=9, padx=2, pady=2) + self.msg_area = tk.Text(self.root, width=76, height=22, + state="disabled", background=self.root.cget('background'), wrap="word") + self.msg_area.grid(row=2, column=0, columnspan=3, rowspan=10, padx=2, pady=2, sticky='NS') self.msg_quit_button = tk.Button(self.root, text="Quit", command=self.quit_button) self.msg_quit_button.grid(row=9, column=1, columnspan=2, rowspan=2) self.msg_continue_button = tk.Button(self.root, text="Continue", command=self.continue_button) self.msg_continue_button.grid(row=7, column=1, columnspan=2, rowspan=2) self.back_button = tk.Button(self.root, text="Back", command=self.back_button) self.back_button.grid(row=7, column=1, columnspan=2, rowspan=2) - self.desc_area = tk.Text(self.root, width=76, height=19, state="disabled", background=self.root.cget('background'), wrap="word") - self.desc_area.grid(row=2, column=0, columnspan=3, rowspan=9, padx=2, pady=2) + self.desc_area = tk.Text(self.root, width=76, height=22, state="disabled", background=self.root.cget('background'), wrap="word") + self.desc_area.grid(row=2, column=0, columnspan=3, rowspan=10, padx=2, pady=2) - self.diff_frame = tk.LabelFrame(text="Difficulty:") - self.diff_frame.grid(row=2, column=3, rowspan=1, sticky='NS', padx=2) + self.save_options = tk.BooleanVar() + self.save_options.set(False) + self.save_options_check = tk.Checkbutton(self.root, text="Save Settings as Default", + variable=self.save_options, onvalue=True, offvalue=False, padx=2, + width=20, anchor=tk.W) + self.save_options_check.grid(row=1, column=3, columnspan=2) + + self.diff_frame = tk.LabelFrame(text="Difficulty:", bd=0) + self.diff_frame.grid(row=2, column=3, sticky='NS', padx=2) + #-- self.diff = tk.IntVar() - self.diff.set(rngopts.RandOptDifficulty.EASY) + self.diff_as_string = tk.StringVar() + self.diff.set(ini_parser.get_option_value(init_options, "difficulty")) + self.diff_as_string.set(rngopts.RandOptDifficulty.as_string(self.diff.get())) self.diff.trace('w', lambda name, index, mode: self.update()) - self.diff_rbutton1 = tk.Radiobutton(self.diff_frame, - text=rngopts.RandOptDifficulty.as_string(rngopts.RandOptDifficulty.EASY), - variable=self.diff, value=rngopts.RandOptDifficulty.EASY, width=10, anchor=tk.W) - self.diff_rbutton2 = tk.Radiobutton(self.diff_frame, - text=rngopts.RandOptDifficulty.as_string(rngopts.RandOptDifficulty.MEDIUM), - variable=self.diff, value=rngopts.RandOptDifficulty.MEDIUM, width=10, anchor=tk.W) - self.diff_rbutton3 = tk.Radiobutton(self.diff_frame, - text=rngopts.RandOptDifficulty.as_string(rngopts.RandOptDifficulty.HARD), - variable=self.diff, value=rngopts.RandOptDifficulty.HARD, width=10, anchor=tk.W) - self.diff_rbutton1.grid(row=0, column=0, sticky='W') - self.diff_rbutton2.grid(row=1, column=0, sticky='W') - self.diff_rbutton3.grid(row=2, column=0, sticky='W') - self.setup_hover_events(self.diff_rbutton1, {"diff": rngopts.RandOptDifficulty.EASY}) - self.setup_hover_events(self.diff_rbutton2, {"diff": rngopts.RandOptDifficulty.MEDIUM}) - self.setup_hover_events(self.diff_rbutton3, {"diff": rngopts.RandOptDifficulty.HARD}) - - self.key_diff_frame = tk.LabelFrame(text="Key Placement:") - self.key_diff_frame.grid(row=3, column=3, rowspan=4, sticky='NS', padx=2) + self.gui_diff = ttk.Combobox(self.diff_frame, + values=rngopts.RandOptDifficulty.as_strings(), + textvariable=self.diff_as_string, + width=13, + state="readonly") + self.gui_diff.bind("<>", lambda _: self.diff.set(rngopts.RandOptDifficulty.from_string(self.diff_as_string.get()))) + self.gui_diff.grid(row=0, column=0, sticky='W') + self.setup_hover_events(self.gui_diff, {"diff": None}, no_emph = True) + + + self.key_diff_frame = tk.LabelFrame(text="Key Placement:", bd=0) + self.key_diff_frame.grid(row=3, column=3, sticky='NS', padx=2) + #-- self.key_diff = tk.IntVar() - self.key_diff.set(rngopts.RandOptKeyDifficulty.RANDOMIZE) + self.key_diff_as_string = tk.StringVar() + self.key_diff.set(ini_parser.get_option_value(init_options, "key_placement")) + self.key_diff_as_string.set(rngopts.RandOptKeyDifficulty.as_string(self.key_diff.get())) self.key_diff.trace('w', lambda name, index, mode: self.update()) - self.key_diff_rbutton1 = tk.Radiobutton(self.key_diff_frame, - text=rngopts.RandOptKeyDifficulty.as_string(rngopts.RandOptKeyDifficulty.LEAVE_ALONE), - variable=self.key_diff, value=rngopts.RandOptKeyDifficulty.LEAVE_ALONE, width=10, anchor=tk.W) - self.key_diff_rbutton2 = tk.Radiobutton(self.key_diff_frame, - text=rngopts.RandOptKeyDifficulty.as_string(rngopts.RandOptKeyDifficulty.RANDOMIZE), - variable=self.key_diff, value=rngopts.RandOptKeyDifficulty.RANDOMIZE, width=10, anchor=tk.W) - self.key_diff_rbutton3 = tk.Radiobutton(self.key_diff_frame, - text=rngopts.RandOptKeyDifficulty.as_string(rngopts.RandOptKeyDifficulty.RACE_MODE), - variable=self.key_diff, value=rngopts.RandOptKeyDifficulty.RACE_MODE, width=10, anchor=tk.W) - self.key_diff_rbutton4 = tk.Radiobutton(self.key_diff_frame, - text=rngopts.RandOptKeyDifficulty.as_string(rngopts.RandOptKeyDifficulty.SPEEDRUN_MODE), - variable=self.key_diff, value=rngopts.RandOptKeyDifficulty.SPEEDRUN_MODE, width=10, anchor=tk.W) - self.key_diff_rbutton1.grid(row=0, column=0, sticky='W') - self.key_diff_rbutton2.grid(row=1, column=0, sticky='W') - self.key_diff_rbutton3.grid(row=2, column=0, sticky='W') - self.key_diff_rbutton4.grid(row=3, column=0, sticky='W') - self.setup_hover_events(self.key_diff_rbutton1, {"key_diff": rngopts.RandOptKeyDifficulty.LEAVE_ALONE, "use_lv": False, "use_lord_souls": False}) - self.setup_hover_events(self.key_diff_rbutton2, {"key_diff": rngopts.RandOptKeyDifficulty.RANDOMIZE}) - self.setup_hover_events(self.key_diff_rbutton3, {"key_diff": rngopts.RandOptKeyDifficulty.RACE_MODE}) - self.setup_hover_events(self.key_diff_rbutton4, {"key_diff": rngopts.RandOptKeyDifficulty.SPEEDRUN_MODE, "diff": rngopts.RandOptDifficulty.EASY}) - - self.soul_frame = tk.LabelFrame(text="Soul Items:") - self.soul_frame.grid(row=7, column=3, rowspan=5, sticky='NS', padx=2, pady=2) + self.gui_key_diff = ttk.Combobox(self.key_diff_frame, + values=rngopts.RandOptKeyDifficulty.as_strings(), + textvariable=self.key_diff_as_string, + width=13, + state="readonly") + self.gui_key_diff.bind("<>", lambda _: self.key_diff.set(rngopts.RandOptKeyDifficulty.from_string(self.key_diff_as_string.get()))) + self.gui_key_diff.grid(row=0, column=0, sticky='W') + self.setup_hover_events(self.gui_key_diff, {"key_diff": None}, no_emph = True) + + self.soul_frame = tk.LabelFrame(text="Soul Items:", bd=0) + self.soul_frame.grid(row=4, column=3, sticky='NS', padx=2, pady=2) + #-- self.soul_diff = tk.IntVar() - self.soul_diff.set(rngopts.RandOptSoulItemsDifficulty.SHUFFLE) + self.soul_diff_as_string = tk.StringVar() + self.soul_diff.set(ini_parser.get_option_value(init_options, "soul_items_diff")) + self.soul_diff_as_string.set(rngopts.RandOptSoulItemsDifficulty.as_string(self.soul_diff.get())) self.soul_diff.trace('w', lambda name, index, mode: self.update()) - self.soul_diff_rbutton1 = tk.Radiobutton(self.soul_frame, - text=rngopts.RandOptSoulItemsDifficulty.as_string(rngopts.RandOptSoulItemsDifficulty.SHUFFLE), - variable=self.soul_diff, value=rngopts.RandOptSoulItemsDifficulty.SHUFFLE, width=10, anchor=tk.W) - self.soul_diff_rbutton2 = tk.Radiobutton(self.soul_frame, - text=rngopts.RandOptSoulItemsDifficulty.as_string(rngopts.RandOptSoulItemsDifficulty.CONSUMABLE), - variable=self.soul_diff, value=rngopts.RandOptSoulItemsDifficulty.CONSUMABLE, width=10, anchor=tk.W) - self.soul_diff_rbutton3 = tk.Radiobutton(self.soul_frame, - text=rngopts.RandOptSoulItemsDifficulty.as_string(rngopts.RandOptSoulItemsDifficulty.TRANSPOSE), - variable=self.soul_diff, value=rngopts.RandOptSoulItemsDifficulty.TRANSPOSE, width=10, anchor=tk.W) - self.soul_diff_rbutton1.grid(row=0, column=0, sticky='W') - self.soul_diff_rbutton2.grid(row=1, column=0, sticky='W') - self.soul_diff_rbutton3.grid(row=2, column=0, sticky='W') - self.setup_hover_events(self.soul_diff_rbutton1, {"souls_diff": rngopts.RandOptSoulItemsDifficulty.SHUFFLE}) - self.setup_hover_events(self.soul_diff_rbutton2, {"souls_diff": rngopts.RandOptSoulItemsDifficulty.CONSUMABLE}) - self.setup_hover_events(self.soul_diff_rbutton3, {"souls_diff": rngopts.RandOptSoulItemsDifficulty.TRANSPOSE}) - - self.start_items_frame = tk.LabelFrame(text="Starting Items:") + self.gui_soul_diff = ttk.Combobox(self.soul_frame, + values=rngopts.RandOptSoulItemsDifficulty.as_strings(), + textvariable=self.soul_diff_as_string, + width=13, + state="readonly") + self.gui_soul_diff.bind("<>", lambda _: self.soul_diff.set(rngopts.RandOptSoulItemsDifficulty.from_string(self.soul_diff_as_string.get()))) + self.gui_soul_diff.grid(row=0, column=0, sticky='W') + self.setup_hover_events(self.gui_soul_diff, {"souls_diff": None}, no_emph = True) + + self.use_lordvessel_frame = tk.LabelFrame(text="Lordvessel:", bd=0) + self.use_lordvessel_frame.grid(row=5, column=3, sticky='NS', padx=2, pady=2) + #-- + self.use_lordvessel = tk.StringVar() + self.use_lordvessel.set(rngopts.RandOptLordvesselLocation.verify(ini_parser.get_option_value(init_options, "use_lordvessel"))) + self.use_lordvessel.trace('w', lambda name, index, mode: self.update()) + self.gui_lordvessel = ttk.Combobox(self.use_lordvessel_frame, + textvariable=self.use_lordvessel, + values=rngopts.RandOptLordvesselLocation.as_strings(), + width=13, + state="readonly") + self.gui_lordvessel.grid(row=0, column=0, sticky='W') + self.setup_hover_events(self.gui_lordvessel, {"use_lv": None}, no_emph = True) + + self.start_items_frame = tk.LabelFrame(text="Starting Items:", bd=0) #wxy self.start_items_frame.grid(row=2, column=4, sticky='NS', padx=2) + #-- self.start_items_diff = tk.IntVar() - self.start_items_diff.set(rngopts.RandOptStartItemsDifficulty.SHIELD_AND_2H) + self.start_items_diff_as_string = tk.StringVar() + self.start_items_diff.set(ini_parser.get_option_value(init_options, "start_items_diff")) + self.start_items_diff_as_string.set(rngopts.RandOptStartItemsDifficulty.as_string(self.start_items_diff.get())) self.start_items_diff.trace('w', lambda name, index, mode: self.update()) - self.start_items_rbutton1 = tk.Radiobutton(self.start_items_frame, - text=rngopts.RandOptStartItemsDifficulty.as_string(rngopts.RandOptStartItemsDifficulty.SHIELD_AND_1H), - variable=self.start_items_diff, value=rngopts.RandOptStartItemsDifficulty.SHIELD_AND_1H, width=20, anchor=tk.W) - self.start_items_rbutton2 = tk.Radiobutton(self.start_items_frame, - text=rngopts.RandOptStartItemsDifficulty.as_string(rngopts.RandOptStartItemsDifficulty.SHIELD_AND_2H), - variable=self.start_items_diff, value=rngopts.RandOptStartItemsDifficulty.SHIELD_AND_2H, width=20, anchor=tk.W) - self.start_items_rbutton3 = tk.Radiobutton(self.start_items_frame, - text=rngopts.RandOptStartItemsDifficulty.as_string(rngopts.RandOptStartItemsDifficulty.COMBINED_POOL_AND_2H), - variable=self.start_items_diff, value=rngopts.RandOptStartItemsDifficulty.COMBINED_POOL_AND_2H, width=20, anchor=tk.W) - self.start_items_rbutton1.grid(row=0, column=0, sticky='W') - self.start_items_rbutton2.grid(row=1, column=0, sticky='W') - self.start_items_rbutton3.grid(row=2, column=0, sticky='W') - self.setup_hover_events(self.start_items_rbutton1, {"start_items": rngopts.RandOptStartItemsDifficulty.SHIELD_AND_1H}) - self.setup_hover_events(self.start_items_rbutton2, {"start_items": rngopts.RandOptStartItemsDifficulty.SHIELD_AND_2H}) - self.setup_hover_events(self.start_items_rbutton3, {"start_items": rngopts.RandOptStartItemsDifficulty.COMBINED_POOL_AND_2H}) + self.gui_start_items_diff = ttk.Combobox(self.start_items_frame, + values=rngopts.RandOptStartItemsDifficulty.as_strings(), + textvariable=self.start_items_diff_as_string, + width=25, + state="readonly") + self.gui_start_items_diff.bind("<>", lambda _: self.start_items_diff.set(rngopts.RandOptStartItemsDifficulty.from_string(self.start_items_diff_as_string.get()))) + self.gui_start_items_diff.grid(row=0, column=0, sticky='W') + self.setup_hover_events(self.gui_start_items_diff, {"start_items": None}, no_emph = True) + self.misc_flags_frame = tk.LabelFrame(text="Other Settings:", bd=0) #wxy + self.misc_flags_frame.grid(row=3, column=4, rowspan=5, sticky='NS', padx=2) + #-- self.fashion_bool = tk.BooleanVar() - self.fashion_bool.set(True) + self.fashion_bool.set(ini_parser.get_option_value(init_options, "fashion_souls")) self.fashion_bool.trace('w', lambda name, index, mode: self.update()) - self.fashion_check = tk.Checkbutton(self.root, text="Fashion Souls", + self.fashion_check = tk.Checkbutton(self.misc_flags_frame, text="Fashion Souls", variable=self.fashion_bool, onvalue=True, offvalue=False, padx=2, width=20, anchor=tk.W) - self.fashion_check.grid(row=3, column=4, sticky='W') - self.setup_hover_events(self.fashion_check, {"fashion": None}, no_emph = True) + self.fashion_check.grid(row=0, column=0, sticky='W') + self.setup_hover_events(self.fashion_check, {"fashion": None}, no_emph=True) self.npc_armor_bool = tk.BooleanVar() - self.npc_armor_bool.set(False) + self.npc_armor_bool.set(ini_parser.get_option_value(init_options, "randomize_npc_armor")) self.npc_armor_bool.trace('w', lambda name, index, mode: self.update()) - self.npc_armor_check = tk.Checkbutton(self.root, text="Laundromat Mixup", + self.npc_armor_check = tk.Checkbutton(self.misc_flags_frame, text="Laundromat Mixup", variable=self.npc_armor_bool, onvalue=True, offvalue=False, padx=2, width=20, anchor=tk.W) - self.npc_armor_check.grid(row=4, column=4, sticky='W') - self.setup_hover_events(self.npc_armor_check, {"npc_armor": None}, no_emph = True) - - self.use_lordvessel = tk.BooleanVar() - self.use_lordvessel.set(False) - self.use_lordvessel.trace('w', lambda name, index, mode: self.update()) - self.lv_check = tk.Checkbutton(self.root, text="Senile Gwynevere", - variable=self.use_lordvessel, onvalue=True, offvalue=False, padx=2, - width=20, anchor=tk.W) - self.lv_check.grid(row=5, column=4, sticky='W') - self.setup_hover_events(self.lv_check, {"use_lv": None}, no_emph = True) - + self.npc_armor_check.grid(row=1, column=0, sticky='W') + self.setup_hover_events(self.npc_armor_check, {"npc_armor": None}, no_emph=True) + self.use_lord_souls = tk.BooleanVar() - self.use_lord_souls.set(False) - self.use_lordvessel.trace('w', lambda name, index, mode: self.update()) - self.lord_soul_check = tk.Checkbutton(self.root, text="Senile Primordial Serpents", + self.use_lord_souls.set(ini_parser.get_option_value(init_options, "use_lord_souls")) + self.use_lord_souls.trace('w', lambda name, index, mode: self.update()) + self.lord_soul_check = tk.Checkbutton(self.misc_flags_frame, text="Senile Primordial Serpents", variable=self.use_lord_souls, onvalue=True, offvalue=False, padx=2, width=20, anchor=tk.W) - self.lord_soul_check.grid(row=6, column=4, sticky='W') + self.lord_soul_check.grid(row=2, column=0, sticky='W') self.setup_hover_events(self.lord_soul_check, {"use_lord_souls": None}, no_emph = True) - + + self.ascend_weapons_bool = tk.BooleanVar() + self.ascend_weapons_bool.set(ini_parser.get_option_value(init_options, "ascend_weapons")) + self.ascend_weapons_bool.trace('w', lambda name, index, mode: self.update()) + self.ascend_weapons_check = tk.Checkbutton(self.misc_flags_frame, text="Eager Smiths", + variable=self.ascend_weapons_bool, onvalue=True, offvalue=False, padx=2, + width=20, anchor=tk.W) + self.ascend_weapons_check.grid(row=3, column=0, sticky='W') + self.setup_hover_events(self.ascend_weapons_check, {"ascend_weapons": None}, no_emph = True) + + self.set_up_hints = tk.BooleanVar() + self.set_up_hints.set(ini_parser.get_option_value(init_options, "set_up_hints")) + self.set_up_hints.trace('w', lambda name, index, mode: self.update()) + self.hint_check = tk.Checkbutton(self.misc_flags_frame, text="Seek Guidance Hints", + variable=self.set_up_hints, onvalue=True, offvalue=False, padx=2, + width=20, anchor=tk.W) + self.hint_check.grid(row=4, column=0, sticky='W') + self.setup_hover_events(self.hint_check, {"set_up_hints": None}, no_emph = True) + + self.keys_not_in_dlc = tk.BooleanVar() + self.keys_not_in_dlc.set(ini_parser.get_option_value(init_options, "keys_not_in_dlc")) + self.keys_not_in_dlc.trace('w', lambda name, index, mode: self.update()) + self.keys_not_in_dlc_check = tk.Checkbutton(self.misc_flags_frame, text="No DLC", + variable=self.keys_not_in_dlc, onvalue=True, offvalue=False, #, padx=2, + width=10, anchor=tk.W) + self.keys_not_in_dlc_check.grid(row=5, column=0, sticky='W') + self.setup_hover_events(self.keys_not_in_dlc_check, {"keys_not_in_dlc": None}, no_emph = True) + + self.no_black_knight_weapons = tk.BooleanVar() + self.no_black_knight_weapons.set(ini_parser.get_option_value(init_options, "no_black_knight_weapons")) + self.no_black_knight_weapons.trace('w', lambda name, index, mode: self.update()) + self.no_black_knight_weapons_gui = tk.Checkbutton(self.misc_flags_frame, text="No Black Knight Weapons", + variable=self.no_black_knight_weapons, onvalue=True, offvalue=False, #, padx=2, + width=20, anchor=tk.W) + self.no_black_knight_weapons_gui.grid(row=6, column=0, sticky='W') + self.setup_hover_events(self.no_black_knight_weapons_gui, {"no_black_knight_weapons": None}, no_emph = True) + self.export_button = tk.Button(self.root, text="Scramble Items &\nExport to GameParam", padx=10, pady=10, command=self.export_to_gameparam) - self.export_button.grid(row=7, rowspan=3, column=4, padx=2, sticky='EW') - - self.cheat_button = tk.Button(self.root, text="Write Seed Info &\nCheatsheet / Hintsheet", command=self.export_seed_info) - self.cheat_button.grid(row=10, rowspan=2, column=4, sticky='EW', padx=2, pady=2) + self.export_button.grid(row=8, rowspan=3, column=4, padx=2, sticky='EW') + self.cheat_button = tk.Button(self.root, text="Write Seed Cheatsheet", command=self.export_seed_info) + self.cheat_button.grid(row=11, rowspan=1, column=4, sticky='EW', padx=2, pady=2) + + # Settings sync popup menu + self.popup_menu = tk.Menu(self.root, tearoff=0) + self.popup_menu.add_command(label="Copy settings sync", command=self.settings_string_copy) + self.popup_menu.add_command(label="Paste settings sync", command=self.settings_string_paste) + self.root.bind("", self.popup_menu_show) + + self.settings_string_io = SettingsStringIO(VERSION_NUM, COMPATIBLE_VERSIONS, [ + SettingsVariable(name='seed', variable=self.seed_string, on_update=lambda: self.seed_entry.disable_placeholder()), + # SettingsVariable(name='gv', variable=self.game_version, options=(rngopts.RandOptGameVersion.PTDE, rngopts.RandOptGameVersion.REMASTERED)), + SettingsVariable(name='diff', variable=self.diff, options=DESC_DICT['diff'].keys()), + SettingsVariable(name='kd', variable=self.key_diff, options=DESC_DICT['key_diff'].keys()), + SettingsVariable(name='sd', variable=self.soul_diff, options=DESC_DICT['souls_diff'].keys()), + SettingsVariable(name='id', variable=self.start_items_diff, options=DESC_DICT['start_items'].keys()), + SettingsVariable(name='fs', variable=self.fashion_bool, options=DESC_DICT['fashion'].keys()), + SettingsVariable(name='npc', variable=self.npc_armor_bool, options=DESC_DICT['npc_armor'].keys()), + SettingsVariable(name='lv', variable=self.use_lordvessel, options=DESC_DICT['use_lv'].keys()), + SettingsVariable(name='ls', variable=self.use_lord_souls, options=DESC_DICT['use_lord_souls'].keys()), + SettingsVariable(name='nodlc', variable=self.keys_not_in_dlc, options=DESC_DICT['keys_not_in_dlc'].keys()), + SettingsVariable(name='aw', variable=self.ascend_weapons_bool, options=DESC_DICT['ascend_weapons'].keys()), + SettingsVariable(name='hints', variable=self.set_up_hints, options=DESC_DICT['set_up_hints'].keys()), + SettingsVariable(name='nobkw', variable=self.no_black_knight_weapons, options=DESC_DICT['no_black_knight_weapons'].keys()), + ], call_after_update=self.update_desc) + self.update_desc() self.detect_game_version() self.check_for_new_version() + self.update() def update_game_version(self): self.game_version_menu.selection_clear() if self.game_version.get() in [rngopts.RandOptGameVersion.PTDE, rngopts.RandOptGameVersion.REMASTERED]: self.game_version_menu.configure(style="GameVersion.TCombobox") - def detect_game_version(self): for filepath in DS1R_GAMEPARAM_PATH_LIST: normed_path = os.path.normpath(os.path.join(os.getcwd(), filepath)) @@ -300,7 +390,7 @@ def detect_game_version(self): return self.game_version.set("No GameParam detected! Exporting has been disabled.") self.game_version_menu.configure(style="Error.GameVersion.TCombobox") - self.export_button.config(state = "disabled") + self.export_button.config(state="disabled") def quit_button(self): self.root.destroy() @@ -325,25 +415,86 @@ def lift_sellout_area(self): self.msg_area.config(state="normal") self.msg_area.delete(1.0, "end") self.msg_area.insert("end", "\n\n") - self.msg_area.insert("end", "If you are interested in supporting the author, please consider donating at\n\n ") - self.msg_area.insert("end", r"https://twitch.streamlabs.com/hotpocketremix/", "hyperlink") - self.msg_area.insert("end", "\n\n Donations are not required, but are much appreciated. \`[T]/") - self.msg_area.tag_config("hyperlink", foreground="blue", underline=1) - self.msg_area.tag_bind("hyperlink", "", lambda _: self.hyperlink_enter()) - self.msg_area.tag_bind("hyperlink", "", lambda _: self.hyperlink_leave()) - self.msg_area.tag_bind("hyperlink", "", lambda _: self.hyperlink_click()) + self.msg_area.insert("end", "\tDark Soul Item Randomizer, developers:\n\n") + self.msg_area.insert("end", "\t\thotpocketremix\n") + self.msg_area.insert("end", "\t\tcaerulius\n") + self.msg_area.insert("end", "\t\tforstycup\n") + self.msg_area.insert("end", "\t\tAbscondWithAPie\n") + self.msg_area.insert("end", "\t\tAzetinnitezA\n") + self.msg_area.insert("end", "\t\teaglevis\n") + self.msg_area.insert("end", "\t\tda66en\n") + self.msg_area.insert("end", "\t\tPrimogenitor33\n") self.msg_area.config(state="disabled") self.msg_area.lift() self.back_button.lift() + + def normalize_game(self): + if self.game_version.get() == rngopts.RandOptGameVersion.PTDE: + paths_to_search = PTDE_GAMEPARAM_PATH_LIST + elif self.game_version.get() == rngopts.RandOptGameVersion.REMASTERED: + paths_to_search = DS1R_GAMEPARAM_PATH_LIST + else: + self.show_error("Critical error!") + return + + gameparam_filepath = "" + gameparambak_filepath = "" + for filepath in paths_to_search: + normed_path = os.path.normpath(os.path.join(os.getcwd(), filepath)) + if os.path.isfile(normed_path): + gameparam_filepath = normed_path + gameparambak_filepath = normed_path + ".bak" + break + + #if gameparam_filepath == "" or not os.path.isfile(gameparam_filepath): + # self.show_error("GameParam.parambnd is missing or cannot be opened. File could not be located.") + # return + + if gameparambak_filepath == "" or not os.path.isfile(gameparambak_filepath): + self.show_error("GameParam.parambnd.bak is missing or cannot be opened. Game could be vanilla already.") + return + + os.remove(gameparam_filepath) + shutil.copy2(gameparambak_filepath, gameparam_filepath) + + if self.game_version.get() == rngopts.RandOptGameVersion.PTDE: + paths_to_search = PTDE_ENGMENU_PATH_LIST + elif self.game_version.get() == rngopts.RandOptGameVersion.REMASTERED: + paths_to_search = DS1R_ENGMENU_PATH_LIST + else: + self.show_error("Critical error!") + return - def hyperlink_enter(self): - self.msg_area.config(cursor="hand2") + enmenu_filepath = "" + enmenubak_filepath = "" + for filepath in paths_to_search: + normed_path = os.path.normpath(os.path.join(os.getcwd(), filepath)) + if os.path.isfile(normed_path): + enmenu_filepath = normed_path + enmenubak_filepath = normed_path + ".bak" + break + + #if enmenu_filepath == "" or not os.path.isfile(enmenu_filepath): + # self.show_error("ENGLISH/menu.msgbnd is missing or cannot be opened. File could not be located.") + # return + + if enmenubak_filepath == "" or not os.path.isfile(enmenubak_filepath): + self.show_error("ENGLISH/menu.msgbnd.bak is missing or cannot be opened. Game could be vanilla already.") + return + + os.remove(enmenu_filepath) + shutil.copy2(enmenubak_filepath, enmenu_filepath) + + self.show_success("Game has been reverted to vanilla Dark Souls.") + + #def hyperlink_enter(self): + # self.msg_area.config(cursor="hand2") - def hyperlink_leave(self): - self.msg_area.config(cursor="") + #def hyperlink_leave(self): + # self.msg_area.config(cursor="") - def hyperlink_click(self): - webbrowser.open_new_tab(r"https://twitch.streamlabs.com/hotpocketremix/") + #def hyperlink_click(self): + # webbrowser.open_new_tab(r"https://twitch.streamlabs.com/hotpocketremix/") def get_current_desc_state(self): desc_specifiers = { @@ -354,7 +505,11 @@ def get_current_desc_state(self): "fashion": (self.fashion_bool.get(), DescriptionState.NORMAL), "npc_armor": (self.npc_armor_bool.get(), DescriptionState.NORMAL), "use_lv": (self.use_lordvessel.get(), DescriptionState.NORMAL), - "use_lord_souls": (self.use_lord_souls.get(), DescriptionState.NORMAL) + "use_lord_souls": (self.use_lord_souls.get(), DescriptionState.NORMAL), + "ascend_weapons": (self.ascend_weapons_bool.get(), DescriptionState.NORMAL), + "set_up_hints": (self.set_up_hints.get(), DescriptionState.NORMAL), + "keys_not_in_dlc": (self.keys_not_in_dlc.get(), DescriptionState.NORMAL), + "no_black_knight_weapons": (self.no_black_knight_weapons.get(), DescriptionState.NORMAL), } return DescriptionState(desc_specifiers, self.desc_area) @@ -385,28 +540,38 @@ def update_desc_for_hover(self, hovered_parts, no_emph = False): self.update_desc_with_fade(overwrite_desc_specifiers) self.has_hovered_desc = True - def setup_hover_events(self, hover_object, hovered_parts, no_emph = False): + def setup_hover_events(self, hover_object, hovered_parts, no_emph=False): hover_object.bind("", lambda _: self.update_desc_for_hover(hovered_parts, no_emph)) hover_object.bind("", lambda _: self.update_desc(respect_current_hover = False)) hover_object.bind("", lambda _: self.update_desc(respect_current_hover = False)) hover_object.bind("", lambda _: self.update_desc(respect_current_hover = False)) - hover_object.configure(command = lambda: self.update_desc_for_hover(hovered_parts, no_emph)) + try: # Combobox doesn't have 'configure' method + hover_object.configure(command = lambda: self.update_desc_for_hover(hovered_parts, no_emph)) + except: + pass - def update_desc(self, respect_current_hover = True): + def update_desc(self, respect_current_hover=True): if not (respect_current_hover and self.has_hovered_desc): self.has_hovered_desc = False self.get_current_desc_state().write_state_to_area() def update(self): if self.key_diff.get() == rngopts.RandOptKeyDifficulty.LEAVE_ALONE: - self.use_lordvessel.set(False) + #self.use_lordvessel.set(False) + self.use_lordvessel.set(rngopts.RandOptLordvesselLocation.GWYNEVERE) self.use_lord_souls.set(False) - self.lv_check.config(state="disabled") + self.keys_not_in_dlc.set(False) + #self.lv_check.config(state="disabled") + self.gui_lordvessel.config(state="disabled") self.lord_soul_check.config(state="disabled") + self.keys_not_in_dlc_check.config(state="disabled") else: - self.lv_check.config(state="normal") + #self.lv_check.config(state="normal") + self.gui_lordvessel.config(state="normal") self.lord_soul_check.config(state="normal") - + self.keys_not_in_dlc_check.config(state="normal") + + """ if self.key_diff.get() == rngopts.RandOptKeyDifficulty.SPEEDRUN_MODE: self.diff.set(rngopts.RandOptDifficulty.EASY) self.diff_rbutton1.config(state="disabled") @@ -416,17 +581,32 @@ def update(self): self.diff_rbutton1.config(state="normal") self.diff_rbutton2.config(state="normal") self.diff_rbutton3.config(state="normal") + """ + if self.key_diff.get() == rngopts.RandOptKeyDifficulty.SPEEDRUN_MODE: + self.gui_diff.config(state="disabled") + else: + self.gui_diff.config(state="normal") + + if self.game_version.get() == rngopts.RandOptGameVersion.REMASTERED: + self.hint_check.config(state="normal") + else: + self.set_up_hints.set(False) + self.hint_check.config(state="disabled") self.update_desc() def randomize_data(self, chr_init_data): options = rngopts.RandomizerOptions(self.diff.get(), self.fashion_bool.get(), self.key_diff.get(), self.use_lordvessel.get(), self.use_lord_souls.get(), self.soul_diff.get(), self.start_items_diff.get(), self.game_version.get(), - self.npc_armor_bool.get()) + self.npc_armor_bool.get(), self.ascend_weapons_bool.get(), self.keys_not_in_dlc.get(), + self.set_up_hints.get(), self.no_black_knight_weapons.get()) + + if self.save_options.get(): + ini_parser.save_ini(INI_FILE, options) #save options right before creating seed rng = random.Random() rng.seed(int(hashlib.sha256(self.seed_string.get().encode('utf-8')).hexdigest(), 16)) - return (options, randomize_item_table.build_table(options, rng, chr_init_data), rng) + return options, randomize_item_table.build_table(options, rng, chr_init_data), rng def get_new_seed(self): new_hex_seed = hashlib.sha256(str(self.seed_rng.random()).encode('utf-8')).hexdigest() @@ -434,18 +614,18 @@ def get_new_seed(self): # Useful for distinguishing letters on-stream, and for those with # impaired vision. - SUBS_DICT = {'0': 'b','1': 'c', '2': 'f', '3': 'i', '4': 'j', + SUBS_DICT = {'0': 'b', '1': 'c', '2': 'f', '3': 'i', '4': 'j', '5': 'k', '6': 'p', '7': 'q', '8': 'r', '9': 'v', 'a': 'x', 'b': 'y', 'c': 'z', 'd': '2', 'e': '4', 'f': '5'} self.seed_string.set(''.join([SUBS_DICT.get(c, '') for c in new_hex_seed])) - self.seed_entry.config(fg = self.entry_state.normal_color, font=self.entry_state.normal_font) + self.seed_entry.config(fg=self.entry_state.normal_color, font=self.entry_state.normal_font) self.entry_state.with_placeholder = False def seed_changed(self): self.limit_seed_length() value = self.seed_string.get() - self.seed_entry.config(bg = "white") + self.seed_entry.config(bg="white") def is_seed_empty(self): seed = self.seed_string.get() @@ -456,7 +636,7 @@ def get_syncnum_string(self, random_source): syncnum_str = syncnum[0:4] + "-" + syncnum[4:7] return syncnum_str - def export_seed_info(self, use_randomized_data=None): + def export_seed_info(self, syncnum=None, use_randomized_data=None): if self.is_seed_empty(): self.seed_entry.config(bg = "light salmon") return @@ -478,24 +658,31 @@ def export_seed_info(self, use_randomized_data=None): (options, randomized_data, rng) = self.randomize_data(None) (table, randomized_chr_data) = randomized_data - - syncnum = self.get_syncnum_string(rng) + #syncnum = self.get_syncnum_string(rng) #confused by this call as the sync already been changed result_ilp = table.build_itemlotparam() ilp_binary_export = result_ilp.export_as_binary() result_slp = table.build_shoplineup() slp_binary_export = result_slp.export_as_binary() + chr_cheatsheet = randomized_chr_data.return_cheatsheet() cip_binary_export = randomized_chr_data.export_as_binary() - cheat_string = table.build_cheatsheet(show_event_flags = False) + cheat_string = table.build_cheatsheet(show_event_flags=False) hint_string = table.build_hintsheet() - seed_info = "Seed: " + str(self.seed_string.get()) + "\n\n" + options.as_string() - + + seed_info = "Randomizer version: " + VERSION_NUM \ + + "\n\nSeed: " + str(self.seed_string.get()) \ + + "\n\n" + options.as_string() \ + + "\nSyncNum: " + (syncnum if syncnum != None else "?") \ + + "\n\nSettings Sync:\n" + self.settings_string_io.construct().decode() + + ITEMLOT_FILEPATH = os.path.join(new_dirpath, "ItemLotParam.param") SHOPLINEUP_FILEPATH = os.path.join(new_dirpath, "ShopLineupParam.param") CHRINIT_FILEPATH = os.path.join(new_dirpath, "CharaInitParam.param") CHEATSHEET_FILEPATH = os.path.join(new_dirpath, "cheatsheet.txt") HINTSHEET_FILEPATH = os.path.join(new_dirpath, "hintsheet.txt") SEEDINFO_FILEPATH = os.path.join(new_dirpath, "seed_info.txt") + HINTDEBUG_FILEPATH = os.path.join(new_dirpath, "hint_debug.txt") with open(ITEMLOT_FILEPATH, 'wb') as f: f.write(ilp_binary_export) @@ -504,21 +691,25 @@ def export_seed_info(self, use_randomized_data=None): with open(CHRINIT_FILEPATH, 'wb') as f: f.write(cip_binary_export) with open(CHEATSHEET_FILEPATH, 'w') as f: - f.write(cheat_string) + f.write(cheat_string + chr_cheatsheet) with open(HINTSHEET_FILEPATH, 'w') as f: f.write(hint_string) with open(SEEDINFO_FILEPATH, 'w') as f: f.write(seed_info) - + + if (options.set_up_hints and (syncnum != None)): + table.hint_builder.WriteDebugFile(HINTDEBUG_FILEPATH) + if not use_randomized_data: self.msg_continue_button.lower() self.msg_area.config(state="normal") self.msg_area.delete(1.0, "end") self.msg_area.insert("end", "\n") self.msg_area.insert("end", "SUCCESS", "yay") - self.msg_area.insert("end", "! The information for this seed has been exported in the directory\n " + - new_dir_name + "\n\n") - self.msg_area.insert("end", "SyncNum: " + syncnum + "\n (When racing, all SyncNums should be equal or settings do not match.)\n\n") + self.msg_area.insert("end", "! The information for this seed has been exported in the directory\n {}\n\n".format(new_dir_name)) + self.msg_area.insert("end", "SyncNum: {}\n (When racing, all SyncNums should be equal or settings do not match.)\n\n".format(syncnum if syncnum != None else "?")) + self.msg_area.insert("end", 'If you want to easily share this seed and settings with your friends, ' + 'right click on this window and select "Copy settings sync". \n\n') self.msg_area.insert("end", "Click \"Back\" to begin again, or click \"Quit\" to exit.\n\n") self.msg_area.tag_config("yay", foreground="green") self.msg_area.config(state="disabled") @@ -531,28 +722,54 @@ def export_seed_info(self, use_randomized_data=None): def export_to_gameparam(self): if self.game_version.get() == rngopts.RandOptGameVersion.PTDE: paths_to_search = PTDE_GAMEPARAM_PATH_LIST + menu_paths = PTDE_ENGMENU_PATH_LIST elif self.game_version.get() == rngopts.RandOptGameVersion.REMASTERED: paths_to_search = DS1R_GAMEPARAM_PATH_LIST + menu_paths = DS1R_ENGMENU_PATH_LIST else: paths_to_search = [] + # find our gameparam file has_gameparam = False for filepath in paths_to_search: normed_path = os.path.normpath(os.path.join(os.getcwd(), filepath)) if os.path.isfile(normed_path): - has_gameparam = True gameparam_filepath = normed_path gameparambak_filepath = normed_path + ".bak" + break + + # find our menu text file + has_engmenu = False + for filepath in menu_paths: + normed_path = os.path.normpath(os.path.join(os.getcwd(), filepath)) + if os.path.isfile(normed_path): + has_engmenu = True + enmenu_filepath = normed_path + enmenubak_filepath = normed_path + ".bak" + break is_remastered = (self.game_version.get() == rngopts.RandOptGameVersion.REMASTERED) - if not has_gameparam: + if gameparam_filepath is None: self.msg_area.config(state="normal") self.msg_area.delete(1.0, "end") self.msg_area.insert("end", "\n\n") self.msg_area.insert("end", "ERROR", "error_red") - self.msg_area.insert("end", ": GameParam.parambnd[.dcx] is missing or cannot be opened." + - " Check that this program is in the correct directory and GameParam.parambnd[.dcx] is present and retry.\n\n" + + self.msg_area.insert("end", ': GameParam.parambnd[.dcx] is missing or cannot be opened.' + ' Check that this program is in the correct directory and GameParam.parambnd[.dcx] is present and retry.\n\n' + 'Click "Continue" to continue in seed-information-only mode, or' + ' click "Quit" to exit.') + self.msg_area.tag_config("error_red", foreground="red") + self.msg_area.config(state="disabled") + self.export_button.config(state="disabled") + self.lift_msg_area() + if not has_engmenu and self.set_up_hints.get(): + self.msg_area.config(state="normal") + self.msg_area.delete(1.0, "end") + self.msg_area.insert("end", "\n\n") + self.msg_area.insert("end", "ERROR", "error_red") + self.msg_area.insert("end", ": menu.msgbnd[.dcx] is missing or cannot be opened." + + " Check that this program is in the correct directory and menu.msgbnd[.dcx] is present and retry.\n\n" + "Click \"Continue\" to continue in seed-information-only mode, or" + " click \"Quit\" to exit.") self.msg_area.tag_config("error_red", foreground="red") @@ -562,9 +779,12 @@ def export_to_gameparam(self): else: if is_remastered: gp_filename = "GameParam.parambnd.dcx" + enmenu_filename = "menu.msgbnd.dcx" else: gp_filename = "GameParam.parambnd" + enmenu_filename = "menu.msgbnd" + # open our gameparam file with open(gameparam_filepath, "rb") as f: content = f.read() try: @@ -578,30 +798,62 @@ def export_to_gameparam(self): self.msg_area.delete(1.0, "end") self.msg_area.insert("end", "\n\n") self.msg_area.insert("end", "ERROR", "error_red") - self.msg_area.insert("end", - ": " + gp_filename + " is malformed or corrupted and cannot be" + - " parsed to export randomized items. If possible, restore " + gp_filename + " from a backup copy.\n\n" + - "Click \"Continue\" to continue in seed-information-only mode, or" + - " click \"Quit\" to exit.") + self.msg_area.insert("end", + ': {0} is malformed or corrupted and cannot be parsed to export randomized items. If possible, restore ' + '{0} from a backup copy.\n\nClick "Continue" to continue in seed-information-only mode, or ' + 'click "Quit" to exit.'.format(gp_filename)) self.msg_area.tag_config("error_red", foreground="red") self.msg_area.config(state="disabled") - self.export_button.config(state = "disabled") + self.export_button.config(state="disabled") self.lift_msg_area() return + # open our menu text file + # TODO: Consolidate this instead of duplicating + if is_remastered and self.set_up_hints.get(): + with open(enmenu_filepath, "rb") as f: + enmenu_content = f.read() + try: + if is_remastered: + if not dcx_handler.appears_dcx(enmenu_content): + raise ValueError(".dcx file does not appear to be DCX-compressed.") + enmenu_content = dcx_handler.uncompress_dcx_content(enmenu_content) + enmenu_content_list = bnd_rebuilder.unpack_bnd(enmenu_content) + except: + self.msg_area.config(state="normal") + self.msg_area.delete(1.0, "end") + self.msg_area.insert("end", "\n\n") + self.msg_area.insert("end", "ERROR", "error_red") + self.msg_area.insert("end", + ": " + enmenu_filename + " is malformed or corrupted and cannot be" + + " parsed to inject hints. If possible, restore " + enmenu_filename + " from a backup copy.\n\n" + + "Click \"Continue\" to continue in seed-information-only mode, or" + + " click \"Quit\" to exit.") + self.msg_area.tag_config("error_red", foreground="red") + self.msg_area.config(state="disabled") + self.export_button.config(state = "disabled") + self.lift_msg_area() + return + # Back up GameParam.parambnd if needed. if not os.path.isfile(gameparambak_filepath): shutil.copy2(gameparam_filepath, gameparambak_filepath) + + # Back up menu.msgbnd if needed. + if not os.path.isfile(enmenubak_filepath): + shutil.copy2(enmenu_filepath, enmenubak_filepath) + # Copy our default msgbnd back if we're not building hints + elif os.path.isfile(enmenubak_filepath) and not self.set_up_hints.get(): + shutil.move(enmenubak_filepath, enmenubak_filepath) if self.is_seed_empty(): self.get_new_seed() for index, (file_id, filepath, filedata) in enumerate(content_list): - if (filepath == "N:\FRPG\data\INTERROOT_win32\param\GameParam\CharaInitParam.param" or - filepath == "N:\FRPG\data\INTERROOT_x64\param\GameParam\CharaInitParam.param"): + if (filepath == "N:\\FRPG\\data\\INTERROOT_win32\\param\\GameParam\\CharaInitParam.param" or + filepath == "N:\\FRPG\\data\\INTERROOT_x64\\param\\GameParam\\CharaInitParam.param"): chr_init_data = filedata - - + # TODO: Implement this system correctly by passing chr_init_data # instead of None to preserve externally modified characters (e.g. another mod). # However, we need some way to determine external modifications @@ -609,41 +861,60 @@ def export_to_gameparam(self): # ChrInit data. (options, randomized_data, rng) = self.randomize_data(None) (item_table, randomized_chr_data) = randomized_data - syncnum = self.get_syncnum_string(rng) + #syncnum = self.get_syncnum_string(rng) result_ilp = item_table.build_itemlotparam() ilp_binary_export = result_ilp.export_as_binary() result_slp = item_table.build_shoplineup() slp_binary_export = result_slp.export_as_binary() cip_binary_export = randomized_chr_data.export_as_binary() - + if options.set_up_hints: + item_table.hint_builder.ConstructHintList(rng) + + syncnum = self.get_syncnum_string(rng) + for index, (file_id, filepath, filedata) in enumerate(content_list): - if (filepath == "N:\FRPG\data\INTERROOT_win32\param\GameParam\ItemLotParam.param" or - filepath == "N:\FRPG\data\INTERROOT_x64\param\GameParam\ItemLotParam.param"): + if (filepath == "N:\\FRPG\\data\\INTERROOT_win32\\param\\GameParam\\ItemLotParam.param" or + filepath == "N:\\FRPG\\data\\INTERROOT_x64\\param\\GameParam\\ItemLotParam.param"): content_list[index] = (file_id, filepath, ilp_binary_export) - if (filepath == "N:\FRPG\data\INTERROOT_win32\param\GameParam\ShopLineupParam.param" or - filepath == "N:\FRPG\data\INTERROOT_x64\param\GameParam\ShopLineupParam.param"): + if (filepath == "N:\\FRPG\\data\\INTERROOT_win32\\param\\GameParam\\ShopLineupParam.param" or + filepath == "N:\\FRPG\\data\\INTERROOT_x64\\param\\GameParam\\ShopLineupParam.param"): content_list[index] = (file_id, filepath, slp_binary_export) - if (filepath == "N:\FRPG\data\INTERROOT_win32\param\GameParam\CharaInitParam.param" or - filepath == "N:\FRPG\data\INTERROOT_x64\param\GameParam\CharaInitParam.param"): + if (filepath == "N:\\FRPG\\data\\INTERROOT_win32\\param\\GameParam\\CharaInitParam.param" or + filepath == "N:\\FRPG\\data\\INTERROOT_x64\\param\\GameParam\\CharaInitParam.param"): content_list[index] = (file_id, filepath, cip_binary_export) new_content = bnd_rebuilder.repack_bnd(content_list) if is_remastered: new_content = dcx_handler.compress_dcx_content(new_content) with open(gameparam_filepath, "wb") as f: f.write(new_content) - seed_folder = self.export_seed_info((options, randomized_data, rng)) + + # Write out our menu text if we need to + if is_remastered: + if options.set_up_hints: + for index, (file_id, filepath, filedata) in enumerate(enmenu_content_list): + if (filepath == "N:\\FRPG\\data\\Msg\\Data_ENGLISH\\Blood_writing_.fmg"): + fmgData = FMGHandler(FMGHandler.load_from_file_content(filedata)) + item_table.hint_builder.AddHintsToBloodMessages(fmgData, rng) + enmenu_content_list[index] = (file_id, filepath, fmgData.export_as_binary()) + new_content = bnd_rebuilder.repack_bnd(enmenu_content_list) + if is_remastered: + new_content = dcx_handler.compress_dcx_content(new_content) + with open(enmenu_filepath, "wb") as f: + f.write(new_content) + + seed_folder = self.export_seed_info(syncnum, (options, randomized_data, rng)) self.msg_continue_button.lower() self.msg_area.config(state="normal") self.msg_area.delete(1.0, "end") - self.msg_area.insert("end", "\n\n") self.msg_area.insert("end", "SUCCESS", "yay") - self.msg_area.insert("end", "! " + gp_filename + " has been modified successfully.\n\n" + - "The information for this seed has been exported in the directory\n\n " + - seed_folder + "\n\n") - self.msg_area.insert("end", "SyncNum: " + syncnum + "\n (When racing, all SyncNums should be equal or settings do not match.)\n\n") - self.msg_area.insert("end", "Click \"Back\" to begin again, or click \"Quit\" to exit.") + self.msg_area.insert("end", "! {} has been modified successfully.\n\n" + "The information for this seed has been exported in the directory\n\n {}\n\n".format(gp_filename, seed_folder)) + self.msg_area.insert("end", "SyncNum: {}\n (When racing, all SyncNums should be equal or settings do not match.)\n\n".format(syncnum)) + self.msg_area.insert("end", 'If you want to easily share this seed and settings with your friends, ' + 'right click on this window and select "Copy settings sync". \n\n') + self.msg_area.insert("end", 'Click "Back" to begin again, or click "Quit" to exit.') self.msg_area.tag_config("yay", foreground="green") self.msg_area.config(state="disabled") self.msg_area.lift() @@ -654,8 +925,9 @@ def limit_seed_length(self): value = self.seed_string.get() if len(value) > MAX_SEED_LENGTH: self.seed_string.set(value[:MAX_SEED_LENGTH]) - - def add_placeholder_to(self, entry, placeholder, color="grey", font=None): + + @staticmethod + def add_placeholder_to(entry, placeholder, color="grey", font=None): normal_color = entry.cget("fg") normal_font = entry.cget("font") @@ -663,32 +935,37 @@ def add_placeholder_to(self, entry, placeholder, color="grey", font=None): font = normal_font state = Placeholder_State() - state.normal_color=normal_color - state.normal_font=normal_font - state.placeholder_color=color - state.placeholder_font=font + state.normal_color = normal_color + state.normal_font = normal_font + state.placeholder_color = color + state.placeholder_font = font state.placeholder_text = placeholder - state.with_placeholder=True + state.with_placeholder = True def on_focusin(event, entry=entry, state=state): if state.with_placeholder: entry.delete(0, "end") - entry.config(fg = state.normal_color, font=state.normal_font) + entry.config(fg=state.normal_color, font=state.normal_font) state.with_placeholder = False def on_focusout(event, entry=entry, state=state): if entry.get() == '': entry.insert(0, state.placeholder_text) - entry.config(fg = state.placeholder_color, font=state.placeholder_font) + entry.config(fg=state.placeholder_color, font=state.placeholder_font) state.with_placeholder = True + def disable_placeholder(entry=entry, state=state): + entry.config(fg=state.normal_color, font=state.normal_font) + state.with_placeholder = False + entry.insert(0, placeholder) - entry.config(fg = color, font=font) + entry.config(fg=color, font=font) entry.bind('', on_focusin, add="+") entry.bind('', on_focusout, add="+") - + entry.placeholder_state = state + entry.disable_placeholder = disable_placeholder return state def check_for_new_version(self): @@ -715,9 +992,63 @@ def check_for_new_version(self): except: pass + def popup_menu_show(self, event): + try: + self.popup_menu.tk_popup(event.x_root, event.y_root, 0) + finally: + self.popup_menu.grab_release() + + def settings_string_copy(self): + if self.is_seed_empty(): + self.get_new_seed() + + try: + constructed_value = self.settings_string_io.construct() + self.root.clipboard_clear() + self.root.clipboard_append(constructed_value) + except InvalidValueError: + tkMB.showerror('Error copying data', 'Invalid or incomplete settings detected, cannot copy') + except Exception as e: + logging.exception('Could not copy settings sync string to clipboard: {}'.format(e)) + tkMB.showerror('Error copying data', 'Could not copy data to clipboard') + + def settings_string_paste(self): + try: + clipboard = self.root.clipboard_get() + if not isinstance(clipboard, str) or len(clipboard) > 512: + raise ValueError + + if not self.settings_string_io.parse(clipboard): + raise Exception + tkMB.showinfo('Yay!', 'Settings applied') + + except IncompatibleVersionError: + tkMB.showerror('Error applying settings', 'Data from an incompatible version in the clipboard') + + except Exception as e: + logging.exception('Could not parsesettings sync string from clipboard: {}'.format(e)) + tkMB.showerror('Error applying settings', 'Invalid data in the clipboard') + + def show_error(self, text): + self.msg_area.config(state="normal") + self.msg_area.delete(1.0, "end") + self.msg_area.insert("end", "\n\n") + self.msg_area.insert("end", "ERROR", "error_red") + self.msg_area.insert("end", ": " + text) + self.msg_area.tag_config("error_red", foreground="red") + self.lift_msg_area() + + def show_success(self, text): + self.msg_area.config(state="normal") + self.msg_area.delete(1.0, "end") + self.msg_area.insert("end", "\n\n") + self.msg_area.insert("end", "SUCCESS", "yay") + self.msg_area.insert("end", ": " + text) + self.msg_area.tag_config("yay", foreground="green") + self.lift_msg_area() + if __name__ == "__main__": logging.basicConfig(stream=sys.stdout, level=logging.WARN) maingui = MainGUI() maingui.root.mainloop() - diff --git a/randomizer_gui.spec b/randomizer_gui.spec new file mode 100644 index 0000000..90093c7 --- /dev/null +++ b/randomizer_gui.spec @@ -0,0 +1,35 @@ +# -*- mode: python -*- + +block_cipher = None + + +a = Analysis(['randomizer_gui.py'], + pathex=['C:\\Darren.Mini\\Darren\\ds_rando'], + binaries=[], + datas=[('favicon.ico', '.'), ('favicon.gif', '.')], + hiddenimports=[], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='DarkSoulsItemRandomizer', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + runtime_tmpdir=None, + console=False , icon='favicon.ico') + +import shutil +shutil.copyfile('randomizer.ini', '{0}/randomizer.ini'.format(DISTPATH)) diff --git a/randomizer_options.py b/randomizer_options.py index 1b20ec9..93ca6ce 100644 --- a/randomizer_options.py +++ b/randomizer_options.py @@ -3,69 +3,95 @@ class RandOptDifficulty: EASY = 0 MEDIUM = 1 HARD = 2 + + NUM = 3 + + strings = ['Fair', 'Unfair', 'Very Unfair'] + + @classmethod + def as_strings(cls): + return cls.strings @classmethod def as_string(cls, diff): - if diff == cls.EASY: - return "Fair" - elif diff == cls.MEDIUM: - return "Unfair" - elif diff == cls.HARD: - return "Very Unfair" - else: - return "" + if (diff >= cls.NUM): + return None #error + return cls.strings[diff] + + @classmethod + def from_string(cls, str): + return cls.strings.index(str) class RandOptKeyDifficulty: LEAVE_ALONE = 0 RANDOMIZE = 1 RACE_MODE = 2 SPEEDRUN_MODE = 3 + + NUM = 4 + + strings = ['Not Shuffled', 'Shuffled', 'Race Mode', 'Race Mode +'] + + @classmethod + def as_strings(cls): + return cls.strings @classmethod def as_string(cls, diff): - if diff == cls.LEAVE_ALONE: - return "Not Shuffled" - elif diff == cls.RANDOMIZE: - return "Shuffled" - elif diff == cls.RACE_MODE: - return "Race Mode" - elif diff == cls.SPEEDRUN_MODE: - return "Race Mode +" - else: - return "" + if (diff >= cls.NUM): + return None #error + return cls.strings[diff] + + @classmethod + def from_string(cls, str): + return cls.strings.index(str) class RandOptStartItemsDifficulty: SHIELD_AND_1H = 0 SHIELD_AND_2H = 1 COMBINED_POOL_AND_2H = 2 + + NUM = 3 + + strings = ['Shield & 1H Weapon', 'Shield & 1/2H Weapon', 'Shield/Weapon & Weapon'] + + @classmethod + def as_strings(cls): + return cls.strings @classmethod def as_string(cls, diff): - if diff == cls.SHIELD_AND_1H: - return "Shield & 1H Weapon" - elif diff == cls.SHIELD_AND_2H: - return "Shield & 1/2H Weapon" - elif diff == cls.COMBINED_POOL_AND_2H: - return "Shield/Weapon & Weapon" - else: - return "" + if (diff >= cls.NUM): + return None #error + return cls.strings[diff] + + @classmethod + def from_string(cls, str): + return cls.strings.index(str) class RandOptSoulItemsDifficulty: SHUFFLE = 0 CONSUMABLE = 1 TRANSPOSE = 2 + + NUM = 3 + + strings = ['Shuffled', 'Replaced', 'Transposed'] + + @classmethod + def as_strings(cls): + return cls.strings @classmethod def as_string(cls, diff): - if diff == cls.SHUFFLE: - return "Shuffled" - elif diff == cls.CONSUMABLE: - return "Replaced" - elif diff == cls.TRANSPOSE: - return "Transposed" - else: - return "" + if (diff >= cls.NUM): + return None #error + return cls.strings[diff] + + @classmethod + def from_string(cls, str): + return cls.strings.index(str) class RandOptGameVersion: PTDE = "DARK SOULS: Prepare To Die Edition" @@ -80,10 +106,30 @@ def as_string(cls, version): else: return "" +class RandOptLordvesselLocation: + GWYNEVERE = 'Gwynevere' + RANDOMIZED = 'Randomized' + FIRELINK = 'Firelink' + + @classmethod + def as_strings(cls): + return [cls.GWYNEVERE, cls.RANDOMIZED, cls.FIRELINK] + + @classmethod + def get_default(cls): + return cls.RANDOMIZED + + @classmethod + def verify(cls, check_value): + if check_value not in cls.as_strings(): + return cls.RANDOMIZED #something bad happened - use default value + return check_value + class RandomizerOptions: def __init__(self, difficulty, fashion_souls, key_placement, use_lordvessel, use_lord_souls, soul_items_diff, start_items_diff, - game_version, randomize_npc_armor): + game_version, randomize_npc_armor, ascend_weapons, keys_not_in_dlc, set_up_hints, + no_black_knight_weapons): self.difficulty = difficulty self.fashion_souls = fashion_souls self.key_placement = key_placement @@ -93,6 +139,12 @@ def __init__(self, difficulty, fashion_souls, key_placement, self.start_items_diff = start_items_diff self.game_version = game_version self.randomize_npc_armor = randomize_npc_armor + self.ascend_weapons = ascend_weapons + self.keys_not_in_dlc = keys_not_in_dlc + self.set_up_hints = set_up_hints + self.no_online_items = True + self.better_start_spells = True + self.no_black_knight_weapons = no_black_knight_weapons def bool_option_to_string(self, b): if b: @@ -106,10 +158,14 @@ def as_string(self): return_string += " Difficulty: " + RandOptDifficulty.as_string(self.difficulty) + "\n" return_string += " Fashion Souls: " + self.bool_option_to_string(self.fashion_souls) + "\n" return_string += " Key Difficulty: " + RandOptKeyDifficulty.as_string(self.key_placement) + "\n" - return_string += " Senile Gwynevere: " + self.bool_option_to_string(self.use_lordvessel) + "\n" + return_string += " Lordvessel: " + self.use_lordvessel + "\n" return_string += " Senile Primordial Serpents: " + self.bool_option_to_string(self.use_lord_souls) + "\n" return_string += " Soul Items: " + RandOptSoulItemsDifficulty.as_string(self.soul_items_diff) + "\n" return_string += " Starting Items: " + RandOptStartItemsDifficulty.as_string(self.start_items_diff) + "\n" return_string += " Laundromat Mixup: " + self.bool_option_to_string(self.randomize_npc_armor) + "\n" + return_string += " Eager Smiths: " + self.bool_option_to_string(self.ascend_weapons) + "\n" + return_string += " No DLC: " + self.bool_option_to_string(self.keys_not_in_dlc) + "\n" + return_string += " Seek Guidance Hints: " + self.bool_option_to_string(self.set_up_hints) + "\n" + return_string += " No Black Knight Weapons: " + self.bool_option_to_string(self.no_black_knight_weapons) + "\n" return return_string diff --git a/settings_string_io.py b/settings_string_io.py new file mode 100644 index 0000000..0efc368 --- /dev/null +++ b/settings_string_io.py @@ -0,0 +1,113 @@ +import base64 +import binascii +import logging +import tkinter as tk + +from ast import literal_eval +from json import dumps, loads +from typing import List, Tuple + +__author__ = 'eagle_vis' + +logger = logging.getLogger(__name__) + + +class IncompatibleVersionError(Exception): + pass + + +class InvalidValueError(Exception): + pass + + +class SettingsVariable: + def __init__(self, name: str, variable: tk.Variable, options: (List, Tuple)=None, on_update=None): + self.name = name + self.variable = variable + self.options = options + self.on_update = on_update + + def get(self) -> str: + value = self.variable.get() + if self.options is not None and len(self.options) and value not in self.options: + raise InvalidValueError('Invalid value {} for {}'.format(value, self.name)) + + return str(value) + + def set(self, value: str): + try: + new_val = None + if isinstance(self.variable, tk.BooleanVar): + new_val = bool(literal_eval(value)) + elif isinstance(self.variable, tk.IntVar): + new_val = int(value) + elif isinstance(self.variable, tk.DoubleVar): + new_val = float(value) + elif isinstance(self.variable, tk.StringVar): + new_val = str(value) + + if self.options is not None and len(self.options): + if new_val not in self.options: + raise ValueError + + self.variable.set(new_val) + if callable(self.on_update): + self.on_update() + except Exception as e: + logger.exception('Invalid value supplied for variable {}: "{}" - '.format(self.name, value, e)) + raise ValueError + + +class SettingsStringIO: + def __init__(self, version: str, compatible_versions: List[str], variables: List[SettingsVariable], call_after_update=None): + self.version = version + self.compatible_version = compatible_versions + self.variables = variables + self.call_after_update = call_after_update + + self.required_keys = [v.name for v in variables] + self.required_keys.append('rv') + self.required_keys.sort() + + def construct(self) -> str: + data_dict = { + 'rv': self.version, + } + + for var in self.variables: + data_dict[var.name] = var.get() + dict_string = bytes(dumps(data_dict, indent=None), 'ascii') + + crc_string = '{:08x}'.format(binascii.crc32(dict_string) & 0xffffffff) + + return base64.b64encode(dict_string + bytes(crc_string, 'ascii')) + + def parse(self, string: str) -> bool: + data_string = base64.b64decode(string, validate=True) + given_crc = data_string[-8:].decode('ascii') + data_string = data_string[:-8] + + computed_crc_string = '{:08x}'.format(binascii.crc32(data_string) & 0xffffffff) + + if computed_crc_string != given_crc: + raise InvalidValueError + + data_dict = loads(data_string) + + if self.version != data_dict['rv']: + raise IncompatibleVersionError + + if len(data_dict.keys()) != len(self.required_keys): + return False + + for k in self.required_keys: + if k not in data_dict: + return False + + for var in self.variables: + var.set(data_dict[var.name]) + + if callable(self.call_after_update): + self.call_after_update() + + return True diff --git a/shops_setup.py b/shops_setup.py index 2dd298a..d7bb5fb 100644 --- a/shops_setup.py +++ b/shops_setup.py @@ -203,7 +203,7 @@ def __init__(self, shop_id, cost = -1, shop_type = 0, mtrl_id = -1): # Modify the Sorcerer's Catalyst that Rickert sells to have a # high price, so that it is a last resort. - 60002102: ShopData(2102, cost = 500000), + 60002102: ShopData(2102, cost = 125000), 60002200: ShopData(2200, cost = 2000), 60002201: ShopData(2201, cost = 2000), diff --git a/version.txt b/version.txt index be58634..c3518d3 100644 --- a/version.txt +++ b/version.txt @@ -1 +1,44 @@ -0.3 +0.8.1 +- When searching for Dark Souls game files to randomize, stop searching the list after it is found. This prioritizes the relative path, meaning if you put the randomizer EXE in the same folder as the Dark Souls folder you want to randomize it will not accidentally grab another folder on the machine if you have several Dark Souls installed. (@Primogenitor33) + +0.8.0 +- Re-tooled the starting spells to give more combat effective spells, especially the Cleric. Seemed like an important de-nerf in the rando as otherwise you hardly find a use for Faith. Also display the starting items and spells in the cheatsheet. (@da66en) +- Added option to remove Black Knight weapons (@da66en) +- Cell at the end of Duke's Prison (where Big Hat Logan is imprisoned) is now a possible Race Mode Key Location. Without this change the Archive Tower Giant Cell Key is not very useful. (@da66en) + +0.7.2 +- Fix for 'Write Seed Cheatsheet' crashing. It's not showing SyncNum or showing 'Seek Guidance hints', to be fixed later. (@da66en) + +0.7.1 +- Changed radio buttons to combobox to make space on the GUI for new features (@da66en) +- Added more info to seed_info.txt so it's easier to recreate the seed (@da66en) +- Fix for 'Seek Guidance hints' settings choice not affecting SyncNum reported (@da66en) +- 'Seek Guidance hints' won't give hints for Lordvessel or Lord Souls if they are not randomized, or give hintes for DLC keys or items in DLC area if not playing items in DLC area. (@da66en) +- Replaced online use items (Covenant Rings, Eye Orbs, Soapstones, Dried Fingers, etc) with random consumables (@da66en) +- Don't let Sunlight Maggot Helm be placed in Tomb of the Giants (@da66en) +- Fix for key items being placed in Chasm of the Abyss even if 'No DLC' was set (@da66en) + +0.7.0 +- Adds 'Revert to Vanilla' (@AzetinnitezA) +- Adds copy to clipboard and paste from clipboard seed and settings on right mouse click (@eaglevis) +- Fix for 'Seek Guidance' not working (@da66en) + +0.6.3 +- Adds debug file output for in-game hints. (@AbscondWithAPie) +- removed 'cast light' from key items, made 'sunlight maggot helm' a key item instead. (@da66en) +- added 'No DLC' option. Key items will NOT be in Painted World or Artorias of the Abyss DLC locations. Also keys needed for those areas will not be placed outside of these areas, to prevent you from getting keys in sections that aren't needed. (@da66en) +- added 'Lordvessel @ Firelink' option. The Lordvessel is at Firelink Shrine, which significantly speeds up the game. (@da66en) +- 'Seek Guidance Help' is actually followed, and won't cause PTDE to crash. I have doubts 'Seek Guidance Help' works as it uses hard coded directory locations on the N:\ drive. (@da66en) +- 'Seek Guidance Help' added to the .ini file saving/loading. (@da66en) +- Changed the about screen to show all the developers who have worked on the Dark Souls Item Randomizer. (@da66en) +- Disable and clear 'Seek Guidance' if PTDE version is being used. (@da66en) + +v0.5.1 +- Added Seek Guidance hints; useful hints replace the dev messages that appear when using the Seek Guidance miracle. See README.md for more information. (@forstycup) + +v0.4.0 +- Added eager smiths mode: select this option to have weapons randomly ascended with an ember. No more are you chained by the shackles of Black Knight Weaponry (@caerulius) +- Added .ini file to allow for settings saving. Creates a .ini file in the same directory as the randomizer, and checking the "save these settings" box will save your settings to this file the next time you create a seed (Thanks to @Stochast1c for this!) + +v0.3.0 and v0.2.1 +- Original releases by @HotPocketRemix