Compare commits

..

84 Commits

Author SHA1 Message Date
PoryGone
a3666f2ae5 SA2B: Fix critical typo #4779 2025-03-30 22:19:24 +02:00
Kaito Sinclaire
c3e000e574 id Tech 1 games: Logic updates (Feb '25) (#4677)
- Across Doom 1993 and Doom 2, any items that are accessible in Ultra-Violence from the start of the level without putting the player in any danger are now considered in logic when that level is first received, without needing any weapons available. This is intended to give generation more possible outs for bad placements.
  - This affects the following maps in Doom 1993:
    - Toxin Refinery (E1M3): 1 location.
    - Command Control (E1M4): 1 location.
    - Computer Station (E1M7): 1 location.
    - Deimos Lab (E2M4): 1 location.
    - Tower of Babel (E2M8): 1 location.
    - Unholy Cathedral (E3M5): 1 location.
  - This affects the following maps in Doom 2:
    - The Waste Tunnels (MAP05): 2 locations.
    - Dead Simple (MAP07): 2 locations.
    - The Pit (MAP09): 1 location.
    - Refueling Base (MAP10): 1 location.
    - Nirvana (MAP21): 1 location, except see below.
    - Icon of Sin (MAP30): 9 locations.
    - Grosse (MAP32): 2 locations.
- Doom 2 has had some more significant logical adjustments made.
  - The following Pro tricks have been added to Pro logic:
    - Circle of Death (MAP11): Lowering the exit wall without the Red key by hitting the switch to do so from the nukage. This makes three items previously locked behind the Red key available early, as well as the exit.
    - Suburbs (MAP16): Reaching the exit without any keys, as the gap between the pillar and the wall is large enough to let you through if you position yourself well. While multiple other squeeze glides exist (for example, you can skip the Yellow key in MAP21 by using one), this one is significantly easier than the rest; it does not require much precision, nor does it require vertical mouse movement.
    - Nirvana (MAP21): Skipping the Blue key, as there is a gigantic gap between the bars that attempt to block you.
    - The Chasm (MAP24): Skipping the Blue key by going extremely far through the nukage and finding one of a couple specific teleporters is now considered a Pro trick, and standard logic now expects the key to be obtained.
  - The following levels have had other logic adjustments:
    - The Waste Tunnels (MAP05): Requirements lowered to Shotgun + Super Shotgun + (Chaingun | Plasma gun).
    - The Crusher (MAP06): Requirements lowered to Shotgun + (Chaingun | Plasma gun) for areas immediately accessible. Going beyond the Blue key door also requires Super Shotgun.
    - The Factory (MAP12): The outdoors area, and the little room to the right of where you start, are accessible in sphere 1. These three items are all easily obtainable with only the pistol. The remaining items that are not in the central area are accessible with (Super Shotgun | Plasma gun), while the items in that area are accessible with Super Shotgun + Chaingun + (Plasma gun | BFG9000). This fixes Episode 2 not having an available sphere 1, and allows solo Episode 2 games.
    - Nirvana (MAP21): As above, the item in the starting room is accessible in sphere 1. Every other item that doesn't require a key is accessible with (Super Shotgun | Plasma gun). The room in which you use the Yellow key is accessible with Super Shotgun + Chaingun + (Plasma gun | BFG9000). This fixes Episode 3 not having an available sphere 1, and allows solo Episode 3 games.
    - The Catacombs (MAP22): The four items in the opening room only require (Shotgun | Super Shotgun | Plasma gun). The rest of the level is as before.
    - Bloodfalls (MAP25): Requirements lowered to Shotgun + Super Shotgun + Chaingun, as this level is unusually easy for its placement in the game. Progressing past the Blue key door additionally requires (Rocket launcher | Plasma gun | BFG9000) solely to deal with the Arch-vile at the end of the level.
    - Wolfenstein (MAP31): Requirements lowered to Chaingun + (Shotgun | Super Shotgun). This is closer to what the game expects from a non-secret hunting player from a pistol start.
- The following logic bugs in Heretic have been fixed:
  - Quay (E5M3): An item in a Blue key locked hallway was previously marked as being in the "Main" region, thus considered to be accessible without that key. It has been moved to the appropriate "Blue" region.
  - Courtyard (E5M4): Logic previously assumed you could reach the Wings of Wrath from the opening room, when that isn't actually possible. Changing this moved some items previously in the "Main" region into a new "Green" region, and items previously in the "Kakis" (Yellow OR Green) are now in a "Yellow" region instead. Fixes #4662.
- For known problematic solo episodes, some additional special cases have been added.
  - Doom 1993, Episode 3: One of either the Shotgun or Chaingun is placed early. Slough of Despair (E3M2) is given as an additional starting level.
  - Doom 2, Episode 3: One of either the Super Shotgun or Plasma gun is placed early.
  - Heretic, Episode 1: The Docks (E1M1) - Yellow key is placed early.
- The following levels (and thus, their items and locations) were renamed, due to typos or other oddities:
  - `Barrels o Fun (MAP23)` -> `Barrels o' Fun (MAP23)`
  - `Wolfenstein2 (MAP31)` -> `Wolfenstein (MAP31)`
  - `Grosse2 (MAP32)` -> `Grosse (MAP32)`
  - `D'Sparil'S Keep (E3M8)` -> `D'Sparil's Keep (E3M8)`
  - `The Aquifier (E3M9)` -> `The Aquifer (E3M9)`
2025-03-29 17:32:33 +01:00
Justus Lind
dd5481930a Muse Dash: Update docs to recommend MelonLoader 0.7.0 rather than 0.6.1 (#4776)
* Tiny version update.

* Update wording because there is no longer a latest button
2025-03-29 01:35:35 +01:00
Scipio Wright
842328c661 TUNIC: Update swamp and atoll fuse logic with weaponry (#4760)
* Update swamp and atoll fuse logic with weaponry

* Add it to the swamp and cath rules too
2025-03-28 21:12:16 +01:00
PoryGone
8f75384e2e SA2B - v2.4 Logic Fixes (#4770)
* Logic tweaks

* Docs updates

* Delete extra file

* One more logic tweak

* Add missing logic change
2025-03-28 21:11:31 +01:00
Fabian Dill
193faa00ce Factorio: fix energylink type back to int (#4768) 2025-03-28 00:28:10 +01:00
Star Rauchenberger
5e5383b399 Lingo: Add painting display names (#4707)
* Lingo: Add painting display names

* Reordered some paintings

* Update generated.dat
2025-03-27 01:32:39 +01:00
threeandthreee
cb6b29dbe3 LADX: fix for unconnected entrances in other worlds #4771 2025-03-25 22:30:25 +01:00
Fabian Dill
82b0819051 Core: ensure requirements files end on newline (#4761) 2025-03-24 22:26:30 +01:00
Jérémie Bolduc
e12ab4afa4 Stardew Valley: Move test option presets to their own file (#4349) 2025-03-24 03:32:34 +01:00
Justus Lind
1416f631cc Core: Add a test that checks all registered patches matches the name of a registered world (#4633)
Co-authored-by: qwint <qwint.42@gmail.com>
2025-03-24 03:30:44 +01:00
Fabian Dill
dbaac47d1e Core: update various requirements (#4731) 2025-03-23 17:24:50 +01:00
Jonathan Tan
cf0ae5e31b The Wind Waker: Implement New Game (#4458)
Adds The Legend of Zelda: The Wind Waker as a supported game in Archipelago. The game uses [LagoLunatic's randomizer](https://github.com/LagoLunatic/wwrando) as its base (regarding logic, options, etc.) and builds from there.
2025-03-23 00:42:17 +01:00
BadMagic100
8891f07362 Core: Allow and require user-provided target name when splitting 1-way entrances for GER (#4746)
* [Core][GER] Allow and require user-provided target name when splitting 1-way entrances

* Move target naming onto a parameter of disconnect_entrance_for_randomization
2025-03-22 20:58:35 +01:00
NewSoupVi
d78974ec59 The Witness: Bump Required Client Version to 0.6.0 (#4763)
The beta client releases already report this.
2025-03-22 20:57:22 +01:00
NewSoupVi
32be26c4d7 The Witness: Make sure the 2025 April Fools feature does not go live with RC3 (#4758) 2025-03-22 20:52:18 +01:00
Jérémie Bolduc
9de49aa419 Stardew Valley: Move all the goal logic into its own file (#4383) 2025-03-22 20:29:16 +01:00
PoryGone
294a67a4b4 SA2B: v2.4 - Minigame Madness (#4663)
Changelog:

Features:
- New Goal
  - Minigame Madness
    - Win a certain number of each type of Minigame Trap, then defeat the Finalhazard to win!
	- How many of each Minigame are required can be set by an Option
	- When the required amount of a Minigame has been received, that Minigame can be replayed in the Chao World Lobby
- New optional Location Checks
  - Bigsanity
    - Go fishing with Big in each stage for a Location Check
  - Itemboxsanity
    - Either Extra Life Boxes or All Item Boxes
- New Items
  - New Traps
    - Literature Trap
	- Controller Drift Trap
	- Poison Trap
	- Bee Trap
  - New Minigame Traps
    - Breakout Trap
	- Fishing Trap
	- Trivia Trap
	- Pokemon Trivia Trap
	- Pokemon Count Trap
	- Number Sequence Trap
	- Light Up Path Trap
	- Pinball Trap
	- Math Quiz Trap
	- Snake Trap
	- Input Sequence Trap
- Trap Link
  - When you receive a trap, you send a copy of it to every other player with Trap Link enabled
- Boss Gate Plando
- Expert Logic Difficulty
	- Use at your own risk. This difficulty requires complete mastery of SA2.
- Missions can now be enabled and disabled per-character, instead of just per-style
- Minigame Difficulty can now be set to "Chaos", which selects a new difficulty randomly per-trap received

Quality of Life:
- Gate Stages and Mission Orders are now displayed in the spoiler log
- Additional play stats are saved and displayed with the randomizer credits
- Stage Locations progress UI now displays in multiple pages when Itemboxsanity is enabled
- Current stage mission order and progress are now shown when paused in-level
- Chaos Emeralds are now shown when paused in-level
- Location Name Groups were created
- Moved SA2B to the new Options system
- Option Presets were created
- Error Messages are more obvious

Bug Fixes:
- Added missing `Dry Lagoon - 12 Animals` location
- Flying Dog boss should no longer crash when you have done at least 3 Intermediate Kart Races
- Invincibility can no longer be received in the King Boom Boo fight, preventing a crash
- Chaos Emeralds should no longer disproportionately end up in Cannon's Core or the final Level Gate
- Going into submenus from the pause menu should no longer reset traps
- `Sonic - Magic Gloves` are now plural
- Junk items will no longer cause a crash when in a falling state
- Chao Garden:
	- Prevent races from occasionally becoming uncompletable when using the "Prize Only" option
	- Properly allow Hero Chao to participate in Dark Races
	- Don't allow the Chao Garden to send locations when connected to an invalid server
	- Prevent the Chao Garden from resetting your life count
	- Fix Chao World Entrance Shuffle causing inaccessible Neutral Garden
	- Fix pressing the 'B' button to take you to the proper location in Chao World Entrance Shuffle
	- Prevent Chao Karate progress icon overflow
	- Prevent changing Chao Timescale while paused or while a Minigame is active
- Logic Fixes:
	- `Mission Street - Chao Key 1` (Hard Logic) now requires no upgrades
	- `Mission Street - Chao Key 2` (Hard Logic) now requires no upgrades
	- `Crazy Gadget - Hidden 1` (Standard Logic) now requires `Sonic - Bounce Bracelet` instead of `Sonic - Light Shoes`
	- `Lost Colony - Hidden 1` (Standard Logic) now requires `Eggman - Jet Engine`
	- `Mad Space - Gold Beetle` (Standard Logic) now only requires `Rouge - Iron Boots`
	- `Cosmic Wall - Gold Beetle` (Standard and Hard Logic) now only requires `Eggman - Jet Engine`
2025-03-22 13:00:07 +01:00
panicbit
0e99888926 LADX: Stop spamming location checks over network (#4757) 2025-03-21 17:10:17 +01:00
qwint
74cbf10930 Civ6: Use AutoPatchRegister to make patch downloadable on webhost #4752 2025-03-20 19:28:16 +01:00
BadMagic100
08d2909b0e Hollow Knight: Include Lumafly links to install mods in docs (#4745) 2025-03-20 11:49:55 -04:00
CaitSith2
0949b11436 ALttP: Don't crash generation if sprite paths don't exist (#4725) 2025-03-20 14:48:30 +01:00
Aaron Wagener
9cdffe7f63 The Messenger: Add display names to the plando options (#4748) 2025-03-19 15:52:14 -04:00
Bryce Wilson
8b2a883669 Pokemon Emerald: Update changelog (#4747) 2025-03-19 02:17:01 +01:00
NewSoupVi
b7fc96100c Revert "Core: update websockets (#4732)" (#4753)
This reverts commit 42eaeb92f0.
2025-03-19 01:39:18 +01:00
Aaron Wagener
63cbc00a40 The Messenger: Fix corrupted future rule (#4749) 2025-03-18 19:01:31 -04:00
CodeGorilla
57b94dba6f Options: Add a column for player ID to --csv_output (#4715) 2025-03-17 21:43:00 +01:00
ironminer888
0dd188e108 LADX: Add more specific "item icon guessing" support for some games (#4706)
* DKC3, PKMN R/B/Em, M&L specific item matches

* MLSS Bean types are now discrete

* Add Doom 1/2 items

* Add Doom 1/2 items, actually

* Add Inscryption items

* Add more SA2B items, Minecraft

* Add VVVVVV

* Add misc items, comma fixes

* Hat in Time items

* Misc changes

* Expand TODO

* Add more OoT items, Pokemon consumables

* KH2

* KH1, adjust KH2 items

* Formatting fixes

* more item changes, fix kh1 name

* Fix KH1 name

* Add Full Heal to MEDICINE graphics

* Final comma fixes before PR

* Add Full Restore as Medicine

* Move some names to generic, drink fixes, double-quotes consistency fix

* moved ROCK SMASH match to PHRASES dict

* Removed some redundant name checks, remove Old Amber check from Emerald

* Added "PASS" generic check as "LETTER" sprite

* Removed TODO

* Corrected KH1 name for real this time

* Icon assignment now uppers freogin item string during comparison

* Doom skull keys are now NIGHTMARE_KEY, added QUILL as generic for FEATHER

* KH2 armor is Blunic, accessories are Ribbons

* KH1 accessories/armor are Blunic

* "ROCK SMASH" is now "BOMB"

* Removed extra space
2025-03-17 11:50:57 -04:00
PoryGone
bf8c840293 Celeste 64: v1.3 Content Update (#4581)
### Features:

- New optional Location Checks
	- Checkpointsanity
- Hair Color
	- Allows for setting of Maddy's hair color in each of No Dash, One Dash, Two Dash, and Feather states
- Other Player Ghosts
	- A game config option allows you to see ghosts of other Celeste 64 players in the multiworld

### Quality of Life:

- Checkpoint Warping
	- Received Checkpoint items allow for warping to their respective checkpoint
		- These items are on their respective checkpoint location if Checkpointsanity is disabled
	- Logic accounts for being able to warp to otherwise inaccessible areas
	- Checkpoints are a possible option for a starting item on Standard Logic + Move Shuffle + Checkpointsanity
- New Options toggle to enable/disable background input

### Bug Fixes:

- Traffic Blocks now correctly appear disabled within Cassettes
2025-03-17 02:46:34 +01:00
black-sliver
c0244f3018 Tests: unroll 2 player gen, add parametrization helper, add docs (#4648)
* Tests: unroll test_multiworlds.TestTwoPlayerMulti

Also adds a helper function that other tests can use to unroll tests.

* Docs: add more details to docs/tests.md

* Explain parametrization, subtests and link to the new helper
* Mention some performance details and work-arounds
* Mention multithreading / pytest-xdist

* Tests: make param.classvar_matrix accept sets

* CI: add test/param.py to type checking

* Tests: add missing typing to test/param.py

* Tests: fix typo in test/param.py doc comment

Co-authored-by: qwint <qwint.42@gmail.com>

* update docs

* Docs: reword note on performance

---------

Co-authored-by: qwint <qwint.42@gmail.com>
2025-03-17 00:16:02 +01:00
black-sliver
8af8502202 CI: pin some actions (#4744) 2025-03-17 00:02:00 +01:00
Fabian Dill
42eaeb92f0 Core: update websockets (#4732) 2025-03-16 22:13:12 +01:00
Alchav
7f35eb8867 Pokémon R/B: Allow generating with all items linked (#4330)
* Pokémon R/B: Allow generating with all items linked

* check priority/excluded locations for pc_item

* Update regions.py

* Un-remove regions.py code
2025-03-16 12:33:24 -04:00
BadMagic100
785569c40c Core: Generic ER fails in stage 1 when the last available target is an indirect conditioned dead end (#4679)
* Add test that stage1 ER will not fail due to speculative sweeping an indirect conditioned dead end

* Skip speculative sweep if it's the last entrance placement

* Better implementation of needs_speculative_sweep

* pep8
2025-03-15 18:56:07 +01:00
Scipio Wright
a9eb70a881 OoT: Remove Outdated Spanish Setup Guide (#4736)
* Remove spanish setup guide from webworld

* Update __init__.py

* Update __init__.py
2025-03-15 07:16:06 -04:00
Scipio Wright
5d3d0c8625 WebHost: Update text for options you can't modify (#4614) 2025-03-15 07:10:07 -04:00
Scipio Wright
7e32feeea3 Webhost: Update random option wording on webhost (#4555)
* Update random option wording on webhost

* Update WebHostLib/templates/playerOptions/macros.html

Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com>
2025-03-15 07:09:04 -04:00
neocerber
0d1935e757 SC2: Add a description of mission order and the impact of collect on a SC2 world (#4398)
* Added mission order to randomized stuff, added a mention to the default option collect on goal, added an issue about mission order progress vs AP collect

* Remove false menion of collect being note modifyable after the mworld was gen

* Simplification of some sentences

* American spelling, header newline, and other

* Revert gray to grey, corrected some colors

* Forgot a gray -> grey

* Replace how the faction color option is described to side-step difference within yaml and client. Both fr/en.
2025-03-14 11:35:58 -04:00
Benny D
9b3ee018e9 Core/Various Worlds: Fix crash/freeze with unicode characters (#4671)
replace colorama.init with just_fix_windows_console
2025-03-14 08:24:37 +01:00
NewSoupVi
1de411ec89 The Witness: Change Regions, Areas and Connections from Dict[str, Any] to dataclasses&NamedTuples (#4415)
* Change Regions, Areas and Connections to dataclasses/NamedTuples

* Move to new file

* we do a little renaming

* Purge the 'lambda' naming in favor of 'rule' or 'WitnessRule'

* missed one

* unnecessary change

* omega oops

* NOOOOOOOO

* Merge error

* mypy thing
2025-03-13 23:59:09 +01:00
LiquidCat64
3192799bbf CVCotM: Clarify the Wii U VC version is unsupported (#4734)
* Comment out VC ROM hash usages and clarify that it's unsupported.

* Update worlds/cvcotm/docs/en_Castlevania - Circle of the Moon.md

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Update worlds/cvcotm/docs/setup_en.md

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2025-03-13 00:21:09 +01:00
Aaron Wagener
2c8dded52f The Messenger: Fix some transition plando issues (#4720)
* don't allow one-way and two-way entrances to be connected to each other

* add special handling for the tower hq nodes since they share the same parent region
2025-03-10 22:13:49 -04:00
justinspatz
06111ac6cf OOT: Have beehives that only appear as a child not be in logic if only adult can break beehives (#4646)
* Change the logic for the 3 Zora's Domain Beehives to support new rule

Implement new logic changes to these 3 locations

* Update LogicHelpers.json with new rule for beehives that only appear for child link

Added below the "can_break_upper_beehive" a new helper called "can_break_upper_beehive_child" which removes the requirement for hookshot to avoid a logic error in the Zora Domain Beehives where it checks whether child or adult can break beehives, even though these beehives do not appear as an adult.

* Update LogicHelpers.json moving the call for is_child

As is_child is already called for can_use (Boomerang), it's a bit redundant to include the check for using the Boomerang, so it's being moved to be with the Bombchu check to ensure that it's not expected if the Bombchu Logic Rule is turned on that Adult can use bombchus to break the beehives. This effectively does the same thing, but should be better on performance.
2025-03-10 17:39:45 +01:00
agilbert1412
d83294efa7 Stardew valley: Fix Aurora Vineyard Tablet logic (#4512)
* - Add requirement on Aurora Vineyard tablet to start the quest

* - Add rule for using the aurora vineyard staircase

* - Added a test for the tablet

* - Add a few missing items to the test

* - Introduce a new item to split the quest from the door and avoir ER issues

* - Optimize imports

* - Forgot to generate the item

* fix Aurora mess

# Conflicts:
#	worlds/stardew_valley/rules.py
#	worlds/stardew_valley/test/mods/TestMods.py

* fix a couple errors in the cherry picked commit, added a method to improve readability and reduce chance of human error on story quest conditions

* - remove blank line

* - Code review comments

* - fixed weird assert name

* - fixed accidentally surviving line

* - Fixed imports

---------

Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com>
2025-03-10 11:39:35 -04:00
Dinopony
be550ff6fb Landstalker: Several small fixes (#4675)
* Landstalker: Fixed duplicate entrance names when using the "No teleport tree requirements" option

* Landstalker: Fixed more cases of duplicate entrance names when using "Shuffle Trees" with open trees

* Landstalker: Fixed endgame locations being present in "Reach Kazalt" goal

* Landstalker: Fixed Lithograph hint pointing at the wrong player

* Landstalker: Updated docs to remove the link to Steam since game got delisted

* Landstalker: Fixed high value hint_count rarely failing at generation

* Landstalker: Fixed dynamic shop prices being potentially invalid in case of a progression balancing (changes by ExemptMedic)
2025-03-10 11:35:58 -04:00
Patrick Lübcke
dd55409209 Pokémon R/B: Fix Rock Tunnel B1F randomization (#4670)
* Bottom to central path sealed off

* Bottom-to-left-path to right path sealed off

* Central opening (r4444): Left unsealed, paths seperated

* Top right half rocks fixed

* Middle to top opening sealed

* Right hallway seal correctly positioned

* Top right ladder: Fixed overlapping walls
2025-03-10 11:35:40 -04:00
Mysteryem
e267714d44 AHiT: Rework Subcon Forest Boss Arena, Boss Firewall and YCHE logic (#4494)
A new `Subcon Forest - Behind Boss Firewall` region is added for
`Subcon Village - Snatcher Statue Chest`. `Subcon Forest Area` connects
to this new region, requiring either the first
`Progressive Painting Unlock`, or Expert logic +
`NoPaintingSkips: false`.

A new `Subcon Forest Boss Arena` region is added for
`Subcon Forest - Boss Arena Chest` because this is immediately
accessible from YCHE. There are connections to this region from
`Your Contract has Expired` (no requirements) and from
`Subcon Forest - Behind Boss Firewall` (requiring either Hard logic or
`Hookshot Badge` + `TOD Access`).

A reverse connection is also added to Expert logic, for
`Subcon Forest Boss Arena` -> `Subcon Forest - Behind Boss Firewall`.
This could be extended to include Hard logic if there is a reasonable
Cherry Bridge setup.

A reverse connection is also added to Expert logic, for
`Subcon Forest - Behind Boss Firewall` -> `Subcon Forest Area`, so long
as `NoPaintingSkips: false` because it is impossible to burn the
paintings to remove the firewall, from behind the firewall.

A new `Your Contract has Expired - Post Fight` region is added for the
Snatcher post fight cutscene to prevent the Snatcher Hover trick giving
access to YCHE, which would otherwise also give access to the new
`Subcon Forest Boss Arena` Region.

The paintings and boss arena gap logic for `Snatcher Statue Chest` and
`Boss Arena Chest` are now handled using the connections to/from these
new regions rather than being on the locations themselves.

The logic for `Act Completion (Toilet of Doom)` remains unchanged
because it has to be in the `Toilet of Doom` region.

In Expert logic, with `NoPaintingSkips: false`, YCHE is added as a rift
access region to Subcon Forest Time Rift entrances.

The `YCHE Access` event is no longer used and has been removed.

- Fixes painting skips logic for Subcon Village - Snatcher Statue Chest
- Fixes Subcon Forest - Boss Arena Chest being inaccessible from YCHE
- Adds Expert logic to reach `Snatcher Statue Chest` from YCHE
- Adds Expert logic to skip the boss firewall in reverse from YCHE so
long as painting skips are not removed from logic
- Adds Expert logic to access Subcon Forest Time Rift entrances from
YCHE so long as painting skips are not removed from logic
2025-03-10 11:34:10 -04:00
Aaron Wagener
7c30c4a169 The Messenger: Transition Shuffle (#4402)
* The Messenger: transition rando

* remove unused import

* always link both directions for plando when using coupled transitions

* er_type was renamed to randomization_type

* use frozenset for things that shouldn't change

* review suggestions

* do portal and transition shuffle in `connect_entrances`

* remove some unnecessary connections that were causing entrance caching collisions

* add test for strictest possible ER settings

* use unittest.skip on the skipped test, so we don't waste time doing setUp and tearDown

* use the world helpers

* make the plando connection description more verbose

* always add searing crags portal if portal shuffle is disabled

* guarantee an arbitrary number of locations with first connection

* make the constraints more lenient for a bit more variety
2025-03-10 11:16:09 -04:00
Alchav
4882366ffc LTTP: Fix TR Big Key Door Entrance Logic (#4712) 2025-03-10 15:56:05 +01:00
Carter Hesterman
5f73c245fc New Game Implementation: Civilization VI (#3736)
* Init

* remove submodule

* Init

* Update docs

* Fix tests

* Update to use apcivvi

* Update Readme and codeowners

* Minor changes

* Remove .value from options (except starting hint)

* Minor updates

* remove unnecessary property

* Cleanup Rules and Region

* Fix output file generation

* Implement feedback

* Remove 'AP' tag and fix issue with format strings and using same quotes

* Update worlds/civ_6/__init__.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Minor docs changes

* minor updates

* Small rework of create items

* Minor updates

* Remove unused variable

* Move client to Launcher Components with rest of similar clients

* Revert "Move client to Launcher Components with rest of similar clients"

This reverts commit f9fd5df9fd.

* modify component

* Fix generation issues

* Fix tests

* Minor change

* Add improvement and test case

* Minor options changes

* .

* Preliminary Review

* Fix failing test due to slot data serialization

* Format json

* Remove exclude missable boosts

* Update options (update goody hut text, make research multiplier a range)

* Update docs punctuation and slot data init

* Move priority/excluded locations into options

* Implement docs PR feedback

* PR Feedback for options

* PR feedback misc

* Update location classification and fix client type

* Fix typings

* Update research cost multiplier

* Remove unnecessary location priority code

* Remove extrenous use of items()

* WIP PR Feedback

* WIP PR Feedback

* Add victory event

* Add option set for death link effect

* PR improvements

* Update post fill hint to support items with multiple classifications

* remove unnecessary len

* Move location exclusion logic

* Update test to use set instead of accidental dict

* Update docs around progressive eras and boost locations

* Update docs for options to be more readable

* Fix issue with filler items and prehints

* Update filler_data to be static

* Update links in docs

* Minor updates and PR feedback

* Update boosts data

* Update era required items

* Update existing techs

* Update existing techs

* move boost data class

* Update reward data

* Update prereq data

* Update new items and progressive districts

* Remove unused code

* Make filler item name func more efficient

* Update death link text

* Move Civ6 to the end of readme

* Fix bug with hidden locations and location.name

* Partial PR Feedback Implementation

* Format changes

* Minor review feedback

* Modify access rules to use list created in generate_early

* Modify boost rules to precalculate requirements

* Remove option checks from access rules

* Fix issue with pre initialized dicts

* Add inno setup for civ6 client

* Update inno_setup.iss

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: Exempt-Medic <ExemptMedic@Gmail.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2025-03-10 14:53:26 +01:00
NewSoupVi
21ffc0fc54 Band-aid Linux Build breaking with the release of PyGObject 3.52.1 (#4716)
* Band-aid Linux Build breaking with the release of PyGObject 3.52.1

* Update build.yml

* Release workflow as well
2025-03-10 14:43:52 +01:00
Scipio Wright
e95a41cf93 TUNIC: Add another alias for ladders #4714 2025-03-10 14:24:37 +01:00
Silvris
04771fa4f0 Core: fix pickling plando texts (#4711) 2025-03-09 20:00:00 +01:00
jamesbrq
2639796255 MLSS: Add new goal + Update basepatch to standalone equivalent (#4409)
* Item groups + small changes

* Add alternate goal

* New Locations and Logic Updates + Basepatch

* Update basepatch.bsdiff

* Update Basepatch

* Update basepatch.bsdiff

* Update bowsers castle logic with emblem hunt

* Update Archipelago Unittests.run.xml

* Update Archipelago Unittests.run.xml

* Fix for overlapping ROM addresses

* Update Rom.py

* Update __init__.py

* Update basepatch.bsdiff

* Update Rom.py

* Update client with new helper function

* Update basepatch.bsdiff

* Update worlds/mlss/__init__.py

Co-authored-by: qwint <qwint.42@gmail.com>

* Update worlds/mlss/__init__.py

Co-authored-by: qwint <qwint.42@gmail.com>

* Review Refactor

* Review Refactor

---------

Co-authored-by: qwint <qwint.42@gmail.com>
2025-03-09 11:37:15 -04:00
Jérémie Bolduc
4ebabc1208 Stardew Valley: Move filler pool generation out of the world class (#4372)
* merge group options so specific handling is not needed when generating filler pool

* fix

* remove unneeded imports

* self review

* remove unneeded imports

* looks like typing was missing woopsi
2025-03-08 12:13:33 -05:00
josephwhite
ce34b60712 Super Mario 64: ItemData class and tables (#4321)
* sm64ex: use item data class

* rearrange imports

* Dict to dict

* remove optional typing

* bonus item descriptions since we can also add stuff for webworld easily

* remove item descriptions (rip) and decrease verbosity for classifications

* formatting
2025-03-08 12:07:50 -05:00
Trevor L
54094c6331 Blasphemous: Restrict right half of map start locations to hard difficulty only (#4002)
* Start locations, location name

* Fix tests
2025-03-08 11:59:35 -05:00
Bryce Wilson
3986f6f11a Pokemon Emerald: Randomize rock smash encounters (#3912)
* Pokemon Emerald: WIP add rock smash encounter randomization

* Pokemon Emerald: Refactor encounter data on maps

* Pokemon Emerald: Remove unused import

* Pokemon Emerald: Swap StrEnum for regular Enum and use .value
2025-03-08 11:57:16 -05:00
sgrunt
5662da6f7d Timespinner: Support new flags and settings from the randomizer (#4559)
* Timespinner: Add "no hell spiders" enemy rando option that is present in upstream settings

* Timespinner: Prism Break support tweaks (including tracker support)

* Timespinner: Add support for upstream Lock Key Amadeus flag

* Timespinner: Add support for upstream Risky Warps flag

* Timespinner: Add support for upstream Pyramid Start flag

* Timespinner: fix error in lab connectivity logic

* Timespinner: use has_all to simplify one check

Per PR suggestion.

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Timespinner: fix apparent logic error inherited from in-rando logic

* Timespinner: adjust "Origins" location logic slightly further to account for a Risky Warps case

* Timespinner: remove the backward compat options for the recent flag additions

* Timespinner: add newly added Gate Keep option from rando

* Timespinner: adjust the laser access colours in the tracker

* Timespinner: fix an item description in the tracker

* Timespinner: based on testing feedback, put Laser Access items in their own category

* Timespinner: add support for new upstream flag Royal Roadblock

* Timespinner: also ensure the new flag gets put in slot data

* Timespinner: fix bug in universal tracker support indicating castle basement is accessible at the lower Rising Tides flooding level

* Timespinner: exclude Talaria Attachment and Timespinner Wheel from pyramid start starter progression items

* Timespinner: fix region logic for the left pyramid warp

* Timespinner: fix main Gyre access logic when Risky Warps warps you behind the lasers

* Timespinner: apply suggested spacing fix

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

---------

Co-authored-by: sgrunt <sgrunt1987@gmail.com>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2025-03-08 11:54:23 -05:00
Scipio Wright
33a75fb2cb TUNIC: Breakable Shuffle (#4489)
* Starting out

* Rules for breakable regions

* make the rest of it work, it's pr ready, boom

* Make it work in not pot shuffle

* Fix after merge

* Fix item id overlap

* Move breakable, grass, and local fill options in yaml

* Fix groups getting overwritten

* Rename, add new breakables

* Rename more stuff

* Time to rename them again

* Make it actually default for breakable shuffle

* Burn the signs down

* Fix west courtyard pot regions

* Fix fortress courtyard and beneath the fortress loc groups again

* More missing loc group conversions

* Replace instances of world.player with player, same for multiworld

* Update worlds/tunic/__init__.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Remove unused import
2025-03-08 11:25:47 -05:00
Jérémie Bolduc
ee9bcb84b7 Stardew Valley: Move progressive tool options handling in features (#4374)
* create tool progression feature and unwrap option

* replace option usage with calling feature

* add comment explaining why some logic is a weird place

* replace item creation logic with feature

* self review and add unit tests

* rename test cuz I named them too long

* add a test for the trash can useful stuff cuz I thought there was a bug but turns out it works

* self review again

* remove price_multiplier, turns out it's unused during generation

* damn it 3.11 why are you like this

* use blacksmith region when checking vanilla tools

* fix rule

* move can mine using in tool logic

* remove changes to performance test

* properly set the option I guess

* properly set options 2

* that's what happen when you code too late
2025-03-08 11:19:29 -05:00
Kaito Sinclaire
b5269e9aa4 id Tech Games: Customizable ammo capacity (#3565)
* Doom, Doom 2, Heretic: customizable ammo capacity

* Do not progression balance capacity up items

* Prog fill still doesn't agree, just go with our original idea

* Clean up the new options a bit

- Gave all options a consistent and easily readable naming scheme
  (`max_ammo_<type>` and `added_ammo_<type>`)
- Don't show the new options in the spoiler log,
  as they do not affect logic
- Fix the Doom games' Split Backpack option accidentally referring to
  Heretic's Bag of Holding

The logging change across all three games is incidental, as at some
point I did run into that condition by happenstance and it turns out
that it throws an exception due to bad formatting if it's reached

* Do the visibility change for Heretic as well

* Update required client version

* Remove spoiler log restriction on options

* Remove Visibility import now made redundant
2025-03-08 10:37:54 -05:00
Bryce Wilson
00a6ac3a52 BizHawkClient: Store seed name sent by the server for clients to check (#4702) 2025-03-08 16:14:25 +01:00
Bryce Wilson
ea8a14b003 Pokemon Emerald: Some dexsanity locations contribute evolution items (#3187)
* Pokemon Emerald: Change some dexsanity vanilla items to evo items

If a species evolves via item use (Fire Stone, Metal Coat, etc.), use that as it's vanilla item instead of a ball

* Pokemon Emerald: Remove accidentally added print

* Pokemon Emerald: Update changelog

* Pokemon Emerald: Adjust changelog

* Pokemon Emerald: Remove unnecessary else

* Pokemon Emerald: Fix changelog
2025-03-08 10:13:58 -05:00
CaitSith2
414ab86422 LttP: Fix dungeon counter options. (#4704) 2025-03-08 16:13:32 +01:00
Scipio Wright
d4e2698ae0 TUNIC: Add exception handling to deal with duplicate apworlds (#4634)
* Add exception handling to deal with duplicate apworlds

* Update worlds/tunic/__init__.py
2025-03-08 09:56:29 -05:00
JaredWeakStrike
3f8e3082c0 KH2: Client Optimizations and some QoL (#4547)
* adding qwints suggestions

* add stat increase protection and ingame yml stuff

* idk how I forgot these

* reword things

* Update worlds/kh2/Client.py

Co-authored-by: qwint <qwint.42@gmail.com>

* 3.12 compat

* too long of a line

* why didnt I do this before lol

* reading is hard

* missed one

* forgot the self

* fix crash if you get datapackage that isnt kh2

* update to main?

* update to use 0.10 as base and fix violet's base 0 on hex values

* reverting this because I'm bad at my job

---------

Co-authored-by: qwint <qwint.42@gmail.com>
2025-03-08 08:58:59 -05:00
Justus Lind
0f738935ee Muse Dash: Update song list to Cosmic Radio. (#4554)
* MSR Anthology Vol.2 update

* Missing new line.

* Update to Cosmic Radio 2024
2025-03-08 08:58:26 -05:00
kbranch
9c57976252 LADX: Autotracker improvements (#4445)
* Expand and validate the RAM cache

* Part way through location improvement

* Fixed location tracking

* Preliminary entrance tracking support

* Actually send entrance messages

* Store found entrances on the server

* Bit of cleanup

* Added rupee count, items linked to checks

* Send Magpie a handshAck

* Got my own version wrong

* Remove the Beta name

* Only send slot_data if there's something in it

* Ask the server for entrance updates

* Small fix to stabilize Link's location when changing rooms

* Oops, server storage is shared between worlds

* Deal with null responses from the server

* Added UNUSED_KEY item
2025-03-08 13:32:45 +01:00
NewSoupVi
3e08acf381 The Witness: Move local_items code earlier #4696 2025-03-08 12:26:59 +01:00
Exempt-Medic
113259bc15 Update links (#4690)
* Update links

* Update two more
2025-03-07 20:17:45 -05:00
Natalie Weizenbaum
61afe76eae DS3: Remove the outdated French translation of the setup docs (#4700)
This was causing confusion and Discord support requests because the
instructions there are no longer compatible with the latest version of
Archipelago.

This also lists me as the primary author of the new setup guide.
2025-03-08 01:45:52 +01:00
NewSoupVi
08b3b3ecf5 The Witness: The Secret Feature (#4370)
* Secret Feature

* Fixes

* Fixes and unit tests

* renaming some variables

* Fix the thing

* unit test for elevator egg

* Docstring

* reword

* Fix duplicate locations I think?

* Remove debug thing

* Add the tests back lol

* Make it so that you can exclude an egg to disable it

* Improve hint text for easter eggs

* Update worlds/witness/options.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Update worlds/witness/player_logic.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Update worlds/witness/options.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Update worlds/witness/player_logic.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Update worlds/witness/rules.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Update test_easter_egg_shuffle.py

* This was actually not necessary, since this is the Egg requirements, nothing to do with location names

* Move one of them

* Improve logic

* Lol

* Moar

* Adjust unit tests

* option docstring adjustment

* Recommend door shuffle

* Don't overlap IDs

* Option description idk

* Change the way the difficulties work to reward playing higher modes

* Fix merge

* add some stuff to generate_data_file (this file is not imported during gen, don't review it :D)

* oop

* space

* This can be earlier than I thought, apparently.

* buffer

* Comment

* Make sure the option is VERY visible

* Some mypy stuff

* apparently ruff wants this

* .

* durinig

* Update options.py

* Explain the additional effects of each difficulty

* Fix logic of flood room secret

* Add Southern Peninsula Area

* oop

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2025-03-08 01:44:06 +01:00
Silent
bc61221ec6 TUNIC: Expanded hexagon quest options (#4076)
* More hex quest updates

- Implement page ability shuffle for hex quest
- Fix keys behind bosses if hex goal is less than 3
- Added check to fix conflicting hex quest options
- Add option to slot data

* Change option comparison

* Change option checking and fix some stuff

- also keep prayer first on low hex counts

* Update option defaulting

* Update option checking

* Fix option assignment again

* Show player name in option warning

* Add new option to universal tracker stuff

* Update __init__.py

* Make helper method for getting total hexagons in itempool

* Update options.py

* Update option value passthrough

* Change ability shuffle to default on

* Check for hexagons option when writing spoiler
2025-03-08 01:43:02 +01:00
threeandthreee
2f0b81e12c LADX: tarins gift improvement (#3970)
* add groups and a preset

* formatting

* pull zig's tarin's gift improvements

* typing

* alias groups for progressive items

* change tarins gift option a bit

* add bush breakers item group

* fix typo

* bush_breaker option, respect non_local_items

* review suggestions

* cleaner
thx exempt

* Update worlds/ladx/__init__.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* fix gen failures for dungeon shuffle

* exclude shovel based on entrance mapping

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-03-08 01:24:58 +01:00
threeandthreee
bb9a6bcd2e LADX: more marin joke text (#3966)
* marin text

* Adds lots of Marin Flavour Text (#32)

* Updates of Splash text 24-09-18

* Re-Adds '

* use pkgutil

* Adds all community suggestions up until 20/09/2024 (#33)

* Adds all community suggestions up until 20/09/2024

* cutting deathlink jokes

---------

Co-authored-by: Alex Nordstrom <a.l.nordstrom@gmail.com>

* drop piracy-adjacent jokes

* marin text was too long

* more submissions

* no longer looking for new maintainer

---------

Co-authored-by: palex00 <32203971+palex00@users.noreply.github.com>
2025-03-08 01:19:51 +01:00
Jérémie Bolduc
c8b7ef1016 Stardew Valley: Fix a logic bug where the Tea Sapling would be considered available without having the recipe (#4703) 2025-03-08 00:14:10 +01:00
Silent
e00467c2a2 TUNIC: Update logic for chest in fortress dark area (#4691)
* Update logic for beneath the vault chest

* use helper method instead

so that it checks the lanternless option
2025-03-06 00:18:27 +01:00
Silent
0eb6150e95 TUNIC: Fix rule for some grass in West Garden (#4682) 2025-03-06 00:17:27 +01:00
Fabian Dill
91d977479d Tests: test that collect and remove have expected behaviour. (#2062)
---------

Co-authored-by: qwint <qwint.42@gmail.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-03-05 23:48:03 +01:00
BadMagic100
cd761db170 Core: Do GER speculative sweep membership checks against a set #4698 2025-02-27 19:21:48 +01:00
Aaron Wagener
026011323e The Messenger: Fix 0 Required Power Seals (#4692) 2025-02-27 11:42:41 -05:00
Silvris
adc5f3a07d MM2: Fix Shuffled Weaknesses Seed Bleed (#4689) 2025-02-27 11:13:37 -05:00
BadMagic100
69940374e1 Core: Only consider requested exits during ER placement and speculative sweep #4684 2025-02-27 17:12:35 +01:00
322 changed files with 29710 additions and 5046 deletions

View File

@@ -2,6 +2,7 @@
"include": [
"../BizHawkClient.py",
"../Patch.py",
"../test/param.py",
"../test/general/test_groups.py",
"../test/general/test_helpers.py",
"../test/general/test_memory.py",

View File

@@ -132,7 +132,7 @@ jobs:
# charset-normalizer was somehow incomplete in the github runner
"${{ env.PYTHON }}" -m venv venv
source venv/bin/activate
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject charset-normalizer
"${{ env.PYTHON }}" -m pip install --upgrade pip "PyGObject<3.51.0" charset-normalizer
python setup.py build_exe --yes bdist_appimage --yes
echo -e "setup.py build output:\n `ls build`"
echo -e "setup.py dist output:\n `ls dist`"

View File

@@ -36,9 +36,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ilammy/msvc-dev-cmd@v1
- uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
if: startsWith(matrix.os,'windows')
- uses: Bacondish2023/setup-googletest@v1
- uses: Bacondish2023/setup-googletest@49065d1f7a6d21f6134864dd65980fe5dbe06c73
with:
build-type: 'Release'
- name: Build tests

View File

@@ -64,7 +64,7 @@ jobs:
# charset-normalizer was somehow incomplete in the github runner
"${{ env.PYTHON }}" -m venv venv
source venv/bin/activate
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject charset-normalizer
"${{ env.PYTHON }}" -m pip install --upgrade pip "PyGObject<3.51.0" charset-normalizer
python setup.py build_exe --yes bdist_appimage --yes
echo -e "setup.py build output:\n `ls build`"
echo -e "setup.py dist output:\n `ls dist`"

2
.gitignore vendored
View File

@@ -4,11 +4,13 @@
*_Spoiler.txt
*.bmbp
*.apbp
*.apcivvi
*.apl2ac
*.apm3
*.apmc
*.apz5
*.aptloz
*.aptww
*.apemerald
*.pyc
*.pyd

View File

@@ -511,7 +511,7 @@ if __name__ == '__main__':
import colorama
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -1128,7 +1128,7 @@ def run_as_textclient(*args):
args = handle_url_arg(args, parser=parser)
# use colorama to display colored text highlighting on windows
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -261,7 +261,7 @@ if __name__ == '__main__':
parser = get_base_parser()
args = parser.parse_args()
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -28,6 +28,7 @@ from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
from NetUtils import ClientStatus
from worlds.ladx.Common import BASE_ID as LABaseID
from worlds.ladx.GpsTracker import GpsTracker
from worlds.ladx.TrackerConsts import storage_key
from worlds.ladx.ItemTracker import ItemTracker
from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
from worlds.ladx.Locations import get_locations_to_id, meta_to_name
@@ -100,19 +101,23 @@ class LAClientConstants:
WRamCheckSize = 0x4
WRamSafetyValue = bytearray([0]*WRamCheckSize)
wRamStart = 0xC000
hRamStart = 0xFF80
hRamSize = 0x80
MinGameplayValue = 0x06
MaxGameplayValue = 0x1A
VictoryGameplayAndSub = 0x0102
class RAGameboy():
cache = []
cache_start = 0
cache_size = 0
last_cache_read = None
socket = None
def __init__(self, address, port) -> None:
self.cache_start = LAClientConstants.wRamStart
self.cache_size = LAClientConstants.hRamStart + LAClientConstants.hRamSize - LAClientConstants.wRamStart
self.address = address
self.port = port
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -131,9 +136,14 @@ class RAGameboy():
async def get_retroarch_status(self):
return await self.send_command("GET_STATUS")
def set_cache_limits(self, cache_start, cache_size):
self.cache_start = cache_start
self.cache_size = cache_size
def set_checks_range(self, checks_start, checks_size):
self.checks_start = checks_start
self.checks_size = checks_size
def set_location_range(self, location_start, location_size, critical_addresses):
self.location_start = location_start
self.location_size = location_size
self.critical_location_addresses = critical_addresses
def send(self, b):
if type(b) is str:
@@ -188,21 +198,57 @@ class RAGameboy():
if not await self.check_safe_gameplay():
return
cache = []
remaining_size = self.cache_size
while remaining_size:
block = await self.async_read_memory(self.cache_start + len(cache), remaining_size)
remaining_size -= len(block)
cache += block
attempts = 0
while True:
# RA doesn't let us do an atomic read of a large enough block of RAM
# Some bytes can't change in between reading location_block and hram_block
location_block = await self.read_memory_block(self.location_start, self.location_size)
hram_block = await self.read_memory_block(LAClientConstants.hRamStart, LAClientConstants.hRamSize)
verification_block = await self.read_memory_block(self.location_start, self.location_size)
valid = True
for address in self.critical_location_addresses:
if location_block[address - self.location_start] != verification_block[address - self.location_start]:
valid = False
if valid:
break
attempts += 1
# Shouldn't really happen, but keep it from choking
if attempts > 5:
return
checks_block = await self.read_memory_block(self.checks_start, self.checks_size)
if not await self.check_safe_gameplay():
return
self.cache = cache
self.cache = bytearray(self.cache_size)
start = self.checks_start - self.cache_start
self.cache[start:start + len(checks_block)] = checks_block
start = self.location_start - self.cache_start
self.cache[start:start + len(location_block)] = location_block
start = LAClientConstants.hRamStart - self.cache_start
self.cache[start:start + len(hram_block)] = hram_block
self.last_cache_read = time.time()
async def read_memory_block(self, address: int, size: int):
block = bytearray()
remaining_size = size
while remaining_size:
chunk = await self.async_read_memory(address + len(block), remaining_size)
remaining_size -= len(chunk)
block += chunk
return block
async def read_memory_cache(self, addresses):
# TODO: can we just update once per frame?
if not self.last_cache_read or self.last_cache_read + 0.1 < time.time():
await self.update_cache()
if not self.cache:
@@ -359,11 +405,12 @@ class LinksAwakeningClient():
auth = binascii.hexlify(await self.gameboy.async_read_memory(0x0134, 12)).decode()
self.auth = auth
async def wait_and_init_tracker(self):
async def wait_and_init_tracker(self, magpie: MagpieBridge):
await self.wait_for_game_ready()
self.tracker = LocationTracker(self.gameboy)
self.item_tracker = ItemTracker(self.gameboy)
self.gps_tracker = GpsTracker(self.gameboy)
magpie.gps_tracker = self.gps_tracker
async def recved_item_from_ap(self, item_id, from_player, next_index):
# Don't allow getting an item until you've got your first check
@@ -405,9 +452,11 @@ class LinksAwakeningClient():
return (await self.gameboy.read_memory_cache([LAClientConstants.wGameplayType]))[LAClientConstants.wGameplayType] == 1
async def main_tick(self, item_get_cb, win_cb, deathlink_cb):
await self.gameboy.update_cache()
await self.tracker.readChecks(item_get_cb)
await self.item_tracker.readItems()
await self.gps_tracker.read_location()
await self.gps_tracker.read_entrances()
current_health = (await self.gameboy.read_memory_cache([LAClientConstants.wLinkHealth]))[LAClientConstants.wLinkHealth]
if self.deathlink_debounce and current_health != 0:
@@ -457,7 +506,7 @@ class LinksAwakeningContext(CommonContext):
la_task = None
client = None
# TODO: does this need to re-read on reset?
found_checks = []
found_checks = set()
last_resend = time.time()
magpie_enabled = False
@@ -465,6 +514,10 @@ class LinksAwakeningContext(CommonContext):
magpie_task = None
won = False
@property
def slot_storage_key(self):
return f"{self.slot_info[self.slot].name}_{storage_key}"
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None:
self.client = LinksAwakeningClient()
self.slot_data = {}
@@ -505,9 +558,17 @@ class LinksAwakeningContext(CommonContext):
self.ui = LADXManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
async def send_new_entrances(self, entrances: typing.Dict[str, str]):
# Store the entrances we find on the server for future sessions
message = [{
"cmd": "Set",
"key": self.slot_storage_key,
"default": {},
"want_reply": False,
"operations": [{"operation": "update", "value": entrances}],
}]
async def send_checks(self):
message = [{"cmd": 'LocationChecks', "locations": self.found_checks}]
await self.send_msgs(message)
had_invalid_slot_data = None
@@ -536,14 +597,20 @@ class LinksAwakeningContext(CommonContext):
logger.info("victory!")
await self.send_msgs(message)
self.won = True
async def request_found_entrances(self):
await self.send_msgs([{"cmd": "Get", "keys": [self.slot_storage_key]}])
# Ask for updates so that players can co-op entrances in a seed
await self.send_msgs([{"cmd": "SetNotify", "keys": [self.slot_storage_key]}])
async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
if self.ENABLE_DEATHLINK:
self.client.pending_deathlink = True
def new_checks(self, item_ids, ladxr_ids):
self.found_checks += item_ids
create_task_log_exception(self.send_checks())
self.found_checks.update(item_ids)
create_task_log_exception(self.check_locations(self.found_checks))
if self.magpie_enabled:
create_task_log_exception(self.magpie.send_new_checks(ladxr_ids))
@@ -576,6 +643,12 @@ class LinksAwakeningContext(CommonContext):
if cmd == "ReceivedItems":
for index, item in enumerate(args["items"], start=args["index"]):
self.client.recvd_checks[index] = item
if cmd == "Retrieved" and self.magpie_enabled and self.slot_storage_key in args["keys"]:
self.client.gps_tracker.receive_found_entrances(args["keys"][self.slot_storage_key])
if cmd == "SetReply" and self.magpie_enabled and args["key"] == self.slot_storage_key:
self.client.gps_tracker.receive_found_entrances(args["value"])
async def sync(self):
sync_msg = [{'cmd': 'Sync'}]
@@ -589,6 +662,12 @@ class LinksAwakeningContext(CommonContext):
checkMetadataTable[check.id])] for check in ladxr_checks]
self.new_checks(checks, [check.id for check in ladxr_checks])
for check in ladxr_checks:
if check.value and check.linkedItem:
linkedItem = check.linkedItem
if 'condition' not in linkedItem or linkedItem['condition'](self.slot_data):
self.client.item_tracker.setExtraItem(check.linkedItem['item'], check.linkedItem['qty'])
async def victory():
await self.send_victory()
@@ -622,21 +701,36 @@ class LinksAwakeningContext(CommonContext):
if not self.client.recvd_checks:
await self.sync()
await self.client.wait_and_init_tracker()
await self.client.wait_and_init_tracker(self.magpie)
min_tick_duration = 0.1
last_tick = time.time()
while True:
await self.client.main_tick(on_item_get, victory, deathlink)
await asyncio.sleep(0.1)
now = time.time()
tick_duration = now - last_tick
sleep_duration = max(min_tick_duration - tick_duration, 0)
await asyncio.sleep(sleep_duration)
last_tick = now
if self.last_resend + 5.0 < now:
self.last_resend = now
await self.send_checks()
await self.check_locations(self.found_checks)
if self.magpie_enabled:
try:
self.magpie.set_checks(self.client.tracker.all_checks)
await self.magpie.set_item_tracker(self.client.item_tracker)
await self.magpie.send_gps(self.client.gps_tracker)
self.magpie.slot_data = self.slot_data
if self.client.gps_tracker.needs_found_entrances:
await self.request_found_entrances()
self.client.gps_tracker.needs_found_entrances = False
new_entrances = await self.magpie.send_gps(self.client.gps_tracker)
if new_entrances:
await self.send_new_entrances(new_entrances)
except Exception:
# Don't let magpie errors take out the client
pass
@@ -705,6 +799,6 @@ async def main():
await ctx.shutdown()
if __name__ == '__main__':
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -370,7 +370,7 @@ if __name__ == "__main__":
import colorama
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -47,7 +47,7 @@ from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, Networ
from BaseClasses import ItemClassification
min_client_version = Version(0, 1, 6)
colorama.init()
colorama.just_fix_windows_console()
def remove_from_list(container, value):

View File

@@ -346,7 +346,7 @@ if __name__ == '__main__':
import colorama
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -1579,6 +1579,7 @@ def dump_player_options(multiworld: MultiWorld) -> None:
player_output = {
"Game": multiworld.game[player],
"Name": multiworld.get_player_name(player),
"ID": player,
}
output.append(player_output)
for option_key, option in world.options_dataclass.type_hints.items():
@@ -1591,7 +1592,7 @@ def dump_player_options(multiworld: MultiWorld) -> None:
game_option_names.append(display_name)
with open(output_path(f"generate_{multiworld.seed_name}.csv"), mode="w", newline="") as file:
fields = ["Game", "Name", *all_option_names]
fields = ["ID", "Game", "Name", *all_option_names]
writer = DictWriter(file, fields)
writer.writeheader()
writer.writerows(output)

View File

@@ -80,6 +80,8 @@ Currently, the following games are supported:
* Saving Princess
* Castlevania: Circle of the Moon
* Inscryption
* Civilization VI
* The Legend of Zelda: The Wind Waker
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@@ -735,6 +735,6 @@ async def main() -> None:
if __name__ == '__main__':
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -500,7 +500,7 @@ def main():
import colorama
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(_main())
colorama.deinit()

View File

@@ -443,7 +443,8 @@ class RestrictedUnpickler(pickle.Unpickler):
else:
mod = importlib.import_module(module)
obj = getattr(mod, name)
if issubclass(obj, (self.options_module.Option, self.options_module.PlandoConnection)):
if issubclass(obj, (self.options_module.Option, self.options_module.PlandoConnection,
self.options_module.PlandoText)):
return obj
# Forbid everything else.
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")

View File

@@ -446,6 +446,6 @@ if __name__ == '__main__':
parser = get_base_parser(description="Wargroove Client, for text interfacing.")
args, rest = parser.parse_known_args()
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -1,11 +1,11 @@
flask>=3.0.3
werkzeug>=3.0.6
flask>=3.1.0
werkzeug>=3.1.3
pony>=0.7.19
waitress>=3.0.0
waitress>=3.0.2
Flask-Caching>=2.3.0
Flask-Compress>=1.15
Flask-Limiter>=3.8.0
bokeh>=3.5.2
markupsafe>=2.1.5
Flask-Compress>=1.17
Flask-Limiter>=3.12
bokeh>=3.6.3
markupsafe>=3.0.2
Markdown>=3.7
mdx-breakless-lists>=1.0.1

View File

@@ -75,6 +75,27 @@
#inventory-table img.acquired.green{ /*32CD32*/
filter: hue-rotate(84deg) saturate(10) brightness(0.7);
}
#inventory-table img.acquired.hotpink{ /*FF69B4*/
filter: sepia(100%) hue-rotate(300deg) saturate(10);
}
#inventory-table img.acquired.lightsalmon{ /*FFA07A*/
filter: sepia(100%) hue-rotate(347deg) saturate(10);
}
#inventory-table img.acquired.crimson{ /*DB143B*/
filter: sepia(100%) hue-rotate(318deg) saturate(10) brightness(0.86);
}
#inventory-table span{
color: #B4B4A0;
font-size: 40px;
max-width: 40px;
max-height: 40px;
filter: grayscale(100%) contrast(75%) brightness(30%);
}
#inventory-table span.acquired{
filter: none;
}
#inventory-table div.image-stack{
display: grid;

View File

@@ -213,7 +213,7 @@
{% endmacro %}
{% macro RandomizeButton(option_name, option) %}
<div class="randomize-button" data-tooltip="Toggle randomization for this option!">
<div class="randomize-button" data-tooltip="Pick a random value for this option.">
<label for="random-{{ option_name }}">
<input type="checkbox" id="random-{{ option_name }}" name="random-{{ option_name }}" class="randomize-checkbox" data-option-name="{{ option_name }}" {{ "checked" if option.default == "random" }} />
🎲

View File

@@ -99,6 +99,52 @@
{% endif %}
</div>
</div>
{% if 'PrismBreak' in options or 'LockKeyAmadeus' in options or 'GateKeep' in options %}
<div class="table-row">
{% if 'PrismBreak' in options %}
<div class="C1">
<div class="image-stack">
<div class="stack-front">
<div class="stack-top-left">
<img src="{{ icons['Laser Access'] }}" class="hotpink {{ 'acquired' if 'Laser Access A' in acquired_items }}" title="Laser Access A" />
</div>
<div class="stack-top-right">
<img src="{{ icons['Laser Access'] }}" class="lightsalmon {{ 'acquired' if 'Laser Access I' in acquired_items }}" title="Laser Access I" />
</div>
<div class="stack-bottum-left">
<img src="{{ icons['Laser Access'] }}" class="crimson {{ 'acquired' if 'Laser Access M' in acquired_items }}" title="Laser Access M" />
</div>
</div>
</div>
</div>
{% endif %}
{% if 'LockKeyAmadeus' in options %}
<div class="C2">
<div class="image-stack">
<div class="stack-front">
<div class="stack-top-left">
<img src="{{ icons['Lab Glasses'] }}" class="{{ 'acquired' if 'Lab Access Genza' in acquired_items }}" title="Lab Access Genza" />
</div>
<div class="stack-top-right">
<img src="{{ icons['Eye Orb'] }}" class="{{ 'acquired' if 'Lab Access Dynamo' in acquired_items }}" title="Lab Access Dynamo" />
</div>
<div class="stack-bottum-left">
<img src="{{ icons['Lab Coat'] }}" class="{{ 'acquired' if 'Lab Access Research' in acquired_items }}" title="Lab Access Research" />
</div>
<div class="stack-bottum-right">
<img src="{{ icons['Demon'] }}" class="{{ 'acquired' if 'Lab Access Experiment' in acquired_items }}" title="Lab Access Experiment" />
</div>
</div>
</div>
</div>
{% endif %}
{% if 'GateKeep' in options %}
<div class="C3">
<span class="{{ 'acquired' if 'Drawbridge Key' in acquired_items }}" title="Drawbridge Key">&#10070;</span>
</div>
{% endif %}
</div>
{% endif %}
</div>
<table id="location-table">

View File

@@ -100,7 +100,7 @@
{% else %}
<div class="unsupported-option">
This option is not supported. Please edit your .yaml file manually.
This option cannot be modified here. Please edit your .yaml file manually.
</div>
{% endif %}

View File

@@ -1071,6 +1071,11 @@ if "Timespinner" in network_data_package["games"]:
"Plasma Orb": "https://timespinnerwiki.com/mediawiki/images/4/44/Plasma_Orb.png",
"Kobo": "https://timespinnerwiki.com/mediawiki/images/c/c6/Familiar_Kobo.png",
"Merchant Crow": "https://timespinnerwiki.com/mediawiki/images/4/4e/Familiar_Crow.png",
"Laser Access": "https://timespinnerwiki.com/mediawiki/images/9/99/Historical_Documents.png",
"Lab Glasses": "https://timespinnerwiki.com/mediawiki/images/4/4a/Lab_Glasses.png",
"Eye Orb": "https://timespinnerwiki.com/mediawiki/images/a/a4/Eye_Orb.png",
"Lab Coat": "https://timespinnerwiki.com/mediawiki/images/5/51/Lab_Coat.png",
"Demon": "https://timespinnerwiki.com/mediawiki/images/f/f8/Familiar_Demon.png",
}
timespinner_location_ids = {
@@ -1118,6 +1123,9 @@ if "Timespinner" in network_data_package["games"]:
timespinner_location_ids["Ancient Pyramid"] += [
1337237, 1337238, 1337239,
1337240, 1337241, 1337242, 1337243, 1337244, 1337245]
if (slot_data["PyramidStart"]):
timespinner_location_ids["Ancient Pyramid"] += [
1337233, 1337234, 1337235]
display_data = {}

View File

@@ -386,7 +386,7 @@ if __name__ == '__main__':
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a Archipelago Binary Patch file')
args = parser.parse_args()
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -45,6 +45,9 @@
# ChecksFinder
/worlds/checksfinder/ @SunCatMC
# Civilization VI
/worlds/civ6/ @hesto2
# Clique
/worlds/clique/ @ThePhar
@@ -211,6 +214,9 @@
# Wargroove
/worlds/wargroove/ @FlySniper
# The Wind Waker
/worlds/tww/ @tanjo3
# The Witness
/worlds/witness/ @NewSoupVi @blastron

View File

@@ -73,15 +73,47 @@ When tests are run, this class will create a multiworld with a single player hav
generic tests, as well as the new custom test. Each test method definition will create its own separate solo multiworld
that will be cleaned up after. If you don't want to run the generic tests on a base, `run_default_tests` can be
overridden. For more information on what methods are available to your class, check the
[WorldTestBase definition](/test/bases.py#L104).
[WorldTestBase definition](/test/bases.py#L106).
#### Alternatives to WorldTestBase
Unit tests can also be created using [TestBase](/test/bases.py#L14) or
Unit tests can also be created using [TestBase](/test/bases.py#L16) or
[unittest.TestCase](https://docs.python.org/3/library/unittest.html#unittest.TestCase) depending on your use case. These
may be useful for generating a multiworld under very specific constraints without using the generic world setup, or for
testing portions of your code that can be tested without relying on a multiworld to be created first.
#### Parametrization
When defining a test that needs to cover a range of inputs it is useful to parameterize (to run the same test
for multiple inputs) the base test. Some important things to consider when attempting to parametrize your test are:
* [Subtests](https://docs.python.org/3/library/unittest.html#distinguishing-test-iterations-using-subtests)
can be used to have parametrized assertions that show up similar to individual tests but without the overhead
of needing to instantiate multiple tests; however, subtests can not be multithreaded and do not have individual
timing data, so they are not suitable for slow tests.
* Archipelago's tests are test-runner-agnostic. That means tests are not allowed to use e.g. `@pytest.mark.parametrize`.
Instead, we define our own parametrization helpers in [test.param](/test/param.py).
* Classes inheriting from `WorldTestBase`, including those created by the helpers in `test.param`, will run all
base tests by default, make sure the produced tests actually do what you aim for and do not waste a lot of
extra CPU time. Consider using `TestBase` or `unittest.TestCase` directly
or setting `WorldTestBase.run_default_tests` to False.
#### Performance Considerations
Archipelago is big enough that the runtime of unittests can have an impact on productivity.
Individual tests should take less than a second, so they can be properly multithreaded.
Ideally, thorough tests are directed at actual code/functionality. Do not just create and/or fill a ton of individual
Multiworlds that spend most of the test time outside what you actually want to test.
Consider generating/validating "random" games as part of your APWorld release workflow rather than having that be part
of continuous integration, and add minimal reproducers to the "normal" tests for problems that were found.
You can use [@unittest.skipIf](https://docs.python.org/3/library/unittest.html#unittest.skipIf) with an environment
variable to keep all the benefits of the test framework while not running the marked tests by default.
## Running Tests
#### Using Pycharm
@@ -100,3 +132,11 @@ next to the run and debug buttons.
#### Running Tests without Pycharm
Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder.
#### Running Tests Multithreaded
pytest can run multiple test runners in parallel with the pytest-xdist extension.
Install with `pip install pytest-xdist`.
Run with `pytest -n12` to spawn 12 process that each run 1/12th of the tests.

View File

@@ -291,7 +291,7 @@ like entrance randomization in logic.
Regions have a list called `exits`, containing `Entrance` objects representing transitions to other regions.
There must be one special region (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L295-L296)),
There must be one special region (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L299)),
from which the logic unfolds. AP assumes that a player will always be able to return to this starting region by resetting the game ("Save and quit").
### Entrances
@@ -331,7 +331,7 @@ Even doing `state.can_reach_location` or `state.can_reach_entrance` is problemat
You can use `multiworld.register_indirect_condition(region, entrance)` to explicitly tell the generator that, when a given region becomes accessible, it is necessary to re-check a specific entrance.
You **must** use `multiworld.register_indirect_condition` if you perform this kind of `can_reach` from an entrance access rule, unless you have a **very** good technical understanding of the relevant code and can reason why it will never lead to problems in your case.
Alternatively, you can set [world.explicit_indirect_conditions = False](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L301),
Alternatively, you can set [world.explicit_indirect_conditions = False](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L301-L304),
avoiding the need for indirect conditions at the expense of performance.
### Item Rules

View File

@@ -157,17 +157,16 @@ class ERPlacementState:
def placed_regions(self) -> set[Region]:
return self.collection_state.reachable_regions[self.world.player]
def find_placeable_exits(self, check_validity: bool) -> list[Entrance]:
def find_placeable_exits(self, check_validity: bool, usable_exits: list[Entrance]) -> list[Entrance]:
if check_validity:
blocked_connections = self.collection_state.blocked_connections[self.world.player]
blocked_connections = sorted(blocked_connections, key=lambda x: x.name)
placeable_randomized_exits = [connection for connection in blocked_connections
if not connection.connected_region
and connection.is_valid_source_transition(self)]
placeable_randomized_exits = [ex for ex in usable_exits
if not ex.connected_region
and ex in blocked_connections
and ex.is_valid_source_transition(self)]
else:
# this is on a beaten minimal attempt, so any exit anywhere is fair game
placeable_randomized_exits = [ex for region in self.world.multiworld.get_regions(self.world.player)
for ex in region.exits if not ex.connected_region]
placeable_randomized_exits = [ex for ex in usable_exits if not ex.connected_region]
self.world.random.shuffle(placeable_randomized_exits)
return placeable_randomized_exits
@@ -181,7 +180,8 @@ class ERPlacementState:
self.placements.append(source_exit)
self.pairings.append((source_exit.name, target_entrance.name))
def test_speculative_connection(self, source_exit: Entrance, target_entrance: Entrance) -> bool:
def test_speculative_connection(self, source_exit: Entrance, target_entrance: Entrance,
usable_exits: set[Entrance]) -> bool:
copied_state = self.collection_state.copy()
# simulated connection. A real connection is unsafe because the region graph is shallow-copied and would
# propagate back to the real multiworld.
@@ -198,6 +198,9 @@ class ERPlacementState:
# ignore the source exit, and, if coupled, the reverse exit. They're not actually new
if _exit.name == source_exit.name or (self.coupled and _exit.name == target_entrance.name):
continue
# make sure we are only paying attention to usable exits
if _exit not in usable_exits:
continue
# technically this should be is_valid_source_transition, but that may rely on side effects from
# on_connect, which have not happened here (because we didn't do a real connection, and if we did, we would
# not want them to persist). can_reach is a close enough approximation most of the time.
@@ -262,14 +265,19 @@ def bake_target_group_lookup(world: World, get_target_groups: Callable[[int], li
return { group: get_target_groups(group) for group in unique_groups }
def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int | None = None) -> None:
def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int | None = None,
one_way_target_name: str | None = None) -> None:
"""
Given an entrance in a "vanilla" region graph, splits that entrance to prepare it for randomization
in randomize_entrances. This should be done after setting the type and group of the entrance.
in randomize_entrances. This should be done after setting the type and group of the entrance. Because it attempts
to meet strict entrance naming requirements for coupled mode, this function may produce unintuitive results when
called only on a single entrance; it produces eventually-correct outputs only after calling it on all entrances.
:param entrance: The entrance which will be disconnected in preparation for randomization.
:param target_group: The group to assign to the created ER target. If not specified, the group from
the original entrance will be copied.
:param one_way_target_name: The name of the created ER target if `entrance` is one-way. This argument
is required for one-way entrances and is ignored otherwise.
"""
child_region = entrance.connected_region
parent_region = entrance.parent_region
@@ -284,8 +292,11 @@ def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int
# targets in the child region will be created when the other direction edge is disconnected
target = parent_region.create_er_target(entrance.name)
else:
# for 1-ways, the child region needs a target and coupling/naming is not a concern
target = child_region.create_er_target(child_region.name)
# for 1-ways, the child region needs a target. naming is not a concern for coupling so we
# allow it to be user provided (and require it, to prevent an unhelpful assumed name in pairings)
if not one_way_target_name:
raise ValueError("Cannot disconnect a one-way entrance without a target name specified")
target = child_region.create_er_target(one_way_target_name)
target.randomization_type = entrance.randomization_type
target.randomization_group = target_group or entrance.randomization_group
@@ -326,6 +337,24 @@ def randomize_entrances(
# similar to fill, skip validity checks on entrances if the game is beatable on minimal accessibility
perform_validity_check = True
if not er_targets:
er_targets = sorted([entrance for region in world.multiworld.get_regions(world.player)
for entrance in region.entrances if not entrance.parent_region], key=lambda x: x.name)
if not exits:
exits = sorted([ex for region in world.multiworld.get_regions(world.player)
for ex in region.exits if not ex.connected_region], key=lambda x: x.name)
if len(er_targets) != len(exits):
raise EntranceRandomizationError(f"Unable to randomize entrances due to a mismatched count of "
f"entrances ({len(er_targets)}) and exits ({len(exits)}.")
# used when membership checks are needed on the exit list, e.g. speculative sweep
exits_set = set(exits)
for entrance in er_targets:
entrance_lookup.add(entrance)
# place the menu region and connected start region(s)
er_state.collection_state.update_reachable_regions(world.player)
def do_placement(source_exit: Entrance, target_entrance: Entrance) -> None:
placed_exits, removed_entrances = er_state.connect(source_exit, target_entrance)
# remove the placed targets from consideration
@@ -337,9 +366,37 @@ def randomize_entrances(
if on_connect:
on_connect(er_state, placed_exits)
def needs_speculative_sweep(dead_end: bool, require_new_exits: bool, placeable_exits: list[Entrance]) -> bool:
# speculative sweep is expensive. We currently only do it as a last resort, if we might cap off the graph
# entirely
if len(placeable_exits) > 1:
return False
# in certain stages of randomization we either expect or don't care if the search space shrinks.
# we should never speculative sweep here.
if dead_end or not require_new_exits or not perform_validity_check:
return False
# edge case - if all dead ends have pre-placed progression or indirect connections, they are pulled forward
# into the non dead end stage. In this case, and only this case, it's possible that the last connection may
# actually be placeable in stage 1. We need to skip speculative sweep in this case because we expect the graph
# to get capped off.
# check to see if we are proposing the last placement
if not coupled:
# in uncoupled, this check is easy as there will only be one target.
is_last_placement = len(entrance_lookup) == 1
else:
# a bit harder, there may be 1 or 2 targets depending on if the exit to place is one way or two way.
# if it is two way, we can safely assume that one of the targets is the logical pair of the exit.
desired_target_count = 2 if placeable_exits[0].randomization_type == EntranceType.TWO_WAY else 1
is_last_placement = len(entrance_lookup) == desired_target_count
# if it's not the last placement, we need a sweep
return not is_last_placement
def find_pairing(dead_end: bool, require_new_exits: bool) -> bool:
nonlocal perform_validity_check
placeable_exits = er_state.find_placeable_exits(perform_validity_check)
placeable_exits = er_state.find_placeable_exits(perform_validity_check, exits)
for source_exit in placeable_exits:
target_groups = target_group_lookup[source_exit.randomization_group]
for target_entrance in entrance_lookup.get_targets(target_groups, dead_end, preserve_group_order):
@@ -350,12 +407,10 @@ def randomize_entrances(
# very last exit and check whatever exits we open up are functionally accessible.
# this requirement can be ignored on a beaten minimal, islands are no issue there.
exit_requirement_satisfied = (not perform_validity_check or not require_new_exits
or target_entrance.connected_region not in er_state.placed_regions)
needs_speculative_sweep = (not dead_end and require_new_exits and perform_validity_check
and len(placeable_exits) == 1)
or target_entrance.connected_region not in er_state.placed_regions)
if exit_requirement_satisfied and source_exit.can_connect_to(target_entrance, dead_end, er_state):
if (needs_speculative_sweep
and not er_state.test_speculative_connection(source_exit, target_entrance)):
if (needs_speculative_sweep(dead_end, require_new_exits, placeable_exits)
and not er_state.test_speculative_connection(source_exit, target_entrance, exits_set)):
continue
do_placement(source_exit, target_entrance)
return True
@@ -407,21 +462,6 @@ def randomize_entrances(
f"All unplaced entrances: {unplaced_entrances}\n"
f"All unplaced exits: {unplaced_exits}")
if not er_targets:
er_targets = sorted([entrance for region in world.multiworld.get_regions(world.player)
for entrance in region.entrances if not entrance.parent_region], key=lambda x: x.name)
if not exits:
exits = sorted([ex for region in world.multiworld.get_regions(world.player)
for ex in region.exits if not ex.connected_region], key=lambda x: x.name)
if len(er_targets) != len(exits):
raise EntranceRandomizationError(f"Unable to randomize entrances due to a mismatched count of "
f"entrances ({len(er_targets)}) and exits ({len(exits)}.")
for entrance in er_targets:
entrance_lookup.add(entrance)
# place the menu region and connected start region(s)
er_state.collection_state.update_reachable_regions(world.player)
# stage 1 - try to place all the non-dead-end entrances
while entrance_lookup.others:
if not find_pairing(dead_end=False, require_new_exits=True):

View File

@@ -221,6 +221,11 @@ Root: HKCR; Subkey: "{#MyAppName}ygo06patch"; ValueData: "Ar
Root: HKCR; Subkey: "{#MyAppName}ygo06patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ygo06patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apcivvi"; ValueData: "{#MyAppName}apcivvipatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}apcivvipatch"; ValueData: "Archipelago Civilization 6 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}apcivvipatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}apcivvipatch\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";

View File

@@ -1,14 +1,14 @@
colorama>=0.4.6
websockets>=13.0.1,<14
PyYAML>=6.0.2
jellyfish>=1.1.0
jinja2>=3.1.4
jellyfish>=1.1.3
jinja2>=3.1.6
schema>=0.7.7
kivy>=2.3.0
bsdiff4>=1.2.4
platformdirs>=4.2.2
certifi>=2024.12.14
cython>=3.0.11
cymem>=2.0.8
orjson>=3.10.7
kivy>=2.3.1
bsdiff4>=1.2.6
platformdirs>=4.3.6
certifi>=2025.1.31
cython>=3.0.12
cymem>=2.0.11
orjson>=3.10.15
typing_extensions>=4.12.2

View File

@@ -148,7 +148,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
e.randomization_group = 1
e.connect(r2)
disconnect_entrance_for_randomization(e)
disconnect_entrance_for_randomization(e, one_way_target_name="foo")
self.assertIsNone(e.connected_region)
self.assertEqual([], r1.entrances)
@@ -158,10 +158,22 @@ class TestDisconnectForRandomization(unittest.TestCase):
self.assertEqual(1, len(r2.entrances))
self.assertIsNone(r2.entrances[0].parent_region)
self.assertEqual("r2", r2.entrances[0].name)
self.assertEqual("foo", r2.entrances[0].name)
self.assertEqual(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
self.assertEqual(1, r2.entrances[0].randomization_group)
def test_disconnect_default_1way_no_vanilla_target_raises(self):
multiworld = generate_test_multiworld()
r1 = Region("r1", 1, multiworld)
r2 = Region("r2", 1, multiworld)
e = r1.create_exit("e")
e.randomization_type = EntranceType.ONE_WAY
e.randomization_group = 1
e.connect(r2)
with self.assertRaises(ValueError):
disconnect_entrance_for_randomization(e)
def test_disconnect_uses_alternate_group(self):
multiworld = generate_test_multiworld()
r1 = Region("r1", 1, multiworld)
@@ -171,7 +183,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
e.randomization_group = 1
e.connect(r2)
disconnect_entrance_for_randomization(e, 2)
disconnect_entrance_for_randomization(e, 2, "foo")
self.assertIsNone(e.connected_region)
self.assertEqual([], r1.entrances)
@@ -181,7 +193,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
self.assertEqual(1, len(r2.entrances))
self.assertIsNone(r2.entrances[0].parent_region)
self.assertEqual("r2", r2.entrances[0].name)
self.assertEqual("foo", r2.entrances[0].name)
self.assertEqual(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
self.assertEqual(2, r2.entrances[0].randomization_group)
@@ -218,7 +230,7 @@ class TestRandomizeEntrances(unittest.TestCase):
self.assertEqual(80, len(result.pairings))
self.assertEqual(80, len(result.placements))
def test_coupling(self):
def test_coupled(self):
"""tests that in coupled mode, all 2 way transitions have an inverse"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
@@ -236,6 +248,36 @@ class TestRandomizeEntrances(unittest.TestCase):
# if we didn't visit every placement the verification on_connect doesn't really mean much
self.assertEqual(len(result.placements), seen_placement_count)
def test_uncoupled_succeeds_stage1_indirect_condition(self):
multiworld = generate_test_multiworld()
menu = multiworld.get_region("Menu", 1)
generate_entrance_pair(menu, "_right", ERTestGroups.RIGHT)
end = Region("End", 1, multiworld)
multiworld.regions.append(end)
generate_entrance_pair(end, "_left", ERTestGroups.LEFT)
multiworld.register_indirect_condition(end, None)
result = randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup)
self.assertSetEqual({
("Menu_right", "End_left"),
("End_left", "Menu_right")
}, set(result.pairings))
def test_coupled_succeeds_stage1_indirect_condition(self):
multiworld = generate_test_multiworld()
menu = multiworld.get_region("Menu", 1)
generate_entrance_pair(menu, "_right", ERTestGroups.RIGHT)
end = Region("End", 1, multiworld)
multiworld.regions.append(end)
generate_entrance_pair(end, "_left", ERTestGroups.LEFT)
multiworld.register_indirect_condition(end, None)
result = randomize_entrances(multiworld.worlds[1], True, directionally_matched_group_lookup)
self.assertSetEqual({
("Menu_right", "End_left"),
("End_left", "Menu_right")
}, set(result.pairings))
def test_uncoupled(self):
"""tests that in uncoupled mode, no transitions have an (intentional) inverse"""
multiworld = generate_test_multiworld()

View File

@@ -1,5 +1,6 @@
import unittest
from BaseClasses import CollectionState
from worlds.AutoWorld import AutoWorldRegister, call_all
from . import setup_solo_multiworld
@@ -8,12 +9,31 @@ class TestBase(unittest.TestCase):
def test_create_item(self):
"""Test that a world can successfully create all items in its datapackage"""
for game_name, world_type in AutoWorldRegister.world_types.items():
proxy_world = setup_solo_multiworld(world_type, ()).worlds[1]
multiworld = setup_solo_multiworld(world_type, steps=("generate_early", "create_regions", "create_items"))
proxy_world = multiworld.worlds[1]
for item_name in world_type.item_name_to_id:
test_state = CollectionState(multiworld)
with self.subTest("Create Item", item_name=item_name, game_name=game_name):
item = proxy_world.create_item(item_name)
with self.subTest("Item Name", item_name=item_name, game_name=game_name):
self.assertEqual(item.name, item_name)
if item.advancement:
with self.subTest("Item State Collect", item_name=item_name, game_name=game_name):
test_state.collect(item, True)
with self.subTest("Item State Remove", item_name=item_name, game_name=game_name):
test_state.remove(item)
self.assertEqual(test_state.prog_items, multiworld.state.prog_items,
"Item Collect -> Remove should restore empty state.")
else:
with self.subTest("Item State Collect No Change", item_name=item_name, game_name=game_name):
# Non-Advancement should not modify state.
test_state.collect(item)
self.assertEqual(test_state.prog_items, multiworld.state.prog_items)
def test_item_name_group_has_valid_item(self):
"""Test that all item name groups contain valid items. """
# This cannot test for Event names that you may have declared for logic, only sendable Items.

View File

@@ -0,0 +1,11 @@
import unittest
from worlds.AutoWorld import AutoWorldRegister
from worlds.Files import AutoPatchRegister
class TestPatches(unittest.TestCase):
def test_patch_name_matches_game(self) -> None:
for game_name in AutoPatchRegister.patch_types:
with self.subTest(game=game_name):
self.assertIn(game_name, AutoWorldRegister.world_types.keys(),
f"Patch '{game_name}' does not match the name of any world.")

View File

@@ -0,0 +1,19 @@
import unittest
import os
class TestBase(unittest.TestCase):
def test_requirements_file_ends_on_newline(self):
"""Test that all requirements files end on a newline"""
import Utils
requirements_files = [Utils.local_path("requirements.txt"),
Utils.local_path("WebHostLib", "requirements.txt")]
worlds_path = Utils.local_path("worlds")
for entry in os.listdir(worlds_path):
requirements_path = os.path.join(worlds_path, entry, "requirements.txt")
if os.path.isfile(requirements_path):
requirements_files.append(requirements_path)
for requirements_file in requirements_files:
with self.subTest(path=requirements_file):
with open(requirements_file) as f:
self.assertEqual(f.read()[-1], "\n")

View File

@@ -1,5 +1,5 @@
import unittest
from typing import List, Tuple
from typing import ClassVar, List, Tuple
from unittest import TestCase
from BaseClasses import CollectionState, Location, MultiWorld
@@ -7,6 +7,7 @@ from Fill import distribute_items_restrictive
from Options import Accessibility
from worlds.AutoWorld import AutoWorldRegister, call_all, call_single
from ..general import gen_steps, setup_multiworld
from ..param import classvar_matrix
class MultiworldTestBase(TestCase):
@@ -63,15 +64,18 @@ class TestAllGamesMultiworld(MultiworldTestBase):
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
@classvar_matrix(game=AutoWorldRegister.world_types.keys())
class TestTwoPlayerMulti(MultiworldTestBase):
game: ClassVar[str]
def test_two_player_single_game_fills(self) -> None:
"""Tests that a multiworld of two players for each registered game world can generate."""
for world_type in AutoWorldRegister.world_types.values():
self.multiworld = setup_multiworld([world_type, world_type], ())
for world in self.multiworld.worlds.values():
world.options.accessibility.value = Accessibility.option_full
self.assertSteps(gen_steps)
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
call_all(self.multiworld, "post_fill")
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
world_type = AutoWorldRegister.world_types[self.game]
self.multiworld = setup_multiworld([world_type, world_type], ())
for world in self.multiworld.worlds.values():
world.options.accessibility.value = Accessibility.option_full
self.assertSteps(gen_steps)
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
call_all(self.multiworld, "post_fill")
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")

46
test/param.py Normal file
View File

@@ -0,0 +1,46 @@
import itertools
import sys
from typing import Any, Callable, Iterable
def classvar_matrix(**kwargs: Iterable[Any]) -> Callable[[type], None]:
"""
Create a new class for each variation of input, allowing to generate a TestCase matrix / parametrization that
supports multi-threading and has better reporting for ``unittest --durations=...`` and ``pytest --durations=...``
than subtests.
The kwargs will be set as ClassVars in the newly created classes. Use as ::
@classvar_matrix(var_name=[value1, value2])
class MyTestCase(unittest.TestCase):
var_name: typing.ClassVar[...]
:param kwargs: A dict of ClassVars to set, where key is the variable name and value is a list of all values.
:return: A decorator to be applied to a class.
"""
keys: tuple[str]
values: Iterable[Iterable[Any]]
keys, values = zip(*kwargs.items())
values = map(lambda v: sorted(v) if isinstance(v, (set, frozenset)) else v, values)
permutations_dicts = [dict(zip(keys, v)) for v in itertools.product(*values)]
def decorator(cls: type) -> None:
mod = sys.modules[cls.__module__]
for permutation in permutations_dicts:
class Unrolled(cls): # type: ignore
pass
for k, v in permutation.items():
setattr(Unrolled, k, v)
params = ", ".join([f"{k}={repr(v)}" for k, v in permutation.items()])
params = f"{{{params}}}"
Unrolled.__module__ = cls.__module__
Unrolled.__qualname__ = f"{cls.__qualname__}{params}"
setattr(mod, f"{cls.__name__}{params}", Unrolled)
return None
return decorator

View File

@@ -41,6 +41,7 @@ class BizHawkClientCommandProcessor(ClientCommandProcessor):
class BizHawkClientContext(CommonContext):
command_processor = BizHawkClientCommandProcessor
server_seed_name: str | None = None
auth_status: AuthStatus
password_requested: bool
client_handler: BizHawkClient | None
@@ -68,6 +69,8 @@ class BizHawkClientContext(CommonContext):
if cmd == "Connected":
self.slot_data = args.get("slot_data", None)
self.auth_status = AuthStatus.AUTHENTICATED
elif cmd == "RoomInfo":
self.server_seed_name = args.get("seed_name", None)
if self.client_handler is not None:
self.client_handler.on_package(self, cmd, args)
@@ -100,6 +103,7 @@ class BizHawkClientContext(CommonContext):
async def disconnect(self, allow_autoreconnect: bool=False):
self.auth_status = AuthStatus.NOT_AUTHENTICATED
self.server_seed_name = None
await super().disconnect(allow_autoreconnect)
@@ -272,6 +276,6 @@ def launch(*launch_args: str) -> None:
Utils.init_logging("BizHawkClient", exception_logger="Client")
import colorama
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -3,4 +3,4 @@ mpyq>=0.2.5
portpicker>=1.5.2
aiohttp>=3.8.4
loguru>=0.7.0
protobuf==3.20.3
protobuf==3.20.3

View File

@@ -261,6 +261,6 @@ def launch():
# options = Utils.get_options()
import colorama
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -206,7 +206,7 @@ ahit_locations = {
"Subcon Village - Graveyard Ice Cube": LocData(2000325077, "Subcon Forest Area"),
"Subcon Village - House Top": LocData(2000325471, "Subcon Forest Area"),
"Subcon Village - Ice Cube House": LocData(2000325469, "Subcon Forest Area"),
"Subcon Village - Snatcher Statue Chest": LocData(2000323730, "Subcon Forest Area", paintings=1),
"Subcon Village - Snatcher Statue Chest": LocData(2000323730, "Subcon Forest Behind Boss Firewall"),
"Subcon Village - Stump Platform Chest": LocData(2000323729, "Subcon Forest Area"),
"Subcon Forest - Giant Tree Climb": LocData(2000325470, "Subcon Forest Area"),
@@ -233,7 +233,7 @@ ahit_locations = {
"Subcon Forest - Long Tree Climb Chest": LocData(2000323734, "Subcon Forest Area",
required_hats=[HatType.DWELLER], paintings=2),
"Subcon Forest - Boss Arena Chest": LocData(2000323735, "Subcon Forest Area"),
"Subcon Forest - Boss Arena Chest": LocData(2000323735, "Subcon Forest Boss Arena"),
"Subcon Forest - Manor Rooftop": LocData(2000325466, "Subcon Forest Area",
hit_type=HitType.dweller_bell, paintings=1),
@@ -411,7 +411,7 @@ act_completions = {
"Act Completion (Mail Delivery Service)": LocData(2000312032, "Mail Delivery Service",
required_hats=[HatType.SPRINT]),
"Act Completion (Your Contract has Expired)": LocData(2000311390, "Your Contract has Expired",
"Act Completion (Your Contract has Expired)": LocData(2000311390, "Your Contract has Expired - Post Fight",
hit_type=HitType.umbrella),
"Act Completion (Time Rift - Pipe)": LocData(2000313069, "Time Rift - Pipe", hookshot=True),
@@ -976,7 +976,6 @@ event_locs = {
**snatcher_coins,
"HUMT Access": LocData(0, "Heating Up Mafia Town"),
"TOD Access": LocData(0, "Toilet of Doom"),
"YCHE Access": LocData(0, "Your Contract has Expired"),
"AFR Access": LocData(0, "Alpine Free Roam"),
"TIHS Access": LocData(0, "The Illness has Spread"),

View File

@@ -347,7 +347,7 @@ def create_regions(world: "HatInTimeWorld"):
sf_act3 = create_region_and_connect(world, "Toilet of Doom", "Subcon Forest - Act 3", subcon_forest)
sf_act4 = create_region_and_connect(world, "Queen Vanessa's Manor", "Subcon Forest - Act 4", subcon_forest)
sf_act5 = create_region_and_connect(world, "Mail Delivery Service", "Subcon Forest - Act 5", subcon_forest)
create_region_and_connect(world, "Your Contract has Expired", "Subcon Forest - Finale", subcon_forest)
sf_finale = create_region_and_connect(world, "Your Contract has Expired", "Subcon Forest - Finale", subcon_forest)
# ------------------------------------------- ALPINE SKYLINE ------------------------------------------ #
alpine_skyline = create_region_and_connect(world, "Alpine Skyline", "Telescope -> Alpine Skyline", spaceship)
@@ -386,11 +386,24 @@ def create_regions(world: "HatInTimeWorld"):
create_rift_connections(world, create_region(world, "Time Rift - Bazaar"))
sf_area: Region = create_region(world, "Subcon Forest Area")
sf_behind_boss_firewall: Region = create_region(world, "Subcon Forest Behind Boss Firewall")
sf_boss_arena: Region = create_region(world, "Subcon Forest Boss Arena")
sf_area.connect(sf_behind_boss_firewall, "SF Area -> SF Behind Boss Firewall")
sf_behind_boss_firewall.connect(sf_boss_arena, "SF Behind Boss Firewall -> SF Boss Arena")
sf_act1.connect(sf_area, "Subcon Forest Entrance CO")
sf_act2.connect(sf_area, "Subcon Forest Entrance SW")
sf_act3.connect(sf_area, "Subcon Forest Entrance TOD")
sf_act4.connect(sf_area, "Subcon Forest Entrance QVM")
sf_act5.connect(sf_area, "Subcon Forest Entrance MDS")
# YCHE puts the player directly in the boss arena, with no access to the rest of Subcon Forest by default.
sf_finale.connect(sf_boss_arena, "Subcon Forest Entrance YCHE")
# To support the Snatcher Hover expert logic for Act Completion (Your Contract has Expired), the act completion has
# to go in a separate region because the Snatcher Hover gives direct access to the Act Completion, but does not
# give access to the act itself.
sf_finale_post_fight: Region = create_region(world, "Your Contract has Expired - Post Fight")
# This connection must never have any rules placed on it because they will not be inherited when setting up act
# connections, only the rules for the entrances to the act and the rules for the Act Completion are inherited.
sf_finale.connect(sf_finale_post_fight, "YCHE -> YCHE - Post Fight")
create_rift_connections(world, create_region(world, "Time Rift - Sleepy Subcon"))
create_rift_connections(world, create_region(world, "Time Rift - Pipe"))
@@ -947,6 +960,16 @@ def get_shuffled_region(world: "HatInTimeWorld", region: str) -> str:
return name
def get_region_shuffled_to(world: "HatInTimeWorld", region: str) -> str:
if world.options.ActRandomizer:
original_ci: str = chapter_act_info[region]
shuffled_ci = world.act_connections[original_ci]
return next(act_name for act_name, ci in chapter_act_info.items()
if ci == shuffled_ci)
else:
return region
def get_region_location_count(world: "HatInTimeWorld", region_name: str, included_only: bool = True) -> int:
count = 0
region = world.multiworld.get_region(region_name, world.player)

View File

@@ -481,9 +481,8 @@ def set_hard_rules(world: "HatInTimeWorld"):
set_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player),
lambda state: has_paintings(state, world, 3))
# Cherry bridge over boss arena gap (painting still expected)
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
lambda state: has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player))
# Cherry bridge over boss arena gap
set_rule(world.get_entrance("SF Behind Boss Firewall -> SF Boss Arena"), lambda state: True)
set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player),
lambda state: has_paintings(state, world, 2, True))
@@ -566,27 +565,61 @@ def set_expert_rules(world: "HatInTimeWorld"):
lambda state: True)
# Expert: Cherry Hovering
subcon_area = world.multiworld.get_region("Subcon Forest Area", world.player)
yche = world.multiworld.get_region("Your Contract has Expired", world.player)
entrance = yche.connect(subcon_area, "Subcon Forest Entrance YCHE")
# Skipping the boss firewall is possible with a Cherry Hover.
set_rule(world.get_entrance("SF Area -> SF Behind Boss Firewall"),
lambda state: has_paintings(state, world, 1, True))
# The boss arena gap can be crossed in reverse with a Cherry Hover.
subcon_boss_arena = world.get_region("Subcon Forest Boss Arena")
subcon_behind_boss_firewall = world.get_region("Subcon Forest Behind Boss Firewall")
subcon_boss_arena.connect(subcon_behind_boss_firewall, "SF Boss Arena -> SF Behind Boss Firewall")
if world.options.NoPaintingSkips:
add_rule(entrance, lambda state: has_paintings(state, world, 1))
subcon_area = world.get_region("Subcon Forest Area")
# The boss firewall can be skipped in reverse with a Cherry Hover, but it is not possible to remove the boss
# firewall from reverse because the paintings to burn to remove the firewall are on the other side of the firewall.
# Therefore, a painting skip is required. The paintings could be burned by already having access to
# "Subcon Forest Area" through another entrance, but making a new connection to "Subcon Forest Area" in that case
# would be pointless.
if not world.options.NoPaintingSkips:
# The import cannot be done at the module-level because it would cause a circular import.
from .Regions import get_region_shuffled_to
subcon_behind_boss_firewall.connect(subcon_area, "SF Behind Boss Firewall -> SF Area")
# Because the Your Contract has Expired entrance can now reach "Subcon Forest Area", it needs to be connected to
# each of the Subcon Forest Time Rift entrances, like the other Subcon Forest Acts.
yche = world.get_region("Your Contract has Expired")
def connect_to_shuffled_act_at(original_act_name):
region_name = get_region_shuffled_to(world, original_act_name)
return yche.connect(world.get_region(region_name), f"{original_act_name} Portal - Entrance YCHE")
# Rules copied from `Rules.set_rift_rules()` with painting logic removed because painting skips must be
# available.
entrance = connect_to_shuffled_act_at("Time Rift - Pipe")
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Subcon Forest - Act 2"))
reg_act_connection(world, world.get_entrance("Subcon Forest - Act 2").connected_region, entrance)
entrance = connect_to_shuffled_act_at("Time Rift - Village")
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Subcon Forest - Act 4"))
reg_act_connection(world, world.get_entrance("Subcon Forest - Act 4").connected_region, entrance)
entrance = connect_to_shuffled_act_at("Time Rift - Sleepy Subcon")
add_rule(entrance, lambda state: has_relic_combo(state, world, "UFO"))
set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player),
lambda state: can_use_hookshot(state, world) and can_hit(state, world)
and has_paintings(state, world, 1, True))
# Set painting rules only. Skipping paintings is determined in has_paintings
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
lambda state: has_paintings(state, world, 1, True))
set_rule(world.multiworld.get_location("Subcon Forest - Magnet Badge Bush", world.player),
lambda state: has_paintings(state, world, 3, True))
# You can cherry hover to Snatcher's post-fight cutscene, which completes the level without having to fight him
subcon_area.connect(yche, "Snatcher Hover")
set_rule(world.multiworld.get_location("Act Completion (Your Contract has Expired)", world.player),
lambda state: True)
yche_post_fight = world.get_region("Your Contract has Expired - Post Fight")
subcon_area.connect(yche_post_fight, "Snatcher Hover")
# Cherry Hover from YCHE also works, so there are no requirements for the Act Completion.
set_rule(world.get_location("Act Completion (Your Contract has Expired)"), lambda state: True)
if world.is_dlc2():
# Expert: clear Rush Hour with nothing
@@ -681,12 +714,18 @@ def set_subcon_rules(world: "HatInTimeWorld"):
lambda state: can_use_hat(state, world, HatType.BREWING) or state.has("Umbrella", world.player)
or can_use_hat(state, world, HatType.DWELLER))
# You can't skip over the boss arena wall without cherry hover, so these two need to be set this way
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
lambda state: state.has("TOD Access", world.player) and can_use_hookshot(state, world)
and has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player))
# You can't skip over the boss arena wall without cherry hover.
set_rule(world.get_entrance("SF Area -> SF Behind Boss Firewall"),
lambda state: has_paintings(state, world, 1, False))
# The painting wall can't be skipped without cherry hover, which is Expert
# The hookpoints to cross the boss arena gap are only present in Toilet of Doom.
set_rule(world.get_entrance("SF Behind Boss Firewall -> SF Boss Arena"),
lambda state: state.has("TOD Access", world.player)
and can_use_hookshot(state, world))
# The Act Completion is in the Toilet of Doom region, so the same rules as passing the boss firewall and crossing
# the boss arena gap are required. "TOD Access" is implied from the region so does not need to be included in the
# rule.
set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player),
lambda state: can_use_hookshot(state, world) and can_hit(state, world)
and has_paintings(state, world, 1, False))

View File

@@ -515,10 +515,15 @@ def _populate_sprite_table():
logging.debug(f"Spritefile {file} could not be loaded as a valid sprite.")
with concurrent.futures.ThreadPoolExecutor() as pool:
for dir in [user_path('data', 'sprites', 'alttpr'), user_path('data', 'sprites', 'custom')]:
sprite_paths = [user_path('data', 'sprites', 'alttpr'), user_path('data', 'sprites', 'custom')]
for dir in [dir for dir in sprite_paths if os.path.isdir(dir)]:
for file in os.listdir(dir):
pool.submit(load_sprite_from_file, os.path.join(dir, file))
if "link" not in _sprite_table:
logging.info("Link sprite was not loaded. Loading link from base rom")
load_sprite_from_file(get_base_rom_path())
class Sprite():
sprite_size = 28672
@@ -554,6 +559,11 @@ class Sprite():
self.sprite = filedata[0x80000:0x87000]
self.palette = filedata[0xDD308:0xDD380]
self.glove_palette = filedata[0xDEDF5:0xDEDF9]
h = hashlib.md5()
h.update(filedata)
if h.hexdigest() == LTTPJPN10HASH:
self.name = "Link"
self.author_name = "Nintendo"
elif filedata.startswith(b'ZSPR'):
self.from_zspr(filedata, filename)
else:
@@ -1547,9 +1557,9 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_byte(0x18003B, 0x01 if world.map_shuffle[player] else 0x00) # maps showing crystals on overworld
# compasses showing dungeon count
if local_world.clock_mode or not world.dungeon_counters[player]:
if local_world.clock_mode or world.dungeon_counters[player] == 'off':
rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location
elif world.dungeon_counters[player] is True:
elif world.dungeon_counters[player] == 'on':
rom.write_byte(0x18003C, 0x02) # always on
elif world.compass_shuffle[player] or world.dungeon_counters[player] == 'pickup':
rom.write_byte(0x18003C, 0x01) # show on pickup

View File

@@ -1120,28 +1120,28 @@ def toss_junk_item(world, player):
raise Exception("Unable to find a junk item to toss to make room for a TR small key")
def set_trock_key_rules(world, player):
def set_trock_key_rules(multiworld, player):
# First set all relevant locked doors to impassible.
for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room) (South)', 'Turtle Rock (Pokey Room) (North)', 'Turtle Rock Big Key Door']:
set_rule(world.get_entrance(entrance, player), lambda state: False)
set_rule(multiworld.get_entrance(entrance, player), lambda state: False)
all_state = world.get_all_state(use_cache=False, allow_partial_entrances=True)
all_state = multiworld.get_all_state(use_cache=False, allow_partial_entrances=True)
all_state.reachable_regions[player] = set() # wipe reachable regions so that the locked doors actually work
all_state.stale[player] = True
# Check if each of the four main regions of the dungoen can be reached. The previous code section prevents key-costing moves within the dungeon.
can_reach_back = all_state.can_reach(world.get_region('Turtle Rock (Eye Bridge)', player))
can_reach_front = all_state.can_reach(world.get_region('Turtle Rock (Entrance)', player))
can_reach_big_chest = all_state.can_reach(world.get_region('Turtle Rock (Big Chest)', player))
can_reach_middle = all_state.can_reach(world.get_region('Turtle Rock (Second Section)', player))
can_reach_back = all_state.can_reach(multiworld.get_region('Turtle Rock (Eye Bridge)', player))
can_reach_front = all_state.can_reach(multiworld.get_region('Turtle Rock (Entrance)', player))
can_reach_big_chest = all_state.can_reach(multiworld.get_region('Turtle Rock (Big Chest)', player))
can_reach_middle = all_state.can_reach(multiworld.get_region('Turtle Rock (Second Section)', player))
# If you can't enter from the back, the door to the front of TR requires only 2 small keys if the big key is in one of these chests since 2 key doors are locked behind the big key door.
# If you can only enter from the middle, this includes all locations that can only be reached by exiting the front. This can include Laser Bridge and Crystaroller if the front and back connect via Dark DM Ledge!
front_locked_locations = {('Turtle Rock - Compass Chest', player), ('Turtle Rock - Roller Room - Left', player), ('Turtle Rock - Roller Room - Right', player)}
if can_reach_middle and not can_reach_back and not can_reach_front:
normal_regions = all_state.reachable_regions[player].copy()
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: True)
set_rule(world.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: True)
set_rule(multiworld.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: True)
set_rule(multiworld.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: True)
all_state.update_reachable_regions(player)
front_locked_regions = all_state.reachable_regions[player].difference(normal_regions)
front_locked_locations = set((location.name, player) for region in front_locked_regions for location in region.locations)
@@ -1151,37 +1151,38 @@ def set_trock_key_rules(world, player):
# Big key door requires the big key, obviously. We removed this rule in the previous section to flag front_locked_locations correctly,
# otherwise crystaroller room might not be properly marked as reachable through the back.
set_rule(world.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player))
set_rule(multiworld.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10) and can_bomb_or_bonk(state, player))
# No matter what, the key requirement for going from the middle to the bottom should be five keys.
set_rule(world.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
set_rule(multiworld.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
# Now we need to set rules based on which entrances we have access to. The most important point is whether we have back access. If we have back access, we
# might open all the locked doors in any order, so we need maximally restrictive rules.
if can_reach_back:
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 6) or location_item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
set_rule(world.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
set_rule(multiworld.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 6) or location_item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
set_rule(multiworld.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
set_rule(multiworld.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
set_rule(world.get_entrance('Turtle Rock (Pokey Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
set_rule(world.get_entrance('Turtle Rock Entrance to Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
set_rule(multiworld.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
set_rule(multiworld.get_entrance('Turtle Rock (Pokey Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
set_rule(multiworld.get_entrance('Turtle Rock Entrance to Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
else:
# Middle to front requires 3 keys if the back is locked by this door, otherwise 5
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3)
set_rule(multiworld.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3)
if item_name_in_location_names(state, 'Big Key (Turtle Rock)', player, front_locked_locations.union({('Turtle Rock - Pokey 1 Key Drop', player)}))
else state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
# Middle to front requires 4 keys if the back is locked by this door, otherwise 6
set_rule(world.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4)
set_rule(multiworld.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4)
if item_name_in_location_names(state, 'Big Key (Turtle Rock)', player, front_locked_locations)
else state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
# Front to middle requires 3 keys (if the middle is accessible then these doors can be avoided, otherwise no keys can be wasted)
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
set_rule(world.get_entrance('Turtle Rock (Pokey Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2))
set_rule(world.get_entrance('Turtle Rock Entrance to Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 1))
set_rule(multiworld.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
set_rule(multiworld.get_entrance('Turtle Rock (Pokey Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2))
set_rule(multiworld.get_entrance('Turtle Rock Entrance to Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 1))
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, tr_big_key_chest_keys_needed(state)))
set_rule(multiworld.get_location('Turtle Rock - Big Key Chest', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, tr_big_key_chest_keys_needed(state)))
def tr_big_key_chest_keys_needed(state):
# This function handles the key requirements for the TR Big Chest in the situations it having the Big Key should logically require 2 keys, small key
@@ -1194,30 +1195,30 @@ def set_trock_key_rules(world, player):
return 6
# If TR is only accessible from the middle, the big key must be further restricted to prevent softlock potential
if not can_reach_front and not world.small_key_shuffle[player]:
if not can_reach_front and not multiworld.small_key_shuffle[player]:
# Must not go in the Big Key Chest - only 1 other chest available and 2+ keys required for all other chests
forbid_item(world.get_location('Turtle Rock - Big Key Chest', player), 'Big Key (Turtle Rock)', player)
forbid_item(multiworld.get_location('Turtle Rock - Big Key Chest', player), 'Big Key (Turtle Rock)', player)
if not can_reach_big_chest:
# Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests
forbid_item(world.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player)
forbid_item(world.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player)
if world.accessibility[player] == 'full':
if world.big_key_shuffle[player] and can_reach_big_chest:
forbid_item(multiworld.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player)
forbid_item(multiworld.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player)
if multiworld.accessibility[player] == 'full':
if multiworld.big_key_shuffle[player] and can_reach_big_chest:
# Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first
for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest',
'Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop',
'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right']:
forbid_item(world.get_location(location, player), 'Big Key (Turtle Rock)', player)
forbid_item(multiworld.get_location(location, player), 'Big Key (Turtle Rock)', player)
else:
# A key is required in the Big Key Chest to prevent a possible softlock. Place an extra key to ensure 100% locations still works
item = item_factory('Small Key (Turtle Rock)', world.worlds[player])
location = world.get_location('Turtle Rock - Big Key Chest', player)
item = item_factory('Small Key (Turtle Rock)', multiworld.worlds[player])
location = multiworld.get_location('Turtle Rock - Big Key Chest', player)
location.place_locked_item(item)
toss_junk_item(world, player)
toss_junk_item(multiworld, player)
if world.accessibility[player] != 'full':
set_always_allow(world.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player
and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player)))
if multiworld.accessibility[player] != 'full':
set_always_allow(multiworld.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player
and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player)))
def set_big_bomb_rules(world, player):

View File

@@ -1,2 +1,2 @@
maseya-z3pr>=1.0.0rc1
xxtea>=3.0.0
xxtea>=3.0.0

View File

@@ -79,12 +79,12 @@ class TestInvertedTurtleRock(TestInverted):
["Turtle Rock - Crystaroller Room", False, [], ['Big Key (Turtle Rock)', 'Lamp']],
["Turtle Rock - Crystaroller Room", False, [], ['Magic Mirror', 'Cane of Somaria']],
["Turtle Rock - Crystaroller Room", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror']],
["Turtle Rock - Crystaroller Room", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria']],
["Turtle Rock - Crystaroller Room", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Cane of Somaria']],
["Turtle Rock - Crystaroller Room", True, ['Lamp', 'Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot', 'Cane of Somaria']],
@@ -97,9 +97,9 @@ class TestInvertedTurtleRock(TestInverted):
["Turtle Rock - Boss", False, [], ['Big Key (Turtle Rock)']],
["Turtle Rock - Boss", False, [], ['Magic Mirror', 'Lamp']],
["Turtle Rock - Boss", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']],
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Big Key (Turtle Rock)']],
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Big Key (Turtle Rock)']],
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Big Key (Turtle Rock)']],
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Flute', 'Magic Mirror', 'Moon Pearl', 'Hookshot', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']]
@@ -117,12 +117,12 @@ class TestInvertedTurtleRock(TestInverted):
[location, False, [], ['Magic Mirror', 'Cane of Somaria']],
[location, False, [], ['Magic Mirror', 'Lamp']],
[location, False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
[location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cane of Byrna']],
[location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cane of Byrna']],
[location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cape']],
[location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cape']],
[location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
[location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cane of Byrna']],
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cane of Byrna']],
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cape']],
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cape']],
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
# Mirroring into Eye Bridge does not require Cane of Somaria
[location, True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Byrna']],

View File

@@ -80,12 +80,12 @@ class TestInvertedTurtleRock(TestInvertedMinor):
["Turtle Rock - Crystaroller Room", False, [], ['Big Key (Turtle Rock)', 'Lamp']],
["Turtle Rock - Crystaroller Room", False, [], ['Magic Mirror', 'Cane of Somaria']],
["Turtle Rock - Crystaroller Room", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot']],
["Turtle Rock - Crystaroller Room", True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Flute', 'Progressive Glove', 'Progressive Glove', 'Magic Mirror']],
["Turtle Rock - Crystaroller Room", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Somaria']],
["Turtle Rock - Crystaroller Room", True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Moon Pearl', 'Hookshot', 'Cane of Somaria']],
["Turtle Rock - Crystaroller Room", True, ['Lamp', 'Moon Pearl', 'Flute', 'Magic Mirror', 'Hookshot', 'Cane of Somaria']],
@@ -98,9 +98,9 @@ class TestInvertedTurtleRock(TestInvertedMinor):
["Turtle Rock - Boss", False, [], ['Big Key (Turtle Rock)']],
["Turtle Rock - Boss", False, [], ['Magic Mirror', 'Lamp']],
["Turtle Rock - Boss", False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Small Key (Turtle Rock)']],
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Flute', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Big Key (Turtle Rock)']],
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Bottle', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Big Key (Turtle Rock)']],
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Progressive Sword', 'Cane of Somaria', 'Magic Upgrade (1/2)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)','Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Big Key (Turtle Rock)']],
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']],
["Turtle Rock - Boss", True, ['Ice Rod', 'Fire Rod', 'Flute', 'Magic Mirror', 'Moon Pearl', 'Hookshot', 'Hammer', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Big Key (Turtle Rock)']]
])
@@ -116,12 +116,12 @@ class TestInvertedTurtleRock(TestInvertedMinor):
[location, False, [], ['Magic Mirror', 'Cane of Somaria']],
[location, False, [], ['Magic Mirror', 'Lamp']],
[location, False, ['Small Key (Turtle Rock)', 'Small Key (Turtle Rock)'], ['Magic Mirror', 'Small Key (Turtle Rock)']],
[location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cane of Byrna']],
[location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cane of Byrna']],
[location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cape']],
[location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cape']],
[location, True, ['Big Key (Turtle Rock)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
[location, True, ['Big Key (Turtle Rock)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cane of Byrna']],
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cane of Byrna']],
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Cape']],
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Cape']],
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Flute', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Lamp', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
[location, True, ['Big Key (Turtle Rock)', 'Bomb Upgrade (50)', 'Lamp', 'Progressive Glove', 'Quake', 'Progressive Sword', 'Cane of Somaria', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Small Key (Turtle Rock)', 'Progressive Shield', 'Progressive Shield', 'Progressive Shield']],
# Mirroring into Eye Bridge does not require Cane of Somaria
[location, True, ['Lamp', 'Magic Mirror', 'Progressive Glove', 'Progressive Glove', 'Cane of Byrna']],

View File

@@ -102,7 +102,7 @@ class TestDungeons(TestInvertedOWG):
["Turtle Rock - Chain Chomps", True, ['Progressive Sword', 'Progressive Sword', 'Pegasus Boots']],
["Turtle Rock - Crystaroller Room", False, []],
["Turtle Rock - Crystaroller Room", True, ['Pegasus Boots', 'Magic Mirror', 'Moon Pearl', 'Big Key (Turtle Rock)']],
["Turtle Rock - Crystaroller Room", True, ['Pegasus Boots', 'Magic Mirror', 'Moon Pearl', 'Big Key (Turtle Rock)', 'Bomb Upgrade (50)']],
["Turtle Rock - Crystaroller Room", True, ['Pegasus Boots', 'Magic Mirror', 'Moon Pearl', 'Lamp', 'Cane of Somaria']],
["Ganons Tower - Hope Room - Left", False, []],

View File

@@ -120,8 +120,8 @@ class TestDungeons(TestVanillaOWG):
#todo: does clip require sword?
#["Turtle Rock - Crystaroller Room", True, ['Moon Pearl', 'Pegasus Boots', 'Big Key (Turtle Rock)']],
["Turtle Rock - Crystaroller Room", True, ['Moon Pearl', 'Pegasus Boots', 'Big Key (Turtle Rock)', 'Progressive Sword']],
["Turtle Rock - Crystaroller Room", True, ['Moon Pearl', 'Pegasus Boots', 'Big Key (Turtle Rock)', 'Hookshot']],
["Turtle Rock - Crystaroller Room", True, ['Pegasus Boots', 'Magic Mirror', 'Big Key (Turtle Rock)']],
["Turtle Rock - Crystaroller Room", True, ['Moon Pearl', 'Pegasus Boots', 'Big Key (Turtle Rock)', 'Hookshot', 'Bomb Upgrade (50)']],
["Turtle Rock - Crystaroller Room", True, ['Pegasus Boots', 'Magic Mirror', 'Big Key (Turtle Rock)', 'Bomb Upgrade (50)']],
["Ganons Tower - Hope Room - Left", False, []],
["Ganons Tower - Hope Room - Left", False, ['Moon Pearl', 'Crystal 1']],

View File

@@ -89,7 +89,7 @@ location_names: Dict[str, str] = {
"RESCUED_CHERUB_15": "DC: Top of elevator Child of Moonlight",
"Lady[D01Z05S22]": "DC: Lady of the Six Sorrows, from MD",
"QI75": "DC: Chalice room",
"Sword[D01Z05S24]": "DC: Mea culpa altar",
"Sword[D01Z05S24]": "DC: Mea Culpa altar",
"CO44": "DC: Elevator shaft ledge",
"RESCUED_CHERUB_22": "DC: Elevator shaft Child of Moonlight",
"Lady[D01Z05S26]": "DC: Lady of the Six Sorrows, elevator shaft",

View File

@@ -67,7 +67,8 @@ class BlasphemousWorld(World):
def generate_early(self):
if not self.options.starting_location.randomized:
if self.options.starting_location == "mourning_havoc" and self.options.difficulty < 2:
if (self.options.starting_location == "knot_of_words" or self.options.starting_location == "rooftops" \
or self.options.starting_location == "mourning_havoc") and self.options.difficulty < 2:
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
f"{self.options.starting_location} cannot be chosen if Difficulty is lower than Hard.")
@@ -83,6 +84,8 @@ class BlasphemousWorld(World):
locations: List[int] = [ 0, 1, 2, 3, 4, 5, 6 ]
if self.options.difficulty < 2:
locations.remove(4)
locations.remove(5)
locations.remove(6)
if self.options.dash_shuffle:

View File

@@ -85,20 +85,7 @@ class TestGrievanceHard(BlasphemousTestBase):
}
class TestKnotOfWordsEasy(BlasphemousTestBase):
options = {
"starting_location": "knot_of_words",
"difficulty": "easy"
}
class TestKnotOfWordsNormal(BlasphemousTestBase):
options = {
"starting_location": "knot_of_words",
"difficulty": "normal"
}
# knot of the three words, rooftops, and mourning and havoc can't be selected on easy or normal. hard only
class TestKnotOfWordsHard(BlasphemousTestBase):
options = {
"starting_location": "knot_of_words",
@@ -106,20 +93,6 @@ class TestKnotOfWordsHard(BlasphemousTestBase):
}
class TestRooftopsEasy(BlasphemousTestBase):
options = {
"starting_location": "rooftops",
"difficulty": "easy"
}
class TestRooftopsNormal(BlasphemousTestBase):
options = {
"starting_location": "rooftops",
"difficulty": "normal"
}
class TestRooftopsHard(BlasphemousTestBase):
options = {
"starting_location": "rooftops",
@@ -127,7 +100,6 @@ class TestRooftopsHard(BlasphemousTestBase):
}
# mourning and havoc can't be selected on easy or normal. hard only
class TestMourningHavocHard(BlasphemousTestBase):
options = {
"starting_location": "mourning_havoc",

View File

@@ -1,6 +1,31 @@
# Celeste 64 - Changelog
## v1.3
### Features:
- New optional Location Checks
- Checkpointsanity
- Hair Color
- Allows for setting of Maddy's hair color in each of No Dash, One Dash, Two Dash, and Feather states
- Other Player Ghosts
- A game config option allows you to see ghosts of other Celeste 64 players in the multiworld
### Quality of Life:
- Checkpoint Warping
- Received Checkpoint items allow for warping to their respective checkpoint
- These items are on their respective checkpoint location if Checkpointsanity is disabled
- Logic accounts for being able to warp to otherwise inaccessible areas
- Checkpoints are a possible option for a starting item on Standard Logic + Move Shuffle + Checkpointsanity
- New Options toggle to enable/disable background input
### Bug Fixes:
- Traffic Blocks now correctly appear disabled within Cassettes
## v1.2
### Features:

View File

@@ -39,6 +39,22 @@ move_item_data_table: Dict[str, Celeste64ItemData] = {
ItemName.climb: Celeste64ItemData(celeste_64_base_id + 0xD, ItemClassification.progression),
}
item_data_table: Dict[str, Celeste64ItemData] = {**collectable_item_data_table, **unlockable_item_data_table, **move_item_data_table}
checkpoint_item_data_table: Dict[str, Celeste64ItemData] = {
ItemName.checkpoint_1: Celeste64ItemData(celeste_64_base_id + 0x20, ItemClassification.progression),
ItemName.checkpoint_2: Celeste64ItemData(celeste_64_base_id + 0x21, ItemClassification.progression),
ItemName.checkpoint_3: Celeste64ItemData(celeste_64_base_id + 0x22, ItemClassification.progression),
ItemName.checkpoint_4: Celeste64ItemData(celeste_64_base_id + 0x23, ItemClassification.progression),
ItemName.checkpoint_5: Celeste64ItemData(celeste_64_base_id + 0x24, ItemClassification.progression),
ItemName.checkpoint_6: Celeste64ItemData(celeste_64_base_id + 0x25, ItemClassification.progression),
ItemName.checkpoint_7: Celeste64ItemData(celeste_64_base_id + 0x26, ItemClassification.progression),
ItemName.checkpoint_8: Celeste64ItemData(celeste_64_base_id + 0x27, ItemClassification.progression),
ItemName.checkpoint_9: Celeste64ItemData(celeste_64_base_id + 0x28, ItemClassification.progression),
ItemName.checkpoint_10: Celeste64ItemData(celeste_64_base_id + 0x29, ItemClassification.progression),
}
item_data_table: Dict[str, Celeste64ItemData] = {**collectable_item_data_table,
**unlockable_item_data_table,
**move_item_data_table,
**checkpoint_item_data_table}
item_table = {name: data.code for name, data in item_data_table.items() if data.code is not None}

View File

@@ -1,7 +1,7 @@
from typing import Dict, NamedTuple, Optional
from BaseClasses import Location
from .Names import LocationName
from .Names import LocationName, RegionName
celeste_64_base_id: int = 0xCA0000
@@ -17,66 +17,80 @@ class Celeste64LocationData(NamedTuple):
strawberry_location_data_table: Dict[str, Celeste64LocationData] = {
LocationName.strawberry_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x00),
LocationName.strawberry_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x01),
LocationName.strawberry_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x02),
LocationName.strawberry_4: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x03),
LocationName.strawberry_5: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x04),
LocationName.strawberry_6: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x05),
LocationName.strawberry_7: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x06),
LocationName.strawberry_8: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x07),
LocationName.strawberry_9: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x08),
LocationName.strawberry_10: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x09),
LocationName.strawberry_11: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0A),
LocationName.strawberry_12: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0B),
LocationName.strawberry_13: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0C),
LocationName.strawberry_14: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0D),
LocationName.strawberry_15: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0E),
LocationName.strawberry_16: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0F),
LocationName.strawberry_17: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x10),
LocationName.strawberry_18: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x11),
LocationName.strawberry_19: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x12),
LocationName.strawberry_20: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x13),
LocationName.strawberry_21: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x14),
LocationName.strawberry_22: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x15),
LocationName.strawberry_23: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x16),
LocationName.strawberry_24: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x17),
LocationName.strawberry_25: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x18),
LocationName.strawberry_26: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x19),
LocationName.strawberry_27: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1A),
LocationName.strawberry_28: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1B),
LocationName.strawberry_29: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1C),
LocationName.strawberry_30: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1D),
LocationName.strawberry_1: Celeste64LocationData(RegionName.intro_islands, celeste_64_base_id + 0x00),
LocationName.strawberry_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x01),
LocationName.strawberry_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x02),
LocationName.strawberry_4: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x03),
LocationName.strawberry_5: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x04),
LocationName.strawberry_6: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x05),
LocationName.strawberry_7: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x06),
LocationName.strawberry_8: Celeste64LocationData(RegionName.nw_girders_island, celeste_64_base_id + 0x07),
LocationName.strawberry_9: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x08),
LocationName.strawberry_10: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x09),
LocationName.strawberry_11: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x0A),
LocationName.strawberry_12: Celeste64LocationData(RegionName.badeline_tower_lower, celeste_64_base_id + 0x0B),
LocationName.strawberry_13: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x0C),
LocationName.strawberry_14: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x0D),
LocationName.strawberry_15: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x0E),
LocationName.strawberry_16: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x0F),
LocationName.strawberry_17: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x10),
LocationName.strawberry_18: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x11),
LocationName.strawberry_19: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x12),
LocationName.strawberry_20: Celeste64LocationData(RegionName.badeline_tower_lower, celeste_64_base_id + 0x13),
LocationName.strawberry_21: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x14),
LocationName.strawberry_22: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x15),
LocationName.strawberry_23: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x16),
LocationName.strawberry_24: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x17),
LocationName.strawberry_25: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x18),
LocationName.strawberry_26: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x19),
LocationName.strawberry_27: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x1A),
LocationName.strawberry_28: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x1B),
LocationName.strawberry_29: Celeste64LocationData(RegionName.badeline_tower_upper, celeste_64_base_id + 0x1C),
LocationName.strawberry_30: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x1D),
}
friend_location_data_table: Dict[str, Celeste64LocationData] = {
LocationName.granny_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x00),
LocationName.granny_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x01),
LocationName.granny_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x02),
LocationName.theo_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x03),
LocationName.theo_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x04),
LocationName.theo_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x05),
LocationName.badeline_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x06),
LocationName.badeline_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x07),
LocationName.badeline_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x08),
LocationName.granny_1: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x00),
LocationName.granny_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x01),
LocationName.granny_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x02),
LocationName.theo_1: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x03),
LocationName.theo_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x04),
LocationName.theo_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x05),
LocationName.badeline_1: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x100 + 0x06),
LocationName.badeline_2: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x100 + 0x07),
LocationName.badeline_3: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x100 + 0x08),
}
sign_location_data_table: Dict[str, Celeste64LocationData] = {
LocationName.sign_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x00),
LocationName.sign_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x01),
LocationName.sign_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x02),
LocationName.sign_4: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x03),
LocationName.sign_5: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x04),
LocationName.sign_1: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x200 + 0x00),
LocationName.sign_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x200 + 0x01),
LocationName.sign_3: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x200 + 0x02),
LocationName.sign_4: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x200 + 0x03),
LocationName.sign_5: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x200 + 0x04),
}
car_location_data_table: Dict[str, Celeste64LocationData] = {
LocationName.car_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x300 + 0x00),
LocationName.car_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x300 + 0x01),
LocationName.car_1: Celeste64LocationData(RegionName.intro_islands, celeste_64_base_id + 0x300 + 0x00),
LocationName.car_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x300 + 0x01),
}
checkpoint_location_data_table: Dict[str, Celeste64LocationData] = {
LocationName.checkpoint_1: Celeste64LocationData(RegionName.intro_islands, celeste_64_base_id + 0x400 + 0x00),
LocationName.checkpoint_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x400 + 0x01),
LocationName.checkpoint_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x400 + 0x02),
LocationName.checkpoint_4: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x400 + 0x03),
LocationName.checkpoint_5: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x400 + 0x04),
LocationName.checkpoint_6: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x400 + 0x05),
LocationName.checkpoint_7: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x400 + 0x06),
LocationName.checkpoint_8: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x400 + 0x07),
LocationName.checkpoint_9: Celeste64LocationData(RegionName.badeline_tower_upper, celeste_64_base_id + 0x400 + 0x08),
LocationName.checkpoint_10: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x400 + 0x09),
}
location_data_table: Dict[str, Celeste64LocationData] = {**strawberry_location_data_table,
**friend_location_data_table,
**sign_location_data_table,
**car_location_data_table}
**car_location_data_table,
**checkpoint_location_data_table}
location_table = {name: data.address for name, data in location_data_table.items() if data.address is not None}

View File

@@ -15,3 +15,18 @@ ground_dash = "Ground Dash"
air_dash = "Air Dash"
skid_jump = "Skid Jump"
climb = "Climb"
# Checkpoint Items
checkpoint_1 = "Intro Checkpoint"
checkpoint_2 = "Granny Checkpoint"
checkpoint_3 = "South-East Tower Checkpoint"
checkpoint_4 = "Climb Sign Checkpoint"
checkpoint_5 = "Freeway Checkpoint"
checkpoint_6 = "Freeway Feather Checkpoint"
checkpoint_7 = "Feather Maze Checkpoint"
checkpoint_8 = "Double Dash House Checkpoint"
checkpoint_9 = "Badeline Tower Checkpoint"
checkpoint_10 = "Badeline Island Checkpoint"
# Item used for logic definitions that are not possible with the given options
cannot_access = "CANNOT ACCESS"

View File

@@ -10,7 +10,7 @@ strawberry_8 = "Traffic Block Strawberry"
strawberry_9 = "South-West Dash Refills Strawberry"
strawberry_10 = "South-East Tower Side Strawberry"
strawberry_11 = "Girders Strawberry"
strawberry_12 = "North-East Tower Bottom Strawberry"
strawberry_12 = "Badeline Tower Bottom Strawberry"
strawberry_13 = "Breakable Blocks Strawberry"
strawberry_14 = "Feather Maze Strawberry"
strawberry_15 = "Feather Chain Strawberry"
@@ -18,7 +18,7 @@ strawberry_16 = "Feather Hidden Strawberry"
strawberry_17 = "Double Dash Puzzle Strawberry"
strawberry_18 = "Double Dash Spike Climb Strawberry"
strawberry_19 = "Double Dash Spring Strawberry"
strawberry_20 = "North-East Tower Breakable Bottom Strawberry"
strawberry_20 = "Badeline Tower Breakable Bottom Strawberry"
strawberry_21 = "Theo Tower Lower Cassette Strawberry"
strawberry_22 = "Theo Tower Upper Cassette Strawberry"
strawberry_23 = "South End of Bridge Cassette Strawberry"
@@ -27,8 +27,8 @@ strawberry_25 = "Cassette Hidden in the House Strawberry"
strawberry_26 = "North End of Bridge Cassette Strawberry"
strawberry_27 = "Distant Feather Cassette Strawberry"
strawberry_28 = "Feather Arches Cassette Strawberry"
strawberry_29 = "North-East Tower Cassette Strawberry"
strawberry_30 = "Badeline Cassette Strawberry"
strawberry_29 = "Badeline Tower Cassette Strawberry"
strawberry_30 = "Badeline Island Cassette Strawberry"
# Friend Locations
granny_1 = "Granny Conversation 1"
@@ -51,3 +51,15 @@ sign_5 = "Credits Sign"
# Car Locations
car_1 = "Intro Car"
car_2 = "Secret Car"
# Checkpoint Locations
checkpoint_1 = "Intro Checkpoint"
checkpoint_2 = "Granny Checkpoint"
checkpoint_3 = "South-East Tower Checkpoint"
checkpoint_4 = "Climb Sign Checkpoint"
checkpoint_5 = "Freeway Checkpoint"
checkpoint_6 = "Freeway Feather Checkpoint"
checkpoint_7 = "Feather Maze Checkpoint"
checkpoint_8 = "Double Dash House Checkpoint"
checkpoint_9 = "Badeline Tower Checkpoint"
checkpoint_10 = "Badeline Island Checkpoint"

View File

@@ -0,0 +1,13 @@
# Level Base Regions
forsaken_city = "Forsaken City"
# Forsaken City Regions
intro_islands = "Intro Islands"
granny_island = "Granny Island"
highway_island = "Freeway Island"
nw_girders_island = "North-West Girders Island"
ne_feathers_island = "North-East Feathers Island"
se_house_island = "South-East House Island"
badeline_tower_lower = "Badeline Tower Lower"
badeline_tower_upper = "Badeline Tower Upper"
badeline_island = "Badeline Island"

View File

@@ -1,6 +1,8 @@
from dataclasses import dataclass
import random
from Options import Choice, Range, Toggle, DeathLink, OptionGroup, PerGameCommonOptions
from Options import Choice, TextChoice, Range, Toggle, DeathLink, OptionGroup, PerGameCommonOptions, OptionError
from worlds.AutoWorld import World
class DeathLinkAmnesty(Range):
@@ -18,7 +20,7 @@ class TotalStrawberries(Range):
"""
display_name = "Total Strawberries"
range_start = 0
range_end = 46
range_end = 55
default = 20
class StrawberriesRequiredPercentage(Range):
@@ -73,6 +75,93 @@ class Carsanity(Toggle):
"""
display_name = "Carsanity"
class Checkpointsanity(Toggle):
"""
Whether activating Checkpoints grants location checks
Activating this will also shuffle items into the pool which allow usage and warping to each Checkpoint
"""
display_name = "Checkpointsanity"
class ColorChoice(TextChoice):
option_strawberry = 0xDB2C00
option_empty = 0x6EC0FF
option_double = 0xFA91FF
option_golden = 0xF2D450
option_baddy = 0x9B3FB5
option_fire_red = 0xFF0000
option_maroon = 0x800000
option_salmon = 0xFF3A65
option_orange = 0xD86E0A
option_lime_green = 0x8DF920
option_bright_green = 0x0DAF05
option_forest_green = 0x132818
option_royal_blue = 0x0036BF
option_brown = 0xB78726
option_black = 0x000000
option_white = 0xFFFFFF
option_grey = 0x808080
option_any_color = -1
@classmethod
def from_text(cls, text: str) -> Choice:
text = text.lower()
if text == "random":
choice_list = list(cls.name_lookup)
choice_list.remove(cls.option_any_color)
return cls(random.choice(choice_list))
return super().from_text(text)
class MadelineOneDashHairColor(ColorChoice):
"""
What color Madeline's hair is when she has one dash
The `any_color` option will choose a fully random color
A custom color entry may be supplied as a 6-character RGB hex color code
e.g. F542C8
"""
display_name = "Madeline One Dash Hair Color"
default = ColorChoice.option_strawberry
class MadelineTwoDashHairColor(ColorChoice):
"""
What color Madeline's hair is when she has two dashes
The `any_color` option will choose a fully random color
A custom color entry may be supplied as a 6-character RGB hex color code
e.g. F542C8
"""
display_name = "Madeline Two Dash Hair Color"
default = ColorChoice.option_double
class MadelineNoDashHairColor(ColorChoice):
"""
What color Madeline's hair is when she has no dashes
The `any_color` option will choose a fully random color
A custom color entry may be supplied as a 6-character RGB hex color code
e.g. F542C8
"""
display_name = "Madeline No Dash Hair Color"
default = ColorChoice.option_empty
class MadelineFeatherHairColor(ColorChoice):
"""
What color Madeline's hair is when she has a feather
The `any_color` option will choose a fully random color
A custom color entry may be supplied as a 6-character RGB hex color code
e.g. F542C8
"""
display_name = "Madeline Feather Hair Color"
default = ColorChoice.option_golden
class BadelineChaserSource(Choice):
"""
@@ -119,6 +208,13 @@ celeste_64_option_groups = [
Friendsanity,
Signsanity,
Carsanity,
Checkpointsanity,
]),
OptionGroup("Aesthetic Options", [
MadelineOneDashHairColor,
MadelineTwoDashHairColor,
MadelineNoDashHairColor,
MadelineFeatherHairColor,
]),
OptionGroup("Badeline Chasers", [
BadelineChaserSource,
@@ -142,7 +238,68 @@ class Celeste64Options(PerGameCommonOptions):
friendsanity: Friendsanity
signsanity: Signsanity
carsanity: Carsanity
checkpointsanity: Checkpointsanity
madeline_one_dash_hair_color: MadelineOneDashHairColor
madeline_two_dash_hair_color: MadelineTwoDashHairColor
madeline_no_dash_hair_color: MadelineNoDashHairColor
madeline_feather_hair_color: MadelineFeatherHairColor
badeline_chaser_source: BadelineChaserSource
badeline_chaser_frequency: BadelineChaserFrequency
badeline_chaser_speed: BadelineChaserSpeed
def resolve_options(world: World):
# One Dash Hair
if isinstance(world.options.madeline_one_dash_hair_color.value, str):
try:
world.madeline_one_dash_hair_color = int(world.options.madeline_one_dash_hair_color.value.strip("#")[:6], 16)
except ValueError:
raise OptionError(f"Invalid input for option `madeline_one_dash_hair_color`:"
f"{world.options.madeline_one_dash_hair_color.value} for "
f"{world.player_name}")
elif world.options.madeline_one_dash_hair_color.value == ColorChoice.option_any_color:
world.madeline_one_dash_hair_color = world.random.randint(0, 0xFFFFFF)
else:
world.madeline_one_dash_hair_color = world.options.madeline_one_dash_hair_color.value
# Two Dash Hair
if isinstance(world.options.madeline_two_dash_hair_color.value, str):
try:
world.madeline_two_dash_hair_color = int(world.options.madeline_two_dash_hair_color.value.strip("#")[:6], 16)
except ValueError:
raise OptionError(f"Invalid input for option `madeline_two_dash_hair_color`:"
f"{world.options.madeline_two_dash_hair_color.value} for "
f"{world.player_name}")
elif world.options.madeline_two_dash_hair_color.value == ColorChoice.option_any_color:
world.madeline_two_dash_hair_color = world.random.randint(0, 0xFFFFFF)
else:
world.madeline_two_dash_hair_color = world.options.madeline_two_dash_hair_color.value
# No Dash Hair
if isinstance(world.options.madeline_no_dash_hair_color.value, str):
try:
world.madeline_no_dash_hair_color = int(world.options.madeline_no_dash_hair_color.value.strip("#")[:6], 16)
except ValueError:
raise OptionError(f"Invalid input for option `madeline_no_dash_hair_color`:"
f"{world.options.madeline_no_dash_hair_color.value} for "
f"{world.player_name}")
elif world.options.madeline_no_dash_hair_color.value == ColorChoice.option_any_color:
world.madeline_no_dash_hair_color = world.random.randint(0, 0xFFFFFF)
else:
world.madeline_no_dash_hair_color = world.options.madeline_no_dash_hair_color.value
# Feather Hair
if isinstance(world.options.madeline_feather_hair_color.value, str):
try:
world.madeline_feather_hair_color = int(world.options.madeline_feather_hair_color.value.strip("#")[:6], 16)
except ValueError:
raise OptionError(f"Invalid input for option `madeline_feather_hair_color`:"
f"{world.options.madeline_feather_hair_color.value} for "
f"{world.player_name}")
elif world.options.madeline_feather_hair_color.value == ColorChoice.option_any_color:
world.madeline_feather_hair_color = world.random.randint(0, 0xFFFFFF)
else:
world.madeline_feather_hair_color = world.options.madeline_feather_hair_color.value

View File

@@ -1,11 +1,23 @@
from typing import Dict, List, NamedTuple
from .Names import RegionName
class Celeste64RegionData(NamedTuple):
connecting_regions: List[str] = []
region_data_table: Dict[str, Celeste64RegionData] = {
"Menu": Celeste64RegionData(["Forsaken City"]),
"Forsaken City": Celeste64RegionData(),
"Menu": Celeste64RegionData([RegionName.forsaken_city]),
RegionName.forsaken_city: Celeste64RegionData([RegionName.intro_islands, RegionName.granny_island, RegionName.highway_island, RegionName.ne_feathers_island, RegionName.se_house_island, RegionName.badeline_tower_upper, RegionName.badeline_island]),
RegionName.intro_islands: Celeste64RegionData([RegionName.granny_island]),
RegionName.granny_island: Celeste64RegionData([RegionName.highway_island, RegionName.nw_girders_island, RegionName.badeline_tower_lower, RegionName.se_house_island]),
RegionName.highway_island: Celeste64RegionData([RegionName.granny_island, RegionName.ne_feathers_island, RegionName.nw_girders_island]),
RegionName.nw_girders_island: Celeste64RegionData([RegionName.highway_island]),
RegionName.ne_feathers_island: Celeste64RegionData([RegionName.se_house_island, RegionName.highway_island, RegionName.badeline_tower_lower, RegionName.badeline_tower_upper]),
RegionName.se_house_island: Celeste64RegionData([RegionName.ne_feathers_island, RegionName.granny_island, RegionName.badeline_tower_lower]),
RegionName.badeline_tower_lower: Celeste64RegionData([RegionName.se_house_island, RegionName.ne_feathers_island, RegionName.granny_island, RegionName.badeline_tower_upper]),
RegionName.badeline_tower_upper: Celeste64RegionData([RegionName.badeline_island, RegionName.badeline_tower_lower, RegionName.se_house_island, RegionName.ne_feathers_island, RegionName.granny_island]),
RegionName.badeline_island: Celeste64RegionData([RegionName.badeline_tower_upper, RegionName.granny_island, RegionName.highway_island]),
}

View File

@@ -1,265 +1,85 @@
from typing import Dict, List
from typing import Dict, List, Tuple, Callable
from BaseClasses import CollectionState
from BaseClasses import CollectionState, Region
from worlds.generic.Rules import set_rule
from . import Celeste64World
from .Names import ItemName, LocationName
from .Names import ItemName, LocationName, RegionName
def set_rules(world: Celeste64World):
if world.options.logic_difficulty == "standard":
if world.options.move_shuffle:
world.active_logic_mapping = location_standard_moves_logic
else:
world.active_logic_mapping = location_standard_logic
world.active_logic_mapping = location_standard_moves_logic
world.active_region_logic_mapping = region_standard_moves_logic
else:
if world.options.move_shuffle:
world.active_logic_mapping = location_hard_moves_logic
else:
world.active_logic_mapping = location_hard_logic
world.active_logic_mapping = location_hard_moves_logic
world.active_region_logic_mapping = region_hard_moves_logic
for location in world.multiworld.get_locations(world.player):
set_rule(location, lambda state, location=location: location_rule(state, world, location.name))
if world.options.logic_difficulty == "standard":
if world.options.move_shuffle:
world.goal_logic_mapping = goal_standard_moves_logic
else:
world.goal_logic_mapping = goal_standard_logic
else:
if world.options.move_shuffle:
world.goal_logic_mapping = goal_hard_moves_logic
else:
world.goal_logic_mapping = goal_hard_logic
# Completion condition.
world.multiworld.completion_condition[world.player] = lambda state: goal_rule(state, world)
goal_standard_logic: List[List[str]] = [[ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.double_dash_refill]]
goal_hard_logic: List[List[str]] = [[]]
goal_standard_moves_logic: List[List[str]] = [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]]
goal_hard_moves_logic: List[List[str]] = [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]]
location_standard_logic: Dict[str, List[List[str]]] = {
LocationName.strawberry_4: [[ItemName.traffic_block, ItemName.breakables]],
LocationName.strawberry_6: [[ItemName.dash_refill],
[ItemName.traffic_block]],
LocationName.strawberry_7: [[ItemName.dash_refill],
[ItemName.traffic_block]],
LocationName.strawberry_8: [[ItemName.traffic_block]],
LocationName.strawberry_9: [[ItemName.dash_refill]],
LocationName.strawberry_11: [[ItemName.dash_refill],
[ItemName.traffic_block]],
LocationName.strawberry_12: [[ItemName.dash_refill, ItemName.double_dash_refill],
[ItemName.traffic_block, ItemName.double_dash_refill]],
LocationName.strawberry_13: [[ItemName.dash_refill, ItemName.breakables],
[ItemName.traffic_block, ItemName.breakables]],
LocationName.strawberry_14: [[ItemName.dash_refill, ItemName.feather],
[ItemName.traffic_block, ItemName.feather]],
LocationName.strawberry_15: [[ItemName.dash_refill, ItemName.feather],
[ItemName.traffic_block, ItemName.feather]],
LocationName.strawberry_16: [[ItemName.dash_refill, ItemName.feather],
[ItemName.traffic_block, ItemName.feather]],
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block]],
LocationName.strawberry_18: [[ItemName.dash_refill, ItemName.double_dash_refill],
[ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill]],
LocationName.strawberry_19: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.spring],
[ItemName.traffic_block, ItemName.double_dash_refill, ItemName.feather, ItemName.spring]],
LocationName.strawberry_20: [[ItemName.dash_refill, ItemName.feather, ItemName.breakables],
[ItemName.traffic_block, ItemName.feather, ItemName.breakables]],
LocationName.strawberry_21: [[ItemName.cassette, ItemName.traffic_block, ItemName.breakables]],
LocationName.strawberry_22: [[ItemName.cassette, ItemName.dash_refill, ItemName.breakables]],
LocationName.strawberry_23: [[ItemName.cassette, ItemName.dash_refill, ItemName.coin],
[ItemName.cassette, ItemName.traffic_block, ItemName.coin]],
LocationName.strawberry_24: [[ItemName.cassette, ItemName.dash_refill, ItemName.traffic_block]],
LocationName.strawberry_25: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill],
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill]],
LocationName.strawberry_26: [[ItemName.cassette, ItemName.dash_refill],
[ItemName.cassette, ItemName.traffic_block]],
LocationName.strawberry_27: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin],
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin]],
LocationName.strawberry_28: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin],
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin]],
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin]],
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.spring, ItemName.breakables]],
LocationName.theo_1: [[ItemName.traffic_block, ItemName.breakables]],
LocationName.theo_2: [[ItemName.traffic_block, ItemName.breakables]],
LocationName.theo_3: [[ItemName.traffic_block, ItemName.breakables]],
LocationName.badeline_1: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
LocationName.badeline_2: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
LocationName.badeline_3: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
LocationName.sign_2: [[ItemName.breakables]],
LocationName.sign_3: [[ItemName.dash_refill],
[ItemName.traffic_block]],
LocationName.sign_4: [[ItemName.dash_refill, ItemName.double_dash_refill],
[ItemName.dash_refill, ItemName.feather],
[ItemName.traffic_block, ItemName.feather]],
LocationName.sign_5: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
LocationName.car_2: [[ItemName.breakables]],
}
location_hard_logic: Dict[str, List[List[str]]] = {
LocationName.strawberry_13: [[ItemName.breakables]],
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.traffic_block]],
LocationName.strawberry_20: [[ItemName.breakables]],
LocationName.strawberry_21: [[ItemName.cassette, ItemName.traffic_block, ItemName.breakables]],
LocationName.strawberry_22: [[ItemName.cassette]],
LocationName.strawberry_23: [[ItemName.cassette, ItemName.coin]],
LocationName.strawberry_24: [[ItemName.cassette]],
LocationName.strawberry_25: [[ItemName.cassette, ItemName.double_dash_refill]],
LocationName.strawberry_26: [[ItemName.cassette]],
LocationName.strawberry_27: [[ItemName.cassette]],
LocationName.strawberry_28: [[ItemName.cassette, ItemName.feather]],
LocationName.strawberry_29: [[ItemName.cassette]],
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables]],
LocationName.sign_2: [[ItemName.breakables]],
LocationName.car_2: [[ItemName.breakables]],
}
location_standard_moves_logic: Dict[str, List[List[str]]] = {
LocationName.strawberry_1: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump],
[ItemName.climb]],
LocationName.strawberry_2: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump],
[ItemName.climb]],
LocationName.strawberry_2: [[ItemName.air_dash],
[ItemName.skid_jump]],
LocationName.strawberry_3: [[ItemName.air_dash],
[ItemName.skid_jump]],
LocationName.strawberry_4: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_5: [[ItemName.air_dash]],
LocationName.strawberry_6: [[ItemName.dash_refill, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash],
[ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.skid_jump],
[ItemName.traffic_block, ItemName.climb]],
LocationName.strawberry_7: [[ItemName.dash_refill, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash],
[ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.skid_jump],
[ItemName.traffic_block, ItemName.climb]],
LocationName.strawberry_8: [[ItemName.traffic_block, ItemName.ground_dash],
[ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.skid_jump],
[ItemName.traffic_block, ItemName.climb]],
LocationName.strawberry_9: [[ItemName.dash_refill, ItemName.air_dash]],
LocationName.strawberry_10: [[ItemName.climb]],
LocationName.strawberry_11: [[ItemName.dash_refill, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.climb]],
LocationName.strawberry_12: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash],
[ItemName.traffic_block, ItemName.double_dash_refill, ItemName.air_dash]],
LocationName.strawberry_13: [[ItemName.dash_refill, ItemName.breakables, ItemName.air_dash],
[ItemName.traffic_block, ItemName.breakables, ItemName.ground_dash],
[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_14: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash],
[ItemName.traffic_block, ItemName.feather, ItemName.air_dash]],
LocationName.strawberry_15: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.feather, ItemName.climb]],
LocationName.strawberry_16: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash],
[ItemName.traffic_block, ItemName.feather]],
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.ground_dash],
[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.skid_jump],
[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.climb]],
LocationName.strawberry_18: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_19: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.spring, ItemName.air_dash],
[ItemName.traffic_block, ItemName.double_dash_refill, ItemName.feather, ItemName.spring, ItemName.air_dash]],
LocationName.strawberry_20: [[ItemName.dash_refill, ItemName.feather, ItemName.breakables, ItemName.air_dash],
[ItemName.traffic_block, ItemName.feather, ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_11: [[ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_13: [[ItemName.breakables, ItemName.air_dash],
[ItemName.breakables, ItemName.ground_dash]],
LocationName.strawberry_14: [[ItemName.feather, ItemName.air_dash]],
LocationName.strawberry_15: [[ItemName.feather, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_16: [[ItemName.feather]],
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block]],
LocationName.strawberry_18: [[ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_19: [[ItemName.double_dash_refill, ItemName.spring, ItemName.air_dash, ItemName.skid_jump]],
LocationName.strawberry_20: [[ItemName.feather, ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_21: [[ItemName.cassette, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_22: [[ItemName.cassette, ItemName.dash_refill, ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_23: [[ItemName.cassette, ItemName.dash_refill, ItemName.coin, ItemName.air_dash, ItemName.climb],
[ItemName.cassette, ItemName.traffic_block, ItemName.coin, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_23: [[ItemName.cassette, ItemName.coin, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_24: [[ItemName.cassette, ItemName.dash_refill, ItemName.traffic_block, ItemName.air_dash]],
LocationName.strawberry_25: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb],
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_26: [[ItemName.cassette, ItemName.dash_refill, ItemName.air_dash, ItemName.climb],
[ItemName.cassette, ItemName.traffic_block, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_27: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin, ItemName.air_dash],
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin, ItemName.air_dash]],
LocationName.strawberry_28: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb],
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.skid_jump]],
LocationName.strawberry_25: [[ItemName.cassette, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_26: [[ItemName.cassette, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_27: [[ItemName.cassette, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_28: [[ItemName.cassette, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.coin, ItemName.air_dash, ItemName.skid_jump]],
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.spring, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
LocationName.granny_1: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump],
[ItemName.climb]],
LocationName.granny_2: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump],
[ItemName.climb]],
LocationName.granny_3: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump],
[ItemName.climb]],
LocationName.theo_1: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
LocationName.theo_2: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
LocationName.theo_3: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
LocationName.badeline_1: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
LocationName.badeline_2: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
LocationName.badeline_3: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
LocationName.sign_1: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump],
[ItemName.climb]],
LocationName.sign_2: [[ItemName.breakables, ItemName.ground_dash],
[ItemName.breakables, ItemName.air_dash]],
LocationName.sign_3: [[ItemName.dash_refill, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash],
[ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.skid_jump],
[ItemName.traffic_block, ItemName.climb]],
LocationName.sign_4: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash],
[ItemName.dash_refill, ItemName.feather, ItemName.air_dash],
[ItemName.traffic_block, ItemName.feather, ItemName.ground_dash],
[ItemName.traffic_block, ItemName.feather, ItemName.air_dash],
[ItemName.traffic_block, ItemName.feather, ItemName.skid_jump],
[ItemName.traffic_block, ItemName.feather, ItemName.climb]],
LocationName.sign_5: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
LocationName.car_2: [[ItemName.breakables, ItemName.ground_dash],
[ItemName.breakables, ItemName.air_dash]],
LocationName.car_2: [[ItemName.breakables, ItemName.ground_dash, ItemName.climb],
[ItemName.breakables, ItemName.air_dash, ItemName.climb]],
}
location_hard_moves_logic: Dict[str, List[List[str]]] = {
LocationName.strawberry_3: [[ItemName.air_dash],
[ItemName.skid_jump]],
LocationName.strawberry_5: [[ItemName.ground_dash],
[ItemName.air_dash]],
LocationName.strawberry_8: [[ItemName.traffic_block],
[ItemName.ground_dash, ItemName.air_dash]],
LocationName.strawberry_10: [[ItemName.air_dash],
[ItemName.climb]],
LocationName.strawberry_11: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump]],
LocationName.strawberry_12: [[ItemName.feather],
[ItemName.ground_dash],
[ItemName.air_dash]],
LocationName.strawberry_13: [[ItemName.breakables, ItemName.ground_dash],
[ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_14: [[ItemName.feather, ItemName.air_dash],
[ItemName.air_dash, ItemName.climb]],
[ItemName.air_dash, ItemName.climb],
[ItemName.double_dash_refill, ItemName.air_dash]],
LocationName.strawberry_15: [[ItemName.feather],
[ItemName.ground_dash, ItemName.air_dash]],
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.traffic_block]],
@@ -287,42 +107,94 @@ location_hard_moves_logic: Dict[str, List[List[str]]] = {
[ItemName.cassette, ItemName.feather, ItemName.climb]],
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.air_dash, ItemName.skid_jump],
[ItemName.cassette, ItemName.ground_dash, ItemName.air_dash]],
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.ground_dash, ItemName.air_dash, ItemName.climb, ItemName.skid_jump],
[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.feather, ItemName.air_dash, ItemName.climb, ItemName.skid_jump],
[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.spring, ItemName.ground_dash, ItemName.air_dash, ItemName.climb],
[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.spring, ItemName.feather, ItemName.air_dash, ItemName.climb]],
LocationName.badeline_1: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
LocationName.badeline_2: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
LocationName.badeline_3: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb, ItemName.skid_jump],
[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.spring, ItemName.air_dash, ItemName.climb]],
LocationName.sign_2: [[ItemName.breakables, ItemName.ground_dash],
[ItemName.breakables, ItemName.air_dash]],
LocationName.sign_5: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
LocationName.car_2: [[ItemName.breakables, ItemName.ground_dash],
[ItemName.breakables, ItemName.air_dash]],
}
def location_rule(state: CollectionState, world: Celeste64World, loc: str) -> bool:
region_standard_moves_logic: Dict[Tuple[str], List[List[str]]] = {
(RegionName.forsaken_city, RegionName.granny_island): [[ItemName.checkpoint_2], [ItemName.checkpoint_3], [ItemName.checkpoint_4]],
(RegionName.forsaken_city, RegionName.highway_island): [[ItemName.checkpoint_5], [ItemName.checkpoint_6]],
(RegionName.forsaken_city, RegionName.ne_feathers_island): [[ItemName.checkpoint_7]],
(RegionName.forsaken_city, RegionName.se_house_island): [[ItemName.checkpoint_8]],
(RegionName.forsaken_city, RegionName.badeline_tower_upper): [[ItemName.checkpoint_9]],
(RegionName.forsaken_city, RegionName.badeline_island): [[ItemName.checkpoint_10]],
(RegionName.intro_islands, RegionName.granny_island): [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump],
[ItemName.climb]],
(RegionName.granny_island, RegionName.highway_island): [[ItemName.air_dash, ItemName.dash_refill]],
(RegionName.granny_island, RegionName.nw_girders_island): [[ItemName.traffic_block]],
(RegionName.granny_island, RegionName.badeline_tower_lower): [[ItemName.air_dash, ItemName.climb, ItemName.dash_refill]],
(RegionName.granny_island, RegionName.se_house_island): [[ItemName.air_dash, ItemName.climb, ItemName.double_dash_refill]],
(RegionName.highway_island, RegionName.granny_island): [[ItemName.traffic_block], [ItemName.air_dash, ItemName.dash_refill]],
(RegionName.highway_island, RegionName.ne_feathers_island): [[ItemName.feather]],
(RegionName.highway_island, RegionName.nw_girders_island): [[ItemName.cannot_access]],
(RegionName.nw_girders_island, RegionName.highway_island): [[ItemName.traffic_block]],
(RegionName.ne_feathers_island, RegionName.highway_island): [[ItemName.feather]],
(RegionName.ne_feathers_island, RegionName.badeline_tower_lower): [[ItemName.feather]],
(RegionName.ne_feathers_island, RegionName.badeline_tower_upper): [[ItemName.climb, ItemName.air_dash, ItemName.feather]],
(RegionName.se_house_island, RegionName.granny_island): [[ItemName.air_dash, ItemName.traffic_block, ItemName.double_dash_refill]],
(RegionName.se_house_island, RegionName.badeline_tower_lower): [[ItemName.air_dash, ItemName.double_dash_refill]],
(RegionName.badeline_tower_lower, RegionName.se_house_island): [[ItemName.cannot_access]],
(RegionName.badeline_tower_lower, RegionName.ne_feathers_island): [[ItemName.air_dash, ItemName.breakables, ItemName.feather]],
(RegionName.badeline_tower_lower, RegionName.granny_island): [[ItemName.cannot_access]],
(RegionName.badeline_tower_lower, RegionName.badeline_tower_upper): [[ItemName.cannot_access]],
(RegionName.badeline_tower_upper, RegionName.badeline_island): [[ItemName.air_dash, ItemName.climb, ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
(RegionName.badeline_tower_upper, RegionName.se_house_island): [[ItemName.air_dash], [ItemName.ground_dash]],
(RegionName.badeline_tower_upper, RegionName.ne_feathers_island): [[ItemName.air_dash], [ItemName.ground_dash]],
(RegionName.badeline_tower_upper, RegionName.granny_island): [[ItemName.dash_refill]],
(RegionName.badeline_island, RegionName.badeline_tower_upper): [[ItemName.air_dash], [ItemName.ground_dash]],
}
region_hard_moves_logic: Dict[Tuple[str], List[List[str]]] = {
(RegionName.forsaken_city, RegionName.granny_island): [[ItemName.checkpoint_2], [ItemName.checkpoint_3], [ItemName.checkpoint_4]],
(RegionName.forsaken_city, RegionName.highway_island): [[ItemName.checkpoint_5], [ItemName.checkpoint_6]],
(RegionName.forsaken_city, RegionName.ne_feathers_island): [[ItemName.checkpoint_7]],
(RegionName.forsaken_city, RegionName.se_house_island): [[ItemName.checkpoint_8]],
(RegionName.forsaken_city, RegionName.badeline_tower_upper): [[ItemName.checkpoint_9]],
(RegionName.forsaken_city, RegionName.badeline_island): [[ItemName.checkpoint_10]],
(RegionName.granny_island, RegionName.nw_girders_island): [[ItemName.traffic_block]],
(RegionName.granny_island, RegionName.badeline_tower_lower): [[ItemName.air_dash], [ItemName.ground_dash]],
(RegionName.granny_island, RegionName.se_house_island): [[ItemName.air_dash, ItemName.double_dash_refill], [ItemName.ground_dash]],
(RegionName.highway_island, RegionName.nw_girders_island): [[ItemName.air_dash, ItemName.ground_dash]],
(RegionName.nw_girders_island, RegionName.highway_island): [[ItemName.traffic_block], [ItemName.air_dash, ItemName.ground_dash]],
(RegionName.ne_feathers_island, RegionName.highway_island): [[ItemName.feather], [ItemName.air_dash], [ItemName.ground_dash], [ItemName.skid_jump]],
(RegionName.ne_feathers_island, RegionName.badeline_tower_lower): [[ItemName.feather], [ItemName.air_dash], [ItemName.ground_dash]],
(RegionName.ne_feathers_island, RegionName.badeline_tower_upper): [[ItemName.feather]],
(RegionName.se_house_island, RegionName.granny_island): [[ItemName.traffic_block]],
(RegionName.se_house_island, RegionName.badeline_tower_lower): [[ItemName.air_dash], [ItemName.ground_dash]],
(RegionName.badeline_tower_upper, RegionName.badeline_island): [[ItemName.air_dash, ItemName.climb, ItemName.feather, ItemName.traffic_block],
[ItemName.air_dash, ItemName.climb, ItemName.feather, ItemName.skid_jump],
[ItemName.air_dash, ItemName.climb, ItemName.ground_dash, ItemName.traffic_block],
[ItemName.air_dash, ItemName.climb, ItemName.ground_dash, ItemName.skid_jump]],
(RegionName.badeline_island, RegionName.badeline_tower_upper): [[ItemName.air_dash], [ItemName.ground_dash]],
}
def location_rule(state: CollectionState, world: Celeste64World, loc: str) -> bool:
if loc not in world.active_logic_mapping:
return True
@@ -332,12 +204,28 @@ def location_rule(state: CollectionState, world: Celeste64World, loc: str) -> bo
return False
def goal_rule(state: CollectionState, world: Celeste64World) -> bool:
if not state.has(ItemName.strawberry, world.player, world.strawberries_required):
return False
def region_connection_rule(state: CollectionState, world: Celeste64World, region_connection: Tuple[str]) -> bool:
if region_connection not in world.active_region_logic_mapping:
return True
for possible_access in world.goal_logic_mapping:
for possible_access in world.active_region_logic_mapping[region_connection]:
if state.has_all(possible_access, world.player):
return True
return False
def goal_rule(state: CollectionState, world: Celeste64World) -> bool:
if not state.has(ItemName.strawberry, world.player, world.strawberries_required):
return False
goal_region: Region = world.multiworld.get_region(RegionName.badeline_island, world.player)
return state.can_reach(goal_region)
def connect_region(world: Celeste64World, region: Region, dest_regions: List[str]):
rules: Dict[str, Callable[[CollectionState], bool]] = {}
for dest_region in dest_regions:
region_connection: Tuple[str] = (region.name, dest_region)
rules[dest_region] = lambda state, region_connection=region_connection: region_connection_rule(state, world, region_connection)
region.add_exits(dest_regions, rules)

View File

@@ -1,13 +1,15 @@
from copy import deepcopy
from typing import Dict, List
from typing import Dict, List, Tuple
from BaseClasses import ItemClassification, Location, Region, Tutorial
from worlds.AutoWorld import WebWorld, World
from .Items import Celeste64Item, unlockable_item_data_table, move_item_data_table, item_data_table, item_table
from .Items import Celeste64Item, unlockable_item_data_table, move_item_data_table, item_data_table,\
checkpoint_item_data_table, item_table
from .Locations import Celeste64Location, strawberry_location_data_table, friend_location_data_table,\
sign_location_data_table, car_location_data_table, location_table
sign_location_data_table, car_location_data_table, checkpoint_location_data_table,\
location_table
from .Names import ItemName, LocationName
from .Options import Celeste64Options, celeste_64_option_groups
from .Options import Celeste64Options, celeste_64_option_groups, resolve_options
class Celeste64WebWorld(WebWorld):
@@ -42,8 +44,15 @@ class Celeste64World(World):
# Instance Data
strawberries_required: int
active_logic_mapping: Dict[str, List[List[str]]]
goal_logic_mapping: Dict[str, List[List[str]]]
active_region_logic_mapping: Dict[Tuple[str], List[List[str]]]
madeline_one_dash_hair_color: int
madeline_two_dash_hair_color: int
madeline_no_dash_hair_color: int
madeline_feather_hair_color: int
def generate_early(self) -> None:
resolve_options(self)
def create_item(self, name: str) -> Celeste64Item:
# Only make required amount of strawberries be Progression
@@ -76,25 +85,49 @@ class Celeste64World(World):
for name in unlockable_item_data_table.keys()
if name not in self.options.start_inventory]
if self.options.move_shuffle:
move_items_for_itempool: List[str] = deepcopy(list(move_item_data_table.keys()))
chosen_start_item: str = ""
if self.options.move_shuffle:
if self.options.logic_difficulty == "standard":
# If the start_inventory already includes a move, don't worry about giving it one
if not [move for move in move_items_for_itempool if move in self.options.start_inventory]:
chosen_start_move = self.random.choice(move_items_for_itempool)
move_items_for_itempool.remove(chosen_start_move)
possible_unwalls: List[str] = [name for name in move_item_data_table.keys()
if name != ItemName.skid_jump]
if self.options.checkpointsanity:
possible_unwalls.extend([name for name in checkpoint_item_data_table.keys()
if name != ItemName.checkpoint_1 and name != ItemName.checkpoint_10])
# If the start_inventory already includes a move or checkpoint, don't worry about giving it one
if not [item for item in possible_unwalls if item in self.multiworld.precollected_items[self.player]]:
chosen_start_item = self.random.choice(possible_unwalls)
if self.options.carsanity:
intro_car_loc: Location = self.multiworld.get_location(LocationName.car_1, self.player)
intro_car_loc.place_locked_item(self.create_item(chosen_start_move))
intro_car_loc.place_locked_item(self.create_item(chosen_start_item))
location_count -= 1
else:
self.multiworld.push_precollected(self.create_item(chosen_start_move))
self.multiworld.push_precollected(self.create_item(chosen_start_item))
item_pool += [self.create_item(name)
for name in move_items_for_itempool
if name not in self.options.start_inventory]
for name in move_item_data_table.keys()
if name not in self.multiworld.precollected_items[self.player]
and name != chosen_start_item]
else:
for start_move in move_item_data_table.keys():
self.multiworld.push_precollected(self.create_item(start_move))
if self.options.checkpointsanity:
location_count += 9
goal_checkpoint_loc: Location = self.multiworld.get_location(LocationName.checkpoint_10, self.player)
goal_checkpoint_loc.place_locked_item(self.create_item(ItemName.checkpoint_10))
item_pool += [self.create_item(name)
for name in checkpoint_item_data_table.keys()
if name not in self.multiworld.precollected_items[self.player]
and name != ItemName.checkpoint_10
and name != chosen_start_item]
else:
for item_name in checkpoint_item_data_table.keys():
checkpoint_loc: Location = self.multiworld.get_location(item_name, self.player)
checkpoint_loc.place_locked_item(self.create_item(item_name))
real_total_strawberries: int = min(self.options.total_strawberries.value, location_count - len(item_pool))
self.strawberries_required = int(real_total_strawberries * (self.options.strawberries_required_percentage / 100))
@@ -140,18 +173,23 @@ class Celeste64World(World):
if location_data.region == region_name
}, Celeste64Location)
region.add_exits(region_data_table[region_name].connecting_regions)
region.add_locations({
location_name: location_data.address for location_name, location_data in checkpoint_location_data_table.items()
if location_data.region == region_name
}, Celeste64Location)
from .Rules import connect_region
connect_region(self, region, region_data_table[region_name].connecting_regions)
# Have to do this here because of other games using State in a way that's bad
from .Rules import set_rules
set_rules(self)
def get_filler_item_name(self) -> str:
return ItemName.raspberry
def set_rules(self) -> None:
from .Rules import set_rules
set_rules(self)
def fill_slot_data(self):
return {
"death_link": self.options.death_link.value,
@@ -161,6 +199,11 @@ class Celeste64World(World):
"friendsanity": self.options.friendsanity.value,
"signsanity": self.options.signsanity.value,
"carsanity": self.options.carsanity.value,
"checkpointsanity": self.options.checkpointsanity.value,
"madeline_one_dash_hair_color": self.madeline_one_dash_hair_color,
"madeline_two_dash_hair_color": self.madeline_two_dash_hair_color,
"madeline_no_dash_hair_color": self.madeline_no_dash_hair_color,
"madeline_feather_hair_color": self.madeline_feather_hair_color,
"badeline_chaser_source": self.options.badeline_chaser_source.value,
"badeline_chaser_frequency": self.options.badeline_chaser_frequency.value,
"badeline_chaser_speed": self.options.badeline_chaser_speed.value,

342
worlds/civ_6/Civ6Client.py Normal file
View File

@@ -0,0 +1,342 @@
import asyncio
import logging
import os
import traceback
from typing import Any, Dict, List, Optional
import zipfile
from CommonClient import ClientCommandProcessor, CommonContext, get_base_parser, logger, server_loop, gui_enabled
from .Data import get_progressive_districts_data
from .DeathLink import handle_check_deathlink
from NetUtils import ClientStatus
import Utils
from .CivVIInterface import CivVIInterface, ConnectionState
from .Enum import CivVICheckType
from .Items import CivVIItemData, generate_item_table, get_item_by_civ_name
from .Locations import CivVILocationData, generate_era_location_table
from .TunerClient import TunerErrorException, TunerTimeoutException
class CivVICommandProcessor(ClientCommandProcessor):
def __init__(self, ctx: CommonContext):
super().__init__(ctx)
def _cmd_deathlink(self):
"""Toggle deathlink from client. Overrides default setting."""
if isinstance(self.ctx, CivVIContext):
self.ctx.death_link_enabled = not self.ctx.death_link_enabled
self.ctx.death_link_just_changed = True
Utils.async_start(self.ctx.update_death_link(
self.ctx.death_link_enabled), name="Update Deathlink")
self.ctx.logger.info(f"Deathlink is now {'enabled' if self.ctx.death_link_enabled else 'disabled'}")
def _cmd_resync(self):
"""Resends all items to client, and has client resend all locations to server. This can take up to a minute if the player has received a lot of items"""
if isinstance(self.ctx, CivVIContext):
logger.info("Resyncing...")
asyncio.create_task(self.ctx.resync())
def _cmd_toggle_progressive_eras(self):
"""If you get stuck for some reason and unable to continue your game, you can run this command to disable the defeat that comes from pushing past the max unlocked era """
if isinstance(self.ctx, CivVIContext):
print("Toggling progressive eras, stand by...")
self.ctx.is_pending_toggle_progressive_eras = True
class CivVIContext(CommonContext):
is_pending_death_link_reset = False
is_pending_toggle_progressive_eras = False
command_processor = CivVICommandProcessor
game = "Civilization VI"
items_handling = 0b111
tuner_sync_task: Optional[asyncio.Task[None]] = None
game_interface: CivVIInterface
location_name_to_civ_location: Dict[str, CivVILocationData] = {}
location_name_to_id: Dict[str, int] = {}
item_id_to_civ_item: Dict[int, CivVIItemData] = {}
item_table: Dict[str, CivVIItemData] = {}
processing_multiple_items = False
received_death_link = False
death_link_message = ""
death_link_enabled = False
slot_data: Dict[str, Any]
death_link_just_changed = False
# Used to prevent the deathlink from triggering when someone re enables it
logger = logger
progressive_items_by_type = get_progressive_districts_data()
item_name_to_id = {
item.name: item.code for item in generate_item_table().values()}
connection_state = ConnectionState.DISCONNECTED
def __init__(self, server_address: Optional[str], password: Optional[str], apcivvi_file: Optional[str] = None):
super().__init__(server_address, password)
self.slot_data: Dict[str, Any] = {}
self.game_interface = CivVIInterface(logger)
location_by_era = generate_era_location_table()
self.item_table = generate_item_table()
self.apcivvi_file = apcivvi_file
# Get tables formatted in a way that is easier to use here
for locations in location_by_era.values():
for location in locations.values():
self.location_name_to_id[location.name] = location.code
self.location_name_to_civ_location[location.name] = location
for item in self.item_table.values():
self.item_id_to_civ_item[item.code] = item
async def resync(self):
if self.processing_multiple_items:
logger.info(
"Waiting for items to finish processing, try again later")
return
await self.game_interface.resync()
await handle_receive_items(self, -1)
logger.info("Resynced")
def on_deathlink(self, data: Utils.Dict[str, Utils.Any]) -> None:
super().on_deathlink(data)
text = data.get("cause", "")
if text:
message = text
else:
message = f"Received from {data['source']}"
self.death_link_message = message
self.received_death_link = True
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(CivVIContext, self).server_auth(password_requested)
await self.get_username()
self.tags = set()
await self.send_connect()
def run_gui(self):
from kvui import GameManager
class CivVIManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago Civilization VI Client"
self.ui = CivVIManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def on_package(self, cmd: str, args: Dict[str, Any]):
if cmd == "Connected":
self.slot_data = args["slot_data"]
if "death_link" in args["slot_data"]:
self.death_link_enabled = bool(args["slot_data"]["death_link"])
Utils.async_start(self.update_death_link(
bool(args["slot_data"]["death_link"])))
def update_connection_status(ctx: CivVIContext, status: ConnectionState):
if ctx.connection_state == status:
return
elif status == ConnectionState.IN_GAME:
ctx.logger.info("Connected to Civ VI")
elif status == ConnectionState.IN_MENU:
ctx.logger.info("Connected to Civ VI, waiting for game to start")
elif status == ConnectionState.DISCONNECTED:
ctx.logger.info("Disconnected from Civ VI, attempting to reconnect...")
ctx.connection_state = status
async def tuner_sync_task(ctx: CivVIContext):
logger.info("Starting CivVI connector")
while not ctx.exit_event.is_set():
if not ctx.slot:
await asyncio.sleep(3)
continue
else:
try:
if ctx.processing_multiple_items:
await asyncio.sleep(3)
else:
state = await ctx.game_interface.is_in_game()
update_connection_status(ctx, state)
if state == ConnectionState.IN_GAME:
await _handle_game_ready(ctx)
else:
await asyncio.sleep(3)
except TunerTimeoutException:
logger.error(
"Timeout occurred while receiving data from Civ VI, this usually isn't a problem unless you see it repeatedly")
await asyncio.sleep(3)
except Exception as e:
if isinstance(e, TunerErrorException):
logger.debug(str(e))
else:
logger.debug(traceback.format_exc())
await asyncio.sleep(3)
continue
async def handle_toggle_progressive_eras(ctx: CivVIContext):
if ctx.is_pending_toggle_progressive_eras:
ctx.is_pending_toggle_progressive_eras = False
current = await ctx.game_interface.get_max_allowed_era()
if current > -1:
await ctx.game_interface.set_max_allowed_era(-1)
logger.info("Disabled progressive eras")
else:
count = 0
for _, network_item in enumerate(ctx.items_received):
item: CivVIItemData = ctx.item_id_to_civ_item[network_item.item]
if item.item_type == CivVICheckType.ERA:
count += 1
await ctx.game_interface.set_max_allowed_era(count)
logger.info(f"Enabled progressive eras, set to {count}")
async def handle_checked_location(ctx: CivVIContext):
checked_locations = await ctx.game_interface.get_checked_locations()
checked_location_ids = [location.code for location_name, location in ctx.location_name_to_civ_location.items(
) if location_name in checked_locations]
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": checked_location_ids}])
async def handle_receive_items(ctx: CivVIContext, last_received_index_override: Optional[int] = None):
try:
last_received_index = last_received_index_override or await ctx.game_interface.get_last_received_index()
if len(ctx.items_received) - last_received_index > 1:
ctx.processing_multiple_items = True
progressive_districts: List[CivVIItemData] = []
progressive_eras: List[CivVIItemData] = []
for index, network_item in enumerate(ctx.items_received):
# Track these separately so if we replace "PROGRESSIVE_DISTRICT" with a specific tech, we can still check if need to add it to the list of districts
item: CivVIItemData = ctx.item_id_to_civ_item[network_item.item]
item_to_send: CivVIItemData = ctx.item_id_to_civ_item[network_item.item]
if index > last_received_index:
if item.item_type == CivVICheckType.PROGRESSIVE_DISTRICT and item.civ_name:
# if the item is progressive, then check how far in that progression type we are and send the appropriate item
count = sum(
1 for count_item in progressive_districts if count_item.civ_name == item.civ_name)
if count >= len(ctx.progressive_items_by_type[item.civ_name]):
logger.error(
f"Received more progressive items than expected for {item.civ_name}")
continue
item_civ_name = ctx.progressive_items_by_type[item.civ_name][count]
actual_item_name = get_item_by_civ_name(item_civ_name, ctx.item_table).name
item_to_send = ctx.item_table[actual_item_name]
sender = ctx.player_names[network_item.player]
if item.item_type == CivVICheckType.ERA:
count = len(progressive_eras) + 1
await ctx.game_interface.give_item_to_player(item_to_send, sender, count)
elif item.item_type == CivVICheckType.GOODY and item_to_send.civ_name:
await ctx.game_interface.give_item_to_player(item_to_send, sender, game_id_override=item_to_send.civ_name)
else:
await ctx.game_interface.give_item_to_player(item_to_send, sender)
await asyncio.sleep(0.02)
if item.item_type == CivVICheckType.PROGRESSIVE_DISTRICT:
progressive_districts.append(item)
elif item.item_type == CivVICheckType.ERA:
progressive_eras.append(item)
ctx.processing_multiple_items = False
finally:
# If something errors out, then unblock item processing
ctx.processing_multiple_items = False
async def handle_check_goal_complete(ctx: CivVIContext):
if ctx.finished_game:
return
result = await ctx.game_interface.check_victory()
if result:
logger.info("Sending Victory to server!")
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
async def _handle_game_ready(ctx: CivVIContext):
if ctx.server:
if not ctx.slot:
await asyncio.sleep(3)
return
await handle_receive_items(ctx)
await handle_checked_location(ctx)
await handle_check_goal_complete(ctx)
if ctx.death_link_enabled:
await handle_check_deathlink(ctx)
# process pending commands
await handle_toggle_progressive_eras(ctx)
await asyncio.sleep(3)
else:
logger.info("Waiting for player to connect to server")
await asyncio.sleep(3)
def main(connect: Optional[str] = None, password: Optional[str] = None, name: Optional[str] = None):
Utils.init_logging("Civilization VI Client")
async def _main(connect: Optional[str], password: Optional[str], name: Optional[str]):
parser = get_base_parser()
parser.add_argument("apcivvi_file", default="", type=str, nargs="?", help="Path to apcivvi file")
args = parser.parse_args()
ctx = CivVIContext(connect, password, args.apcivvi_file)
if args.apcivvi_file:
parent_dir: str = os.path.dirname(args.apcivvi_file)
target_name: str = os.path.basename(args.apcivvi_file).replace(".apcivvi", "-MOD-FILES")
target_path: str = os.path.join(parent_dir, target_name)
if not os.path.exists(target_path):
os.makedirs(target_path, exist_ok=True)
logger.info("Extracting mod files to %s", target_path)
with zipfile.ZipFile(args.apcivvi_file, "r") as zip_ref:
for member in zip_ref.namelist():
zip_ref.extract(member, target_path)
ctx.auth = name
ctx.server_task = asyncio.create_task(
server_loop(ctx), name="ServerLoop")
if gui_enabled:
ctx.run_gui()
await asyncio.sleep(1)
ctx.tuner_sync_task = asyncio.create_task(
tuner_sync_task(ctx), name="TunerSync")
await ctx.exit_event.wait()
ctx.server_address = None
await ctx.shutdown()
if ctx.tuner_sync_task:
await asyncio.sleep(3)
await ctx.tuner_sync_task
import colorama
colorama.init()
asyncio.run(_main(connect, password, name))
colorama.deinit()
def debug_main():
parser = get_base_parser()
parser.add_argument("apcivvi_file", default="", type=str, nargs="?", help="Path to apcivvi file")
parser.add_argument("--name", default=None,
help="Slot Name to connect as.")
parser.add_argument("--debug", default=None,
help="debug mode, additional logging")
args = parser.parse_args()
if args.debug:
logger.setLevel(logging.DEBUG)
main(args.connect, args.password, args.name)

View File

@@ -0,0 +1,119 @@
from enum import Enum
from logging import Logger
from typing import List, Optional
from .Items import CivVIItemData
from .TunerClient import TunerClient, TunerConnectionException, TunerTimeoutException
class ConnectionState(Enum):
DISCONNECTED = 0
IN_GAME = 1
IN_MENU = 2
class CivVIInterface:
logger: Logger
tuner: TunerClient
last_error: Optional[str] = None
def __init__(self, logger: Logger):
self.logger = logger
self.tuner = TunerClient(logger)
async def is_in_game(self) -> ConnectionState:
command = "IsInGame()"
try:
result = await self.tuner.send_game_command(command)
if result == "false":
return ConnectionState.IN_MENU
self.last_error = None
return ConnectionState.IN_GAME
except TunerTimeoutException:
self.print_connection_error(
"Not connected to game, waiting for connection to be available")
return ConnectionState.DISCONNECTED
except TunerConnectionException as e:
if "The remote computer refused the network connection" in str(e):
self.print_connection_error(
"Unable to connect to game. Verify that the tuner is enabled. Attempting to reconnect")
else:
self.print_connection_error(
"Not connected to game, waiting for connection to be available")
return ConnectionState.DISCONNECTED
except Exception as e:
if "attempt to index a nil valuestack traceback" in str(e) \
or ".. is not supported for string .. nilstack traceback" in str(e):
return ConnectionState.IN_MENU
return ConnectionState.DISCONNECTED
def print_connection_error(self, error: str) -> None:
if error != self.last_error:
self.last_error = error
self.logger.info(error)
async def give_item_to_player(self, item: CivVIItemData, sender: str = "", amount: int = 1, game_id_override: Optional[str] = None) -> None:
if game_id_override:
item_id = f'"{game_id_override}"'
else:
item_id = item.civ_vi_id
command = f"HandleReceiveItem({item_id}, \"{item.name}\", \"{item.item_type.value}\", \"{sender}\", {amount})"
await self.tuner.send_game_command(command)
async def resync(self) -> None:
"""Has the client resend all the checked locations"""
command = "Resync()"
await self.tuner.send_game_command(command)
async def check_victory(self) -> bool:
command = "ClientGetVictory()"
result = await self.tuner.send_game_command(command)
return result == "true"
async def get_checked_locations(self) -> List[str]:
command = "GetUnsentCheckedLocations()"
result = await self.tuner.send_game_command(command, 2048 * 4)
return result.split(",")
async def get_deathlink(self) -> str:
"""returns either "false" or the name of the unit that killed the player's unit"""
command = "ClientGetDeathLink()"
result = await self.tuner.send_game_command(command)
return result
async def kill_unit(self, message: str) -> None:
command = f"KillUnit(\"{message}\")"
await self.tuner.send_game_command(command)
async def get_last_received_index(self) -> int:
command = "ClientGetLastReceivedIndex()"
result = await self.tuner.send_game_command(command)
return int(result)
async def send_notification(self, item: CivVIItemData, sender: str = "someone") -> None:
command = f"GameCore.NotificationManager:SendNotification(GameCore.NotificationTypes.USER_DEFINED_2, \"{item.name} Received\", \"You have received {item.name} from \" .. \"{sender}\", 0, {item.civ_vi_id})"
await self.tuner.send_command(command)
async def decrease_gold_by_percent(self, percent: int, message: str) -> None:
command = f"DecreaseGoldByPercent({percent}, \"{message}\")"
await self.tuner.send_game_command(command)
async def decrease_faith_by_percent(self, percent: int, message: str) -> None:
command = f"DecreaseFaithByPercent({percent}, \"{message}\")"
await self.tuner.send_game_command(command)
async def decrease_era_score_by_amount(self, amount: int, message: str) -> None:
command = f"DecreaseEraScoreByAmount({amount}, \"{message}\")"
await self.tuner.send_game_command(command)
async def set_max_allowed_era(self, count: int) -> None:
command = f"SetMaxAllowedEra(\"{count}\")"
await self.tuner.send_game_command(command)
async def get_max_allowed_era(self) -> int:
command = "ClientGetMaxAllowedEra()"
result = await self.tuner.send_game_command(command)
if result == "":
return -1
return int(result)

224
worlds/civ_6/Container.py Normal file
View File

@@ -0,0 +1,224 @@
from dataclasses import dataclass
import os
import io
from typing import TYPE_CHECKING, Dict, List, Optional, cast
import zipfile
from BaseClasses import Location
from worlds.Files import APContainer, AutoPatchRegister
from .Enum import CivVICheckType
from .Locations import CivVILocation, CivVILocationData
if TYPE_CHECKING:
from . import CivVIWorld
# Python fstrings don't allow backslashes, so we use this workaround
nl = "\n"
tab = "\t"
apo = "\'"
@dataclass
class CivTreeItem:
name: str
cost: int
ui_tree_row: int
class CivVIContainer(APContainer, metaclass=AutoPatchRegister):
"""
Responsible for generating the dynamic mod files for the Civ VI multiworld
"""
game: Optional[str] = "Civilization VI"
patch_file_ending = ".apcivvi"
def __init__(self, patch_data: Dict[str, str] | io.BytesIO, base_path: str = "", output_directory: str = "",
player: Optional[int] = None, player_name: str = "", server: str = ""):
if isinstance(patch_data, io.BytesIO):
super().__init__(patch_data, player, player_name, server)
else:
self.patch_data = patch_data
self.file_path = base_path
container_path = os.path.join(output_directory, base_path + ".apcivvi")
super().__init__(container_path, player, player_name, server)
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
for filename, yml in self.patch_data.items():
opened_zipfile.writestr(filename, yml)
super().write_contents(opened_zipfile)
def get_cost(world: 'CivVIWorld', location: CivVILocationData) -> int:
"""
Returns the cost of the item based on the game options
"""
# Research cost is between 50 and 150 where 100 equals the default cost
multiplier = world.options.research_cost_multiplier / 100
return int(world.location_table[location.name].cost * multiplier)
def get_formatted_player_name(world: 'CivVIWorld', player: int) -> str:
"""
Returns the name of the player in the world
"""
if player != world.player:
return f"{world.multiworld.player_name[player]}{apo}s"
return "Your"
def get_advisor_type(world: 'CivVIWorld', location: Location) -> str:
if world.options.advisor_show_progression_items and location.item and location.item.advancement:
return "ADVISOR_PROGRESSIVE"
return "ADVISOR_GENERIC"
def generate_new_items(world: 'CivVIWorld') -> str:
"""
Generates the XML for the new techs/civics as well as the blockers used to prevent players from researching their own items
"""
locations: List[CivVILocation] = cast(List[CivVILocation], world.multiworld.get_filled_locations(world.player))
techs = [location for location in locations if location.location_type ==
CivVICheckType.TECH]
civics = [location for location in locations if location.location_type ==
CivVICheckType.CIVIC]
boost_techs = []
boost_civics = []
if world.options.boostsanity:
boost_techs = [location for location in locations if location.location_type == CivVICheckType.BOOST and location.name.split("_")[1] == "TECH"]
boost_civics = [location for location in locations if location.location_type == CivVICheckType.BOOST and location.name.split("_")[1] == "CIVIC"]
techs += boost_techs
civics += boost_civics
return f"""<?xml version="1.0" encoding="utf-8"?>
<GameInfo>
<Types>
<Row Type="TECH_BLOCKER" Kind="KIND_TECH" />
<Row Type="CIVIC_BLOCKER" Kind="KIND_CIVIC" />
{"".join([f'{tab}<Row Type="{tech.name}" Kind="KIND_TECH" />{nl}' for
tech in techs])}
{"".join([f'{tab}<Row Type="{civic.name}" Kind="KIND_CIVIC" />{nl}' for
civic in civics])}
</Types>
<Technologies>
<Row TechnologyType="TECH_BLOCKER" Name="TECH_BLOCKER" EraType="ERA_ANCIENT" UITreeRow="0" Cost="99999" AdvisorType="ADVISOR_GENERIC" Description="Archipelago Tech created to prevent players from researching their own tech. If you can read this, then congrats you have reached the end of your tree before beating the game!"/>
{"".join([f'{tab}<Row TechnologyType="{location.name}" '
f'Name="{get_formatted_player_name(world, location.item.player)} '
f'{location.item.name}" '
f'EraType="{world.location_table[location.name].era_type}" '
f'UITreeRow="{world.location_table[location.name].uiTreeRow}" '
f'Cost="{get_cost(world, world.location_table[location.name])}" '
f'Description="{location.name}" '
f'AdvisorType="{get_advisor_type(world, location)}"'
f'/>{nl}'
for location in techs if location.item])}
</Technologies>
<TechnologyPrereqs>
{"".join([f'{tab}<Row Technology="{location.name}" PrereqTech="TECH_BLOCKER" />{nl}' for location in boost_techs])}
</TechnologyPrereqs>
<Civics>
<Row CivicType="CIVIC_BLOCKER" Name="CIVIC_BLOCKER" EraType="ERA_ANCIENT" UITreeRow="0" Cost="99999" AdvisorType="ADVISOR_GENERIC" Description="Archipelago Civic created to prevent players from researching their own civics. If you can read this, then congrats you have reached the end of your tree before beating the game!"/>
{"".join([f'{tab}<Row CivicType="{location.name}" '
f'Name="{get_formatted_player_name(world, location.item.player)} '
f'{location.item.name}" '
f'EraType="{world.location_table[location.name].era_type}" '
f'UITreeRow="{world.location_table[location.name].uiTreeRow}" '
f'Cost="{get_cost(world, world.location_table[location.name])}" '
f'Description="{location.name}" '
f'AdvisorType="{get_advisor_type(world, location)}"'
f'/>{nl}'
for location in civics if location.item])}
</Civics>
<CivicPrereqs>
{"".join([f'{tab}<Row Civic="{location.name}" PrereqCivic="CIVIC_BLOCKER" />{nl}' for location in boost_civics])}
</CivicPrereqs>
<Civics_XP2>
{"".join([f'{tab}<Row CivicType="{location.name}" HiddenUntilPrereqComplete="true" RandomPrereqs="false"/>{nl}' for location in civics if world.options.hide_item_names])}
</Civics_XP2>
<Technologies_XP2>
{"".join([f'{tab}<Row TechnologyType="{location.name}" HiddenUntilPrereqComplete="true" RandomPrereqs="false"/>{nl}' for location in techs if world.options.hide_item_names])}
</Technologies_XP2>
</GameInfo>
"""
def generate_setup_file(world: 'CivVIWorld') -> str:
"""
Generates the Lua for the setup file. This sets initial variables and state that affect gameplay around Progressive Eras
"""
setup = "-- Setup"
if world.options.progression_style == "eras_and_districts":
setup += f"""
-- Init Progressive Era Value if it hasn't been set already
if Game.GetProperty("MaxAllowedEra") == nil then
print("Setting MaxAllowedEra to 0")
Game.SetProperty("MaxAllowedEra", 0)
end
"""
if world.options.boostsanity:
setup += f"""
-- Init Boosts
if Game.GetProperty("BoostsAsChecks") == nil then
print("Setting Boosts As Checks to True")
Game.SetProperty("BoostsAsChecks", true)
end
"""
return setup
def generate_goody_hut_sql(world: 'CivVIWorld') -> str:
"""
Generates the SQL for the goody huts or an empty string if they are disabled since the mod expects the file to be there
"""
if world.options.shuffle_goody_hut_rewards:
return f"""
UPDATE GoodyHutSubTypes SET Description = NULL WHERE GoodyHut NOT IN ('METEOR_GOODIES', 'GOODYHUT_SAILOR_WONDROUS', 'DUMMY_GOODY_BUILDIER') AND Weight > 0;
INSERT INTO Modifiers
(ModifierId, ModifierType, RunOnce, Permanent, SubjectRequirementSetId)
SELECT ModifierID||'_AI', ModifierType, RunOnce, Permanent, 'PLAYER_IS_AI'
FROM Modifiers
WHERE EXISTS (
SELECT ModifierId
FROM GoodyHutSubTypes
WHERE Modifiers.ModifierId = GoodyHutSubTypes.ModifierId AND GoodyHutSubTypes.GoodyHut NOT IN ('METEOR_GOODIES', 'GOODYHUT_SAILOR_WONDROUS', 'DUMMY_GOODY_BUILDIER') AND GoodyHutSubTypes.Weight > 0);
INSERT INTO ModifierArguments
(ModifierId, Name, Type, Value)
SELECT ModifierID||'_AI', Name, Type, Value
FROM ModifierArguments
WHERE EXISTS (
SELECT ModifierId
FROM GoodyHutSubTypes
WHERE ModifierArguments.ModifierId = GoodyHutSubTypes.ModifierId AND GoodyHutSubTypes.GoodyHut NOT IN ('METEOR_GOODIES', 'GOODYHUT_SAILOR_WONDROUS', 'DUMMY_GOODY_BUILDIER') AND GoodyHutSubTypes.Weight > 0);
UPDATE GoodyHutSubTypes
SET ModifierID = ModifierID||'_AI'
WHERE GoodyHut NOT IN ('METEOR_GOODIES', 'GOODYHUT_SAILOR_WONDROUS', 'DUMMY_GOODY_BUILDIER') AND Weight > 0;
"""
return "-- Goody Huts are disabled, no changes needed"
def generate_update_boosts_sql(world: 'CivVIWorld') -> str:
"""
Generates the SQL for existing boosts in boostsanity or an empty string if they are disabled since the mod expects the file to be there
"""
if world.options.boostsanity:
return f"""
UPDATE Boosts
SET TechnologyType = 'BOOST_' || TechnologyType
WHERE TechnologyType IS NOT NULL;
UPDATE Boosts
SET CivicType = 'BOOST_' || CivicType
WHERE CivicType IS NOT NULL AND CivicType NOT IN ('CIVIC_CORPORATE_LIBERTARIANISM', 'CIVIC_DIGITAL_DEMOCRACY', 'CIVIC_SYNTHETIC_TECHNOCRACY', 'CIVIC_NEAR_FUTURE_GOVERNANCE');
"""
return "-- Boostsanity is disabled, no changes needed"

70
worlds/civ_6/Data.py Normal file
View File

@@ -0,0 +1,70 @@
from typing import Dict, List
from .ItemData import (
CivVIBoostData,
CivicPrereqData,
ExistingItemData,
GoodyHutRewardData,
NewItemData,
TechPrereqData,
)
def get_boosts_data() -> List[CivVIBoostData]:
from .data.boosts import boosts
return boosts
def get_era_required_items_data() -> Dict[str, List[str]]:
from .data.era_required_items import era_required_items
return era_required_items
def get_existing_civics_data() -> List[ExistingItemData]:
from .data.existing_civics import existing_civics
return existing_civics
def get_existing_techs_data() -> List[ExistingItemData]:
from .data.existing_tech import existing_tech
return existing_tech
def get_goody_hut_rewards_data() -> List[GoodyHutRewardData]:
from .data.goody_hut_rewards import reward_data
return reward_data
def get_new_civic_prereqs_data() -> List[CivicPrereqData]:
from .data.new_civic_prereqs import new_civic_prereqs
return new_civic_prereqs
def get_new_civics_data() -> List[NewItemData]:
from .data.new_civics import new_civics
return new_civics
def get_new_tech_prereqs_data() -> List[TechPrereqData]:
from .data.new_tech_prereqs import new_tech_prereqs
return new_tech_prereqs
def get_new_techs_data() -> List[NewItemData]:
from .data.new_tech import new_tech
return new_tech
def get_progressive_districts_data() -> Dict[str, List[str]]:
from .data.progressive_districts import progressive_districts
return progressive_districts

74
worlds/civ_6/DeathLink.py Normal file
View File

@@ -0,0 +1,74 @@
import random
from typing import TYPE_CHECKING, List
if TYPE_CHECKING:
from .Civ6Client import CivVIContext
# any is also an option but should not be considered an effect
DEATH_LINK_EFFECTS = ["Gold", "Faith", "Era Score", "Unit Killed"]
async def handle_receive_deathlink(ctx: 'CivVIContext', message: str):
"""Resolves the effects of a deathlink received from the multiworld based on the options selected by the player"""
chosen_effects: List[str] = ctx.slot_data["death_link_effect"]
effect = random.choice(chosen_effects)
percent = ctx.slot_data["death_link_effect_percent"]
if effect == "Gold":
ctx.logger.info(f"Decreasing gold by {percent}%")
await ctx.game_interface.decrease_gold_by_percent(percent, message)
elif effect == "Faith":
ctx.logger.info(f"Decreasing faith by {percent}%")
await ctx.game_interface.decrease_faith_by_percent(percent, message)
elif effect == "Era Score":
ctx.logger.info("Decreasing era score by 1")
await ctx.game_interface.decrease_era_score_by_amount(1, message)
elif effect == "Unit Killed":
ctx.logger.info("Destroying a random unit")
await ctx.game_interface.kill_unit(message)
async def handle_check_deathlink(ctx: 'CivVIContext'):
"""Checks if the local player should send out a deathlink to the multiworld as well as if we should respond to any pending deathlinks sent to us """
# check if we received a death link
if ctx.received_death_link:
ctx.received_death_link = False
await handle_receive_deathlink(ctx, ctx.death_link_message)
# Check if we should send out a death link
result = await ctx.game_interface.get_deathlink()
if ctx.death_link_just_changed:
ctx.death_link_just_changed = False
return
if result != "false":
messages = [f"lost a unit to a {result}",
f"offered a sacrifice to the great {result}",
f"was killed by a {result}",
f"made a donation to the {result} fund",
f"made a tactical error",
f"picked a fight with a {result} and lost",
f"tried to befriend an enemy {result}",
f"used a {result} to reduce their military spend",
f"was defeated by a {result} in combat",
f"bravely struck a {result} and paid the price",
f"had a lapse in judgement against a {result}",
f"learned at the hands of a {result}",
f"attempted to non peacefully negotiate with a {result}",
f"was outsmarted by a {result}",
f"received a lesson from a {result}",
f"now understands the importance of not fighting a {result}",
f"let a {result} get the better of them",
f"allowed a {result} to show them the error of their ways",
f"heard the tragedy of Darth Plagueis the Wise from a {result}",
f"refused to join a {result} in their quest for power",
f"was tired of sitting in BK and decided to fight a {result} instead",
f"purposely lost to a {result} as a cry for help",
f"is wanting to remind everyone that they are here to have fun and not to win",
f"is reconsidering their pursuit of a domination victory",
f"had their plans toppled by a {result}",
]
if ctx.slot is not None:
player = ctx.player_names[ctx.slot]
message = random.choice(messages)
await ctx.send_death(f"{player} {message}")

39
worlds/civ_6/Enum.py Normal file
View File

@@ -0,0 +1,39 @@
from enum import Enum
from BaseClasses import ItemClassification
class EraType(Enum):
ERA_ANCIENT = "ERA_ANCIENT"
ERA_CLASSICAL = "ERA_CLASSICAL"
ERA_MEDIEVAL = "ERA_MEDIEVAL"
ERA_RENAISSANCE = "ERA_RENAISSANCE"
ERA_INDUSTRIAL = "ERA_INDUSTRIAL"
ERA_MODERN = "ERA_MODERN"
ERA_ATOMIC = "ERA_ATOMIC"
ERA_INFORMATION = "ERA_INFORMATION"
ERA_FUTURE = "ERA_FUTURE"
class CivVICheckType(Enum):
TECH = "TECH"
CIVIC = "CIVIC"
PROGRESSIVE_DISTRICT = "PROGRESSIVE_DISTRICT"
ERA = "ERA"
GOODY = "GOODY"
BOOST = "BOOST"
EVENT = "EVENT"
class CivVIHintClassification(Enum):
PROGRESSION = "Progression"
USEFUL = "Useful"
FILLER = "Filler"
def to_item_classification(self) -> ItemClassification:
if self == CivVIHintClassification.PROGRESSION:
return ItemClassification.progression
if self == CivVIHintClassification.USEFUL:
return ItemClassification.useful
if self == CivVIHintClassification.FILLER:
return ItemClassification.filler
assert False

38
worlds/civ_6/ItemData.py Normal file
View File

@@ -0,0 +1,38 @@
from dataclasses import dataclass
from typing import List, TypedDict
class NewItemData(TypedDict):
Type: str
Cost: int
UITreeRow: int
EraType: str
class ExistingItemData(NewItemData):
Name: str
@dataclass
class CivVIBoostData:
Type: str
EraType: str
Prereq: List[str]
PrereqRequiredCount: int
Classification: str
class GoodyHutRewardData(TypedDict):
Type: str
Name: str
Rarity: str
class CivicPrereqData(TypedDict):
Civic: str
PrereqTech: str
class TechPrereqData(TypedDict):
Technology: str
PrereqTech: str

353
worlds/civ_6/Items.py Normal file
View File

@@ -0,0 +1,353 @@
from enum import Enum
from typing import Dict, Optional, TYPE_CHECKING, List
from BaseClasses import Item, ItemClassification
from .Data import (
GoodyHutRewardData,
get_era_required_items_data,
get_existing_civics_data,
get_existing_techs_data,
get_goody_hut_rewards_data,
get_progressive_districts_data,
)
from .Enum import CivVICheckType, EraType
from .ProgressiveDistricts import get_flat_progressive_districts
if TYPE_CHECKING:
from . import CivVIWorld
CIV_VI_AP_ITEM_ID_BASE = 5041000
NON_PROGRESSION_DISTRICTS = ["PROGRESSIVE_PRESERVE", "PROGRESSIVE_NEIGHBORHOOD"]
# Items required as progression for boostsanity mode
BOOSTSANITY_PROGRESSION_ITEMS = [
"TECH_THE_WHEEL",
"TECH_MASONRY",
"TECH_ARCHERY",
"TECH_ENGINEERING",
"TECH_CONSTRUCTION",
"TECH_GUNPOWDER",
"TECH_MACHINERY",
"TECH_SIEGE_TACTICS",
"TECH_STIRRUPS",
"TECH_ASTRONOMY",
"TECH_BALLISTICS",
"TECH_STEAM_POWER",
"TECH_SANITATION",
"TECH_COMPUTERS",
"TECH_COMBUSTION",
"TECH_TELECOMMUNICATIONS",
"TECH_ROBOTICS",
"CIVIC_FEUDALISM",
"CIVIC_GUILDS",
"CIVIC_THE_ENLIGHTENMENT",
"CIVIC_MERCANTILISM",
"CIVIC_CONSERVATION",
"CIVIC_CIVIL_SERVICE",
"CIVIC_GLOBALIZATION",
"CIVIC_COLD_WAR",
"CIVIC_URBANIZATION",
"CIVIC_NATIONALISM",
"CIVIC_MOBILIZATION",
"PROGRESSIVE_NEIGHBORHOOD",
"PROGRESSIVE_PRESERVE",
]
class FillerItemRarity(Enum):
COMMON = "COMMON"
UNCOMMON = "UNCOMMON"
RARE = "RARE"
FILLER_DISTRIBUTION: Dict[FillerItemRarity, float] = {
FillerItemRarity.RARE: 0.025,
FillerItemRarity.UNCOMMON: 0.2,
FillerItemRarity.COMMON: 0.775,
}
class FillerItemData:
name: str
type: str
rarity: FillerItemRarity
civ_name: str
def __init__(self, data: GoodyHutRewardData):
self.name = data["Name"]
self.rarity = FillerItemRarity(data["Rarity"])
self.civ_name = data["Type"]
filler_data: Dict[str, FillerItemData] = {
item["Name"]: FillerItemData(item) for item in get_goody_hut_rewards_data()
}
class CivVIItemData:
civ_vi_id: int
classification: ItemClassification
name: str
code: int
cost: int
item_type: CivVICheckType
progressive_name: Optional[str]
civ_name: Optional[str]
era: Optional[EraType]
def __init__(
self,
name: str,
civ_vi_id: int,
cost: int,
item_type: CivVICheckType,
id_offset: int,
classification: ItemClassification,
progressive_name: Optional[str],
civ_name: Optional[str] = None,
era: Optional[EraType] = None,
):
self.classification = classification
self.civ_vi_id = civ_vi_id
self.name = name
self.code = civ_vi_id + CIV_VI_AP_ITEM_ID_BASE + id_offset
self.cost = cost
self.item_type = item_type
self.progressive_name = progressive_name
self.civ_name = civ_name
self.era = era
class CivVIEvent(Item):
game: str = "Civilization VI"
class CivVIItem(Item):
game: str = "Civilization VI"
civ_vi_id: int
item_type: CivVICheckType
def __init__(
self,
item: CivVIItemData,
player: int,
classification: Optional[ItemClassification] = None,
):
super().__init__(
item.name, classification or item.classification, item.code, player
)
self.civ_vi_id = item.civ_vi_id
self.item_type = item.item_type
def format_item_name(name: str) -> str:
name_parts = name.split("_")
return " ".join([part.capitalize() for part in name_parts])
_items_by_civ_name: Dict[str, CivVIItemData] = {}
def get_item_by_civ_name(
item_name: str, item_table: Dict[str, "CivVIItemData"]
) -> "CivVIItemData":
"""Gets the names of the items in the item_table"""
if not _items_by_civ_name:
for item in item_table.values():
if item.civ_name:
_items_by_civ_name[item.civ_name] = item
try:
return _items_by_civ_name[item_name]
except KeyError as e:
raise KeyError(f"Item {item_name} not found in item_table") from e
def _generate_tech_items(
id_base: int, required_items: List[str], progressive_items: Dict[str, str]
) -> Dict[str, CivVIItemData]:
# Generate Techs
existing_techs = get_existing_techs_data()
tech_table: Dict[str, CivVIItemData] = {}
tech_id = 0
for tech in existing_techs:
classification = ItemClassification.useful
name = tech["Name"]
civ_name = tech["Type"]
if civ_name in required_items:
classification = ItemClassification.progression
progressive_name = None
check_type = CivVICheckType.TECH
if civ_name in progressive_items.keys():
progressive_name = format_item_name(progressive_items[civ_name])
tech_table[name] = CivVIItemData(
name=name,
civ_vi_id=tech_id,
cost=tech["Cost"],
item_type=check_type,
id_offset=id_base,
classification=classification,
progressive_name=progressive_name,
civ_name=civ_name,
era=EraType(tech["EraType"]),
)
tech_id += 1
return tech_table
def _generate_civics_items(
id_base: int, required_items: List[str], progressive_items: Dict[str, str]
) -> Dict[str, CivVIItemData]:
civic_id = 0
civic_table: Dict[str, CivVIItemData] = {}
existing_civics = get_existing_civics_data()
for civic in existing_civics:
name = civic["Name"]
civ_name = civic["Type"]
progressive_name = None
check_type = CivVICheckType.CIVIC
if civ_name in progressive_items.keys():
progressive_name = format_item_name(progressive_items[civ_name])
classification = ItemClassification.useful
if civ_name in required_items:
classification = ItemClassification.progression
civic_table[name] = CivVIItemData(
name=name,
civ_vi_id=civic_id,
cost=civic["Cost"],
item_type=check_type,
id_offset=id_base,
classification=classification,
progressive_name=progressive_name,
civ_name=civ_name,
era=EraType(civic["EraType"]),
)
civic_id += 1
return civic_table
def _generate_progressive_district_items(id_base: int) -> Dict[str, CivVIItemData]:
progressive_table: Dict[str, CivVIItemData] = {}
progressive_id_base = 0
progressive_items = get_progressive_districts_data()
for item_name in progressive_items.keys():
classification = (
ItemClassification.useful
if item_name in NON_PROGRESSION_DISTRICTS
else ItemClassification.progression
)
name = format_item_name(item_name)
progressive_table[name] = CivVIItemData(
name=name,
civ_vi_id=progressive_id_base,
cost=0,
item_type=CivVICheckType.PROGRESSIVE_DISTRICT,
id_offset=id_base,
classification=classification,
progressive_name=None,
civ_name=item_name,
)
progressive_id_base += 1
return progressive_table
def _generate_progressive_era_items(id_base: int) -> Dict[str, CivVIItemData]:
"""Generates the single progressive district item"""
era_table: Dict[str, CivVIItemData] = {}
# Generate progressive eras
progressive_era_name = format_item_name("PROGRESSIVE_ERA")
era_table[progressive_era_name] = CivVIItemData(
name=progressive_era_name,
civ_vi_id=0,
cost=0,
item_type=CivVICheckType.ERA,
id_offset=id_base,
classification=ItemClassification.progression,
progressive_name=None,
civ_name="PROGRESSIVE_ERA",
)
return era_table
def _generate_goody_hut_items(id_base: int) -> Dict[str, CivVIItemData]:
# Generate goody hut items
goody_huts = {
item["Name"]: FillerItemData(item) for item in get_goody_hut_rewards_data()
}
goody_table: Dict[str, CivVIItemData] = {}
goody_base = 0
for value in goody_huts.values():
goody_table[value.name] = CivVIItemData(
name=value.name,
civ_vi_id=goody_base,
cost=0,
item_type=CivVICheckType.GOODY,
id_offset=id_base,
classification=ItemClassification.filler,
progressive_name=None,
civ_name=value.civ_name,
)
goody_base += 1
return goody_table
def generate_item_table() -> Dict[str, CivVIItemData]:
era_required_items = get_era_required_items_data()
required_items: List[str] = []
for value in era_required_items.values():
required_items += value
progressive_items = get_flat_progressive_districts()
item_table: Dict[str, CivVIItemData] = {}
def get_id_base():
return len(item_table.keys())
item_table.update(
**_generate_tech_items(get_id_base(), required_items, progressive_items)
)
item_table.update(
**_generate_civics_items(get_id_base(), required_items, progressive_items)
)
item_table.update(**_generate_progressive_district_items(get_id_base()))
item_table.update(**_generate_progressive_era_items(get_id_base()))
item_table.update(**_generate_goody_hut_items(get_id_base()))
return item_table
def get_items_by_type(
item_type: CivVICheckType, item_table: Dict[str, CivVIItemData]
) -> List[CivVIItemData]:
"""
Returns a list of items that match the given item type
"""
return [item for item in item_table.values() if item.item_type == item_type]
fillers_by_rarity: Dict[FillerItemRarity, List[FillerItemData]] = {
rarity: [item for item in filler_data.values() if item.rarity == rarity]
for rarity in FillerItemRarity
}
def get_random_filler_by_rarity(
world: "CivVIWorld", rarity: FillerItemRarity
) -> FillerItemData:
"""
Returns a random filler item by rarity
"""
return world.random.choice(fillers_by_rarity[rarity])

21
worlds/civ_6/LICENSE.md Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright © 2024 tanjo3
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

156
worlds/civ_6/Locations.py Normal file
View File

@@ -0,0 +1,156 @@
from collections import defaultdict
from dataclasses import dataclass
from typing import Optional, Dict
from BaseClasses import Location, Region
from .Data import get_boosts_data, get_new_civics_data, get_new_techs_data
from .Enum import CivVICheckType, EraType
CIV_VI_AP_LOCATION_ID_BASE = 5041000
# Locs that should not have progression items
GOODY_HUT_LOCATION_NAMES = [
"GOODY_HUT_1",
"GOODY_HUT_2",
"GOODY_HUT_3",
"GOODY_HUT_4",
"GOODY_HUT_5",
"GOODY_HUT_6",
"GOODY_HUT_7",
"GOODY_HUT_8",
"GOODY_HUT_9",
"GOODY_HUT_10",
]
@dataclass
class CivVILocationData:
name: str
cost: int
uiTreeRow: int
civ_id: int
era_type: str
location_type: CivVICheckType
game: str = "Civilization VI"
@property
def code(self):
return self.civ_id + CIV_VI_AP_LOCATION_ID_BASE
class CivVILocation(Location):
game: str = "Civilization VI"
location_type: CivVICheckType
def __init__(
self,
player: int,
name: str = "",
address: Optional[int] = None,
parent: Optional[Region] = None,
):
super().__init__(player, name, address, parent)
category = name.split("_")[0]
if "victory" in category:
self.location_type = CivVICheckType.EVENT
else:
self.location_type = CivVICheckType(category)
def generate_flat_location_table() -> Dict[str, CivVILocationData]:
"""
Generates a flat location table in the following format:
{
"TECH_AP_ANCIENT_00": CivVILocationData,
"TECH_AP_ANCIENT_01": CivVILocationData,
"CIVIC_AP_ANCIENT_00": CivVILocationData,
...
}
"""
era_locations = generate_era_location_table()
flat_locations: Dict[str, CivVILocationData] = {}
for locations in era_locations.values():
for location_id, location_data in locations.items():
flat_locations[location_id] = location_data
return flat_locations
def generate_era_location_table() -> Dict[str, Dict[str, CivVILocationData]]:
"""
Uses the data from existing_tech.json to generate a location table in the following format:
{
"ERA_ANCIENT": {
"TECH_AP_ANCIENT_00": CivVILocationData,
"TECH_AP_ANCIENT_01": CivVILocationData,
"CIVIC_AP_ANCIENT_00": CivVILocationData,
},
...
}
"""
new_techs = get_new_techs_data()
era_locations: Dict[str, Dict[str, CivVILocationData]] = defaultdict(dict)
id_base = 0
# Techs
for data in new_techs:
era_type = data["EraType"]
era_locations[era_type][data["Type"]] = CivVILocationData(
data["Type"],
data["Cost"],
data["UITreeRow"],
id_base,
era_type,
CivVICheckType.TECH,
)
id_base += 1
# Civics
new_civics = get_new_civics_data()
for data in new_civics:
era_type = data["EraType"]
era_locations[era_type][data["Type"]] = CivVILocationData(
data["Type"],
data["Cost"],
data["UITreeRow"],
id_base,
era_type,
CivVICheckType.CIVIC,
)
id_base += 1
# Eras
for era in EraType:
if era == EraType.ERA_ANCIENT:
continue
era_locations[era.name][era.name] = CivVILocationData(
era.name, 0, 0, id_base, era.name, CivVICheckType.ERA
)
id_base += 1
# Goody Huts, defaults to 10 goody huts as location checks (rarely will a player get more than this)
for i in range(10):
era_locations[EraType.ERA_ANCIENT.value]["GOODY_HUT_" + str(i + 1)] = (
CivVILocationData(
"GOODY_HUT_" + str(i + 1),
0,
0,
id_base,
EraType.ERA_ANCIENT.value,
CivVICheckType.GOODY,
)
)
id_base += 1
# Boosts
boosts = get_boosts_data()
for boost in boosts:
location = CivVILocationData(
boost.Type, 0, 0, id_base, boost.EraType, CivVICheckType.BOOST
)
era_locations["ERA_ANCIENT"][boost.Type] = location
id_base += 1
return era_locations

130
worlds/civ_6/Options.py Normal file
View File

@@ -0,0 +1,130 @@
from dataclasses import dataclass
from Options import (
Choice,
DefaultOnToggle,
OptionSet,
PerGameCommonOptions,
Range,
StartInventoryPool,
Toggle,
)
from .Enum import CivVIHintClassification
class ProgressionStyle(Choice):
"""
**Districts Only**: Each tech/civic that would normally unlock a district or building now has a logical progression.
Example: TECH_BRONZE_WORKING is now PROGRESSIVE_ENCAMPMENT
**Eras and Districts**: Players will be defeated if they play until the world era advances beyond the currently unlocked maximum era.
Unlocked eras can be seen in both the tech and civic trees. Includes all progressive districts.
**None**: No progressive items will be included. This means you can get district upgrades that won't be usable until the relevant district is unlocked.
"""
rich_text_doc = True
display_name = "Progression Style"
option_districts_only = 0
option_eras_and_districts = 1
option_none = 2
default = option_districts_only
class ShuffleGoodyHuts(DefaultOnToggle):
"""Shuffles the goody hut rewards.
Goody huts will only contain junk items and locations are checked sequentially (First goody hut gives GOODY_HUT_1, second gives GOODY_HUT_2, etc.).
"""
display_name = "Shuffle Goody Hut Rewards"
class BoostSanity(Toggle):
"""Boosts for Civics/Techs are location checks. Boosts can now be triggered even if the item has already been
researched.
**Note**: If a boost is dependent upon a unit that is now obsolete, you can click to toggle on/off the relevant tech in
the tech tree."""
rich_text_doc = True
display_name = "Boostsanity"
class ResearchCostMultiplier(Range):
"""Multiplier for research cost of techs and civics, higher values make research more expensive."""
display_name = "Tech/Civic Cost Multiplier"
range_start = 50
range_end = 150
default = 100
class PreHintItems(OptionSet):
"""Controls what items from the tech/civics trees are pre-hinted for the multiworld.
**Progression**: Include Progression items in hints
**Useful**: Include Useful items in hints
**Filler**: Include Filler items in hints
"""
display_name = "Tech/Civic Tree pre-hinted Items"
valid_keys = {classification.value for classification in CivVIHintClassification} # type: ignore
class HideItemNames(Toggle):
"""Each Tech and Civic Location will have a title of 'Unrevealed' until its prereqs have been researched. Note that
hints will still be precollected if that option is enabled."""
display_name = "Hide Item Names"
class InGameFlagProgressionItems(DefaultOnToggle):
"""If enabled, an advisor icon will be added to any location that contains a progression item."""
display_name = "Advisor Indicates Progression Items"
class CivDeathLink(Toggle):
"""If enabled, losing a unit will trigger a death link effect on other players in the multiworld. When a death link is received, the player will receive the effect specified in 'Death Link Effect'."""
display_name = "Death Link"
class DeathLinkEffect(OptionSet):
"""What happens when a unit dies.
**Unit Killed**: A random unit will be killed when a death link is received.
**Faith**: Faith will be decreased by the amount specified in 'Death Link Effect Percent'.
**Gold**: Gold will be decreased by the amount specified in 'Death Link Effect Percent'.
**Era Score**: Era score is decreased by 1.
"""
rich_text_doc = True
display_name = "Death Link Effect"
valid_keys = ["Unit Killed", "Faith", "Gold", "Era Score"] # type: ignore
default = frozenset({"Unit Killed"})
class DeathLinkEffectPercent(Range):
"""The percentage of the effect that will be applied. Only applicable for Gold and Faith effects."""
display_name = "Death Link Effect Percent"
default = 20
range_start = 1
range_end = 100
@dataclass
class CivVIOptions(PerGameCommonOptions):
start_inventory_from_pool: StartInventoryPool
progression_style: ProgressionStyle
shuffle_goody_hut_rewards: ShuffleGoodyHuts
boostsanity: BoostSanity
research_cost_multiplier: ResearchCostMultiplier
pre_hint_items: PreHintItems
hide_item_names: HideItemNames
advisor_show_progression_items: InGameFlagProgressionItems
death_link: CivDeathLink
death_link_effect: DeathLinkEffect
death_link_effect_percent: DeathLinkEffectPercent

View File

@@ -0,0 +1,35 @@
from typing import Dict, List, Optional
from .Data import get_progressive_districts_data
_flat_progressive_districts: Optional[Dict[str, str]] = {}
def get_flat_progressive_districts() -> Dict[str, str]:
"""Returns a dictionary of all items that are associated with a progressive item.
Key is the item name ("TECH_WRITING") and the value is the associated progressive
item ("PROGRESSIVE_CAMPUS")"""
if _flat_progressive_districts:
return _flat_progressive_districts
progressive_districts = get_progressive_districts_data()
flat_progressive_districts: Dict[str, str] = {}
for key, value in progressive_districts.items():
for item in value:
flat_progressive_districts[item] = key
return flat_progressive_districts
def convert_items_to_progressive_items(items: List[str]):
"""converts a list of items to instead be their associated progressive item if
they have one. ["TECH_MINING", "TECH_WRITING"] -> ["TECH_MINING", "PROGRESSIVE_CAMPUS]
"""
flat_progressive_districts = get_flat_progressive_districts()
return [flat_progressive_districts.get(item, item) for item in items]
def convert_item_to_progressive_item(item: str):
"""converts an items to instead be its associated progressive item if
it has one. "TECH_WRITING" -> "PROGRESSIVE_CAMPUS"""
flat_progressive_districts = get_flat_progressive_districts()
return flat_progressive_districts.get(item, item)

128
worlds/civ_6/Regions.py Normal file
View File

@@ -0,0 +1,128 @@
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Union
from BaseClasses import CollectionState, LocationProgressType, Region
from worlds.generic.Rules import add_rule, set_rule
from .Data import (
get_boosts_data,
)
from .Enum import EraType
from .Locations import GOODY_HUT_LOCATION_NAMES, CivVILocation
if TYPE_CHECKING:
from . import CivVIWorld
def has_progressive_eras(
state: CollectionState, era: EraType, world: "CivVIWorld"
) -> bool:
return state.has(
"Progressive Era", world.player, world.era_required_progressive_era_counts[era]
)
def has_non_progressive_items(
state: CollectionState, era: EraType, world: "CivVIWorld"
) -> bool:
return state.has_all(world.era_required_non_progressive_items[era], world.player)
def has_progressive_items(
state: CollectionState, era: EraType, world: "CivVIWorld"
) -> bool:
return state.has_all_counts(
world.era_required_progressive_items_counts[era], world.player
)
def create_regions(world: "CivVIWorld"):
menu = Region("Menu", world.player, world.multiworld)
world.multiworld.regions.append(menu)
optional_location_inclusions: Dict[str, Union[bool, int]] = {
"ERA": world.options.progression_style
== world.options.progression_style.option_eras_and_districts,
"GOODY": world.options.shuffle_goody_hut_rewards.value,
"BOOST": world.options.boostsanity.value,
}
regions: List[Region] = []
previous_era: EraType = EraType.ERA_ANCIENT
for era in EraType:
era_region = Region(era.value, world.player, world.multiworld)
era_locations: Dict[str, Optional[int]] = {}
for key, location in world.location_by_era[era.value].items():
category = key.split("_")[0]
if optional_location_inclusions.get(category, True):
era_locations[location.name] = location.code
era_region.add_locations(era_locations, CivVILocation)
regions.append(era_region)
world.multiworld.regions.append(era_region)
# Connect era to previous era if not ancient era
if era == EraType.ERA_ANCIENT:
menu.connect(world.get_region(EraType.ERA_ANCIENT.value))
continue
connection = world.get_region(previous_era.value).connect(
world.get_region(era.value)
)
# Access rules for eras
add_rule(
connection,
lambda state, previous_era=previous_era, world=world: has_non_progressive_items(
state, previous_era, world
),
)
if world.options.progression_style == "eras_and_districts":
add_rule(
connection,
lambda state, previous_era=previous_era, world=world: has_progressive_eras(
state, previous_era, world
),
)
if world.options.progression_style != "none":
add_rule(
connection,
lambda state, previous_era=previous_era, world=world: has_progressive_items(
state, previous_era, world
),
)
previous_era = era
future_era = world.get_region(EraType.ERA_FUTURE.value)
victory = CivVILocation(world.player, "Complete a victory type", None, future_era)
victory.place_locked_item(world.create_event("Victory"))
future_era.locations.append(victory)
set_rule(
victory,
lambda state: state.can_reach_region(EraType.ERA_FUTURE.value, world.player),
)
world.multiworld.completion_condition[world.player] = lambda state: state.has(
"Victory", world.player
)
exclude_necessary_locations(world)
def exclude_necessary_locations(world: "CivVIWorld"):
forced_excluded_location_names: Set[str] = set()
if world.options.shuffle_goody_hut_rewards:
forced_excluded_location_names.update(GOODY_HUT_LOCATION_NAMES)
if world.options.boostsanity:
boost_data_list = get_boosts_data()
excluded_boosts = {
boost_data.Type
for boost_data in boost_data_list
if boost_data.Classification == "EXCLUDED"
}
forced_excluded_location_names.update(excluded_boosts)
for location_name in forced_excluded_location_names:
location = world.get_location(location_name)
location.progress_type = LocationProgressType.EXCLUDED

109
worlds/civ_6/Rules.py Normal file
View File

@@ -0,0 +1,109 @@
from typing import TYPE_CHECKING, List, Tuple
from BaseClasses import CollectionState
from .ItemData import CivVIBoostData
from .Items import format_item_name
from .Data import get_boosts_data, get_progressive_districts_data
from .Enum import CivVICheckType
from .ProgressiveDistricts import convert_item_to_progressive_item
from worlds.generic.Rules import forbid_item, set_rule
if TYPE_CHECKING:
from . import CivVIWorld
def generate_requirements_for_boosts(
world: "CivVIWorld", boost_data: CivVIBoostData
) -> Tuple[List[str], List[Tuple[str, int]]]:
required_non_progressive_items: List[str] = []
required_progressive_item_counts: List[Tuple[str, int]] = []
for item in boost_data.Prereq:
progressive_item_name = convert_item_to_progressive_item(item)
if (
world.options.progression_style != "none"
and "PROGRESSIVE" in progressive_item_name
):
required_progressive_item_counts.append(
(
format_item_name(progressive_item_name),
get_progressive_districts_data()[progressive_item_name].index(item)
+ 1,
)
)
else:
ap_item_name = world.item_by_civ_name[item]
required_non_progressive_items.append(ap_item_name)
return required_non_progressive_items, required_progressive_item_counts
def create_boost_rules(world: "CivVIWorld"):
boost_data_list = get_boosts_data()
boost_locations = [
location
for location in world.location_table.values()
if location.location_type == CivVICheckType.BOOST
]
for location in boost_locations:
boost_data = next(
(boost for boost in boost_data_list if boost.Type == location.name), None
)
world_location = world.get_location(location.name)
forbid_item(world_location, "Progressive Era", world.player)
if boost_data and boost_data.PrereqRequiredCount > 0:
required_non_progressive_items, required_progressive_item_counts = (
generate_requirements_for_boosts(world, boost_data)
)
if world.options.progression_style != "none":
set_rule(
world_location,
lambda state, non_progressive_prereqs=required_non_progressive_items, progressive_prereq_counts=required_progressive_item_counts, required_count=boost_data.PrereqRequiredCount: has_required_items_progressive(
state,
non_progressive_prereqs,
progressive_prereq_counts,
required_count,
world,
),
)
else:
set_rule(
world_location,
lambda state, prereqs=required_non_progressive_items, required_count=boost_data.PrereqRequiredCount: has_required_items_non_progressive(
state, prereqs, required_count, world
),
)
def has_required_items_progressive(
state: CollectionState,
non_progressive_prereqs: List[str],
progressive_prereq_counts: List[Tuple[str, int]],
required_count: int,
world: "CivVIWorld",
) -> bool:
collected_count = 0
for item, count in progressive_prereq_counts:
if state.has(item, world.player, count):
collected_count += 1
# early out if we've already gotten enough
if collected_count >= required_count:
return True
for item in non_progressive_prereqs:
if state.has(item, world.player):
collected_count += 1
# early out if we've already gotten enough
if collected_count >= required_count:
return True
return False
def has_required_items_non_progressive(
state: CollectionState, prereqs: List[str], required_count: int, world: "CivVIWorld"
) -> bool:
return state.has_from_list_unique(
prereqs,
world.player,
required_count,
)

105
worlds/civ_6/TunerClient.py Normal file
View File

@@ -0,0 +1,105 @@
import asyncio
from logging import Logger
import socket
from typing import Any
ADDRESS = "127.0.0.1"
PORT = 4318
CLIENT_PREFIX = "APSTART:"
CLIENT_POSTFIX = ":APEND"
def decode_mixed_string(data: bytes) -> str:
return "".join(chr(b) if 32 <= b < 127 else "?" for b in data)
class TunerException(Exception):
pass
class TunerTimeoutException(TunerException):
pass
class TunerErrorException(TunerException):
pass
class TunerConnectionException(TunerException):
pass
class TunerClient:
"""Interfaces with Civilization via the tuner socket"""
logger: Logger
def __init__(self, logger: Logger):
self.logger = logger
def __parse_response(self, response: str) -> str:
"""Parses the response from the tuner socket"""
split = response.split(CLIENT_PREFIX)
if len(split) > 1:
start = split[1]
end = start.split(CLIENT_POSTFIX)[0]
return end
elif "ERR:" in response:
raise TunerErrorException(response.replace("?", ""))
else:
return ""
async def send_game_command(self, command_string: str, size: int = 64):
"""Small helper that prefixes a command with GameCore.Game."""
return await self.send_command("GameCore.Game." + command_string, size)
async def send_command(self, command_string: str, size: int = 64):
"""Send a raw commannd"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(False)
b_command_string = command_string.encode("utf-8")
# Send data to the server
command_prefix = b"CMD:0:"
delimiter = b"\x00"
full_command = b_command_string
message = command_prefix + full_command + delimiter
message_length = len(message).to_bytes(1, byteorder="little")
# game expects this to be added before any command that is sent, indicates payload size
message_header = message_length + b"\x00\x00\x00\x03\x00\x00\x00"
data = message_header + command_prefix + full_command + delimiter
server_address = (ADDRESS, PORT)
loop = asyncio.get_event_loop()
try:
await loop.sock_connect(sock, server_address)
await loop.sock_sendall(sock, data)
# Add a delay before receiving data
await asyncio.sleep(.02)
received_data = await self.async_recv(sock)
response = decode_mixed_string(received_data)
return self.__parse_response(response)
except socket.timeout:
self.logger.debug("Timeout occurred while receiving data")
raise TunerTimeoutException()
except Exception as e:
self.logger.debug(f"Error occurred while receiving data: {str(e)}")
# check if No connection could be made is present in the error message
connection_errors = [
"The remote computer refused the network connection",
]
if any(error in str(e) for error in connection_errors):
raise TunerConnectionException(e)
else:
raise TunerErrorException(e)
finally:
sock.close()
async def async_recv(self, sock: Any, timeout: float = 2.0, size: int = 4096):
response = await asyncio.wait_for(asyncio.get_event_loop().sock_recv(sock, size), timeout)
return response

326
worlds/civ_6/__init__.py Normal file
View File

@@ -0,0 +1,326 @@
from collections import defaultdict
import math
import os
from typing import Any, Dict, List, Set
from .ProgressiveDistricts import get_flat_progressive_districts
from worlds.generic.Rules import forbid_item
from .Data import (
get_boosts_data,
get_era_required_items_data,
)
from .Rules import create_boost_rules
from .Container import (
CivVIContainer,
generate_goody_hut_sql,
generate_new_items,
generate_setup_file,
generate_update_boosts_sql,
)
from .Enum import CivVICheckType, CivVIHintClassification
from .Items import (
BOOSTSANITY_PROGRESSION_ITEMS,
FILLER_DISTRIBUTION,
CivVIEvent,
CivVIItemData,
FillerItemRarity,
format_item_name,
generate_item_table,
CivVIItem,
get_item_by_civ_name,
get_random_filler_by_rarity,
)
from .Locations import (
CivVILocation,
CivVILocationData,
EraType,
generate_era_location_table,
generate_flat_location_table,
)
from .Options import CivVIOptions
from .Regions import create_regions
from BaseClasses import Item, ItemClassification, MultiWorld, Tutorial
from worlds.AutoWorld import World, WebWorld
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch_subprocess # type: ignore
def run_client(*args: Any):
print("Running Civ6 Client")
from .Civ6Client import main # lazy import
launch_subprocess(main, name="Civ6Client")
components.append(
Component(
"Civ6 Client",
func=run_client,
component_type=Type.CLIENT,
file_identifier=SuffixIdentifier(".apcivvi"),
)
)
class CivVIWeb(WebWorld):
tutorials = [
Tutorial(
"Multiworld Setup Guide",
"A guide to setting up Civilization VI for MultiWorld.",
"English",
"setup_en.md",
"setup/en",
["hesto2"],
)
]
theme = "ocean"
class CivVIWorld(World):
"""
Civilization VI is a turn-based strategy video game in which one or more players compete alongside computer-controlled opponents to grow their individual civilization from a small tribe to control the entire planet across several periods of development.
"""
game = "Civilization VI"
topology_present = False
options_dataclass = CivVIOptions
options: CivVIOptions # type: ignore
web = CivVIWeb()
item_name_to_id = {item.name: item.code for item in generate_item_table().values()}
location_name_to_id = {
location.name: location.code
for location in generate_flat_location_table().values()
}
item_table: Dict[str, CivVIItemData] = {}
location_by_era: Dict[str, Dict[str, CivVILocationData]]
required_client_version = (0, 4, 5)
location_table: Dict[str, CivVILocationData]
era_required_non_progressive_items: Dict[EraType, List[str]]
era_required_progressive_items_counts: Dict[EraType, Dict[str, int]]
era_required_progressive_era_counts: Dict[EraType, int]
item_by_civ_name: Dict[str, str]
def __init__(self, multiworld: MultiWorld, player: int):
super().__init__(multiworld, player)
self.location_by_era = generate_era_location_table()
self.location_table: Dict[str, CivVILocationData] = {}
self.item_table = generate_item_table()
self.era_required_non_progressive_items = {}
self.era_required_progressive_items_counts = {}
self.era_required_progressive_era_counts = {}
for locations in self.location_by_era.values():
for location in locations.values():
self.location_table[location.name] = location
def generate_early(self) -> None:
flat_progressive_items = get_flat_progressive_districts()
self.item_by_civ_name = {
item.civ_name: get_item_by_civ_name(item.civ_name, self.item_table).name
for item in self.item_table.values()
if item.civ_name
}
previous_era_counts = None
eras_list = [e.value for e in EraType]
for era in EraType:
# Initialize era_required_progressive_era_counts
era_index = eras_list.index(era.value)
self.era_required_progressive_era_counts[era] = (
0
if era in {EraType.ERA_FUTURE, EraType.ERA_INFORMATION}
else era_index + 1
)
# Initialize era_required_progressive_items_counts
self.era_required_progressive_items_counts[era] = defaultdict(int)
if previous_era_counts:
self.era_required_progressive_items_counts[era].update(
previous_era_counts
)
# Initialize era_required_non_progressive_items and add to item counts
self.era_required_non_progressive_items[era] = []
for item in get_era_required_items_data()[era.value]:
if (
item in flat_progressive_items
and self.options.progression_style != "none"
):
progressive_name = format_item_name(flat_progressive_items[item])
self.era_required_progressive_items_counts[era][
progressive_name
] += 1
else:
self.era_required_non_progressive_items[era].append(
self.item_by_civ_name[item]
)
previous_era_counts = self.era_required_progressive_items_counts[era].copy()
def get_filler_item_name(self) -> str:
return get_random_filler_by_rarity(self, FillerItemRarity.COMMON).name
def create_regions(self) -> None:
create_regions(self)
def set_rules(self) -> None:
if self.options.boostsanity:
create_boost_rules(self)
def create_event(self, event: str):
return CivVIEvent(event, ItemClassification.progression, None, self.player)
def create_item(self, name: str) -> Item:
item: CivVIItemData = self.item_table[name]
classification = item.classification
if self.options.boostsanity:
if item.civ_name in BOOSTSANITY_PROGRESSION_ITEMS:
classification = ItemClassification.progression
return CivVIItem(item, self.player, classification)
def create_items(self) -> None:
data = get_era_required_items_data()
early_items = data[EraType.ERA_ANCIENT.value]
early_locations = [
location
for location in self.location_table.values()
if location.era_type == EraType.ERA_ANCIENT.value
]
for item_name, item_data in self.item_table.items():
# These item types are handled individually
if item_data.item_type in [
CivVICheckType.PROGRESSIVE_DISTRICT,
CivVICheckType.ERA,
CivVICheckType.GOODY,
]:
continue
# If we're using progressive districts, we need to check if we need to create a different item instead
item_to_create = item_name
item: CivVIItemData = self.item_table[item_name]
if self.options.progression_style != "none":
if item.progressive_name:
item_to_create = self.item_table[item.progressive_name].name
self.multiworld.itempool += [self.create_item(item_to_create)]
if item.civ_name in early_items:
self.multiworld.early_items[self.player][item_to_create] = 1
elif self.item_table[item_name].era in [
EraType.ERA_ATOMIC,
EraType.ERA_INFORMATION,
EraType.ERA_FUTURE,
]:
for location in early_locations:
found_location = None
try:
found_location = self.get_location(location.name)
forbid_item(found_location, item_to_create, self.player)
except KeyError:
pass
# Era items
if self.options.progression_style == "eras_and_districts":
# Add one less than the total number of eras (start in ancient, don't need to find it)
for era in EraType:
if era.value == "ERA_ANCIENT":
continue
progressive_era_item = self.item_table.get("Progressive Era")
assert progressive_era_item is not None
self.multiworld.itempool += [
self.create_item(progressive_era_item.name)
]
self.multiworld.early_items[self.player]["Progressive Era"] = 2
num_filler_items = 0
# Goody items, create 10 by default if options are enabled
if self.options.shuffle_goody_hut_rewards:
num_filler_items += 10
if self.options.boostsanity:
num_filler_items += len(get_boosts_data())
filler_count = {
rarity: math.ceil(FILLER_DISTRIBUTION[rarity] * num_filler_items)
for rarity in FillerItemRarity.__reversed__()
}
filler_count[FillerItemRarity.COMMON] -= (
sum(filler_count.values()) - num_filler_items
)
self.multiworld.itempool += [
self.create_item(get_random_filler_by_rarity(self, rarity).name)
for rarity, count in filler_count.items()
for _ in range(count)
]
def post_fill(self) -> None:
if not self.options.pre_hint_items.value:
return
def is_hintable_filler_item(item: Item) -> bool:
return (
item.classification == 0
and CivVIHintClassification.FILLER.value
in self.options.pre_hint_items.value
)
start_location_hints: Set[str] = self.options.start_location_hints.value
non_filler_flags = [
CivVIHintClassification(flag).to_item_classification()
for flag in self.options.pre_hint_items.value
if flag != CivVIHintClassification.FILLER.value
]
for location_name, location_data in self.location_table.items():
if (
location_data.location_type != CivVICheckType.CIVIC
and location_data.location_type != CivVICheckType.TECH
):
continue
location: CivVILocation = self.get_location(location_name) # type: ignore
if location.item and (
is_hintable_filler_item(location.item)
or any(
flag in location.item.classification for flag in non_filler_flags
)
):
start_location_hints.add(location_name)
def fill_slot_data(self) -> Dict[str, Any]:
return self.options.as_dict(
"progression_style",
"death_link",
"research_cost_multiplier",
"death_link_effect",
"death_link_effect_percent",
)
def generate_output(self, output_directory: str):
mod_name = self.multiworld.get_out_file_name_base(self.player)
mod_dir = os.path.join(output_directory, mod_name)
mod_files = {
f"NewItems.xml": generate_new_items(self),
f"InitOptions.lua": generate_setup_file(self),
f"GoodyHutOverride.sql": generate_goody_hut_sql(self),
f"UpdateExistingBoosts.sql": generate_update_boosts_sql(self),
}
mod = CivVIContainer(
mod_files,
mod_dir,
output_directory,
self.player,
self.multiworld.get_file_safe_player_name(self.player),
)
mod.write()

919
worlds/civ_6/data/boosts.py Normal file
View File

@@ -0,0 +1,919 @@
from typing import List
from ..ItemData import CivVIBoostData
boosts: List[CivVIBoostData] = [
CivVIBoostData("BOOST_TECH_SAILING", "ERA_ANCIENT", [], 0, "DEFAULT"),
CivVIBoostData(
"BOOST_TECH_ASTROLOGY",
"ERA_ANCIENT",
[],
0,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_IRRIGATION",
"ERA_ANCIENT",
[],
0,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_ARCHERY",
"ERA_ANCIENT",
[],
0,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_WRITING",
"ERA_ANCIENT",
[],
0,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_MASONRY",
"ERA_ANCIENT",
["TECH_MINING"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_BRONZE_WORKING",
"ERA_ANCIENT",
[],
0,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_TECH_THE_WHEEL",
"ERA_ANCIENT",
["TECH_MINING"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_CELESTIAL_NAVIGATION",
"ERA_CLASSICAL",
["TECH_SAILING"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_CURRENCY",
"ERA_CLASSICAL",
["CIVIC_FOREIGN_TRADE"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_HORSEBACK_RIDING",
"ERA_CLASSICAL",
["TECH_ANIMAL_HUSBANDRY"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_IRON_WORKING",
"ERA_CLASSICAL",
["TECH_MINING"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_SHIPBUILDING",
"ERA_CLASSICAL",
["TECH_SAILING"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_MATHEMATICS",
"ERA_CLASSICAL",
[
"TECH_CURRENCY",
"TECH_BRONZE_WORKING",
"TECH_CELESTIAL_NAVIGATION",
"TECH_WRITING",
"TECH_APPRENTICESHIP",
"TECH_FLIGHT",
"CIVIC_GAMES_RECREATION",
"CIVIC_DRAMA_POETRY",
],
3,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_CONSTRUCTION",
"ERA_CLASSICAL",
["TECH_THE_WHEEL"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_ENGINEERING",
"ERA_CLASSICAL",
["TECH_MASONRY"],
1,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_TECH_MILITARY_TACTICS",
"ERA_MEDIEVAL",
["TECH_BRONZE_WORKING"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_APPRENTICESHIP",
"ERA_MEDIEVAL",
["TECH_MINING"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_MACHINERY",
"ERA_MEDIEVAL",
["TECH_ARCHERY"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_EDUCATION",
"ERA_MEDIEVAL",
["TECH_WRITING"],
1,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_TECH_STIRRUPS",
"ERA_MEDIEVAL",
["CIVIC_FEUDALISM"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_MILITARY_ENGINEERING",
"ERA_MEDIEVAL",
["TECH_ENGINEERING"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_CASTLES",
"ERA_MEDIEVAL",
[
"CIVIC_DIVINE_RIGHT",
"CIVIC_EXPLORATION",
"CIVIC_REFORMED_CHURCH",
"CIVIC_SUFFRAGE",
"CIVIC_TOTALITARIANISM",
"CIVIC_CLASS_STRUGGLE",
"CIVIC_DIGITAL_DEMOCRACY",
"CIVIC_CORPORATE_LIBERTARIANISM",
"CIVIC_SYNTHETIC_TECHNOCRACY",
],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_CARTOGRAPHY",
"ERA_RENAISSANCE",
["TECH_CELESTIAL_NAVIGATION"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_MASS_PRODUCTION",
"ERA_RENAISSANCE",
["TECH_CONSTRUCTION"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_BANKING",
"ERA_RENAISSANCE",
["CIVIC_GUILDS"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_GUNPOWDER",
"ERA_RENAISSANCE",
["TECH_BRONZE_WORKING", "TECH_MILITARY_ENGINEERING"],
2,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_PRINTING",
"ERA_RENAISSANCE",
["TECH_WRITING", "TECH_EDUCATION"],
2,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_SQUARE_RIGGING",
"ERA_RENAISSANCE",
["TECH_GUNPOWDER"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_ASTRONOMY",
"ERA_RENAISSANCE",
["TECH_EDUCATION"],
1,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_TECH_METAL_CASTING",
"ERA_RENAISSANCE",
["TECH_MACHINERY"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_SIEGE_TACTICS",
"ERA_RENAISSANCE",
["TECH_MILITARY_ENGINEERING"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_INDUSTRIALIZATION",
"ERA_INDUSTRIAL",
["TECH_APPRENTICESHIP"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_SCIENTIFIC_THEORY",
"ERA_INDUSTRIAL",
["CIVIC_THE_ENLIGHTENMENT"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_BALLISTICS",
"ERA_INDUSTRIAL",
["TECH_SIEGE_TACTICS", "TECH_MILITARY_ENGINEERING"],
2,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_MILITARY_SCIENCE",
"ERA_INDUSTRIAL",
["TECH_STIRRUPS"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_STEAM_POWER",
"ERA_INDUSTRIAL",
["TECH_MASS_PRODUCTION"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_SANITATION",
"ERA_INDUSTRIAL",
["CIVIC_URBANIZATION"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_ECONOMICS",
"ERA_INDUSTRIAL",
["TECH_CURRENCY", "TECH_BANKING"],
2,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_RIFLING",
"ERA_INDUSTRIAL",
["TECH_MINING", "TECH_MILITARY_ENGINEERING"],
2,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_FLIGHT",
"ERA_MODERN",
[],
0,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_TECH_REPLACEABLE_PARTS",
"ERA_MODERN",
["TECH_MILITARY_SCIENCE"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_STEEL",
"ERA_MODERN",
["TECH_MINING", "TECH_STEAM_POWER", "TECH_INDUSTRIALIZATION"],
3,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_ELECTRICITY",
"ERA_MODERN",
["CIVIC_MERCANTILISM", "TECH_CELESTIAL_NAVIGATION"],
2,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_RADIO",
"ERA_MODERN",
["CIVIC_CONSERVATION"],
1,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_TECH_CHEMISTRY",
"ERA_MODERN",
["CIVIC_CIVIL_SERVICE"],
1,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_TECH_COMBUSTION",
"ERA_MODERN",
["CIVIC_NATURAL_HISTORY", "CIVIC_HUMANISM"],
2,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_TECH_ADVANCED_FLIGHT",
"ERA_ATOMIC",
["TECH_FLIGHT"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_ROCKETRY",
"ERA_ATOMIC",
["CIVIC_DIPLOMATIC_SERVICE"],
1,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_TECH_ADVANCED_BALLISTICS",
"ERA_ATOMIC",
[
"TECH_ELECTRICITY",
"TECH_REFINING",
"TECH_APPRENTICESHIP",
"TECH_INDUSTRIALIZATION",
],
4,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_COMBINED_ARMS",
"ERA_ATOMIC",
["CIVIC_MOBILIZATION", "CIVIC_NATIONALISM"],
2,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_PLASTICS",
"ERA_ATOMIC",
["TECH_REFINING"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_COMPUTERS",
"ERA_ATOMIC",
[
"CIVIC_SUFFRAGE",
"CIVIC_TOTALITARIANISM",
"CIVIC_CLASS_STRUGGLE",
"CIVIC_DIGITAL_DEMOCRACY",
"CIVIC_CORPORATE_LIBERTARIANISM",
"CIVIC_SYNTHETIC_TECHNOCRACY",
],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_NUCLEAR_FISSION",
"ERA_ATOMIC",
["CIVIC_DIPLOMATIC_SERVICE"],
1,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_TECH_SYNTHETIC_MATERIALS",
"ERA_ATOMIC",
["TECH_FLIGHT"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_TELECOMMUNICATIONS",
"ERA_INFORMATION",
["CIVIC_DIPLOMATIC_SERVICE"],
1,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_TECH_SATELLITES",
"ERA_INFORMATION",
["CIVIC_DRAMA_POETRY", "CIVIC_HUMANISM", "TECH_RADIO"],
3,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_GUIDANCE_SYSTEMS",
"ERA_INFORMATION",
[],
0,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_TECH_LASERS",
"ERA_INFORMATION",
["TECH_COMPUTERS"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_COMPOSITES",
"ERA_INFORMATION",
["TECH_COMBUSTION"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_STEALTH_TECHNOLOGY",
"ERA_INFORMATION",
[],
0,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_TECH_ROBOTICS",
"ERA_INFORMATION",
["CIVIC_GLOBALIZATION"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_NANOTECHNOLOGY",
"ERA_INFORMATION",
["TECH_MINING", "TECH_RADIO"],
2,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_NUCLEAR_FUSION",
"ERA_INFORMATION",
[
"TECH_APPRENTICESHIP",
"TECH_INDUSTRIALIZATION",
"TECH_ELECTRICITY",
"TECH_NUCLEAR_FISSION",
],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_BUTTRESS",
"ERA_MEDIEVAL",
[],
0,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_TECH_REFINING",
"ERA_MODERN",
["TECH_INDUSTRIALIZATION", "TECH_MINING", "TECH_APPRENTICESHIP"],
3,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_SEASTEADS",
"ERA_FUTURE",
[],
0,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_TECH_ADVANCED_AI",
"ERA_FUTURE",
[],
0,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_TECH_ADVANCED_POWER_CELLS",
"ERA_FUTURE",
[],
0,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_TECH_CYBERNETICS",
"ERA_FUTURE",
[],
0,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_TECH_SMART_MATERIALS",
"ERA_FUTURE",
[],
0,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_TECH_PREDICTIVE_SYSTEMS",
"ERA_FUTURE",
[],
0,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_TECH_OFFWORLD_MISSION",
"ERA_FUTURE",
[],
0,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_CIVIC_CRAFTSMANSHIP",
"ERA_ANCIENT",
[
"TECH_IRRIGATION",
"TECH_MINING",
"TECH_CONSTRUCTION",
"TECH_ANIMAL_HUSBANDRY",
"TECH_SAILING",
],
3,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_FOREIGN_TRADE",
"ERA_ANCIENT",
["TECH_CARTOGRAPHY"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_MILITARY_TRADITION",
"ERA_ANCIENT",
[],
0,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_CIVIC_STATE_WORKFORCE",
"ERA_ANCIENT",
[
"TECH_CURRENCY",
"TECH_BRONZE_WORKING",
"TECH_CELESTIAL_NAVIGATION",
"TECH_WRITING",
"TECH_APPRENTICESHIP",
"TECH_FLIGHT",
"CIVIC_GAMES_RECREATION",
"CIVIC_DRAMA_POETRY",
],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_EARLY_EMPIRE",
"ERA_ANCIENT",
[],
0,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_MYSTICISM",
"ERA_ANCIENT",
[],
0,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_GAMES_RECREATION",
"ERA_CLASSICAL",
["TECH_CONSTRUCTION"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_POLITICAL_PHILOSOPHY",
"ERA_CLASSICAL",
[],
0,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_CIVIC_DRAMA_POETRY",
"ERA_CLASSICAL",
[],
0,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_CIVIC_MILITARY_TRAINING",
"ERA_CLASSICAL",
["TECH_BRONZE_WORKING"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_DEFENSIVE_TACTICS",
"ERA_CLASSICAL",
[],
0,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_CIVIC_RECORDED_HISTORY",
"ERA_CLASSICAL",
["TECH_WRITING"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_THEOLOGY",
"ERA_CLASSICAL",
["TECH_ASTROLOGY"],
1,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_CIVIC_NAVAL_TRADITION",
"ERA_MEDIEVAL",
["TECH_SHIPBUILDING"],
1,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_CIVIC_FEUDALISM",
"ERA_MEDIEVAL",
[],
0,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_CIVIL_SERVICE",
"ERA_MEDIEVAL",
[],
0,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_MERCENARIES",
"ERA_MEDIEVAL",
[],
0,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_MEDIEVAL_FAIRES",
"ERA_MEDIEVAL",
["CIVIC_FOREIGN_TRADE", "TECH_CURRENCY"],
2,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_GUILDS",
"ERA_MEDIEVAL",
["TECH_CURRENCY"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_DIVINE_RIGHT",
"ERA_MEDIEVAL",
["CIVIC_THEOLOGY", "TECH_ASTROLOGY"],
2,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_EXPLORATION",
"ERA_RENAISSANCE",
["TECH_CARTOGRAPHY", "TECH_CELESTIAL_NAVIGATION"],
2,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_HUMANISM",
"ERA_RENAISSANCE",
["CIVIC_DRAMA_POETRY"],
1,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_CIVIC_DIPLOMATIC_SERVICE",
"ERA_RENAISSANCE",
[],
0,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_CIVIC_REFORMED_CHURCH",
"ERA_RENAISSANCE",
["TECH_ASTROLOGY"],
1,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_CIVIC_MERCANTILISM",
"ERA_RENAISSANCE",
["TECH_CURRENCY"],
1,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_CIVIC_THE_ENLIGHTENMENT",
"ERA_RENAISSANCE",
[],
0,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_CIVIC_COLONIALISM",
"ERA_INDUSTRIAL",
["TECH_ASTRONOMY"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_CIVIL_ENGINEERING",
"ERA_INDUSTRIAL",
[
"TECH_CURRENCY",
"TECH_BRONZE_WORKING",
"TECH_CELESTIAL_NAVIGATION",
"TECH_WRITING",
"TECH_APPRENTICESHIP",
"TECH_FLIGHT",
"CIVIC_GAMES_RECREATION",
"CIVIC_DRAMA_POETRY",
],
8,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_NATIONALISM",
"ERA_INDUSTRIAL",
[],
0,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_CIVIC_OPERA_BALLET",
"ERA_INDUSTRIAL",
["CIVIC_HUMANISM", "CIVIC_DRAMA_POETRY"],
2,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_NATURAL_HISTORY",
"ERA_INDUSTRIAL",
["CIVIC_HUMANISM", "CIVIC_DRAMA_POETRY"],
2,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_SCORCHED_EARTH",
"ERA_INDUSTRIAL",
["TECH_BALLISTICS"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_URBANIZATION",
"ERA_INDUSTRIAL",
[],
0,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_CONSERVATION",
"ERA_MODERN",
["CIVIC_URBANIZATION"],
1,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_CIVIC_CAPITALISM",
"ERA_MODERN",
["TECH_CURRENCY", "TECH_BANKING", "TECH_ECONOMICS"],
3,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_NUCLEAR_PROGRAM",
"ERA_MODERN",
["TECH_WRITING", "TECH_EDUCATION", "TECH_CHEMISTRY"],
3,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_MASS_MEDIA",
"ERA_MODERN",
["TECH_RADIO"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_MOBILIZATION",
"ERA_MODERN",
["CIVIC_NATIONALISM"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_SUFFRAGE",
"ERA_MODERN",
["TECH_SANITATION"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_TOTALITARIANISM",
"ERA_MODERN",
[
"TECH_BRONZE_WORKING",
"TECH_MILITARY_ENGINEERING",
"TECH_MILITARY_SCIENCE",
],
3,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_CLASS_STRUGGLE",
"ERA_MODERN",
["TECH_APPRENTICESHIP", "TECH_INDUSTRIALIZATION"],
2,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_COLD_WAR",
"ERA_ATOMIC",
["TECH_NUCLEAR_FISSION"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_PROFESSIONAL_SPORTS",
"ERA_ATOMIC",
["CIVIC_GAMES_RECREATION"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_CULTURAL_HERITAGE",
"ERA_ATOMIC",
[],
0,
"EXCLUDED",
),
CivVIBoostData(
"BOOST_CIVIC_RAPID_DEPLOYMENT",
"ERA_ATOMIC",
["TECH_FLIGHT", "TECH_CARTOGRAPHY", "TECH_SHIPBUILDING"],
3,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_SPACE_RACE",
"ERA_ATOMIC",
["TECH_ROCKETRY"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_GLOBALIZATION",
"ERA_INFORMATION",
["TECH_FLIGHT", "TECH_ADVANCED_FLIGHT"],
2,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_SOCIAL_MEDIA",
"ERA_INFORMATION",
["TECH_TELECOMMUNICATIONS"],
1,
"DEFAULT",
),
CivVIBoostData(
"BOOST_CIVIC_ENVIRONMENTALISM",
"ERA_INFORMATION",
["TECH_SATELLITES"],
1,
"DEFAULT",
),
]

View File

@@ -0,0 +1,75 @@
from typing import Dict, List
era_required_items: Dict[str, List[str]] = {
"ERA_ANCIENT": [
"TECH_MINING",
"TECH_BRONZE_WORKING",
"TECH_ASTROLOGY",
"TECH_WRITING",
"TECH_IRRIGATION",
"TECH_SAILING",
"TECH_ANIMAL_HUSBANDRY",
"CIVIC_STATE_WORKFORCE",
"CIVIC_FOREIGN_TRADE",
],
"ERA_CLASSICAL": [
"TECH_CELESTIAL_NAVIGATION",
"TECH_CURRENCY",
"TECH_MATHEMATICS",
"TECH_SHIPBUILDING",
"CIVIC_GAMES_RECREATION",
"CIVIC_POLITICAL_PHILOSOPHY",
"CIVIC_DRAMA_POETRY",
"CIVIC_THEOLOGY",
],
"ERA_MEDIEVAL": [
"TECH_APPRENTICESHIP",
"TECH_EDUCATION",
"TECH_MILITARY_ENGINEERING",
"CIVIC_DIVINE_RIGHT",
],
"ERA_RENAISSANCE": [
"TECH_MASS_PRODUCTION",
"TECH_BANKING",
"CIVIC_EXPLORATION",
"CIVIC_HUMANISM",
"CIVIC_REFORMED_CHURCH",
"CIVIC_DIPLOMATIC_SERVICE",
"TECH_CARTOGRAPHY",
],
"ERA_INDUSTRIAL": [
"TECH_INDUSTRIALIZATION",
"TECH_MILITARY_SCIENCE",
"TECH_ECONOMICS",
"CIVIC_NATIONALISM",
"CIVIC_NATURAL_HISTORY",
],
"ERA_MODERN": [
"TECH_FLIGHT",
"TECH_REFINING",
"TECH_ELECTRICITY",
"TECH_RADIO",
"TECH_CHEMISTRY",
"CIVIC_SUFFRAGE",
"CIVIC_TOTALITARIANISM",
"CIVIC_CLASS_STRUGGLE",
],
"ERA_ATOMIC": [
"TECH_ADVANCED_FLIGHT",
"TECH_ROCKETRY",
"TECH_COMBINED_ARMS",
"TECH_PLASTICS",
"TECH_NUCLEAR_FISSION",
"CIVIC_PROFESSIONAL_SPORTS",
],
"ERA_INFORMATION": [
"TECH_SATELLITES",
"TECH_NANOTECHNOLOGY",
"TECH_SMART_MATERIALS",
"CIVIC_CORPORATE_LIBERTARIANISM",
"CIVIC_DIGITAL_DEMOCRACY",
"CIVIC_SYNTHETIC_TECHNOCRACY",
],
"ERA_FUTURE": [],
}

View File

@@ -0,0 +1,435 @@
from typing import TYPE_CHECKING, List
if TYPE_CHECKING:
from ..Data import ExistingItemData
existing_civics: List["ExistingItemData"] = [
{
"Type": "CIVIC_CODE_OF_LAWS",
"Name": "Code of Laws",
"Cost": 20,
"EraType": "ERA_ANCIENT",
"UITreeRow": 0,
},
{
"Type": "CIVIC_CRAFTSMANSHIP",
"Name": "Craftsmanship",
"Cost": 40,
"EraType": "ERA_ANCIENT",
"UITreeRow": -2,
},
{
"Type": "CIVIC_FOREIGN_TRADE",
"Name": "Foreign Trade",
"Cost": 40,
"EraType": "ERA_ANCIENT",
"UITreeRow": 2,
},
{
"Type": "CIVIC_MILITARY_TRADITION",
"Name": "Military Tradition",
"Cost": 50,
"EraType": "ERA_ANCIENT",
"UITreeRow": -3,
},
{
"Type": "CIVIC_STATE_WORKFORCE",
"Name": "State Workforce",
"Cost": 70,
"EraType": "ERA_ANCIENT",
"UITreeRow": 0,
},
{
"Type": "CIVIC_EARLY_EMPIRE",
"Name": "Early Empire",
"Cost": 70,
"EraType": "ERA_ANCIENT",
"UITreeRow": 1,
},
{
"Type": "CIVIC_MYSTICISM",
"Name": "Mysticism",
"Cost": 50,
"EraType": "ERA_ANCIENT",
"UITreeRow": 3,
},
{
"Type": "CIVIC_GAMES_RECREATION",
"Name": "Games Recreation",
"Cost": 110,
"EraType": "ERA_CLASSICAL",
"UITreeRow": -2,
},
{
"Type": "CIVIC_POLITICAL_PHILOSOPHY",
"Name": "Political Philosophy",
"Cost": 110,
"EraType": "ERA_CLASSICAL",
"UITreeRow": 0,
},
{
"Type": "CIVIC_DRAMA_POETRY",
"Name": "Drama and Poetry",
"Cost": 110,
"EraType": "ERA_CLASSICAL",
"UITreeRow": 2,
},
{
"Type": "CIVIC_MILITARY_TRAINING",
"Name": "Military Training",
"Cost": 120,
"EraType": "ERA_CLASSICAL",
"UITreeRow": -3,
},
{
"Type": "CIVIC_DEFENSIVE_TACTICS",
"Name": "Defensive Tactics",
"Cost": 175,
"EraType": "ERA_CLASSICAL",
"UITreeRow": -1,
},
{
"Type": "CIVIC_RECORDED_HISTORY",
"Name": "Recorded History",
"Cost": 175,
"EraType": "ERA_CLASSICAL",
"UITreeRow": 1,
},
{
"Type": "CIVIC_THEOLOGY",
"Name": "Theology",
"Cost": 120,
"EraType": "ERA_CLASSICAL",
"UITreeRow": 3,
},
{
"Type": "CIVIC_NAVAL_TRADITION",
"Name": "Naval Tradition",
"Cost": 220,
"EraType": "ERA_MEDIEVAL",
"UITreeRow": -2,
},
{
"Type": "CIVIC_FEUDALISM",
"Name": "Feudalism",
"Cost": 300,
"EraType": "ERA_MEDIEVAL",
"UITreeRow": -1,
},
{
"Type": "CIVIC_CIVIL_SERVICE",
"Name": "Civil Service",
"Cost": 300,
"EraType": "ERA_MEDIEVAL",
"UITreeRow": 1,
},
{
"Type": "CIVIC_MERCENARIES",
"Name": "Mercenaries",
"Cost": 340,
"EraType": "ERA_MEDIEVAL",
"UITreeRow": -3,
},
{
"Type": "CIVIC_MEDIEVAL_FAIRES",
"Name": "Medieval Faires",
"Cost": 420,
"EraType": "ERA_MEDIEVAL",
"UITreeRow": -1,
},
{
"Type": "CIVIC_GUILDS",
"Name": "Guilds",
"Cost": 420,
"EraType": "ERA_MEDIEVAL",
"UITreeRow": 1,
},
{
"Type": "CIVIC_DIVINE_RIGHT",
"Name": "Divine Right",
"Cost": 340,
"EraType": "ERA_MEDIEVAL",
"UITreeRow": 3,
},
{
"Type": "CIVIC_EXPLORATION",
"Name": "Exploration",
"Cost": 440,
"EraType": "ERA_RENAISSANCE",
"UITreeRow": -3,
},
{
"Type": "CIVIC_HUMANISM",
"Name": "Humanism",
"Cost": 600,
"EraType": "ERA_RENAISSANCE",
"UITreeRow": -1,
},
{
"Type": "CIVIC_DIPLOMATIC_SERVICE",
"Name": "Diplomatic Service",
"Cost": 600,
"EraType": "ERA_RENAISSANCE",
"UITreeRow": 1,
},
{
"Type": "CIVIC_REFORMED_CHURCH",
"Name": "Reformed Church",
"Cost": 440,
"EraType": "ERA_RENAISSANCE",
"UITreeRow": 3,
},
{
"Type": "CIVIC_MERCANTILISM",
"Name": "Mercantilism",
"Cost": 720,
"EraType": "ERA_RENAISSANCE",
"UITreeRow": -1,
},
{
"Type": "CIVIC_THE_ENLIGHTENMENT",
"Name": "The Enlightenment",
"Cost": 720,
"EraType": "ERA_RENAISSANCE",
"UITreeRow": 1,
},
{
"Type": "CIVIC_COLONIALISM",
"Name": "Colonialism",
"Cost": 800,
"EraType": "ERA_INDUSTRIAL",
"UITreeRow": -3,
},
{
"Type": "CIVIC_CIVIL_ENGINEERING",
"Name": "Civil Engineering",
"Cost": 1010,
"EraType": "ERA_INDUSTRIAL",
"UITreeRow": -1,
},
{
"Type": "CIVIC_NATIONALISM",
"Name": "Nationalism",
"Cost": 1010,
"EraType": "ERA_INDUSTRIAL",
"UITreeRow": 0,
},
{
"Type": "CIVIC_OPERA_BALLET",
"Name": "Opera and Ballet",
"Cost": 800,
"EraType": "ERA_INDUSTRIAL",
"UITreeRow": 2,
},
{
"Type": "CIVIC_NATURAL_HISTORY",
"Name": "Natural History",
"Cost": 1050,
"EraType": "ERA_INDUSTRIAL",
"UITreeRow": -3,
},
{
"Type": "CIVIC_SCORCHED_EARTH",
"Name": "Scorched Earth",
"Cost": 1210,
"EraType": "ERA_INDUSTRIAL",
"UITreeRow": 2,
},
{
"Type": "CIVIC_URBANIZATION",
"Name": "Urbanization",
"Cost": 1210,
"EraType": "ERA_INDUSTRIAL",
"UITreeRow": -1,
},
{
"Type": "CIVIC_CONSERVATION",
"Name": "Conservation",
"Cost": 1540,
"EraType": "ERA_MODERN",
"UITreeRow": -3,
},
{
"Type": "CIVIC_CAPITALISM",
"Name": "Capitalism",
"Cost": 1580,
"EraType": "ERA_MODERN",
"UITreeRow": -2,
},
{
"Type": "CIVIC_NUCLEAR_PROGRAM",
"Name": "Nuclear Program",
"Cost": 1715,
"EraType": "ERA_MODERN",
"UITreeRow": -2,
},
{
"Type": "CIVIC_MASS_MEDIA",
"Name": "Mass Media",
"Cost": 1540,
"EraType": "ERA_MODERN",
"UITreeRow": -1,
},
{
"Type": "CIVIC_MOBILIZATION",
"Name": "Mobilization",
"Cost": 1540,
"EraType": "ERA_MODERN",
"UITreeRow": 1,
},
{
"Type": "CIVIC_IDEOLOGY",
"Name": "Ideology",
"Cost": 1640,
"EraType": "ERA_MODERN",
"UITreeRow": -1,
},
{
"Type": "CIVIC_SUFFRAGE",
"Name": "Suffrage",
"Cost": 1640,
"EraType": "ERA_MODERN",
"UITreeRow": 0,
},
{
"Type": "CIVIC_TOTALITARIANISM",
"Name": "Totalitarianism",
"Cost": 1640,
"EraType": "ERA_MODERN",
"UITreeRow": 2,
},
{
"Type": "CIVIC_CLASS_STRUGGLE",
"Name": "Class Struggle",
"Cost": 1640,
"EraType": "ERA_MODERN",
"UITreeRow": 3,
},
{
"Type": "CIVIC_COLD_WAR",
"Name": "Cold War",
"Cost": 2185,
"EraType": "ERA_ATOMIC",
"UITreeRow": -1,
},
{
"Type": "CIVIC_PROFESSIONAL_SPORTS",
"Name": "Professional Sports",
"Cost": 2185,
"EraType": "ERA_ATOMIC",
"UITreeRow": 2,
},
{
"Type": "CIVIC_CULTURAL_HERITAGE",
"Name": "Cultural Heritage",
"Cost": 1955,
"EraType": "ERA_ATOMIC",
"UITreeRow": -3,
},
{
"Type": "CIVIC_RAPID_DEPLOYMENT",
"Name": "Rapid Deployment",
"Cost": 2415,
"EraType": "ERA_ATOMIC",
"UITreeRow": -1,
},
{
"Type": "CIVIC_SPACE_RACE",
"Name": "Space Race",
"Cost": 2415,
"EraType": "ERA_ATOMIC",
"UITreeRow": 1,
},
{
"Type": "CIVIC_GLOBALIZATION",
"Name": "Globalization",
"Cost": 2880,
"EraType": "ERA_INFORMATION",
"UITreeRow": 0,
},
{
"Type": "CIVIC_SOCIAL_MEDIA",
"Name": "Social Media",
"Cost": 2880,
"EraType": "ERA_INFORMATION",
"UITreeRow": 2,
},
{
"Type": "CIVIC_FUTURE_CIVIC",
"Name": "Future Civic",
"Cost": 3500,
"EraType": "ERA_FUTURE",
"UITreeRow": 1,
},
{
"Type": "CIVIC_ENVIRONMENTALISM",
"Name": "Environmentalism",
"Cost": 2880,
"EraType": "ERA_INFORMATION",
"UITreeRow": -2,
},
{
"Type": "CIVIC_CORPORATE_LIBERTARIANISM",
"Name": "Corporate Libertarianism",
"Cost": 3000,
"EraType": "ERA_INFORMATION",
"UITreeRow": 0,
},
{
"Type": "CIVIC_DIGITAL_DEMOCRACY",
"Name": "Digital Democracy",
"Cost": 3000,
"EraType": "ERA_INFORMATION",
"UITreeRow": 1,
},
{
"Type": "CIVIC_SYNTHETIC_TECHNOCRACY",
"Name": "Synthetic Technocracy",
"Cost": 3000,
"EraType": "ERA_INFORMATION",
"UITreeRow": 2,
},
{
"Type": "CIVIC_NEAR_FUTURE_GOVERNANCE",
"Name": "Near Future Governance",
"Cost": 3100,
"EraType": "ERA_INFORMATION",
"UITreeRow": -1,
},
{
"Type": "CIVIC_GLOBAL_WARMING_MITIGATION",
"Name": "Global Warming Mitigation",
"Cost": 3200,
"EraType": "ERA_FUTURE",
"UITreeRow": -2,
},
{
"Type": "CIVIC_SMART_POWER_DOCTRINE",
"Name": "Smart Power Doctrine",
"Cost": 3200,
"EraType": "ERA_FUTURE",
"UITreeRow": -1,
},
{
"Type": "CIVIC_INFORMATION_WARFARE",
"Name": "Information Warfare",
"Cost": 3200,
"EraType": "ERA_FUTURE",
"UITreeRow": 0,
},
{
"Type": "CIVIC_EXODUS_IMPERATIVE",
"Name": "Exodus Imperative",
"Cost": 3200,
"EraType": "ERA_FUTURE",
"UITreeRow": 1,
},
{
"Type": "CIVIC_CULTURAL_HEGEMONY",
"Name": "Cultural Hegemony",
"Cost": 3200,
"EraType": "ERA_FUTURE",
"UITreeRow": 2,
},
]

View File

@@ -0,0 +1,546 @@
from typing import List
from ..ItemData import ExistingItemData
existing_tech: List[ExistingItemData] = [
{
"Type": "TECH_POTTERY",
"Cost": 25,
"UITreeRow": 0,
"EraType": "ERA_ANCIENT",
"Name": "Pottery",
},
{
"Type": "TECH_ANIMAL_HUSBANDRY",
"Cost": 25,
"UITreeRow": 1,
"EraType": "ERA_ANCIENT",
"Name": "Animal Husbandry",
},
{
"Type": "TECH_MINING",
"Cost": 25,
"UITreeRow": 3,
"EraType": "ERA_ANCIENT",
"Name": "Mining",
},
{
"Type": "TECH_SAILING",
"Cost": 50,
"UITreeRow": -3,
"EraType": "ERA_ANCIENT",
"Name": "Sailing",
},
{
"Type": "TECH_ASTROLOGY",
"Cost": 50,
"UITreeRow": -2,
"EraType": "ERA_ANCIENT",
"Name": "Astrology",
},
{
"Type": "TECH_IRRIGATION",
"Cost": 50,
"UITreeRow": -1,
"EraType": "ERA_ANCIENT",
"Name": "Irrigation",
},
{
"Type": "TECH_ARCHERY",
"Cost": 50,
"UITreeRow": 1,
"EraType": "ERA_ANCIENT",
"Name": "Archery",
},
{
"Type": "TECH_WRITING",
"Cost": 50,
"UITreeRow": 0,
"EraType": "ERA_ANCIENT",
"Name": "Writing",
},
{
"Type": "TECH_MASONRY",
"Cost": 80,
"UITreeRow": 2,
"EraType": "ERA_ANCIENT",
"Name": "Masonry",
},
{
"Type": "TECH_BRONZE_WORKING",
"Cost": 80,
"UITreeRow": 3,
"EraType": "ERA_ANCIENT",
"Name": "Bronze Working",
},
{
"Type": "TECH_THE_WHEEL",
"Cost": 80,
"UITreeRow": 4,
"EraType": "ERA_ANCIENT",
"Name": "The Wheel",
},
{
"Type": "TECH_CELESTIAL_NAVIGATION",
"Cost": 120,
"UITreeRow": -2,
"EraType": "ERA_CLASSICAL",
"Name": "Celestial Navigation",
},
{
"Type": "TECH_CURRENCY",
"Cost": 120,
"UITreeRow": 0,
"EraType": "ERA_CLASSICAL",
"Name": "Currency",
},
{
"Type": "TECH_HORSEBACK_RIDING",
"Cost": 120,
"UITreeRow": 1,
"EraType": "ERA_CLASSICAL",
"Name": "Horseback Riding",
},
{
"Type": "TECH_IRON_WORKING",
"Cost": 120,
"UITreeRow": 3,
"EraType": "ERA_CLASSICAL",
"Name": "Iron Working",
},
{
"Type": "TECH_SHIPBUILDING",
"Cost": 200,
"UITreeRow": -3,
"EraType": "ERA_CLASSICAL",
"Name": "Shipbuilding",
},
{
"Type": "TECH_MATHEMATICS",
"Cost": 200,
"UITreeRow": -1,
"EraType": "ERA_CLASSICAL",
"Name": "Mathematics",
},
{
"Type": "TECH_CONSTRUCTION",
"Cost": 200,
"UITreeRow": 2,
"EraType": "ERA_CLASSICAL",
"Name": "Construction",
},
{
"Type": "TECH_ENGINEERING",
"Cost": 200,
"UITreeRow": 4,
"EraType": "ERA_CLASSICAL",
"Name": "Engineering",
},
{
"Type": "TECH_MILITARY_TACTICS",
"Cost": 300,
"UITreeRow": -2,
"EraType": "ERA_MEDIEVAL",
"Name": "Military Tactics",
},
{
"Type": "TECH_APPRENTICESHIP",
"Cost": 300,
"UITreeRow": 0,
"EraType": "ERA_MEDIEVAL",
"Name": "Apprenticeship",
},
{
"Type": "TECH_MACHINERY",
"Cost": 300,
"UITreeRow": 4,
"EraType": "ERA_MEDIEVAL",
"Name": "Machinery",
},
{
"Type": "TECH_EDUCATION",
"Cost": 390,
"UITreeRow": -1,
"EraType": "ERA_MEDIEVAL",
"Name": "Education",
},
{
"Type": "TECH_STIRRUPS",
"Cost": 390,
"UITreeRow": 1,
"EraType": "ERA_MEDIEVAL",
"Name": "Stirrups",
},
{
"Type": "TECH_MILITARY_ENGINEERING",
"Cost": 390,
"UITreeRow": 2,
"EraType": "ERA_MEDIEVAL",
"Name": "Military Engineering",
},
{
"Type": "TECH_CASTLES",
"Cost": 390,
"UITreeRow": 3,
"EraType": "ERA_MEDIEVAL",
"Name": "Castles",
},
{
"Type": "TECH_CARTOGRAPHY",
"Cost": 600,
"UITreeRow": -3,
"EraType": "ERA_RENAISSANCE",
"Name": "Cartography",
},
{
"Type": "TECH_MASS_PRODUCTION",
"Cost": 600,
"UITreeRow": -2,
"EraType": "ERA_RENAISSANCE",
"Name": "Mass Production",
},
{
"Type": "TECH_BANKING",
"Cost": 600,
"UITreeRow": 0,
"EraType": "ERA_RENAISSANCE",
"Name": "Banking",
},
{
"Type": "TECH_GUNPOWDER",
"Cost": 600,
"UITreeRow": 1,
"EraType": "ERA_RENAISSANCE",
"Name": "Gunpowder",
},
{
"Type": "TECH_PRINTING",
"Cost": 600,
"UITreeRow": 4,
"EraType": "ERA_RENAISSANCE",
"Name": "Printing",
},
{
"Type": "TECH_SQUARE_RIGGING",
"Cost": 730,
"UITreeRow": -3,
"EraType": "ERA_RENAISSANCE",
"Name": "Square Rigging",
},
{
"Type": "TECH_ASTRONOMY",
"Cost": 730,
"UITreeRow": -1,
"EraType": "ERA_RENAISSANCE",
"Name": "Astronomy",
},
{
"Type": "TECH_METAL_CASTING",
"Cost": 730,
"UITreeRow": 1,
"EraType": "ERA_RENAISSANCE",
"Name": "Metal Casting",
},
{
"Type": "TECH_SIEGE_TACTICS",
"Cost": 730,
"UITreeRow": 3,
"EraType": "ERA_RENAISSANCE",
"Name": "Siege Tactics",
},
{
"Type": "TECH_INDUSTRIALIZATION",
"Cost": 930,
"UITreeRow": -2,
"EraType": "ERA_INDUSTRIAL",
"Name": "Industrialization",
},
{
"Type": "TECH_SCIENTIFIC_THEORY",
"Cost": 930,
"UITreeRow": -1,
"EraType": "ERA_INDUSTRIAL",
"Name": "Scientific Theory",
},
{
"Type": "TECH_BALLISTICS",
"Cost": 930,
"UITreeRow": 1,
"EraType": "ERA_INDUSTRIAL",
"Name": "Ballistics",
},
{
"Type": "TECH_MILITARY_SCIENCE",
"Cost": 930,
"UITreeRow": 3,
"EraType": "ERA_INDUSTRIAL",
"Name": "Military Science",
},
{
"Type": "TECH_STEAM_POWER",
"Cost": 1070,
"UITreeRow": -3,
"EraType": "ERA_INDUSTRIAL",
"Name": "Steam Power",
},
{
"Type": "TECH_SANITATION",
"Cost": 1070,
"UITreeRow": -1,
"EraType": "ERA_INDUSTRIAL",
"Name": "Sanitation",
},
{
"Type": "TECH_ECONOMICS",
"Cost": 1070,
"UITreeRow": 0,
"EraType": "ERA_INDUSTRIAL",
"Name": "Economics",
},
{
"Type": "TECH_RIFLING",
"Cost": 1070,
"UITreeRow": 2,
"EraType": "ERA_INDUSTRIAL",
"Name": "Rifling",
},
{
"Type": "TECH_FLIGHT",
"Cost": 1250,
"UITreeRow": -2,
"EraType": "ERA_MODERN",
"Name": "Flight",
},
{
"Type": "TECH_REPLACEABLE_PARTS",
"Cost": 1250,
"UITreeRow": 0,
"EraType": "ERA_MODERN",
"Name": "Replaceable Parts",
},
{
"Type": "TECH_STEEL",
"Cost": 1250,
"UITreeRow": 1,
"EraType": "ERA_MODERN",
"Name": "Steel",
},
{
"Type": "TECH_ELECTRICITY",
"Cost": 1370,
"UITreeRow": -3,
"EraType": "ERA_MODERN",
"Name": "Electricity",
},
{
"Type": "TECH_RADIO",
"Cost": 1370,
"UITreeRow": -2,
"EraType": "ERA_MODERN",
"Name": "Radio",
},
{
"Type": "TECH_CHEMISTRY",
"Cost": 1370,
"UITreeRow": -1,
"EraType": "ERA_MODERN",
"Name": "Chemistry",
},
{
"Type": "TECH_COMBUSTION",
"Cost": 1370,
"UITreeRow": 2,
"EraType": "ERA_MODERN",
"Name": "Combustion",
},
{
"Type": "TECH_ADVANCED_FLIGHT",
"Cost": 1480,
"UITreeRow": -2,
"EraType": "ERA_ATOMIC",
"Name": "Advanced Flight",
},
{
"Type": "TECH_ROCKETRY",
"Cost": 1480,
"UITreeRow": -1,
"EraType": "ERA_ATOMIC",
"Name": "Rocketry",
},
{
"Type": "TECH_ADVANCED_BALLISTICS",
"Cost": 1480,
"UITreeRow": 0,
"EraType": "ERA_ATOMIC",
"Name": "Advanced Ballistics",
},
{
"Type": "TECH_COMBINED_ARMS",
"Cost": 1480,
"UITreeRow": 1,
"EraType": "ERA_ATOMIC",
"Name": "Combined Arms",
},
{
"Type": "TECH_PLASTICS",
"Cost": 1480,
"UITreeRow": 2,
"EraType": "ERA_ATOMIC",
"Name": "Plastics",
},
{
"Type": "TECH_COMPUTERS",
"Cost": 1660,
"UITreeRow": -3,
"EraType": "ERA_ATOMIC",
"Name": "Computers",
},
{
"Type": "TECH_NUCLEAR_FISSION",
"Cost": 1660,
"UITreeRow": 1,
"EraType": "ERA_ATOMIC",
"Name": "Nuclear Fission",
},
{
"Type": "TECH_SYNTHETIC_MATERIALS",
"Cost": 1660,
"UITreeRow": 2,
"EraType": "ERA_ATOMIC",
"Name": "Synthetic Materials",
},
{
"Type": "TECH_TELECOMMUNICATIONS",
"Cost": 1850,
"UITreeRow": -3,
"EraType": "ERA_INFORMATION",
"Name": "Telecommunications",
},
{
"Type": "TECH_SATELLITES",
"Cost": 1850,
"UITreeRow": -1,
"EraType": "ERA_INFORMATION",
"Name": "Satellites",
},
{
"Type": "TECH_GUIDANCE_SYSTEMS",
"Cost": 1850,
"UITreeRow": 0,
"EraType": "ERA_INFORMATION",
"Name": "Guidance Systems",
},
{
"Type": "TECH_LASERS",
"Cost": 1850,
"UITreeRow": 1,
"EraType": "ERA_INFORMATION",
"Name": "Lasers",
},
{
"Type": "TECH_COMPOSITES",
"Cost": 1850,
"UITreeRow": 2,
"EraType": "ERA_INFORMATION",
"Name": "Composites",
},
{
"Type": "TECH_STEALTH_TECHNOLOGY",
"Cost": 1850,
"UITreeRow": 3,
"EraType": "ERA_INFORMATION",
"Name": "Stealth Technology",
},
{
"Type": "TECH_ROBOTICS",
"Cost": 2155,
"UITreeRow": -2,
"EraType": "ERA_INFORMATION",
"Name": "Robotics",
},
{
"Type": "TECH_NANOTECHNOLOGY",
"Cost": 2155,
"UITreeRow": 2,
"EraType": "ERA_INFORMATION",
"Name": "Nanotechnology",
},
{
"Type": "TECH_NUCLEAR_FUSION",
"Cost": 2155,
"UITreeRow": 1,
"EraType": "ERA_INFORMATION",
"Name": "Nuclear Fusion",
},
{
"Type": "TECH_BUTTRESS",
"Cost": 300,
"UITreeRow": -3,
"EraType": "ERA_MEDIEVAL",
"Name": "Buttress",
},
{
"Type": "TECH_REFINING",
"Cost": 1250,
"UITreeRow": 3,
"EraType": "ERA_MODERN",
"Name": "Refining",
},
{
"Type": "TECH_SEASTEADS",
"Cost": 2200,
"UITreeRow": -3,
"EraType": "ERA_FUTURE",
"Name": "Seasteads",
},
{
"Type": "TECH_ADVANCED_AI",
"Cost": 2200,
"UITreeRow": -2,
"EraType": "ERA_FUTURE",
"Name": "Advanced AI",
},
{
"Type": "TECH_ADVANCED_POWER_CELLS",
"Cost": 2200,
"UITreeRow": -1,
"EraType": "ERA_FUTURE",
"Name": "Advanced Power Cells",
},
{
"Type": "TECH_CYBERNETICS",
"Cost": 2200,
"UITreeRow": 0,
"EraType": "ERA_FUTURE",
"Name": "Cybernetics",
},
{
"Type": "TECH_SMART_MATERIALS",
"Cost": 2200,
"UITreeRow": 1,
"EraType": "ERA_FUTURE",
"Name": "Smart Materials",
},
{
"Type": "TECH_PREDICTIVE_SYSTEMS",
"Cost": 2200,
"UITreeRow": 2,
"EraType": "ERA_FUTURE",
"Name": "Predictive Systems",
},
{
"Type": "TECH_OFFWORLD_MISSION",
"Cost": 2500,
"UITreeRow": 0,
"EraType": "ERA_FUTURE",
"Name": "Offworld Mission",
},
{
"Type": "TECH_FUTURE_TECH",
"Cost": 2600,
"UITreeRow": 0,
"EraType": "ERA_FUTURE",
"Name": "Future Tech",
},
]

View File

@@ -0,0 +1,81 @@
from typing import List
from ..ItemData import GoodyHutRewardData
reward_data: List[GoodyHutRewardData] = [
{
"Type": "GOODY_GOLD_SMALL_MODIFIER",
"Rarity": "COMMON",
"Name": "Gold: Small"
},
{
"Type": "GOODY_GOLD_MEDIUM_MODIFIER",
"Rarity": "COMMON",
"Name": "Gold: Medium"
},
{
"Type": "GOODY_GOLD_LARGE_MODIFIER",
"Rarity": "UNCOMMON",
"Name": "Gold: Large"
},
{
"Type": "GOODY_FAITH_SMALL_MODIFIER",
"Rarity": "COMMON",
"Name": "Faith: Small"
},
{
"Type": "GOODY_FAITH_MEDIUM_MODIFIER",
"Rarity": "COMMON",
"Name": "Faith: Medium"
},
{
"Type": "GOODY_FAITH_LARGE_MODIFIER",
"Rarity": "UNCOMMON",
"Name": "Faith: Large"
},
{
"Type": "GOODY_DIPLOMACY_GRANT_FAVOR",
"Rarity": "COMMON",
"Name": "Diplomatic Favor"
},
{
"Type": "GOODY_DIPLOMACY_GRANT_GOVERNOR_TITLE",
"Rarity": "RARE",
"Name": "Governor Title"
},
{
"Type": "GOODY_DIPLOMACY_GRANT_ENVOY",
"Rarity": "UNCOMMON",
"Name": "Envoy"
},
{
"Type": "GOODY_CULTURE_GRANT_ONE_RELIC",
"Rarity": "RARE",
"Name": "Relic"
},
{
"Type": "GOODY_MILITARY_GRANT_SCOUT",
"Rarity": "UNCOMMON",
"Name": "Scout"
},
{
"Type": "GOODY_SURVIVORS_ADD_POPULATION",
"Rarity": "UNCOMMON",
"Name": "Additional Population"
},
{
"Type": "GOODY_SURVIVORS_GRANT_BUILDER",
"Rarity": "UNCOMMON",
"Name": "Builder"
},
{
"Type": "GOODY_SURVIVORS_GRANT_TRADER",
"Rarity": "UNCOMMON",
"Name": "Trader"
},
{
"Type": "GOODY_SURVIVORS_GRANT_SETTLER",
"Rarity": "UNCOMMON",
"Name": "Settler"
}
]

View File

@@ -0,0 +1,92 @@
from typing import List
from ..ItemData import CivicPrereqData
new_civic_prereqs: List[CivicPrereqData] = [
{"Civic": "CIVIC_AP_ANCIENT_01", "PrereqCivic": "CIVIC_AP_ANCIENT_00"},
{"Civic": "CIVIC_AP_ANCIENT_02", "PrereqCivic": "CIVIC_AP_ANCIENT_00"},
{"Civic": "CIVIC_AP_ANCIENT_03", "PrereqCivic": "CIVIC_AP_ANCIENT_01"},
{"Civic": "CIVIC_AP_ANCIENT_04", "PrereqCivic": "CIVIC_AP_ANCIENT_01"},
{"Civic": "CIVIC_AP_ANCIENT_05", "PrereqCivic": "CIVIC_AP_ANCIENT_02"},
{"Civic": "CIVIC_AP_ANCIENT_06", "PrereqCivic": "CIVIC_AP_ANCIENT_02"},
{"Civic": "CIVIC_AP_CLASSICAL_07", "PrereqCivic": "CIVIC_AP_ANCIENT_04"},
{"Civic": "CIVIC_AP_CLASSICAL_08", "PrereqCivic": "CIVIC_AP_ANCIENT_04"},
{"Civic": "CIVIC_AP_CLASSICAL_08", "PrereqCivic": "CIVIC_AP_ANCIENT_05"},
{"Civic": "CIVIC_AP_CLASSICAL_09", "PrereqCivic": "CIVIC_AP_ANCIENT_05"},
{"Civic": "CIVIC_AP_CLASSICAL_10", "PrereqCivic": "CIVIC_AP_ANCIENT_03"},
{"Civic": "CIVIC_AP_CLASSICAL_10", "PrereqCivic": "CIVIC_AP_CLASSICAL_07"},
{"Civic": "CIVIC_AP_CLASSICAL_11", "PrereqCivic": "CIVIC_AP_CLASSICAL_07"},
{"Civic": "CIVIC_AP_CLASSICAL_11", "PrereqCivic": "CIVIC_AP_CLASSICAL_08"},
{"Civic": "CIVIC_AP_CLASSICAL_12", "PrereqCivic": "CIVIC_AP_CLASSICAL_08"},
{"Civic": "CIVIC_AP_CLASSICAL_12", "PrereqCivic": "CIVIC_AP_CLASSICAL_09"},
{"Civic": "CIVIC_AP_CLASSICAL_13", "PrereqCivic": "CIVIC_AP_CLASSICAL_09"},
{"Civic": "CIVIC_AP_CLASSICAL_13", "PrereqCivic": "CIVIC_AP_ANCIENT_06"},
{"Civic": "CIVIC_AP_MEDIEVAL_14", "PrereqCivic": "CIVIC_AP_CLASSICAL_11"},
{"Civic": "CIVIC_AP_MEDIEVAL_15", "PrereqCivic": "CIVIC_AP_CLASSICAL_11"},
{"Civic": "CIVIC_AP_MEDIEVAL_16", "PrereqCivic": "CIVIC_AP_CLASSICAL_11"},
{"Civic": "CIVIC_AP_MEDIEVAL_16", "PrereqCivic": "CIVIC_AP_CLASSICAL_12"},
{"Civic": "CIVIC_AP_MEDIEVAL_17", "PrereqCivic": "CIVIC_AP_CLASSICAL_10"},
{"Civic": "CIVIC_AP_MEDIEVAL_17", "PrereqCivic": "CIVIC_AP_MEDIEVAL_15"},
{"Civic": "CIVIC_AP_MEDIEVAL_18", "PrereqCivic": "CIVIC_AP_MEDIEVAL_15"},
{"Civic": "CIVIC_AP_MEDIEVAL_19", "PrereqCivic": "CIVIC_AP_MEDIEVAL_15"},
{"Civic": "CIVIC_AP_MEDIEVAL_19", "PrereqCivic": "CIVIC_AP_MEDIEVAL_16"},
{"Civic": "CIVIC_AP_MEDIEVAL_20", "PrereqCivic": "CIVIC_AP_MEDIEVAL_16"},
{"Civic": "CIVIC_AP_MEDIEVAL_20", "PrereqCivic": "CIVIC_AP_CLASSICAL_13"},
{"Civic": "CIVIC_AP_RENAISSANCE_21", "PrereqCivic": "CIVIC_AP_MEDIEVAL_17"},
{"Civic": "CIVIC_AP_RENAISSANCE_21", "PrereqCivic": "CIVIC_AP_MEDIEVAL_18"},
{"Civic": "CIVIC_AP_RENAISSANCE_22", "PrereqCivic": "CIVIC_AP_MEDIEVAL_18"},
{"Civic": "CIVIC_AP_RENAISSANCE_22", "PrereqCivic": "CIVIC_AP_MEDIEVAL_19"},
{"Civic": "CIVIC_AP_RENAISSANCE_23", "PrereqCivic": "CIVIC_AP_MEDIEVAL_19"},
{"Civic": "CIVIC_AP_RENAISSANCE_24", "PrereqCivic": "CIVIC_AP_MEDIEVAL_19"},
{"Civic": "CIVIC_AP_RENAISSANCE_24", "PrereqCivic": "CIVIC_AP_MEDIEVAL_20"},
{"Civic": "CIVIC_AP_RENAISSANCE_25", "PrereqCivic": "CIVIC_AP_RENAISSANCE_22"},
{"Civic": "CIVIC_AP_RENAISSANCE_26", "PrereqCivic": "CIVIC_AP_RENAISSANCE_22"},
{"Civic": "CIVIC_AP_RENAISSANCE_26", "PrereqCivic": "CIVIC_AP_RENAISSANCE_23"},
{"Civic": "CIVIC_AP_INDUSTRIAL_27", "PrereqCivic": "CIVIC_AP_RENAISSANCE_25"},
{"Civic": "CIVIC_AP_INDUSTRIAL_28", "PrereqCivic": "CIVIC_AP_RENAISSANCE_25"},
{"Civic": "CIVIC_AP_INDUSTRIAL_29", "PrereqCivic": "CIVIC_AP_RENAISSANCE_26"},
{"Civic": "CIVIC_AP_INDUSTRIAL_30", "PrereqCivic": "CIVIC_AP_RENAISSANCE_26"},
{"Civic": "CIVIC_AP_INDUSTRIAL_31", "PrereqCivic": "CIVIC_AP_INDUSTRIAL_27"},
{"Civic": "CIVIC_AP_INDUSTRIAL_32", "PrereqCivic": "CIVIC_AP_INDUSTRIAL_29"},
{"Civic": "CIVIC_AP_INDUSTRIAL_33", "PrereqCivic": "CIVIC_AP_INDUSTRIAL_28"},
{"Civic": "CIVIC_AP_INDUSTRIAL_33", "PrereqCivic": "CIVIC_AP_INDUSTRIAL_29"},
{"Civic": "CIVIC_AP_MODERN_34", "PrereqCivic": "CIVIC_AP_INDUSTRIAL_31"},
{"Civic": "CIVIC_AP_MODERN_37", "PrereqCivic": "CIVIC_AP_INDUSTRIAL_31"},
{"Civic": "CIVIC_AP_MODERN_37", "PrereqCivic": "CIVIC_AP_INDUSTRIAL_33"},
{"Civic": "CIVIC_AP_MODERN_35", "PrereqCivic": "CIVIC_AP_MODERN_37"},
{"Civic": "CIVIC_AP_MODERN_38", "PrereqCivic": "CIVIC_AP_INDUSTRIAL_33"},
{"Civic": "CIVIC_AP_MODERN_39", "PrereqCivic": "CIVIC_AP_MODERN_37"},
{"Civic": "CIVIC_AP_MODERN_39", "PrereqCivic": "CIVIC_AP_MODERN_38"},
{"Civic": "CIVIC_AP_MODERN_36", "PrereqCivic": "CIVIC_AP_MODERN_39"},
{"Civic": "CIVIC_AP_MODERN_40", "PrereqCivic": "CIVIC_AP_MODERN_39"},
{"Civic": "CIVIC_AP_MODERN_41", "PrereqCivic": "CIVIC_AP_MODERN_39"},
{"Civic": "CIVIC_AP_MODERN_42", "PrereqCivic": "CIVIC_AP_MODERN_39"},
{"Civic": "CIVIC_AP_ATOMIC_43", "PrereqCivic": "CIVIC_AP_MODERN_39"},
{"Civic": "CIVIC_AP_ATOMIC_44", "PrereqCivic": "CIVIC_AP_MODERN_39"},
{"Civic": "CIVIC_AP_ATOMIC_45", "PrereqCivic": "CIVIC_AP_MODERN_34"},
{"Civic": "CIVIC_AP_ATOMIC_46", "PrereqCivic": "CIVIC_AP_ATOMIC_43"},
{"Civic": "CIVIC_AP_ATOMIC_47", "PrereqCivic": "CIVIC_AP_ATOMIC_43"},
{"Civic": "CIVIC_AP_INFORMATION_48", "PrereqCivic": "CIVIC_AP_ATOMIC_46"},
{"Civic": "CIVIC_AP_INFORMATION_48", "PrereqCivic": "CIVIC_AP_ATOMIC_47"},
{"Civic": "CIVIC_AP_INFORMATION_49", "PrereqCivic": "CIVIC_AP_ATOMIC_47"},
{"Civic": "CIVIC_AP_INFORMATION_49", "PrereqCivic": "CIVIC_AP_ATOMIC_44"},
{"Civic": "CIVIC_AP_FUTURE_50", "PrereqCivic": "CIVIC_AP_INFORMATION_48"},
{"Civic": "CIVIC_AP_FUTURE_50", "PrereqCivic": "CIVIC_AP_INFORMATION_49"},
{"Civic": "CIVIC_AP_MODERN_38", "PrereqCivic": "CIVIC_AP_INDUSTRIAL_32"},
{"Civic": "CIVIC_AP_INFORMATION_51", "PrereqCivic": "CIVIC_AP_ATOMIC_45"},
{"Civic": "CIVIC_AP_INFORMATION_51", "PrereqCivic": "CIVIC_AP_ATOMIC_46"},
{"Civic": "CIVIC_AP_INFORMATION_52", "PrereqCivic": "CIVIC_AP_INFORMATION_48"},
{"Civic": "CIVIC_AP_INFORMATION_52", "PrereqCivic": "CIVIC_AP_INFORMATION_49"},
{"Civic": "CIVIC_AP_INFORMATION_53", "PrereqCivic": "CIVIC_AP_INFORMATION_48"},
{"Civic": "CIVIC_AP_INFORMATION_53", "PrereqCivic": "CIVIC_AP_INFORMATION_49"},
{"Civic": "CIVIC_AP_INFORMATION_54", "PrereqCivic": "CIVIC_AP_INFORMATION_48"},
{"Civic": "CIVIC_AP_INFORMATION_54", "PrereqCivic": "CIVIC_AP_INFORMATION_49"},
{"Civic": "CIVIC_AP_INFORMATION_55", "PrereqCivic": "CIVIC_AP_INFORMATION_51"},
{"Civic": "CIVIC_AP_INFORMATION_55", "PrereqCivic": "CIVIC_AP_INFORMATION_48"},
{"Civic": "CIVIC_AP_FUTURE_56", "PrereqCivic": "CIVIC_AP_FUTURE_50"},
{"Civic": "CIVIC_AP_FUTURE_57", "PrereqCivic": "CIVIC_AP_FUTURE_50"},
{"Civic": "CIVIC_AP_FUTURE_58", "PrereqCivic": "CIVIC_AP_FUTURE_50"},
{"Civic": "CIVIC_AP_FUTURE_59", "PrereqCivic": "CIVIC_AP_FUTURE_50"},
{"Civic": "CIVIC_AP_FUTURE_60", "PrereqCivic": "CIVIC_AP_FUTURE_50"},
]

View File

@@ -0,0 +1,372 @@
from typing import List
from ..ItemData import NewItemData
new_civics: List[NewItemData] = [
{
"Type": "CIVIC_AP_ANCIENT_00",
"Cost": 20,
"UITreeRow": 0,
"EraType": "ERA_ANCIENT",
},
{
"Type": "CIVIC_AP_ANCIENT_01",
"Cost": 40,
"UITreeRow": -2,
"EraType": "ERA_ANCIENT",
},
{
"Type": "CIVIC_AP_ANCIENT_02",
"Cost": 40,
"UITreeRow": 2,
"EraType": "ERA_ANCIENT",
},
{
"Type": "CIVIC_AP_ANCIENT_03",
"Cost": 50,
"UITreeRow": -3,
"EraType": "ERA_ANCIENT",
},
{
"Type": "CIVIC_AP_ANCIENT_04",
"Cost": 70,
"UITreeRow": 0,
"EraType": "ERA_ANCIENT",
},
{
"Type": "CIVIC_AP_ANCIENT_05",
"Cost": 70,
"UITreeRow": 1,
"EraType": "ERA_ANCIENT",
},
{
"Type": "CIVIC_AP_ANCIENT_06",
"Cost": 50,
"UITreeRow": 3,
"EraType": "ERA_ANCIENT",
},
{
"Type": "CIVIC_AP_CLASSICAL_07",
"Cost": 110,
"UITreeRow": -2,
"EraType": "ERA_CLASSICAL",
},
{
"Type": "CIVIC_AP_CLASSICAL_08",
"Cost": 110,
"UITreeRow": 0,
"EraType": "ERA_CLASSICAL",
},
{
"Type": "CIVIC_AP_CLASSICAL_09",
"Cost": 110,
"UITreeRow": 2,
"EraType": "ERA_CLASSICAL",
},
{
"Type": "CIVIC_AP_CLASSICAL_10",
"Cost": 120,
"UITreeRow": -3,
"EraType": "ERA_CLASSICAL",
},
{
"Type": "CIVIC_AP_CLASSICAL_11",
"Cost": 175,
"UITreeRow": -1,
"EraType": "ERA_CLASSICAL",
},
{
"Type": "CIVIC_AP_CLASSICAL_12",
"Cost": 175,
"UITreeRow": 1,
"EraType": "ERA_CLASSICAL",
},
{
"Type": "CIVIC_AP_CLASSICAL_13",
"Cost": 120,
"UITreeRow": 3,
"EraType": "ERA_CLASSICAL",
},
{
"Type": "CIVIC_AP_MEDIEVAL_14",
"Cost": 220,
"UITreeRow": -2,
"EraType": "ERA_MEDIEVAL",
},
{
"Type": "CIVIC_AP_MEDIEVAL_15",
"Cost": 300,
"UITreeRow": -1,
"EraType": "ERA_MEDIEVAL",
},
{
"Type": "CIVIC_AP_MEDIEVAL_16",
"Cost": 300,
"UITreeRow": 1,
"EraType": "ERA_MEDIEVAL",
},
{
"Type": "CIVIC_AP_MEDIEVAL_17",
"Cost": 340,
"UITreeRow": -3,
"EraType": "ERA_MEDIEVAL",
},
{
"Type": "CIVIC_AP_MEDIEVAL_18",
"Cost": 420,
"UITreeRow": -1,
"EraType": "ERA_MEDIEVAL",
},
{
"Type": "CIVIC_AP_MEDIEVAL_19",
"Cost": 420,
"UITreeRow": 1,
"EraType": "ERA_MEDIEVAL",
},
{
"Type": "CIVIC_AP_MEDIEVAL_20",
"Cost": 340,
"UITreeRow": 3,
"EraType": "ERA_MEDIEVAL",
},
{
"Type": "CIVIC_AP_RENAISSANCE_21",
"Cost": 440,
"UITreeRow": -3,
"EraType": "ERA_RENAISSANCE",
},
{
"Type": "CIVIC_AP_RENAISSANCE_22",
"Cost": 600,
"UITreeRow": -1,
"EraType": "ERA_RENAISSANCE",
},
{
"Type": "CIVIC_AP_RENAISSANCE_23",
"Cost": 600,
"UITreeRow": 1,
"EraType": "ERA_RENAISSANCE",
},
{
"Type": "CIVIC_AP_RENAISSANCE_24",
"Cost": 440,
"UITreeRow": 3,
"EraType": "ERA_RENAISSANCE",
},
{
"Type": "CIVIC_AP_RENAISSANCE_25",
"Cost": 720,
"UITreeRow": -1,
"EraType": "ERA_RENAISSANCE",
},
{
"Type": "CIVIC_AP_RENAISSANCE_26",
"Cost": 720,
"UITreeRow": 1,
"EraType": "ERA_RENAISSANCE",
},
{
"Type": "CIVIC_AP_INDUSTRIAL_27",
"Cost": 800,
"UITreeRow": -3,
"EraType": "ERA_INDUSTRIAL",
},
{
"Type": "CIVIC_AP_INDUSTRIAL_28",
"Cost": 1010,
"UITreeRow": -1,
"EraType": "ERA_INDUSTRIAL",
},
{
"Type": "CIVIC_AP_INDUSTRIAL_29",
"Cost": 1010,
"UITreeRow": 0,
"EraType": "ERA_INDUSTRIAL",
},
{
"Type": "CIVIC_AP_INDUSTRIAL_30",
"Cost": 800,
"UITreeRow": 2,
"EraType": "ERA_INDUSTRIAL",
},
{
"Type": "CIVIC_AP_INDUSTRIAL_31",
"Cost": 1050,
"UITreeRow": -3,
"EraType": "ERA_INDUSTRIAL",
},
{
"Type": "CIVIC_AP_INDUSTRIAL_32",
"Cost": 1210,
"UITreeRow": 2,
"EraType": "ERA_INDUSTRIAL",
},
{
"Type": "CIVIC_AP_INDUSTRIAL_33",
"Cost": 1210,
"UITreeRow": -1,
"EraType": "ERA_INDUSTRIAL",
},
{
"Type": "CIVIC_AP_MODERN_34",
"Cost": 1540,
"UITreeRow": -3,
"EraType": "ERA_MODERN",
},
{
"Type": "CIVIC_AP_MODERN_35",
"Cost": 1580,
"UITreeRow": -2,
"EraType": "ERA_MODERN",
},
{
"Type": "CIVIC_AP_MODERN_36",
"Cost": 1715,
"UITreeRow": -2,
"EraType": "ERA_MODERN",
},
{
"Type": "CIVIC_AP_MODERN_37",
"Cost": 1540,
"UITreeRow": -1,
"EraType": "ERA_MODERN",
},
{
"Type": "CIVIC_AP_MODERN_38",
"Cost": 1540,
"UITreeRow": 1,
"EraType": "ERA_MODERN",
},
{
"Type": "CIVIC_AP_MODERN_39",
"Cost": 1640,
"UITreeRow": -1,
"EraType": "ERA_MODERN",
},
{
"Type": "CIVIC_AP_MODERN_40",
"Cost": 1640,
"UITreeRow": 0,
"EraType": "ERA_MODERN",
},
{
"Type": "CIVIC_AP_MODERN_41",
"Cost": 1640,
"UITreeRow": 2,
"EraType": "ERA_MODERN",
},
{
"Type": "CIVIC_AP_MODERN_42",
"Cost": 1640,
"UITreeRow": 3,
"EraType": "ERA_MODERN",
},
{
"Type": "CIVIC_AP_ATOMIC_43",
"Cost": 2185,
"UITreeRow": -1,
"EraType": "ERA_ATOMIC",
},
{
"Type": "CIVIC_AP_ATOMIC_44",
"Cost": 2185,
"UITreeRow": 2,
"EraType": "ERA_ATOMIC",
},
{
"Type": "CIVIC_AP_ATOMIC_45",
"Cost": 1955,
"UITreeRow": -3,
"EraType": "ERA_ATOMIC",
},
{
"Type": "CIVIC_AP_ATOMIC_46",
"Cost": 2415,
"UITreeRow": -1,
"EraType": "ERA_ATOMIC",
},
{
"Type": "CIVIC_AP_ATOMIC_47",
"Cost": 2415,
"UITreeRow": 1,
"EraType": "ERA_ATOMIC",
},
{
"Type": "CIVIC_AP_INFORMATION_48",
"Cost": 2880,
"UITreeRow": 0,
"EraType": "ERA_INFORMATION",
},
{
"Type": "CIVIC_AP_INFORMATION_49",
"Cost": 2880,
"UITreeRow": 2,
"EraType": "ERA_INFORMATION",
},
{
"Type": "CIVIC_AP_FUTURE_50",
"Cost": 3200,
"UITreeRow": 3,
"EraType": "ERA_FUTURE",
},
{
"Type": "CIVIC_AP_INFORMATION_51",
"Cost": 2880,
"UITreeRow": -2,
"EraType": "ERA_INFORMATION",
},
{
"Type": "CIVIC_AP_INFORMATION_52",
"Cost": 3000,
"UITreeRow": 0,
"EraType": "ERA_INFORMATION",
},
{
"Type": "CIVIC_AP_INFORMATION_53",
"Cost": 3000,
"UITreeRow": 1,
"EraType": "ERA_INFORMATION",
},
{
"Type": "CIVIC_AP_INFORMATION_54",
"Cost": 3000,
"UITreeRow": 2,
"EraType": "ERA_INFORMATION",
},
{
"Type": "CIVIC_AP_INFORMATION_55",
"Cost": 3100,
"UITreeRow": -1,
"EraType": "ERA_INFORMATION",
},
{
"Type": "CIVIC_AP_FUTURE_56",
"Cost": 3200,
"UITreeRow": -2,
"EraType": "ERA_FUTURE",
},
{
"Type": "CIVIC_AP_FUTURE_57",
"Cost": 3200,
"UITreeRow": -1,
"EraType": "ERA_FUTURE",
},
{
"Type": "CIVIC_AP_FUTURE_58",
"Cost": 3200,
"UITreeRow": 0,
"EraType": "ERA_FUTURE",
},
{
"Type": "CIVIC_AP_FUTURE_59",
"Cost": 3200,
"UITreeRow": 1,
"EraType": "ERA_FUTURE",
},
{
"Type": "CIVIC_AP_FUTURE_60",
"Cost": 3200,
"UITreeRow": 2,
"EraType": "ERA_FUTURE",
},
]

View File

@@ -0,0 +1,468 @@
from typing import List
from ..ItemData import NewItemData
new_tech: List[NewItemData] = [
{
"Type": "TECH_AP_ANCIENT_00",
"Cost": 25,
"UITreeRow": 0,
"EraType": "ERA_ANCIENT",
},
{
"Type": "TECH_AP_ANCIENT_01",
"Cost": 25,
"UITreeRow": 1,
"EraType": "ERA_ANCIENT",
},
{
"Type": "TECH_AP_ANCIENT_02",
"Cost": 25,
"UITreeRow": 3,
"EraType": "ERA_ANCIENT",
},
{
"Type": "TECH_AP_ANCIENT_03",
"Cost": 50,
"UITreeRow": -3,
"EraType": "ERA_ANCIENT",
},
{
"Type": "TECH_AP_ANCIENT_04",
"Cost": 50,
"UITreeRow": -2,
"EraType": "ERA_ANCIENT",
},
{
"Type": "TECH_AP_ANCIENT_05",
"Cost": 50,
"UITreeRow": -1,
"EraType": "ERA_ANCIENT",
},
{
"Type": "TECH_AP_ANCIENT_06",
"Cost": 50,
"UITreeRow": 1,
"EraType": "ERA_ANCIENT",
},
{
"Type": "TECH_AP_ANCIENT_07",
"Cost": 50,
"UITreeRow": 0,
"EraType": "ERA_ANCIENT",
},
{
"Type": "TECH_AP_ANCIENT_08",
"Cost": 80,
"UITreeRow": 2,
"EraType": "ERA_ANCIENT",
},
{
"Type": "TECH_AP_ANCIENT_09",
"Cost": 80,
"UITreeRow": 3,
"EraType": "ERA_ANCIENT",
},
{
"Type": "TECH_AP_ANCIENT_10",
"Cost": 80,
"UITreeRow": 4,
"EraType": "ERA_ANCIENT",
},
{
"Type": "TECH_AP_CLASSICAL_11",
"Cost": 120,
"UITreeRow": -2,
"EraType": "ERA_CLASSICAL",
},
{
"Type": "TECH_AP_CLASSICAL_12",
"Cost": 120,
"UITreeRow": 0,
"EraType": "ERA_CLASSICAL",
},
{
"Type": "TECH_AP_CLASSICAL_13",
"Cost": 120,
"UITreeRow": 1,
"EraType": "ERA_CLASSICAL",
},
{
"Type": "TECH_AP_CLASSICAL_14",
"Cost": 120,
"UITreeRow": 3,
"EraType": "ERA_CLASSICAL",
},
{
"Type": "TECH_AP_CLASSICAL_15",
"Cost": 200,
"UITreeRow": -3,
"EraType": "ERA_CLASSICAL",
},
{
"Type": "TECH_AP_CLASSICAL_16",
"Cost": 200,
"UITreeRow": -1,
"EraType": "ERA_CLASSICAL",
},
{
"Type": "TECH_AP_CLASSICAL_17",
"Cost": 200,
"UITreeRow": 2,
"EraType": "ERA_CLASSICAL",
},
{
"Type": "TECH_AP_CLASSICAL_18",
"Cost": 200,
"UITreeRow": 4,
"EraType": "ERA_CLASSICAL",
},
{
"Type": "TECH_AP_MEDIEVAL_19",
"Cost": 300,
"UITreeRow": -2,
"EraType": "ERA_MEDIEVAL",
},
{
"Type": "TECH_AP_MEDIEVAL_20",
"Cost": 300,
"UITreeRow": 0,
"EraType": "ERA_MEDIEVAL",
},
{
"Type": "TECH_AP_MEDIEVAL_21",
"Cost": 300,
"UITreeRow": 4,
"EraType": "ERA_MEDIEVAL",
},
{
"Type": "TECH_AP_MEDIEVAL_22",
"Cost": 390,
"UITreeRow": -1,
"EraType": "ERA_MEDIEVAL",
},
{
"Type": "TECH_AP_MEDIEVAL_23",
"Cost": 390,
"UITreeRow": 1,
"EraType": "ERA_MEDIEVAL",
},
{
"Type": "TECH_AP_MEDIEVAL_24",
"Cost": 390,
"UITreeRow": 2,
"EraType": "ERA_MEDIEVAL",
},
{
"Type": "TECH_AP_MEDIEVAL_25",
"Cost": 390,
"UITreeRow": 3,
"EraType": "ERA_MEDIEVAL",
},
{
"Type": "TECH_AP_RENAISSANCE_26",
"Cost": 600,
"UITreeRow": -3,
"EraType": "ERA_RENAISSANCE",
},
{
"Type": "TECH_AP_RENAISSANCE_27",
"Cost": 600,
"UITreeRow": -2,
"EraType": "ERA_RENAISSANCE",
},
{
"Type": "TECH_AP_RENAISSANCE_28",
"Cost": 600,
"UITreeRow": 0,
"EraType": "ERA_RENAISSANCE",
},
{
"Type": "TECH_AP_RENAISSANCE_29",
"Cost": 600,
"UITreeRow": 1,
"EraType": "ERA_RENAISSANCE",
},
{
"Type": "TECH_AP_RENAISSANCE_30",
"Cost": 600,
"UITreeRow": 4,
"EraType": "ERA_RENAISSANCE",
},
{
"Type": "TECH_AP_RENAISSANCE_31",
"Cost": 730,
"UITreeRow": -3,
"EraType": "ERA_RENAISSANCE",
},
{
"Type": "TECH_AP_RENAISSANCE_32",
"Cost": 730,
"UITreeRow": -1,
"EraType": "ERA_RENAISSANCE",
},
{
"Type": "TECH_AP_RENAISSANCE_33",
"Cost": 730,
"UITreeRow": 1,
"EraType": "ERA_RENAISSANCE",
},
{
"Type": "TECH_AP_RENAISSANCE_34",
"Cost": 730,
"UITreeRow": 3,
"EraType": "ERA_RENAISSANCE",
},
{
"Type": "TECH_AP_INDUSTRIAL_35",
"Cost": 930,
"UITreeRow": -2,
"EraType": "ERA_INDUSTRIAL",
},
{
"Type": "TECH_AP_INDUSTRIAL_36",
"Cost": 930,
"UITreeRow": -1,
"EraType": "ERA_INDUSTRIAL",
},
{
"Type": "TECH_AP_INDUSTRIAL_37",
"Cost": 930,
"UITreeRow": 1,
"EraType": "ERA_INDUSTRIAL",
},
{
"Type": "TECH_AP_INDUSTRIAL_38",
"Cost": 930,
"UITreeRow": 3,
"EraType": "ERA_INDUSTRIAL",
},
{
"Type": "TECH_AP_INDUSTRIAL_39",
"Cost": 1070,
"UITreeRow": -3,
"EraType": "ERA_INDUSTRIAL",
},
{
"Type": "TECH_AP_INDUSTRIAL_40",
"Cost": 1070,
"UITreeRow": -1,
"EraType": "ERA_INDUSTRIAL",
},
{
"Type": "TECH_AP_INDUSTRIAL_41",
"Cost": 1070,
"UITreeRow": 0,
"EraType": "ERA_INDUSTRIAL",
},
{
"Type": "TECH_AP_INDUSTRIAL_42",
"Cost": 1070,
"UITreeRow": 2,
"EraType": "ERA_INDUSTRIAL",
},
{
"Type": "TECH_AP_MODERN_43",
"Cost": 1250,
"UITreeRow": -2,
"EraType": "ERA_MODERN",
},
{
"Type": "TECH_AP_MODERN_44",
"Cost": 1250,
"UITreeRow": 0,
"EraType": "ERA_MODERN",
},
{
"Type": "TECH_AP_MODERN_45",
"Cost": 1250,
"UITreeRow": 1,
"EraType": "ERA_MODERN",
},
{
"Type": "TECH_AP_MODERN_46",
"Cost": 1370,
"UITreeRow": -3,
"EraType": "ERA_MODERN",
},
{
"Type": "TECH_AP_MODERN_47",
"Cost": 1370,
"UITreeRow": -2,
"EraType": "ERA_MODERN",
},
{
"Type": "TECH_AP_MODERN_48",
"Cost": 1370,
"UITreeRow": -1,
"EraType": "ERA_MODERN",
},
{
"Type": "TECH_AP_MODERN_49",
"Cost": 1370,
"UITreeRow": 2,
"EraType": "ERA_MODERN",
},
{
"Type": "TECH_AP_ATOMIC_50",
"Cost": 1480,
"UITreeRow": -2,
"EraType": "ERA_ATOMIC",
},
{
"Type": "TECH_AP_ATOMIC_51",
"Cost": 1480,
"UITreeRow": -1,
"EraType": "ERA_ATOMIC",
},
{
"Type": "TECH_AP_ATOMIC_52",
"Cost": 1480,
"UITreeRow": 0,
"EraType": "ERA_ATOMIC",
},
{
"Type": "TECH_AP_ATOMIC_53",
"Cost": 1480,
"UITreeRow": 1,
"EraType": "ERA_ATOMIC",
},
{
"Type": "TECH_AP_ATOMIC_54",
"Cost": 1480,
"UITreeRow": 2,
"EraType": "ERA_ATOMIC",
},
{
"Type": "TECH_AP_ATOMIC_55",
"Cost": 1660,
"UITreeRow": -3,
"EraType": "ERA_ATOMIC",
},
{
"Type": "TECH_AP_ATOMIC_56",
"Cost": 1660,
"UITreeRow": 1,
"EraType": "ERA_ATOMIC",
},
{
"Type": "TECH_AP_ATOMIC_57",
"Cost": 1660,
"UITreeRow": 2,
"EraType": "ERA_ATOMIC",
},
{
"Type": "TECH_AP_INFORMATION_58",
"Cost": 1850,
"UITreeRow": -3,
"EraType": "ERA_INFORMATION",
},
{
"Type": "TECH_AP_INFORMATION_59",
"Cost": 1850,
"UITreeRow": -1,
"EraType": "ERA_INFORMATION",
},
{
"Type": "TECH_AP_INFORMATION_60",
"Cost": 1850,
"UITreeRow": 0,
"EraType": "ERA_INFORMATION",
},
{
"Type": "TECH_AP_INFORMATION_61",
"Cost": 1850,
"UITreeRow": 1,
"EraType": "ERA_INFORMATION",
},
{
"Type": "TECH_AP_INFORMATION_62",
"Cost": 1850,
"UITreeRow": 2,
"EraType": "ERA_INFORMATION",
},
{
"Type": "TECH_AP_INFORMATION_63",
"Cost": 1850,
"UITreeRow": 3,
"EraType": "ERA_INFORMATION",
},
{
"Type": "TECH_AP_INFORMATION_64",
"Cost": 2155,
"UITreeRow": -2,
"EraType": "ERA_INFORMATION",
},
{
"Type": "TECH_AP_INFORMATION_65",
"Cost": 2155,
"UITreeRow": 2,
"EraType": "ERA_INFORMATION",
},
{
"Type": "TECH_AP_INFORMATION_66",
"Cost": 2155,
"UITreeRow": 1,
"EraType": "ERA_INFORMATION",
},
{
"Type": "TECH_AP_MEDIEVAL_67",
"Cost": 300,
"UITreeRow": -3,
"EraType": "ERA_MEDIEVAL",
},
{
"Type": "TECH_AP_MODERN_68",
"Cost": 1250,
"UITreeRow": 3,
"EraType": "ERA_MODERN",
},
{
"Type": "TECH_AP_FUTURE_69",
"Cost": 2200,
"UITreeRow": -3,
"EraType": "ERA_FUTURE",
},
{
"Type": "TECH_AP_FUTURE_70",
"Cost": 2200,
"UITreeRow": -2,
"EraType": "ERA_FUTURE",
},
{
"Type": "TECH_AP_FUTURE_71",
"Cost": 2200,
"UITreeRow": -1,
"EraType": "ERA_FUTURE",
},
{
"Type": "TECH_AP_FUTURE_72",
"Cost": 2200,
"UITreeRow": 0,
"EraType": "ERA_FUTURE",
},
{
"Type": "TECH_AP_FUTURE_73",
"Cost": 2200,
"UITreeRow": 1,
"EraType": "ERA_FUTURE",
},
{
"Type": "TECH_AP_FUTURE_74",
"Cost": 2200,
"UITreeRow": 2,
"EraType": "ERA_FUTURE",
},
{
"Type": "TECH_AP_FUTURE_75",
"Cost": 2500,
"UITreeRow": 0,
"EraType": "ERA_FUTURE",
},
{
"Type": "TECH_AP_FUTURE_76",
"Cost": 2600,
"UITreeRow": 0,
"EraType": "ERA_FUTURE",
},
]

View File

@@ -0,0 +1,110 @@
from typing import List
from ..ItemData import TechPrereqData
new_tech_prereqs: List[TechPrereqData] = [
{"Technology": "TECH_AP_ANCIENT_06", "PrereqTech": "TECH_AP_ANCIENT_01"},
{"Technology": "TECH_AP_ANCIENT_07", "PrereqTech": "TECH_AP_ANCIENT_00"},
{"Technology": "TECH_AP_ANCIENT_05", "PrereqTech": "TECH_AP_ANCIENT_00"},
{"Technology": "TECH_AP_ANCIENT_08", "PrereqTech": "TECH_AP_ANCIENT_02"},
{"Technology": "TECH_AP_ANCIENT_09", "PrereqTech": "TECH_AP_ANCIENT_02"},
{"Technology": "TECH_AP_ANCIENT_10", "PrereqTech": "TECH_AP_ANCIENT_02"},
{"Technology": "TECH_AP_CLASSICAL_15", "PrereqTech": "TECH_AP_ANCIENT_03"},
{"Technology": "TECH_AP_CLASSICAL_11", "PrereqTech": "TECH_AP_ANCIENT_03"},
{"Technology": "TECH_AP_CLASSICAL_11", "PrereqTech": "TECH_AP_ANCIENT_04"},
{"Technology": "TECH_AP_CLASSICAL_12", "PrereqTech": "TECH_AP_ANCIENT_07"},
{"Technology": "TECH_AP_CLASSICAL_13", "PrereqTech": "TECH_AP_ANCIENT_06"},
{"Technology": "TECH_AP_CLASSICAL_14", "PrereqTech": "TECH_AP_ANCIENT_09"},
{"Technology": "TECH_AP_CLASSICAL_16", "PrereqTech": "TECH_AP_CLASSICAL_12"},
{"Technology": "TECH_AP_CLASSICAL_17", "PrereqTech": "TECH_AP_ANCIENT_08"},
{"Technology": "TECH_AP_CLASSICAL_17", "PrereqTech": "TECH_AP_CLASSICAL_13"},
{"Technology": "TECH_AP_CLASSICAL_18", "PrereqTech": "TECH_AP_ANCIENT_10"},
{"Technology": "TECH_AP_MEDIEVAL_19", "PrereqTech": "TECH_AP_CLASSICAL_16"},
{"Technology": "TECH_AP_MEDIEVAL_20", "PrereqTech": "TECH_AP_CLASSICAL_12"},
{"Technology": "TECH_AP_MEDIEVAL_20", "PrereqTech": "TECH_AP_CLASSICAL_13"},
{"Technology": "TECH_AP_MEDIEVAL_23", "PrereqTech": "TECH_AP_CLASSICAL_13"},
{"Technology": "TECH_AP_MEDIEVAL_21", "PrereqTech": "TECH_AP_CLASSICAL_14"},
{"Technology": "TECH_AP_MEDIEVAL_21", "PrereqTech": "TECH_AP_CLASSICAL_18"},
{"Technology": "TECH_AP_MEDIEVAL_22", "PrereqTech": "TECH_AP_CLASSICAL_16"},
{"Technology": "TECH_AP_MEDIEVAL_22", "PrereqTech": "TECH_AP_MEDIEVAL_20"},
{"Technology": "TECH_AP_MEDIEVAL_25", "PrereqTech": "TECH_AP_CLASSICAL_17"},
{"Technology": "TECH_AP_MEDIEVAL_24", "PrereqTech": "TECH_AP_CLASSICAL_17"},
{"Technology": "TECH_AP_RENAISSANCE_27", "PrereqTech": "TECH_AP_MEDIEVAL_22"},
{"Technology": "TECH_AP_RENAISSANCE_28", "PrereqTech": "TECH_AP_MEDIEVAL_22"},
{"Technology": "TECH_AP_RENAISSANCE_28", "PrereqTech": "TECH_AP_MEDIEVAL_23"},
{"Technology": "TECH_AP_RENAISSANCE_29", "PrereqTech": "TECH_AP_MEDIEVAL_20"},
{"Technology": "TECH_AP_RENAISSANCE_29", "PrereqTech": "TECH_AP_MEDIEVAL_23"},
{"Technology": "TECH_AP_RENAISSANCE_29", "PrereqTech": "TECH_AP_MEDIEVAL_24"},
{"Technology": "TECH_AP_RENAISSANCE_30", "PrereqTech": "TECH_AP_MEDIEVAL_21"},
{"Technology": "TECH_AP_RENAISSANCE_31", "PrereqTech": "TECH_AP_RENAISSANCE_26"},
{"Technology": "TECH_AP_RENAISSANCE_32", "PrereqTech": "TECH_AP_MEDIEVAL_22"},
{"Technology": "TECH_AP_RENAISSANCE_33", "PrereqTech": "TECH_AP_RENAISSANCE_29"},
{"Technology": "TECH_AP_RENAISSANCE_34", "PrereqTech": "TECH_AP_MEDIEVAL_25"},
{"Technology": "TECH_AP_INDUSTRIAL_35", "PrereqTech": "TECH_AP_RENAISSANCE_31"},
{"Technology": "TECH_AP_INDUSTRIAL_35", "PrereqTech": "TECH_AP_RENAISSANCE_27"},
{"Technology": "TECH_AP_INDUSTRIAL_36", "PrereqTech": "TECH_AP_RENAISSANCE_32"},
{"Technology": "TECH_AP_INDUSTRIAL_36", "PrereqTech": "TECH_AP_RENAISSANCE_28"},
{"Technology": "TECH_AP_INDUSTRIAL_41", "PrereqTech": "TECH_AP_INDUSTRIAL_36"},
{"Technology": "TECH_AP_INDUSTRIAL_41", "PrereqTech": "TECH_AP_RENAISSANCE_33"},
{"Technology": "TECH_AP_INDUSTRIAL_38", "PrereqTech": "TECH_AP_RENAISSANCE_34"},
{"Technology": "TECH_AP_INDUSTRIAL_38", "PrereqTech": "TECH_AP_RENAISSANCE_30"},
{"Technology": "TECH_AP_INDUSTRIAL_39", "PrereqTech": "TECH_AP_INDUSTRIAL_35"},
{"Technology": "TECH_AP_INDUSTRIAL_40", "PrereqTech": "TECH_AP_INDUSTRIAL_36"},
{"Technology": "TECH_AP_INDUSTRIAL_37", "PrereqTech": "TECH_AP_RENAISSANCE_33"},
{"Technology": "TECH_AP_INDUSTRIAL_42", "PrereqTech": "TECH_AP_INDUSTRIAL_37"},
{"Technology": "TECH_AP_INDUSTRIAL_42", "PrereqTech": "TECH_AP_INDUSTRIAL_38"},
{"Technology": "TECH_AP_MODERN_43", "PrereqTech": "TECH_AP_INDUSTRIAL_35"},
{"Technology": "TECH_AP_MODERN_43", "PrereqTech": "TECH_AP_INDUSTRIAL_36"},
{"Technology": "TECH_AP_MODERN_44", "PrereqTech": "TECH_AP_INDUSTRIAL_41"},
{"Technology": "TECH_AP_MODERN_45", "PrereqTech": "TECH_AP_INDUSTRIAL_42"},
{"Technology": "TECH_AP_MODERN_46", "PrereqTech": "TECH_AP_INDUSTRIAL_39"},
{"Technology": "TECH_AP_MODERN_47", "PrereqTech": "TECH_AP_INDUSTRIAL_39"},
{"Technology": "TECH_AP_MODERN_47", "PrereqTech": "TECH_AP_MODERN_43"},
{"Technology": "TECH_AP_MODERN_48", "PrereqTech": "TECH_AP_INDUSTRIAL_40"},
{"Technology": "TECH_AP_MODERN_49", "PrereqTech": "TECH_AP_MODERN_45"},
{"Technology": "TECH_AP_ATOMIC_55", "PrereqTech": "TECH_AP_MODERN_46"},
{"Technology": "TECH_AP_ATOMIC_55", "PrereqTech": "TECH_AP_MODERN_47"},
{"Technology": "TECH_AP_ATOMIC_50", "PrereqTech": "TECH_AP_MODERN_47"},
{"Technology": "TECH_AP_ATOMIC_51", "PrereqTech": "TECH_AP_MODERN_47"},
{"Technology": "TECH_AP_ATOMIC_51", "PrereqTech": "TECH_AP_MODERN_48"},
{"Technology": "TECH_AP_ATOMIC_52", "PrereqTech": "TECH_AP_MODERN_44"},
{"Technology": "TECH_AP_ATOMIC_52", "PrereqTech": "TECH_AP_MODERN_45"},
{"Technology": "TECH_AP_ATOMIC_53", "PrereqTech": "TECH_AP_MODERN_45"},
{"Technology": "TECH_AP_ATOMIC_53", "PrereqTech": "TECH_AP_MODERN_49"},
{"Technology": "TECH_AP_ATOMIC_56", "PrereqTech": "TECH_AP_ATOMIC_52"},
{"Technology": "TECH_AP_ATOMIC_56", "PrereqTech": "TECH_AP_ATOMIC_53"},
{"Technology": "TECH_AP_ATOMIC_54", "PrereqTech": "TECH_AP_MODERN_49"},
{"Technology": "TECH_AP_ATOMIC_57", "PrereqTech": "TECH_AP_ATOMIC_54"},
{"Technology": "TECH_AP_INFORMATION_58", "PrereqTech": "TECH_AP_ATOMIC_55"},
{"Technology": "TECH_AP_INFORMATION_64", "PrereqTech": "TECH_AP_ATOMIC_55"},
{"Technology": "TECH_AP_INFORMATION_59", "PrereqTech": "TECH_AP_ATOMIC_50"},
{"Technology": "TECH_AP_INFORMATION_59", "PrereqTech": "TECH_AP_ATOMIC_51"},
{"Technology": "TECH_AP_INFORMATION_60", "PrereqTech": "TECH_AP_ATOMIC_51"},
{"Technology": "TECH_AP_INFORMATION_60", "PrereqTech": "TECH_AP_ATOMIC_52"},
{"Technology": "TECH_AP_INFORMATION_61", "PrereqTech": "TECH_AP_ATOMIC_56"},
{"Technology": "TECH_AP_INFORMATION_62", "PrereqTech": "TECH_AP_ATOMIC_57"},
{"Technology": "TECH_AP_INFORMATION_63", "PrereqTech": "TECH_AP_ATOMIC_57"},
{"Technology": "TECH_AP_INFORMATION_65", "PrereqTech": "TECH_AP_INFORMATION_62"},
{"Technology": "TECH_AP_INFORMATION_66", "PrereqTech": "TECH_AP_INFORMATION_61"},
{"Technology": "TECH_AP_MEDIEVAL_67", "PrereqTech": "TECH_AP_CLASSICAL_15"},
{"Technology": "TECH_AP_MEDIEVAL_67", "PrereqTech": "TECH_AP_CLASSICAL_16"},
{"Technology": "TECH_AP_MEDIEVAL_23", "PrereqTech": "TECH_AP_MEDIEVAL_20"},
{"Technology": "TECH_AP_MODERN_68", "PrereqTech": "TECH_AP_INDUSTRIAL_42"},
{"Technology": "TECH_AP_MODERN_49", "PrereqTech": "TECH_AP_MODERN_68"},
{"Technology": "TECH_AP_RENAISSANCE_26", "PrereqTech": "TECH_AP_MEDIEVAL_67"},
{"Technology": "TECH_AP_RENAISSANCE_27", "PrereqTech": "TECH_AP_MEDIEVAL_67"},
{"Technology": "TECH_AP_RENAISSANCE_27", "PrereqTech": "TECH_AP_MEDIEVAL_19"},
{"Technology": "TECH_AP_MODERN_48", "PrereqTech": "TECH_AP_MODERN_44"},
{"Technology": "TECH_AP_INFORMATION_64", "PrereqTech": "TECH_AP_INFORMATION_59"},
{"Technology": "TECH_AP_INFORMATION_64", "PrereqTech": "TECH_AP_INFORMATION_60"},
{"Technology": "TECH_AP_INFORMATION_64", "PrereqTech": "TECH_AP_INFORMATION_61"},
{"Technology": "TECH_AP_FUTURE_69", "PrereqTech": "TECH_AP_AP60"},
{"Technology": "TECH_AP_FUTURE_70", "PrereqTech": "TECH_AP_AP60"},
{"Technology": "TECH_AP_FUTURE_71", "PrereqTech": "TECH_AP_AP60"},
{"Technology": "TECH_AP_FUTURE_72", "PrereqTech": "TECH_AP_AP60"},
{"Technology": "TECH_AP_FUTURE_73", "PrereqTech": "TECH_AP_AP60"},
{"Technology": "TECH_AP_FUTURE_74", "PrereqTech": "TECH_AP_AP60"},
{"Technology": "TECH_AP_FUTURE_75", "PrereqTech": "TECH_AP_AP60"},
{"Technology": "TECH_AP_FUTURE_76", "PrereqTech": "TECH_AP_AP60"},
]

View File

@@ -0,0 +1,41 @@
from typing import Dict, List
progressive_districts: Dict[str, List[str]] = {
"PROGRESSIVE_CAMPUS": ["TECH_WRITING", "TECH_EDUCATION", "TECH_CHEMISTRY"],
"PROGRESSIVE_THEATER": ["CIVIC_DRAMA_POETRY", "CIVIC_HUMANISM", "TECH_RADIO"],
"PROGRESSIVE_HOLY_SITE": ["TECH_ASTROLOGY", "CIVIC_THEOLOGY"],
"PROGRESSIVE_ENCAMPMENT": [
"TECH_BRONZE_WORKING",
"TECH_MILITARY_ENGINEERING",
"TECH_MILITARY_SCIENCE",
],
"PROGRESSIVE_COMMERCIAL_HUB": ["TECH_CURRENCY", "TECH_BANKING", "TECH_ECONOMICS"],
"PROGRESSIVE_HARBOR": ["TECH_CELESTIAL_NAVIGATION", "TECH_MASS_PRODUCTION"],
"PROGRESSIVE_INDUSTRIAL_ZONE": [
"TECH_APPRENTICESHIP",
"TECH_INDUSTRIALIZATION",
"TECH_ELECTRICITY",
"TECH_NUCLEAR_FISSION",
],
"PROGRESSIVE_PRESERVE": ["CIVIC_MYSTICISM", "CIVIC_CONSERVATION"],
"PROGRESSIVE_ENTERTAINMENT_COMPLEX": [
"CIVIC_GAMES_RECREATION",
"CIVIC_NATURAL_HISTORY",
"CIVIC_PROFESSIONAL_SPORTS",
],
"PROGRESSIVE_NEIGHBORHOOD": [
"CIVIC_URBANIZATION",
"TECH_REPLACEABLE_PARTS",
"CIVIC_CAPITALISM",
],
"PROGRESSIVE_AERODROME": ["TECH_FLIGHT", "TECH_ADVANCED_FLIGHT"],
"PROGRESSIVE_DIPLOMATIC_QUARTER": ["TECH_MATHEMATICS", "CIVIC_DIPLOMATIC_SERVICE"],
"PROGRESSIVE_SPACE_PORT": [
"TECH_ROCKETRY",
"TECH_SATELLITES",
"TECH_NANOTECHNOLOGY",
"TECH_SMART_MATERIALS",
"TECH_OFFWORLD_MISSION",
],
}

View File

@@ -0,0 +1,59 @@
# Civilization 6 Archipelago
## What does randomization do to this game?
In Civilization VI, the tech and civic trees are both shuffled. This presents some interesting ways to play the game in a non-standard way. If you are feeling adventurous, you can enable the "boostsanity" option in order to really change up the way you normally would play a Civ game. Details on the option can be found in the [Boostsanity](#boostsanity) section below.
There are a few changes that the Archipelago mod introduces in order to make this playable/fun. These are detailed in the [__FAQ__](#faqs) section below.
## What is the goal of Civilization VI when randomized?
The goal of randomized Civilization VI remains the same. Pursue any victory type you have enabled in your game settings, the one you normally go for may or may not be feasible based on how things have been changed up!
## Which items can be in another player's world?
All technologies and civics can be found in another player's world.
## What does another world's item look like in Civilization VI?
Each item from another world is represented as a researchable tech/civic in your normal tech/civic trees.
## When the player receives an item, what happens?
A short period after receiving an item, you will get a notification indicating you have discovered the relevant tech/civic. You will also get the regular popup that details what the given item has unlocked for you.
## FAQs
- Do I need the DLC to play this?
- Yes, you need both Rise & Fall and Gathering Storm.
- Does this work with Multiplayer?
- It does not and, despite my best efforts, probably won't until there's a new way for external programs to be able to interact with the game.
- Does my mod that reskins Barbarians as various Pro Wrestlers work with this?
- Only one way to find out! Any mods that modify techs/civics will most likely cause issues, though.
- "Help! I can't see any of the items that have been sent to me!"
- Both trees by default will show you the researchable Archipelago locations. To view the normal tree, you can click "Toggle Archipelago Tree" in the top-left corner of the tree view.
- "Oh no! I received the Machinery tech and now instead of getting an Archer next turn, I have to wait an additional 10 turns to get a Crossbowman!"
- Vanilla prevents you from building units of the same class from an earlier tech level after you have researched a later variant. For example, this could be problematic if someone unlocks Crossbowmen for you right out the gate since you won't be able to make Archers (which have a much lower production cost).
Solution: You can now go in to the tech tree, click "Toggle Archipelago Tree" to view your unlocked techs, and then can click any tech you have unlocked to toggle whether it is currently active or not.
- "How does DeathLink work? Am I going to have to start a new game every time one of my friends dies?"
- Heavens no, my fellow Archipelago appreciator. When configuring your Archipelago options for Civilization on the options page, there are several choices available for you to fine tune the way you'd like to be punished for the follies of your friends. These include: Having a random unit destroyed, losing a percentage of gold or faith, or even losing a point on your era score. If you can't make up your mind, you can elect to have any of them be selected every time a death link is sent your way.
In the event you lose one of your units in combat (this means captured units don't count), then you will send a death link event to the rest of your friends.
- I enabled `progressive districts` but I have no idea what tech or civic a progressive district unlocks for me!
- Any technology or civic that grants you a new building in a district (or grants you the district itself) is now locked behind a progressive item. For example, `PROGRESSIVE_CAMPUS` would give you these items in the following order:
1. `TECH_WRITING`
2. `TECH_EDUCATION`
3. `TECH_CHEMISTRY`
- If you want to see the details around each item, you can review [this file](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/civ_6/data/progressive_districts.json).
## Boostsanity
Boostsanity takes all of the Eureka & Inspiration events and makes them location checks. This feature is the one to change up the way Civilization is played in an AP multiworld/randomizer. What normally are mundane tasks that are passively collected now become a novel and interesting bucket list that you need to pay attention to in order to unlock items for yourself and others!
Boosts have logic associated with them in order to verify you can always reach the ones you need to, when you need to. One side effect of this is that when boostsanity is enabled, some previously "Useful" items are now flagged as "Progression" (Urbanization, Pottery, The Wheel, to name a few).
### Boostsanity FAQs
- Someone sent me a tech/civic, and I'm worried I won't be able to boost it anymore!
- Fear not! Through a lot of wizardry 🧙‍♂️ you can boost civics/techs that have already been received. Additionally, the UI has been updated to show you whether they have been boosted or not after receiving them.
- I need to kill a unit with a slinger/archer/musketman or some other obsolete unit I can't build anymore, how can I do this?
- Don't forget you can go into the Tech Tree and click on a Vanilla tech you've received in order to toggle it on/off. This is necessary in order to pursue some of the boosts if you receive techs in certain orders.
- Something happened, and I'm not able to unlock the boost due to game rules!
- A few scenarios you may worry about: "Found a religion", "Make an alliance with another player", "Develop an alliance to level 2", "Build a wonder from X Era", to name a few. Any boost that is "miss-able" has been flagged as an "Excluded" location and will not ever receive a progression item. For a list of how each boost is flagged, take a look [here](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/civ_6/data/boosts.json).
- I'm worried that my `PROGRESSIVE_ERA` item is going to be stuck in a boost I won't have time to complete before my maximum unlocked era ends!
- The unpredictable timing of boosts and unlocking them can occasionally lead to scenarios where you'll have to first encounter a locked era defeat and then load a previous save. To help reduce the frequency of this, local `PROGRESSIVE_ERA` items will never be located at a boost check.
- There's too many boosts, how will I know which one's I should focus on?!
- In order to give a little more focus to all the boosts rather than just arbitrarily picking them at random, items in both of the vanilla trees will now have an advisor icon on them if its associated boost contains a progression item.

View File

@@ -0,0 +1,51 @@
# Setup Guide for Civilization VI Archipelago
This guide is meant to help you get up and running with Civilization VI in Archipelago. Note that this requires you to have both Rise & Fall and Gathering Storm installed. This will not work unless both of those DLCs are enabled.
## Requirements
The following are required in order to play Civ VI in Archipelago:
- Windows OS (Firaxis does not support the necessary tooling for Mac, or Linux)
- Installed [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) v0.4.5 or higher.
- The latest version of the [Civ VI AP Mod](https://github.com/hesto2/civilization_archipelago_mod/releases/latest).
## Enabling the tuner
Depending on how you installed Civ 6 you will have to navigate to one of the following:
- `YOUR_USER/Documents/My Games/Sid Meier's Civilization VI/AppOptions.txt`
- `YOUR_USER/AppData/Local/Firaxis Games/Sid Meier's Civilization VI/AppOptions.txt`
Once you have located your `AppOptions.txt`, do a search for `Enable FireTuner`. Set `EnableTuner` to `1` instead of `0`. **NOTE**: While this is active, achievements will be disabled.
## Mod Installation
1. Download and unzip the latest release of the mod from [GitHub](https://github.com/hesto2/civilization_archipelago_mod/releases/latest).
2. Copy the folder containing the mod files to your Civ VI mods folder. On Windows, this is usually located at `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods`.
3. After the Archipelago host generates a game, you should be given a `.apcivvi` file. Associate the file with the Archipelago Launcher and double click it.
4. Copy the contents of the new folder it generates (it will have the same name as the `.apcivvi` file) into your Civilization VI Archipelago Mod folder.
5. Your finished mod folder should look something like this:
- Civ VI Mods Directory
- civilization_archipelago_mod
- NewItems.xml
- InitOptions.lua
- Archipelago.modinfo
- All the other mod files, etc.
## Configuring your game
When configuring your game, make sure to start the game in the Ancient Era and leave all settings related to starting technologies and civics as the defaults. Other than that, configure difficulty, AI, etc. as you normally would.
## Troubleshooting
- If you are getting an error: "The remote computer refused the network connection", or something else related to the client (or tuner) not being able to connect, it likely indicates the tuner is not actually enabled. One simple way to verify that it is enabled is, after completing the setup steps, go to Main Menu &rarr; Options &rarr; Look for an option named "Tuner" and verify it is set to "Enabled"
- If your game gets in a state where someone has sent you items or you have sent locations but these are not correctly sent to the multiworld, you can run `/resync` from the Civ 6 client. This may take up to a minute depending on how many items there are.

View File

@@ -0,0 +1,107 @@
from Fill import distribute_items_restrictive
from ..Data import get_boosts_data
from . import CivVITestBase
class TestBoostsanityIncluded(CivVITestBase):
auto_construct = False
options = {
"progressive_eras": "true",
"boostsanity": "true",
"progression_style": "none",
"shuffle_goody_hut_rewards": "false",
}
def test_boosts_get_included(self) -> None:
self.world_setup()
distribute_items_restrictive(self.multiworld)
locations = self.multiworld.get_locations(self.player)
found_locations = 0
for location in locations:
if "BOOST" in location.name:
found_locations += 1
num_boost_locations = len(get_boosts_data())
self.assertEqual(found_locations, num_boost_locations)
def test_boosts_require_prereqs_no_progressives(self) -> None:
self.world_setup()
location = "BOOST_TECH_ADVANCED_BALLISTICS"
items_to_give = ["Refining", "Electricity", "Apprenticeship", "Industrialization"]
self.assertFalse(self.can_reach_location(location))
for prereq in items_to_give:
self.collect_by_name(prereq)
is_last_prereq = prereq == items_to_give[-1]
self.assertEqual(self.can_reach_location(location), is_last_prereq)
class TestBoostsanityIncludedNoProgressiveDistricts(CivVITestBase):
auto_construct = False
options = {
"progressive_eras": "true",
"boostsanity": "true",
"progression_style": "districts_only",
"shuffle_goody_hut_rewards": "false",
}
def test_boosts_get_included(self) -> None:
self.world_setup()
distribute_items_restrictive(self.multiworld)
locations = self.multiworld.get_locations(self.player)
found_locations = 0
for location in locations:
if "BOOST" in location.name:
found_locations += 1
num_boost_locations = len(get_boosts_data())
self.assertEqual(found_locations, num_boost_locations)
class TestBoostsanityPrereqsWithProgressiveDistricts(CivVITestBase):
options = {
"progressive_eras": "true",
"boostsanity": "true",
"progression_style": "districts_only",
"shuffle_goody_hut_rewards": "false",
}
def test_boosts_require_progressive_prereqs_optional(self) -> None:
location = "BOOST_TECH_NUCLEAR_FUSION"
items_to_give = ["Progressive Industrial Zone", "Progressive Industrial Zone"]
self.assertFalse(self.can_reach_location(location))
for prereq in items_to_give:
self.collect_by_name(prereq)
is_last_prereq = prereq == items_to_give[-1]
self.assertEqual(self.can_reach_location(location), is_last_prereq)
def tests_boosts_require_correct_progressive_district_count(self) -> None:
location = "BOOST_TECH_RIFLING"
items_to_give = ["Mining", "Progressive Encampment", "Progressive Encampment"]
self.assertFalse(self.can_reach_location(location))
for prereq in items_to_give:
self.collect_by_name(prereq)
is_last_prereq = prereq == items_to_give[-1]
self.assertEqual(self.can_reach_location(location), is_last_prereq)
class TestBoostsanityExcluded(CivVITestBase):
auto_construct = False
options = {
"progressive_eras": "true",
"death_link": "true",
"boostsanity": "false",
"death_link_effect": "unit_killed",
"progressive_districts": "true",
"shuffle_goody_hut_rewards": "false",
}
def test_boosts_are_not_included(self) -> None:
self.world_setup()
distribute_items_restrictive(self.multiworld)
locations = self.multiworld.get_locations(self.player)
found_locations = 0
for location in locations:
if "BOOST" in location.name:
found_locations += 1
self.assertEqual(found_locations, 0)

View File

@@ -0,0 +1,114 @@
from typing import Dict
from BaseClasses import ItemClassification
from Fill import distribute_items_restrictive
from ..Items import FillerItemRarity, filler_data
from . import CivVITestBase
class TestGoodyHutsIncluded(CivVITestBase):
auto_construct = False
options = {
"progressive_eras": "true",
"progressive_districts": "true",
"shuffle_goody_hut_rewards": "true",
}
def test_goody_huts_get_included(self) -> None:
self.world_setup()
self.world.generate_early()
distribute_items_restrictive(self.multiworld)
expected_goody_huts = 10
found = 0
for location in self.multiworld.get_locations(self.player):
if location.name.startswith("GOODY_HUT_"):
found += 1
self.assertEqual(found, expected_goody_huts)
class TestGoodyHutsExcluded(CivVITestBase):
auto_construct = False
options = {
"progressive_eras": "true",
"progressive_districts": "true",
"shuffle_goody_hut_rewards": "false",
}
def test_goody_huts_are_not_included(self) -> None:
self.world_setup()
self.world.generate_early()
distribute_items_restrictive(self.multiworld)
found_goody_huts = 0
for location in self.multiworld.get_locations(self.player):
if location.name.startswith("GOODY_HUT_"):
found_goody_huts += 1
self.assertEqual(found_goody_huts, 0)
class TestFillerItemsIncludedByRarity(CivVITestBase):
auto_construct = False
options = {
"progressive_eras": "true",
"progressive_districts": "true",
"shuffle_goody_hut_rewards": "true",
"boostsanity": "true"
}
def test_filler_items_are_included_by_rarity(self) -> None:
self.world_setup()
self.world.generate_early()
distribute_items_restrictive(self.multiworld)
rarity_counts: Dict[FillerItemRarity, int] = {
FillerItemRarity.COMMON: 0,
FillerItemRarity.UNCOMMON: 0,
FillerItemRarity.RARE: 0,
}
total_filler_items = 0
for item in self.multiworld.itempool:
if item.classification == ItemClassification.filler:
rarity = filler_data[item.name].rarity
rarity_counts[rarity] += 1
total_filler_items += 1
expected_counts = {
FillerItemRarity.COMMON: 101,
FillerItemRarity.UNCOMMON: 27,
FillerItemRarity.RARE: 4,
}
for rarity, expected in expected_counts.items():
self.assertEqual(rarity_counts[rarity], expected, f"Expected {expected} {rarity} items, found {rarity_counts[rarity]}")
class TestFillerItemsIncludedByRarityWithoutBoostsanity(CivVITestBase):
auto_construct = False
options = {
"progressive_eras": "true",
"progressive_districts": "true",
"shuffle_goody_hut_rewards": "true",
"boostsanity": "false"
}
def test_filler_items_are_included_by_rarity_without_boostsanity(self) -> None:
self.world_setup()
self.world.generate_early()
distribute_items_restrictive(self.multiworld)
rarity_counts: Dict[FillerItemRarity, int] = {
FillerItemRarity.COMMON: 0,
FillerItemRarity.UNCOMMON: 0,
FillerItemRarity.RARE: 0,
}
total_filler_items = 0
for item in self.multiworld.itempool:
if item.classification == ItemClassification.filler:
rarity = filler_data[item.name].rarity
rarity_counts[rarity] += 1
total_filler_items += 1
expected_counts = {
FillerItemRarity.COMMON: 7,
FillerItemRarity.UNCOMMON: 2,
FillerItemRarity.RARE: 1,
}
for rarity, expected in expected_counts.items():
self.assertEqual(rarity_counts[rarity], expected, f"Expected {expected} {rarity} items, found {rarity_counts[rarity]}")

View File

@@ -0,0 +1,234 @@
from typing import Callable, List
from BaseClasses import CollectionState
from ..Data import get_era_required_items_data
from ..Enum import EraType
from ..ProgressiveDistricts import convert_items_to_progressive_items
from ..Items import get_item_by_civ_name
from . import CivVITestBase
def collect_items_for_era(test: CivVITestBase, era: EraType) -> None:
era_required_items = get_era_required_items_data()
items = [
get_item_by_civ_name(item, test.world.item_table).name
for item in era_required_items[era.value]
]
test.collect_by_name(items)
def collect_items_for_era_progressive(test: CivVITestBase, era: EraType) -> None:
era_progression_items = get_era_required_items_data()
progressive_items = convert_items_to_progressive_items(
era_progression_items[era.value]
)
items = [
get_item_by_civ_name(item, test.world.item_table).name
for item in progressive_items
]
for item in items:
test.collect(test.get_item_by_name(item))
def verify_eras_accessible(
test: CivVITestBase,
state: CollectionState,
collect_func: Callable[[CivVITestBase, EraType], None],
) -> None:
"""Collect for an era, then check if the next era is accessible and the one after that is not"""
for era in EraType:
if era == EraType.ERA_ANCIENT:
test.assertTrue(state.can_reach(era.value, "Region", test.player))
else:
test.assertFalse(state.can_reach(era.value, "Region", test.player))
eras = [
EraType.ERA_ANCIENT,
EraType.ERA_CLASSICAL,
EraType.ERA_MEDIEVAL,
EraType.ERA_RENAISSANCE,
EraType.ERA_INDUSTRIAL,
EraType.ERA_MODERN,
EraType.ERA_ATOMIC,
EraType.ERA_INFORMATION,
EraType.ERA_FUTURE,
]
for i in range(len(eras) - 1):
collect_func(test, eras[i])
test.assertTrue(state.can_reach(eras[i + 1].value, "Region", test.player))
if i + 2 < len(eras):
test.assertFalse(state.can_reach(eras[i + 2].value, "Region", test.player))
class TestNonProgressiveRegionRequirements(CivVITestBase):
options = {
"progression_style": "none",
"boostsanity": "false",
}
def test_eras_are_accessible_without_progressive_districts(self) -> None:
state = self.multiworld.state
verify_eras_accessible(self, state, collect_items_for_era)
class TestNonProgressiveRegionRequirementsWithBoostsanity(CivVITestBase):
options = {
"progression_style": "none",
"boostsanity": "true",
}
def test_eras_are_accessible_without_progressive_districts(self) -> None:
state = self.multiworld.state
verify_eras_accessible(self, state, collect_items_for_era)
class TestProgressiveDistrictRequirementsWithBoostsanity(CivVITestBase):
options = {
"progression_style": "districts_only",
"boostsanity": "true",
}
def test_eras_are_accessible_with_progressive_districts(self) -> None:
state = self.multiworld.state
verify_eras_accessible(self, state, collect_items_for_era_progressive)
class TestProgressiveDistrictRequirements(CivVITestBase):
options = {
"progression_style": "districts_only",
"boostsanity": "false",
}
def test_eras_are_accessible_with_progressive_districts(self) -> None:
state = self.multiworld.state
verify_eras_accessible(self, state, collect_items_for_era_progressive)
def test_progressive_districts_are_required(self) -> None:
state = self.multiworld.state
self.collect_all_but(["Progressive Encampment"])
self.assertFalse(state.can_reach("ERA_CLASSICAL", "Region", self.player))
self.assertFalse(state.can_reach("ERA_RENAISSANCE", "Region", self.player))
self.assertFalse(state.can_reach("ERA_MODERN", "Region", self.player))
self.collect(self.get_item_by_name("Progressive Encampment"))
self.assertTrue(state.can_reach("ERA_CLASSICAL", "Region", self.player))
self.assertFalse(state.can_reach("ERA_RENAISSANCE", "Region", self.player))
self.assertFalse(state.can_reach("ERA_MODERN", "Region", self.player))
self.collect(self.get_item_by_name("Progressive Encampment"))
self.assertTrue(state.can_reach("ERA_RENAISSANCE", "Region", self.player))
self.assertFalse(state.can_reach("ERA_MODERN", "Region", self.player))
self.collect(self.get_item_by_name("Progressive Encampment"))
self.assertTrue(state.can_reach("ERA_MODERN", "Region", self.player))
class TestProgressiveEraRequirements(CivVITestBase):
options = {
"progression_style": "eras_and_districts",
}
def test_eras_are_accessible_with_progressive_eras(self) -> None:
state = self.multiworld.state
self.collect_all_but(["Progressive Era"])
def check_eras_accessible(eras: List[EraType]):
for era in EraType:
if era in eras:
self.assertTrue(state.can_reach(era.value, "Region", self.player))
else:
self.assertFalse(state.can_reach(era.value, "Region", self.player))
progresive_era_item = self.get_item_by_name("Progressive Era")
accessible_eras = [EraType.ERA_ANCIENT]
check_eras_accessible(accessible_eras)
# Classical era requires 2 progressive era items
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_CLASSICAL]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_MEDIEVAL]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_RENAISSANCE]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_INDUSTRIAL]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_MODERN]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_ATOMIC]
check_eras_accessible(accessible_eras)
# Since we collect 2 in the ancient era, information and future era have same logic requirement
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_INFORMATION]
accessible_eras += [EraType.ERA_FUTURE]
check_eras_accessible(accessible_eras)
class TestProgressiveEraRequirementsWithBoostsanity(CivVITestBase):
options = {
"progression_style": "eras_and_districts",
"boostsanity": "true",
}
def test_eras_are_accessible_with_progressive_eras(self) -> None:
state = self.multiworld.state
self.collect_all_but(["Progressive Era"])
def check_eras_accessible(eras: List[EraType]):
for era in EraType:
if era in eras:
self.assertTrue(
state.can_reach(era.value, "Region", self.player),
"Failed for era: " + era.value,
)
else:
self.assertFalse(
state.can_reach(era.value, "Region", self.player),
"Failed for era: " + era.value,
)
progresive_era_item = self.get_item_by_name("Progressive Era")
accessible_eras = [EraType.ERA_ANCIENT]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_CLASSICAL]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_MEDIEVAL]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_RENAISSANCE]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_INDUSTRIAL]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_MODERN]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_ATOMIC]
check_eras_accessible(accessible_eras)
# Since we collect 2 in the ancient era, information and future era have same logic requirement
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_INFORMATION]
accessible_eras += [EraType.ERA_FUTURE]
check_eras_accessible(accessible_eras)

View File

@@ -0,0 +1,125 @@
from BaseClasses import ItemClassification
from Fill import distribute_items_restrictive
from ..Enum import CivVICheckType
from . import CivVITestBase
class TestStartingHints(CivVITestBase):
run_default_tests = False # type: ignore
auto_construct = False
options = {
"progressive_eras": "true",
"death_link": "true",
"death_link_effect": "unit_killed",
"progressive_districts": "true",
"pre_hint_items": set({"Progression", "Useful", "Filler"}),
}
def test_all_tech_civic_items_are_hinted_default(self) -> None:
self.world_setup()
distribute_items_restrictive(self.multiworld)
self.world.post_fill()
start_location_hints = self.world.options.start_location_hints.value
for location_name, location_data in self.world.location_table.items():
if location_data.location_type == CivVICheckType.CIVIC or location_data.location_type == CivVICheckType.TECH:
self.assertIn(location_name, start_location_hints)
else:
self.assertNotIn(location_name, start_location_hints)
class TestOnlyProgressionItemsHinted(CivVITestBase):
run_default_tests = False # type: ignore
auto_construct = False
options = {
"progressive_eras": "true",
"death_link": "true",
"death_link_effect": "unit_killed",
"progressive_districts": "true",
"pre_hint_items": set({"Progression"}),
}
def test_only_progression_items_are_hinted(self) -> None:
self.world_setup()
distribute_items_restrictive(self.multiworld)
self.world.post_fill()
start_location_hints = self.world.options.start_location_hints.value
self.assertTrue(len(start_location_hints) > 0)
for hint in start_location_hints:
location_data = self.world.get_location(hint)
if location_data.item:
self.assertTrue(location_data.item.classification == ItemClassification.progression)
else:
self.assertTrue(False, "Location has no item")
class TestNoJunkItemsHinted(CivVITestBase):
run_default_tests = False # type: ignore
auto_construct = False
options = {
"progressive_eras": "true",
"death_link": "true",
"death_link_effect": "unit_killed",
"progressive_districts": "true",
"pre_hint_items": set({"Progression", "Useful"}),
"boostsanity": "true",
"shuffle_goody_hut_rewards": "true",
}
def test_no_junk_items_are_hinted(self) -> None:
self.world_setup()
distribute_items_restrictive(self.multiworld)
item = self.multiworld.get_location("TECH_AP_ANCIENT_01", self.player).item
self.assertIsNotNone(item)
if item:
item.classification = ItemClassification.filler
self.world.post_fill()
start_location_hints = self.world.options.start_location_hints.value
self.assertTrue(len(start_location_hints) > 0)
self.assertNotIn("TECH_AP_ANCIENT_01", start_location_hints)
class TestOnlyJunkItemsHinted(CivVITestBase):
run_default_tests = False # type: ignore
auto_construct = False
options = {
"progressive_eras": "true",
"death_link": "true",
"death_link_effect": "unit_killed",
"progressive_districts": "true",
"pre_hint_items": set({"Filler"}),
}
def test_only_junk_items_are_hinted(self) -> None:
self.world_setup()
distribute_items_restrictive(self.multiworld)
item = self.multiworld.get_location("TECH_AP_ANCIENT_01", self.player).item
self.assertIsNotNone(item)
if item:
item.classification = ItemClassification.filler
self.world.post_fill()
start_location_hints = self.world.options.start_location_hints.value
self.assertEqual(len(start_location_hints), 1)
self.assertIn("TECH_AP_ANCIENT_01", start_location_hints)
class TestNoItemsHinted(CivVITestBase):
run_default_tests = False # type: ignore
auto_construct = False
options = {
"progressive_eras": "true",
"death_link": "true",
"death_link_effect": "unit_killed",
"progressive_districts": "true",
"pre_hint_items": set({}),
}
def test_no_items_are_hinted(self) -> None:
self.world_setup()
distribute_items_restrictive(self.multiworld)
self.world.post_fill()
start_location_hints = self.world.options.start_location_hints.value
self.assertEqual(len(start_location_hints), 0)

View File

@@ -0,0 +1,8 @@
from typing import ClassVar
from test.bases import WorldTestBase
class CivVITestBase(WorldTestBase):
game = "Civilization VI"
player: ClassVar[int] = 1

View File

@@ -19,8 +19,8 @@ from worlds.AutoWorld import WebWorld, World
from .aesthetics import shuffle_sub_weapons, get_location_data, get_countdown_flags, populate_enemy_drops, \
get_start_inventory_data
from .rom import RomData, patch_rom, get_base_rom_path, CVCotMProcedurePatch, CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, \
CVCOTM_VC_US_HASH
from .rom import RomData, patch_rom, get_base_rom_path, CVCotMProcedurePatch, CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH
# CVCOTM_VC_US_HASH
from .client import CastlevaniaCotMClient
@@ -29,7 +29,8 @@ class CVCotMSettings(settings.Group):
"""File name of the Castlevania CotM US rom"""
copy_to = "Castlevania - Circle of the Moon (USA).gba"
description = "Castlevania CotM (US) ROM File"
md5s = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]
# md5s = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]
md5s = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH]
rom_file: RomFile = RomFile(RomFile.copy_to)

View File

@@ -153,11 +153,10 @@ Advance Collection ROM; most notably the fact that the audio does not function w
which is currently a requirement to connect to a multiworld. This happens because all audio code was stripped
from the ROM, and all sound is instead played by the collection through external means.
For this reason, it is most recommended to obtain the ROM by dumping it from an original cartridge of the game that you legally own.
Though, the Advance Collection *can* still technically be an option if you cannot do that and don't mind the lack of sound.
The Wii U Virtual Console version does not work due to changes in the code in that version.
The Wii U Virtual Console version is currently untested. If you happen to have purchased it before the Wii U eShop shut down, you can try
dumping and playing with it. However, at the moment, we cannot guarantee that it will work well due to it being untested.
Due to the reasons mentioned above, it is most recommended to obtain the ROM by dumping it from an original cartridge of the
game that you legally own. However, the Advance Collection *is* an option if you cannot do that and don't mind the lack of sound.
Regardless of which released ROM you intend to try playing with, the US version of the game is required.

View File

@@ -4,7 +4,7 @@
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest).
- A Castlevania: Circle of the Moon ROM of the US version specifically. The Archipelago community cannot provide this.
The Castlevania Advance Collection ROM can technically be used, but it has no audio. The Wii U Virtual Console ROM is untested.
The Castlevania Advance Collection ROM can be used, but it has no audio. The Wii U Virtual Console ROM does not work.
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later.
### Configuring BizHawk

View File

@@ -22,11 +22,9 @@ if TYPE_CHECKING:
CVCOTM_CT_US_HASH = "50a1089600603a94e15ecf287f8d5a1f" # Original GBA cartridge ROM
CVCOTM_AC_US_HASH = "87a1bd6577b6702f97a60fc55772ad74" # Castlevania Advance Collection ROM
CVCOTM_VC_US_HASH = "2cc38305f62b337281663bad8c901cf9" # Wii U Virtual Console ROM
# CVCOTM_VC_US_HASH = "2cc38305f62b337281663bad8c901cf9" # Wii U Virtual Console ROM
# NOTE: The Wii U VC version is untested as of when this comment was written. I am only including its hash in case it
# does work. If someone who has it can confirm it does indeed work, this comment should be removed. If it doesn't, the
# hash should be removed in addition. See the Game Page for more information about supported versions.
# The Wii U VC version is not currently supported. See the Game Page for more info.
ARCHIPELAGO_IDENTIFIER_START = 0x7FFF00
ARCHIPELAGO_IDENTIFIER = "ARCHIPELAG03"
@@ -518,7 +516,8 @@ class CVCotMPatchExtensions(APPatchExtension):
class CVCotMProcedurePatch(APProcedurePatch, APTokenMixin):
hash = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]
# hash = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]
hash = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH]
patch_file_ending: str = ".apcvcotm"
result_file_ending: str = ".gba"
@@ -585,7 +584,8 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if basemd5.hexdigest() not in [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]:
# if basemd5.hexdigest() not in [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]:
if basemd5.hexdigest() not in [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH]:
raise Exception("Supplied Base ROM does not match known MD5s for Castlevania: Circle of the Moon USA."
"Get the correct game and version, then dump it.")
setattr(get_base_rom_bytes, "base_rom_bytes", base_rom_bytes)

Some files were not shown because too many files have changed in this diff Show More