mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-10 01:23:48 -07:00
Compare commits
44 Commits
NewSoupVi-
...
empty-deat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c5418d708 | ||
|
|
2c8dded52f | ||
|
|
06111ac6cf | ||
|
|
d83294efa7 | ||
|
|
be550ff6fb | ||
|
|
dd55409209 | ||
|
|
e267714d44 | ||
|
|
7c30c4a169 | ||
|
|
4882366ffc | ||
|
|
5f73c245fc | ||
|
|
21ffc0fc54 | ||
|
|
e95a41cf93 | ||
|
|
04771fa4f0 | ||
|
|
2639796255 | ||
|
|
4ebabc1208 | ||
|
|
ce34b60712 | ||
|
|
54094c6331 | ||
|
|
3986f6f11a | ||
|
|
5662da6f7d | ||
|
|
33a75fb2cb | ||
|
|
ee9bcb84b7 | ||
|
|
b5269e9aa4 | ||
|
|
00a6ac3a52 | ||
|
|
ea8a14b003 | ||
|
|
414ab86422 | ||
|
|
d4e2698ae0 | ||
|
|
3f8e3082c0 | ||
|
|
0f738935ee | ||
|
|
9c57976252 | ||
|
|
3e08acf381 | ||
|
|
113259bc15 | ||
|
|
61afe76eae | ||
|
|
08b3b3ecf5 | ||
|
|
bc61221ec6 | ||
|
|
2f0b81e12c | ||
|
|
bb9a6bcd2e | ||
|
|
c8b7ef1016 | ||
|
|
e00467c2a2 | ||
|
|
0eb6150e95 | ||
|
|
91d977479d | ||
|
|
cd761db170 | ||
|
|
026011323e | ||
|
|
adc5f3a07d | ||
|
|
69940374e1 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -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`"
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -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`"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@
|
||||
*_Spoiler.txt
|
||||
*.bmbp
|
||||
*.apbp
|
||||
*.apcivvi
|
||||
*.apl2ac
|
||||
*.apm3
|
||||
*.apmc
|
||||
|
||||
@@ -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:
|
||||
@@ -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 = {}
|
||||
@@ -507,7 +560,19 @@ class LinksAwakeningContext(CommonContext):
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
async def send_checks(self):
|
||||
message = [{"cmd": 'LocationChecks', "locations": self.found_checks}]
|
||||
message = [{"cmd": "LocationChecks", "locations": self.found_checks}]
|
||||
await self.send_msgs(message)
|
||||
|
||||
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}],
|
||||
}]
|
||||
|
||||
await self.send_msgs(message)
|
||||
|
||||
had_invalid_slot_data = None
|
||||
@@ -536,6 +601,12 @@ 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:
|
||||
@@ -576,6 +647,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 +666,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,12 +705,20 @@ 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()
|
||||
@@ -635,8 +726,15 @@ class LinksAwakeningContext(CommonContext):
|
||||
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
|
||||
|
||||
@@ -80,6 +80,7 @@ Currently, the following games are supported:
|
||||
* Saving Princess
|
||||
* Castlevania: Circle of the Moon
|
||||
* Inscryption
|
||||
* Civilization VI
|
||||
|
||||
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
|
||||
|
||||
3
Utils.py
3
Utils.py
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">❖</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<table id="location-table">
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
# ChecksFinder
|
||||
/worlds/checksfinder/ @SunCatMC
|
||||
|
||||
# Civilization VI
|
||||
/worlds/civ6/ @hesto2
|
||||
|
||||
# Clique
|
||||
/worlds/clique/ @ThePhar
|
||||
|
||||
|
||||
@@ -756,8 +756,8 @@ Tags are represented as a list of strings, the common client tags follow:
|
||||
### DeathLink
|
||||
A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:
|
||||
|
||||
| Name | Type | Notes |
|
||||
|--------|-------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| time | float | Unix Time Stamp of time of death. |
|
||||
| cause | str | Optional. Text to explain the cause of death. When provided, or checked, this should contain the player name, ex. "Berserker was run over by a train." |
|
||||
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |
|
||||
| Name | Type | Notes |
|
||||
|--------|-------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| time | float | Unix Time Stamp of time of death. |
|
||||
| cause | str | Optional. Text to explain the cause of death. When provided, or checked, if the string is non-empty, it should contain the player name, ex. "Berserker was run over by a train." |
|
||||
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |
|
||||
|
||||
@@ -73,11 +73,11 @@ 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -326,6 +329,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
|
||||
@@ -339,7 +360,7 @@ def randomize_entrances(
|
||||
|
||||
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):
|
||||
@@ -355,7 +376,7 @@ def randomize_entrances(
|
||||
and len(placeable_exits) == 1)
|
||||
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)):
|
||||
and not er_state.test_speculative_connection(source_exit, target_entrance, exits_set)):
|
||||
continue
|
||||
do_placement(source_exit, target_entrance)
|
||||
return True
|
||||
@@ -407,21 +428,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):
|
||||
|
||||
@@ -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: "";
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -1547,9 +1547,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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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']],
|
||||
|
||||
@@ -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']],
|
||||
|
||||
@@ -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, []],
|
||||
|
||||
@@ -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']],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
342
worlds/civ_6/Civ6Client.py
Normal file
342
worlds/civ_6/Civ6Client.py
Normal 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)
|
||||
119
worlds/civ_6/CivVIInterface.py
Normal file
119
worlds/civ_6/CivVIInterface.py
Normal 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)
|
||||
219
worlds/civ_6/Container.py
Normal file
219
worlds/civ_6/Container.py
Normal file
@@ -0,0 +1,219 @@
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, cast
|
||||
import zipfile
|
||||
from BaseClasses import Location
|
||||
from worlds.Files import APContainer
|
||||
|
||||
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):
|
||||
"""
|
||||
Responsible for generating the dynamic mod files for the Civ VI multiworld
|
||||
"""
|
||||
game: Optional[str] = "Civilization VI"
|
||||
|
||||
def __init__(self, patch_data: Dict[str, str], base_path: str, output_directory: str,
|
||||
player: Optional[int] = None, player_name: str = "", server: str = ""):
|
||||
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
70
worlds/civ_6/Data.py
Normal 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
74
worlds/civ_6/DeathLink.py
Normal 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
39
worlds/civ_6/Enum.py
Normal 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
38
worlds/civ_6/ItemData.py
Normal 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
353
worlds/civ_6/Items.py
Normal 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
21
worlds/civ_6/LICENSE.md
Normal 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
156
worlds/civ_6/Locations.py
Normal 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
130
worlds/civ_6/Options.py
Normal 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
|
||||
35
worlds/civ_6/ProgressiveDistricts.py
Normal file
35
worlds/civ_6/ProgressiveDistricts.py
Normal 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
128
worlds/civ_6/Regions.py
Normal 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
109
worlds/civ_6/Rules.py
Normal 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
105
worlds/civ_6/TunerClient.py
Normal 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
326
worlds/civ_6/__init__.py
Normal 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
919
worlds/civ_6/data/boosts.py
Normal 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",
|
||||
),
|
||||
]
|
||||
75
worlds/civ_6/data/era_required_items.py
Normal file
75
worlds/civ_6/data/era_required_items.py
Normal 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": [],
|
||||
}
|
||||
435
worlds/civ_6/data/existing_civics.py
Normal file
435
worlds/civ_6/data/existing_civics.py
Normal 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,
|
||||
},
|
||||
]
|
||||
546
worlds/civ_6/data/existing_tech.py
Normal file
546
worlds/civ_6/data/existing_tech.py
Normal 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",
|
||||
},
|
||||
]
|
||||
81
worlds/civ_6/data/goody_hut_rewards.py
Normal file
81
worlds/civ_6/data/goody_hut_rewards.py
Normal 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"
|
||||
}
|
||||
]
|
||||
92
worlds/civ_6/data/new_civic_prereqs.py
Normal file
92
worlds/civ_6/data/new_civic_prereqs.py
Normal 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"},
|
||||
]
|
||||
372
worlds/civ_6/data/new_civics.py
Normal file
372
worlds/civ_6/data/new_civics.py
Normal 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",
|
||||
},
|
||||
]
|
||||
468
worlds/civ_6/data/new_tech.py
Normal file
468
worlds/civ_6/data/new_tech.py
Normal 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",
|
||||
},
|
||||
]
|
||||
110
worlds/civ_6/data/new_tech_prereqs.py
Normal file
110
worlds/civ_6/data/new_tech_prereqs.py
Normal 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"},
|
||||
]
|
||||
41
worlds/civ_6/data/progressive_districts.py
Normal file
41
worlds/civ_6/data/progressive_districts.py
Normal 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",
|
||||
],
|
||||
}
|
||||
59
worlds/civ_6/docs/en_Civilization VI.md
Normal file
59
worlds/civ_6/docs/en_Civilization VI.md
Normal 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.
|
||||
|
||||
51
worlds/civ_6/docs/setup_en.md
Normal file
51
worlds/civ_6/docs/setup_en.md
Normal 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 → Options → 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.
|
||||
107
worlds/civ_6/test/TestBoostsanity.py
Normal file
107
worlds/civ_6/test/TestBoostsanity.py
Normal 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)
|
||||
114
worlds/civ_6/test/TestGoodyHuts.py
Normal file
114
worlds/civ_6/test/TestGoodyHuts.py
Normal 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]}")
|
||||
234
worlds/civ_6/test/TestRegionRequirements.py
Normal file
234
worlds/civ_6/test/TestRegionRequirements.py
Normal 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)
|
||||
125
worlds/civ_6/test/TestStartingHints.py
Normal file
125
worlds/civ_6/test/TestStartingHints.py
Normal 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)
|
||||
8
worlds/civ_6/test/__init__.py
Normal file
8
worlds/civ_6/test/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from typing import ClassVar
|
||||
|
||||
from test.bases import WorldTestBase
|
||||
|
||||
|
||||
class CivVITestBase(WorldTestBase):
|
||||
game = "Civilization VI"
|
||||
player: ClassVar[int] = 1
|
||||
@@ -25,19 +25,10 @@ class DarkSouls3Web(WebWorld):
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["Marech"]
|
||||
["Natalie", "Marech"]
|
||||
)
|
||||
|
||||
setup_fr = Tutorial(
|
||||
setup_en.tutorial_name,
|
||||
setup_en.description,
|
||||
"Français",
|
||||
"setup_fr.md",
|
||||
"setup/fr",
|
||||
["Marech"]
|
||||
)
|
||||
|
||||
tutorials = [setup_en, setup_fr]
|
||||
tutorials = [setup_en]
|
||||
option_groups = option_groups
|
||||
item_descriptions = item_descriptions
|
||||
rich_text_options_doc = True
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# Guide d'installation de Dark Souls III Randomizer
|
||||
|
||||
## Logiciels requis
|
||||
|
||||
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
|
||||
- [Client AP de Dark Souls III](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases)
|
||||
|
||||
## Concept général
|
||||
|
||||
Le client Archipelago de Dark Souls III est un fichier dinput8.dll. Cette .dll va lancer une invite de commande Windows
|
||||
permettant de lire des informations de la partie et écrire des commandes pour intéragir avec le serveur Archipelago.
|
||||
|
||||
## Procédures d'installation
|
||||
|
||||
<span style="color:#ff7800">
|
||||
**Il y a des risques de bannissement permanent des serveurs FromSoftware si ce mod est utilisé en ligne.**
|
||||
</span>
|
||||
Ce client a été testé sur la version Steam officielle du jeu (v1.15/1.35), peu importe les DLCs actuellement installés.
|
||||
|
||||
Télécharger le fichier dinput8.dll disponible dans le [Client AP de Dark Souls III](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) et
|
||||
placez-le à la racine du jeu (ex: "SteamLibrary\steamapps\common\DARK SOULS III\Game")
|
||||
|
||||
## Rejoindre une partie Multiworld
|
||||
|
||||
1. Lancer DarkSoulsIII.exe ou lancer le jeu depuis Steam
|
||||
2. Ecrire "/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME}" dans l'invite de commande Windows ouverte au lancement du jeu
|
||||
3. Une fois connecté, créez une nouvelle partie, choisissez une classe et attendez que les autres soient prêts avant de lancer
|
||||
4. Vous pouvez quitter et lancer le jeu n'importe quand pendant une partie
|
||||
|
||||
## Où trouver le fichier de configuration ?
|
||||
|
||||
La [Page de configuration](/games/Dark%20Souls%20III/player-options) sur le site vous permez de configurer vos
|
||||
paramètres et de les exporter sous la forme d'un fichier.
|
||||
@@ -650,8 +650,8 @@ item_table: Dict[int, ItemDict] = {
|
||||
'doom_type': 2006,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
350106: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
350106: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Backpack',
|
||||
'doom_type': 8,
|
||||
'episode': -1,
|
||||
@@ -1160,6 +1160,30 @@ item_table: Dict[int, ItemDict] = {
|
||||
'doom_type': 2026,
|
||||
'episode': 4,
|
||||
'map': 9},
|
||||
350191: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Bullet capacity',
|
||||
'doom_type': 65001,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
350192: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Shell capacity',
|
||||
'doom_type': 65002,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
350193: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Energy cell capacity',
|
||||
'doom_type': 65003,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
350194: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Rocket capacity',
|
||||
'doom_type': 65004,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from Options import PerGameCommonOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
|
||||
from Options import PerGameCommonOptions, Range, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@@ -144,6 +144,84 @@ class Episode4(Toggle):
|
||||
display_name = "Episode 4"
|
||||
|
||||
|
||||
class SplitBackpack(Toggle):
|
||||
"""Split the Backpack into four individual items, each one increasing ammo capacity for one type of weapon only."""
|
||||
display_name = "Split Backpack"
|
||||
|
||||
|
||||
class BackpackCount(Range):
|
||||
"""How many Backpacks will be available.
|
||||
If Split Backpack is set, this will be the number of each capacity upgrade available."""
|
||||
display_name = "Backpack Count"
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
default = 1
|
||||
|
||||
|
||||
class MaxAmmoBullets(Range):
|
||||
"""Set the starting ammo capacity for bullets."""
|
||||
display_name = "Max Ammo - Bullets"
|
||||
range_start = 200
|
||||
range_end = 999
|
||||
default = 200
|
||||
|
||||
|
||||
class MaxAmmoShells(Range):
|
||||
"""Set the starting ammo capacity for shotgun shells."""
|
||||
display_name = "Max Ammo - Shells"
|
||||
range_start = 50
|
||||
range_end = 999
|
||||
default = 50
|
||||
|
||||
|
||||
class MaxAmmoRockets(Range):
|
||||
"""Set the starting ammo capacity for rockets."""
|
||||
display_name = "Max Ammo - Rockets"
|
||||
range_start = 50
|
||||
range_end = 999
|
||||
default = 50
|
||||
|
||||
|
||||
class MaxAmmoEnergyCells(Range):
|
||||
"""Set the starting ammo capacity for energy cells."""
|
||||
display_name = "Max Ammo - Energy Cells"
|
||||
range_start = 300
|
||||
range_end = 999
|
||||
default = 300
|
||||
|
||||
|
||||
class AddedAmmoBullets(Range):
|
||||
"""Set the amount of bullet capacity added when collecting a backpack or capacity upgrade."""
|
||||
display_name = "Added Ammo - Bullets"
|
||||
range_start = 20
|
||||
range_end = 999
|
||||
default = 200
|
||||
|
||||
|
||||
class AddedAmmoShells(Range):
|
||||
"""Set the amount of shotgun shell capacity added when collecting a backpack or capacity upgrade."""
|
||||
display_name = "Added Ammo - Shells"
|
||||
range_start = 5
|
||||
range_end = 999
|
||||
default = 50
|
||||
|
||||
|
||||
class AddedAmmoRockets(Range):
|
||||
"""Set the amount of rocket capacity added when collecting a backpack or capacity upgrade."""
|
||||
display_name = "Added Ammo - Rockets"
|
||||
range_start = 5
|
||||
range_end = 999
|
||||
default = 50
|
||||
|
||||
|
||||
class AddedAmmoEnergyCells(Range):
|
||||
"""Set the amount of energy cell capacity added when collecting a backpack or capacity upgrade."""
|
||||
display_name = "Added Ammo - Energy Cells"
|
||||
range_start = 30
|
||||
range_end = 999
|
||||
default = 300
|
||||
|
||||
|
||||
@dataclass
|
||||
class DOOM1993Options(PerGameCommonOptions):
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
@@ -163,3 +241,14 @@ class DOOM1993Options(PerGameCommonOptions):
|
||||
episode3: Episode3
|
||||
episode4: Episode4
|
||||
|
||||
split_backpack: SplitBackpack
|
||||
backpack_count: BackpackCount
|
||||
max_ammo_bullets: MaxAmmoBullets
|
||||
max_ammo_shells: MaxAmmoShells
|
||||
max_ammo_rockets: MaxAmmoRockets
|
||||
max_ammo_energy_cells: MaxAmmoEnergyCells
|
||||
added_ammo_bullets: AddedAmmoBullets
|
||||
added_ammo_shells: AddedAmmoShells
|
||||
added_ammo_rockets: AddedAmmoRockets
|
||||
added_ammo_energy_cells: AddedAmmoEnergyCells
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class DOOM1993World(World):
|
||||
options: DOOM1993Options
|
||||
game = "DOOM 1993"
|
||||
web = DOOM1993Web()
|
||||
required_client_version = (0, 3, 9)
|
||||
required_client_version = (0, 5, 0) # 1.2.0-prerelease or higher
|
||||
|
||||
item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()}
|
||||
item_name_groups = Items.item_name_groups
|
||||
@@ -204,6 +204,15 @@ class DOOM1993World(World):
|
||||
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
|
||||
itempool += [self.create_item(item["name"]) for _ in range(count)]
|
||||
|
||||
# Backpack(s) based on options
|
||||
if self.options.split_backpack.value:
|
||||
itempool += [self.create_item("Bullet capacity") for _ in range(self.options.backpack_count.value)]
|
||||
itempool += [self.create_item("Shell capacity") for _ in range(self.options.backpack_count.value)]
|
||||
itempool += [self.create_item("Energy cell capacity") for _ in range(self.options.backpack_count.value)]
|
||||
itempool += [self.create_item("Rocket capacity") for _ in range(self.options.backpack_count.value)]
|
||||
else:
|
||||
itempool += [self.create_item("Backpack") for _ in range(self.options.backpack_count.value)]
|
||||
|
||||
# Place end level items in locked locations
|
||||
for map_name in Maps.map_names:
|
||||
loc_name = map_name + " - Exit"
|
||||
@@ -265,7 +274,7 @@ class DOOM1993World(World):
|
||||
# Was balanced for 3 episodes (We added 4th episode, but keep same ratio)
|
||||
count = min(remaining_loc, max(1, int(round(self.items_ratio[item_name] * ep_count / 3))))
|
||||
if count == 0:
|
||||
logger.warning("Warning, no ", item_name, " will be placed.")
|
||||
logger.warning(f"Warning, no {item_name} will be placed.")
|
||||
return
|
||||
|
||||
for i in range(count):
|
||||
@@ -281,4 +290,14 @@ class DOOM1993World(World):
|
||||
# an older version, the player would end up stuck.
|
||||
slot_data["two_ways_keydoors"] = True
|
||||
|
||||
# Send slot data for ammo capacity values; this must be generic because Heretic uses it too
|
||||
slot_data["ammo1start"] = self.options.max_ammo_bullets.value
|
||||
slot_data["ammo2start"] = self.options.max_ammo_shells.value
|
||||
slot_data["ammo3start"] = self.options.max_ammo_energy_cells.value
|
||||
slot_data["ammo4start"] = self.options.max_ammo_rockets.value
|
||||
slot_data["ammo1add"] = self.options.added_ammo_bullets.value
|
||||
slot_data["ammo2add"] = self.options.added_ammo_shells.value
|
||||
slot_data["ammo3add"] = self.options.added_ammo_energy_cells.value
|
||||
slot_data["ammo4add"] = self.options.added_ammo_rockets.value
|
||||
|
||||
return slot_data
|
||||
|
||||
@@ -56,8 +56,8 @@ item_table: Dict[int, ItemDict] = {
|
||||
'doom_type': 82,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
360007: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
360007: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Backpack',
|
||||
'doom_type': 8,
|
||||
'episode': -1,
|
||||
@@ -1058,6 +1058,30 @@ item_table: Dict[int, ItemDict] = {
|
||||
'doom_type': 2026,
|
||||
'episode': 4,
|
||||
'map': 2},
|
||||
360600: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Bullet capacity',
|
||||
'doom_type': 65001,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
360601: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Shell capacity',
|
||||
'doom_type': 65002,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
360602: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Energy cell capacity',
|
||||
'doom_type': 65003,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
360603: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Rocket capacity',
|
||||
'doom_type': 65004,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import typing
|
||||
|
||||
from Options import PerGameCommonOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
|
||||
from Options import PerGameCommonOptions, Range, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@@ -136,6 +136,84 @@ class SecretLevels(Toggle):
|
||||
display_name = "Secret Levels"
|
||||
|
||||
|
||||
class SplitBackpack(Toggle):
|
||||
"""Split the Backpack into four individual items, each one increasing ammo capacity for one type of weapon only."""
|
||||
display_name = "Split Backpack"
|
||||
|
||||
|
||||
class BackpackCount(Range):
|
||||
"""How many Backpacks will be available.
|
||||
If Split Backpack is set, this will be the number of each capacity upgrade available."""
|
||||
display_name = "Backpack Count"
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
default = 1
|
||||
|
||||
|
||||
class MaxAmmoBullets(Range):
|
||||
"""Set the starting ammo capacity for bullets."""
|
||||
display_name = "Max Ammo - Bullets"
|
||||
range_start = 200
|
||||
range_end = 999
|
||||
default = 200
|
||||
|
||||
|
||||
class MaxAmmoShells(Range):
|
||||
"""Set the starting ammo capacity for shotgun shells."""
|
||||
display_name = "Max Ammo - Shells"
|
||||
range_start = 50
|
||||
range_end = 999
|
||||
default = 50
|
||||
|
||||
|
||||
class MaxAmmoRockets(Range):
|
||||
"""Set the starting ammo capacity for rockets."""
|
||||
display_name = "Max Ammo - Rockets"
|
||||
range_start = 50
|
||||
range_end = 999
|
||||
default = 50
|
||||
|
||||
|
||||
class MaxAmmoEnergyCells(Range):
|
||||
"""Set the starting ammo capacity for energy cells."""
|
||||
display_name = "Max Ammo - Energy Cells"
|
||||
range_start = 300
|
||||
range_end = 999
|
||||
default = 300
|
||||
|
||||
|
||||
class AddedAmmoBullets(Range):
|
||||
"""Set the amount of bullet capacity added when collecting a backpack or capacity upgrade."""
|
||||
display_name = "Added Ammo - Bullets"
|
||||
range_start = 20
|
||||
range_end = 999
|
||||
default = 200
|
||||
|
||||
|
||||
class AddedAmmoShells(Range):
|
||||
"""Set the amount of shotgun shell capacity added when collecting a backpack or capacity upgrade."""
|
||||
display_name = "Added Ammo - Shells"
|
||||
range_start = 5
|
||||
range_end = 999
|
||||
default = 50
|
||||
|
||||
|
||||
class AddedAmmoRockets(Range):
|
||||
"""Set the amount of rocket capacity added when collecting a backpack or capacity upgrade."""
|
||||
display_name = "Added Ammo - Rockets"
|
||||
range_start = 5
|
||||
range_end = 999
|
||||
default = 50
|
||||
|
||||
|
||||
class AddedAmmoEnergyCells(Range):
|
||||
"""Set the amount of energy cell capacity added when collecting a backpack or capacity upgrade."""
|
||||
display_name = "Added Ammo - Energy Cells"
|
||||
range_start = 30
|
||||
range_end = 999
|
||||
default = 300
|
||||
|
||||
|
||||
@dataclass
|
||||
class DOOM2Options(PerGameCommonOptions):
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
@@ -153,3 +231,14 @@ class DOOM2Options(PerGameCommonOptions):
|
||||
episode2: Episode2
|
||||
episode3: Episode3
|
||||
episode4: SecretLevels
|
||||
|
||||
split_backpack: SplitBackpack
|
||||
backpack_count: BackpackCount
|
||||
max_ammo_bullets: MaxAmmoBullets
|
||||
max_ammo_shells: MaxAmmoShells
|
||||
max_ammo_rockets: MaxAmmoRockets
|
||||
max_ammo_energy_cells: MaxAmmoEnergyCells
|
||||
added_ammo_bullets: AddedAmmoBullets
|
||||
added_ammo_shells: AddedAmmoShells
|
||||
added_ammo_rockets: AddedAmmoRockets
|
||||
added_ammo_energy_cells: AddedAmmoEnergyCells
|
||||
|
||||
@@ -43,7 +43,7 @@ class DOOM2World(World):
|
||||
options: DOOM2Options
|
||||
game = "DOOM II"
|
||||
web = DOOM2Web()
|
||||
required_client_version = (0, 3, 9)
|
||||
required_client_version = (0, 5, 0) # 1.2.0-prerelease or higher
|
||||
|
||||
item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()}
|
||||
item_name_groups = Items.item_name_groups
|
||||
@@ -196,6 +196,15 @@ class DOOM2World(World):
|
||||
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
|
||||
itempool += [self.create_item(item["name"]) for _ in range(count)]
|
||||
|
||||
# Backpack(s) based on options
|
||||
if self.options.split_backpack.value:
|
||||
itempool += [self.create_item("Bullet capacity") for _ in range(self.options.backpack_count.value)]
|
||||
itempool += [self.create_item("Shell capacity") for _ in range(self.options.backpack_count.value)]
|
||||
itempool += [self.create_item("Energy cell capacity") for _ in range(self.options.backpack_count.value)]
|
||||
itempool += [self.create_item("Rocket capacity") for _ in range(self.options.backpack_count.value)]
|
||||
else:
|
||||
itempool += [self.create_item("Backpack") for _ in range(self.options.backpack_count.value)]
|
||||
|
||||
# Place end level items in locked locations
|
||||
for map_name in Maps.map_names:
|
||||
loc_name = map_name + " - Exit"
|
||||
@@ -258,11 +267,23 @@ class DOOM2World(World):
|
||||
# Was balanced based on DOOM 1993's first 3 episodes
|
||||
count = min(remaining_loc, max(1, int(round(self.items_ratio[item_name] * ep_count / 3))))
|
||||
if count == 0:
|
||||
logger.warning("Warning, no ", item_name, " will be placed.")
|
||||
logger.warning(f"Warning, no {item_name} will be placed.")
|
||||
return
|
||||
|
||||
for i in range(count):
|
||||
itempool.append(self.create_item(item_name))
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
return self.options.as_dict("difficulty", "random_monsters", "random_pickups", "random_music", "flip_levels", "allow_death_logic", "pro", "death_link", "reset_level_on_death", "episode1", "episode2", "episode3", "episode4")
|
||||
slot_data = self.options.as_dict("difficulty", "random_monsters", "random_pickups", "random_music", "flip_levels", "allow_death_logic", "pro", "death_link", "reset_level_on_death", "episode1", "episode2", "episode3", "episode4")
|
||||
|
||||
# Send slot data for ammo capacity values; this must be generic because Heretic uses it too
|
||||
slot_data["ammo1start"] = self.options.max_ammo_bullets.value
|
||||
slot_data["ammo2start"] = self.options.max_ammo_shells.value
|
||||
slot_data["ammo3start"] = self.options.max_ammo_energy_cells.value
|
||||
slot_data["ammo4start"] = self.options.max_ammo_rockets.value
|
||||
slot_data["ammo1add"] = self.options.added_ammo_bullets.value
|
||||
slot_data["ammo2add"] = self.options.added_ammo_shells.value
|
||||
slot_data["ammo3add"] = self.options.added_ammo_energy_cells.value
|
||||
slot_data["ammo4add"] = self.options.added_ammo_rockets.value
|
||||
|
||||
return slot_data
|
||||
|
||||
@@ -50,8 +50,8 @@ item_table: Dict[int, ItemDict] = {
|
||||
'doom_type': 2004,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
370006: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
370006: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Bag of Holding',
|
||||
'doom_type': 8,
|
||||
'episode': -1,
|
||||
@@ -1592,6 +1592,42 @@ item_table: Dict[int, ItemDict] = {
|
||||
'doom_type': 35,
|
||||
'episode': 5,
|
||||
'map': 9},
|
||||
370600: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Crystal Capacity',
|
||||
'doom_type': 65001,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
370601: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Ethereal Arrow Capacity',
|
||||
'doom_type': 65002,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
370602: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Claw Orb Capacity',
|
||||
'doom_type': 65003,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
370603: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Rune Capacity',
|
||||
'doom_type': 65004,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
370604: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Flame Orb Capacity',
|
||||
'doom_type': 65005,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
370605: {'classification': ItemClassification.useful,
|
||||
'count': 0,
|
||||
'name': 'Mace Sphere Capacity',
|
||||
'doom_type': 65006,
|
||||
'episode': -1,
|
||||
'map': -1},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from Options import PerGameCommonOptions, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
|
||||
from Options import PerGameCommonOptions, Range, Choice, Toggle, DeathLink, DefaultOnToggle, StartInventoryPool
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@@ -144,6 +144,116 @@ class Episode5(Toggle):
|
||||
display_name = "Episode 5"
|
||||
|
||||
|
||||
class SplitBagOfHolding(Toggle):
|
||||
"""Split the Bag of Holding into six individual items, each one increasing ammo capacity for one type of weapon only."""
|
||||
display_name = "Split Bag of Holding"
|
||||
|
||||
|
||||
class BagOfHoldingCount(Range):
|
||||
"""How many Bags of Holding will be available.
|
||||
If Split Bag of Holding is set, this will be the number of each capacity upgrade available."""
|
||||
display_name = "Bag of Holding Count"
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
default = 1
|
||||
|
||||
|
||||
class MaxAmmoCrystals(Range):
|
||||
"""Set the starting ammo capacity for crystals (Elven Wand ammo)."""
|
||||
display_name = "Max Ammo - Crystals"
|
||||
range_start = 100
|
||||
range_end = 999
|
||||
default = 100
|
||||
|
||||
|
||||
class MaxAmmoArrows(Range):
|
||||
"""Set the starting ammo capacity for arrows (Ethereal Crossbow ammo)."""
|
||||
display_name = "Max Ammo - Arrows"
|
||||
range_start = 50
|
||||
range_end = 999
|
||||
default = 50
|
||||
|
||||
|
||||
class MaxAmmoClawOrbs(Range):
|
||||
"""Set the starting ammo capacity for claw orbs (Dragon Claw ammo)."""
|
||||
display_name = "Max Ammo - Claw Orbs"
|
||||
range_start = 200
|
||||
range_end = 999
|
||||
default = 200
|
||||
|
||||
|
||||
class MaxAmmoRunes(Range):
|
||||
"""Set the starting ammo capacity for runes (Hellstaff ammo)."""
|
||||
display_name = "Max Ammo - Runes"
|
||||
range_start = 200
|
||||
range_end = 999
|
||||
default = 200
|
||||
|
||||
|
||||
class MaxAmmoFlameOrbs(Range):
|
||||
"""Set the starting ammo capacity for flame orbs (Phoenix Rod ammo)."""
|
||||
display_name = "Max Ammo - Flame Orbs"
|
||||
range_start = 20
|
||||
range_end = 999
|
||||
default = 20
|
||||
|
||||
|
||||
class MaxAmmoSpheres(Range):
|
||||
"""Set the starting ammo capacity for spheres (Firemace ammo)."""
|
||||
display_name = "Max Ammo - Spheres"
|
||||
range_start = 150
|
||||
range_end = 999
|
||||
default = 150
|
||||
|
||||
|
||||
class AddedAmmoCrystals(Range):
|
||||
"""Set the amount of crystal capacity gained when collecting a bag of holding or a capacity upgrade."""
|
||||
display_name = "Added Ammo - Crystals"
|
||||
range_start = 10
|
||||
range_end = 999
|
||||
default = 100
|
||||
|
||||
|
||||
class AddedAmmoArrows(Range):
|
||||
"""Set the amount of arrow capacity gained when collecting a bag of holding or a capacity upgrade."""
|
||||
display_name = "Added Ammo - Arrows"
|
||||
range_start = 5
|
||||
range_end = 999
|
||||
default = 50
|
||||
|
||||
|
||||
class AddedAmmoClawOrbs(Range):
|
||||
"""Set the amount of claw orb capacity gained when collecting a bag of holding or a capacity upgrade."""
|
||||
display_name = "Added Ammo - Claw Orbs"
|
||||
range_start = 20
|
||||
range_end = 999
|
||||
default = 200
|
||||
|
||||
|
||||
class AddedAmmoRunes(Range):
|
||||
"""Set the amount of rune capacity gained when collecting a bag of holding or a capacity upgrade."""
|
||||
display_name = "Added Ammo - Runes"
|
||||
range_start = 20
|
||||
range_end = 999
|
||||
default = 200
|
||||
|
||||
|
||||
class AddedAmmoFlameOrbs(Range):
|
||||
"""Set the amount of flame orb capacity gained when collecting a bag of holding or a capacity upgrade."""
|
||||
display_name = "Added Ammo - Flame Orbs"
|
||||
range_start = 2
|
||||
range_end = 999
|
||||
default = 20
|
||||
|
||||
|
||||
class AddedAmmoSpheres(Range):
|
||||
"""Set the amount of sphere capacity gained when collecting a bag of holding or a capacity upgrade."""
|
||||
display_name = "Added Ammo - Spheres"
|
||||
range_start = 15
|
||||
range_end = 999
|
||||
default = 150
|
||||
|
||||
|
||||
@dataclass
|
||||
class HereticOptions(PerGameCommonOptions):
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
@@ -163,3 +273,18 @@ class HereticOptions(PerGameCommonOptions):
|
||||
episode3: Episode3
|
||||
episode4: Episode4
|
||||
episode5: Episode5
|
||||
|
||||
split_bag_of_holding: SplitBagOfHolding
|
||||
bag_of_holding_count: BagOfHoldingCount
|
||||
max_ammo_crystals: MaxAmmoCrystals
|
||||
max_ammo_arrows: MaxAmmoArrows
|
||||
max_ammo_claw_orbs: MaxAmmoClawOrbs
|
||||
max_ammo_runes: MaxAmmoRunes
|
||||
max_ammo_flame_orbs: MaxAmmoFlameOrbs
|
||||
max_ammo_spheres: MaxAmmoSpheres
|
||||
added_ammo_crystals: AddedAmmoCrystals
|
||||
added_ammo_arrows: AddedAmmoArrows
|
||||
added_ammo_claw_orbs: AddedAmmoClawOrbs
|
||||
added_ammo_runes: AddedAmmoRunes
|
||||
added_ammo_flame_orbs: AddedAmmoFlameOrbs
|
||||
added_ammo_spheres: AddedAmmoSpheres
|
||||
|
||||
@@ -695,13 +695,11 @@ def set_episode5_rules(player, multiworld, pro):
|
||||
state.has("Phoenix Rod", player, 1) and
|
||||
state.has("Firemace", player, 1) and
|
||||
state.has("Hellstaff", player, 1) and
|
||||
state.has("Gauntlets of the Necromancer", player, 1) and
|
||||
state.has("Bag of Holding", player, 1))
|
||||
state.has("Gauntlets of the Necromancer", player, 1))
|
||||
|
||||
# Skein of D'Sparil (E5M9)
|
||||
set_rule(multiworld.get_entrance("Hub -> Skein of D'Sparil (E5M9) Main", player), lambda state:
|
||||
state.has("Skein of D'Sparil (E5M9)", player, 1) and
|
||||
state.has("Bag of Holding", player, 1) and
|
||||
state.has("Hellstaff", player, 1) and
|
||||
state.has("Phoenix Rod", player, 1) and
|
||||
state.has("Dragon Claw", player, 1) and
|
||||
|
||||
@@ -41,7 +41,7 @@ class HereticWorld(World):
|
||||
options: HereticOptions
|
||||
game = "Heretic"
|
||||
web = HereticWeb()
|
||||
required_client_version = (0, 3, 9)
|
||||
required_client_version = (0, 5, 0) # 1.2.0-prerelease or higher
|
||||
|
||||
item_name_to_id = {data["name"]: item_id for item_id, data in Items.item_table.items()}
|
||||
item_name_groups = Items.item_name_groups
|
||||
@@ -206,6 +206,17 @@ class HereticWorld(World):
|
||||
count = item["count"] if item["name"] not in self.starting_level_for_episode else item["count"] - 1
|
||||
itempool += [self.create_item(item["name"]) for _ in range(count)]
|
||||
|
||||
# Bag(s) of Holding based on options
|
||||
if self.options.split_bag_of_holding.value:
|
||||
itempool += [self.create_item("Crystal Capacity") for _ in range(self.options.bag_of_holding_count.value)]
|
||||
itempool += [self.create_item("Ethereal Arrow Capacity") for _ in range(self.options.bag_of_holding_count.value)]
|
||||
itempool += [self.create_item("Claw Orb Capacity") for _ in range(self.options.bag_of_holding_count.value)]
|
||||
itempool += [self.create_item("Rune Capacity") for _ in range(self.options.bag_of_holding_count.value)]
|
||||
itempool += [self.create_item("Flame Orb Capacity") for _ in range(self.options.bag_of_holding_count.value)]
|
||||
itempool += [self.create_item("Mace Sphere Capacity") for _ in range(self.options.bag_of_holding_count.value)]
|
||||
else:
|
||||
itempool += [self.create_item("Bag of Holding") for _ in range(self.options.bag_of_holding_count.value)]
|
||||
|
||||
# Place end level items in locked locations
|
||||
for map_name in Maps.map_names:
|
||||
loc_name = map_name + " - Exit"
|
||||
@@ -274,7 +285,7 @@ class HereticWorld(World):
|
||||
episode_count = self.get_episode_count()
|
||||
count = min(remaining_loc, max(1, self.items_ratio[item_name] * episode_count))
|
||||
if count == 0:
|
||||
logger.warning("Warning, no " + item_name + " will be placed.")
|
||||
logger.warning(f"Warning, no {item_name} will be placed.")
|
||||
return
|
||||
|
||||
for i in range(count):
|
||||
@@ -290,4 +301,18 @@ class HereticWorld(World):
|
||||
slot_data["episode4"] = self.included_episodes[3]
|
||||
slot_data["episode5"] = self.included_episodes[4]
|
||||
|
||||
# Send slot data for ammo capacity values; this must be generic because Doom uses it too
|
||||
slot_data["ammo1start"] = self.options.max_ammo_crystals.value
|
||||
slot_data["ammo2start"] = self.options.max_ammo_arrows.value
|
||||
slot_data["ammo3start"] = self.options.max_ammo_claw_orbs.value
|
||||
slot_data["ammo4start"] = self.options.max_ammo_runes.value
|
||||
slot_data["ammo5start"] = self.options.max_ammo_flame_orbs.value
|
||||
slot_data["ammo6start"] = self.options.max_ammo_spheres.value
|
||||
slot_data["ammo1add"] = self.options.added_ammo_crystals.value
|
||||
slot_data["ammo2add"] = self.options.added_ammo_arrows.value
|
||||
slot_data["ammo3add"] = self.options.added_ammo_claw_orbs.value
|
||||
slot_data["ammo4add"] = self.options.added_ammo_runes.value
|
||||
slot_data["ammo5add"] = self.options.added_ammo_flame_orbs.value
|
||||
slot_data["ammo6add"] = self.options.added_ammo_spheres.value
|
||||
|
||||
return slot_data
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ModuleUpdate
|
||||
import Utils
|
||||
|
||||
ModuleUpdate.update()
|
||||
|
||||
@@ -23,6 +24,7 @@ class KH2Context(CommonContext):
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
super(KH2Context, self).__init__(server_address, password)
|
||||
|
||||
self.goofy_ability_to_slot = dict()
|
||||
self.donald_ability_to_slot = dict()
|
||||
self.all_weapon_location_id = None
|
||||
@@ -35,6 +37,7 @@ class KH2Context(CommonContext):
|
||||
self.serverconneced = False
|
||||
self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()}
|
||||
self.location_name_to_data = {name: data for name, data, in all_locations.items()}
|
||||
self.kh2_data_package = {}
|
||||
self.kh2_loc_name_to_id = None
|
||||
self.kh2_item_name_to_id = None
|
||||
self.lookup_id_to_item = None
|
||||
@@ -83,6 +86,8 @@ class KH2Context(CommonContext):
|
||||
},
|
||||
}
|
||||
self.kh2seedname = None
|
||||
self.kh2_seed_save_path_join = None
|
||||
|
||||
self.kh2slotdata = None
|
||||
self.mem_json = None
|
||||
self.itemamount = {}
|
||||
@@ -114,26 +119,18 @@ class KH2Context(CommonContext):
|
||||
# 255: {}, # starting screen
|
||||
}
|
||||
self.last_world_int = -1
|
||||
# 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room
|
||||
# self.sveroom = 0x2A09C00 + 0x41
|
||||
# 0 not in battle 1 in yellow battle 2 red battle #short
|
||||
# self.inBattle = 0x2A0EAC4 + 0x40
|
||||
# self.onDeath = 0xAB9078
|
||||
# PC Address anchors
|
||||
# self.Now = 0x0714DB8 old address
|
||||
# epic addresses
|
||||
# epic .10 addresses
|
||||
self.Now = 0x0716DF8
|
||||
self.Save = 0x09A92F0
|
||||
self.Save = 0x9A9330
|
||||
self.Journal = 0x743260
|
||||
self.Shop = 0x743350
|
||||
self.Slot1 = 0x2A22FD8
|
||||
# self.Sys3 = 0x2A59DF0
|
||||
# self.Bt10 = 0x2A74880
|
||||
# self.BtlEnd = 0x2A0D3E0
|
||||
# self.Slot1 = 0x2A20C98 old address
|
||||
self.Slot1 = 0x2A23018
|
||||
|
||||
self.kh2_game_version = None # can be egs or steam
|
||||
|
||||
self.kh2_seed_save_path = None
|
||||
|
||||
self.chest_set = set(exclusion_table["Chests"])
|
||||
self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"])
|
||||
self.staff_set = set(CheckDupingItems["Weapons"]["Staffs"])
|
||||
@@ -194,8 +191,7 @@ class KH2Context(CommonContext):
|
||||
self.kh2connected = False
|
||||
self.serverconneced = False
|
||||
if self.kh2seedname is not None and self.auth is not None:
|
||||
with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"),
|
||||
'w') as f:
|
||||
with open(self.kh2_seed_save_path_join, 'w') as f:
|
||||
f.write(json.dumps(self.kh2_seed_save, indent=4))
|
||||
await super(KH2Context, self).connection_closed()
|
||||
|
||||
@@ -203,8 +199,7 @@ class KH2Context(CommonContext):
|
||||
self.kh2connected = False
|
||||
self.serverconneced = False
|
||||
if self.kh2seedname not in {None} and self.auth not in {None}:
|
||||
with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"),
|
||||
'w') as f:
|
||||
with open(self.kh2_seed_save_path_join, 'w') as f:
|
||||
f.write(json.dumps(self.kh2_seed_save, indent=4))
|
||||
await super(KH2Context, self).disconnect()
|
||||
|
||||
@@ -217,8 +212,7 @@ class KH2Context(CommonContext):
|
||||
|
||||
async def shutdown(self):
|
||||
if self.kh2seedname not in {None} and self.auth not in {None}:
|
||||
with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"),
|
||||
'w') as f:
|
||||
with open(self.kh2_seed_save_path_join, 'w') as f:
|
||||
f.write(json.dumps(self.kh2_seed_save, indent=4))
|
||||
await super(KH2Context, self).shutdown()
|
||||
|
||||
@@ -232,7 +226,7 @@ class KH2Context(CommonContext):
|
||||
return self.kh2.write_bytes(self.kh2.base_address + address, value.to_bytes(1, 'big'), 1)
|
||||
|
||||
def kh2_read_byte(self, address):
|
||||
return int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + address, 1), "big")
|
||||
return int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + address, 1))
|
||||
|
||||
def kh2_read_int(self, address):
|
||||
return self.kh2.read_int(self.kh2.base_address + address)
|
||||
@@ -244,11 +238,14 @@ class KH2Context(CommonContext):
|
||||
return self.kh2.read_string(self.kh2.base_address + address, length)
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd in {"RoomInfo"}:
|
||||
if cmd == "RoomInfo":
|
||||
self.kh2seedname = args['seed_name']
|
||||
self.kh2_seed_save_path = f"kh2save2{self.kh2seedname}{self.auth}.json"
|
||||
self.kh2_seed_save_path_join = os.path.join(self.game_communication_path, self.kh2_seed_save_path)
|
||||
|
||||
if not os.path.exists(self.game_communication_path):
|
||||
os.makedirs(self.game_communication_path)
|
||||
if not os.path.exists(self.game_communication_path + f"\kh2save2{self.kh2seedname}{self.auth}.json"):
|
||||
if not os.path.exists(self.kh2_seed_save_path_join):
|
||||
self.kh2_seed_save = {
|
||||
"Levels": {
|
||||
"SoraLevel": 0,
|
||||
@@ -261,12 +258,11 @@ class KH2Context(CommonContext):
|
||||
},
|
||||
"SoldEquipment": [],
|
||||
}
|
||||
with open(os.path.join(self.game_communication_path, f"kh2save2{self.kh2seedname}{self.auth}.json"),
|
||||
'wt') as f:
|
||||
with open(self.kh2_seed_save_path_join, 'wt') as f:
|
||||
pass
|
||||
# self.locations_checked = set()
|
||||
elif os.path.exists(self.game_communication_path + f"\kh2save2{self.kh2seedname}{self.auth}.json"):
|
||||
with open(self.game_communication_path + f"\kh2save2{self.kh2seedname}{self.auth}.json", 'r') as f:
|
||||
elif os.path.exists(self.kh2_seed_save_path_join):
|
||||
with open(self.kh2_seed_save_path_join) as f:
|
||||
self.kh2_seed_save = json.load(f)
|
||||
if self.kh2_seed_save is None:
|
||||
self.kh2_seed_save = {
|
||||
@@ -284,13 +280,22 @@ class KH2Context(CommonContext):
|
||||
# self.locations_checked = set(self.kh2_seed_save_cache["LocationsChecked"])
|
||||
# self.serverconneced = True
|
||||
|
||||
if cmd in {"Connected"}:
|
||||
asyncio.create_task(self.send_msgs([{"cmd": "GetDataPackage", "games": ["Kingdom Hearts 2"]}]))
|
||||
if cmd == "Connected":
|
||||
self.kh2slotdata = args['slot_data']
|
||||
# self.kh2_local_items = {int(location): item for location, item in self.kh2slotdata["LocalItems"].items()}
|
||||
|
||||
self.kh2_data_package = Utils.load_data_package_for_checksum(
|
||||
"Kingdom Hearts 2", self.checksums["Kingdom Hearts 2"])
|
||||
|
||||
if "location_name_to_id" in self.kh2_data_package:
|
||||
self.data_package_kh2_cache(
|
||||
self.kh2_data_package["location_name_to_id"], self.kh2_data_package["item_name_to_id"])
|
||||
self.connect_to_game()
|
||||
else:
|
||||
asyncio.create_task(self.send_msgs([{"cmd": "GetDataPackage", "games": ["Kingdom Hearts 2"]}]))
|
||||
|
||||
self.locations_checked = set(args["checked_locations"])
|
||||
|
||||
if cmd in {"ReceivedItems"}:
|
||||
if cmd == "ReceivedItems":
|
||||
# 0x2546
|
||||
# 0x2658
|
||||
# 0x276A
|
||||
@@ -338,42 +343,44 @@ class KH2Context(CommonContext):
|
||||
for item in args['items']:
|
||||
asyncio.create_task(self.give_item(item.item, item.location))
|
||||
|
||||
if cmd in {"RoomUpdate"}:
|
||||
if cmd == "RoomUpdate":
|
||||
if "checked_locations" in args:
|
||||
new_locations = set(args["checked_locations"])
|
||||
self.locations_checked |= new_locations
|
||||
|
||||
if cmd in {"DataPackage"}:
|
||||
if cmd == "DataPackage":
|
||||
if "Kingdom Hearts 2" in args["data"]["games"]:
|
||||
self.data_package_kh2_cache(args)
|
||||
if "KeybladeAbilities" in self.kh2slotdata.keys():
|
||||
# sora ability to slot
|
||||
self.AbilityQuantityDict.update(self.kh2slotdata["KeybladeAbilities"])
|
||||
# itemid:[slots that are available for that item]
|
||||
self.AbilityQuantityDict.update(self.kh2slotdata["StaffAbilities"])
|
||||
self.AbilityQuantityDict.update(self.kh2slotdata["ShieldAbilities"])
|
||||
self.data_package_kh2_cache(
|
||||
args["data"]["games"]["Kingdom Hearts 2"]["location_name_to_id"],
|
||||
args["data"]["games"]["Kingdom Hearts 2"]["item_name_to_id"])
|
||||
self.connect_to_game()
|
||||
asyncio.create_task(self.send_msgs([{'cmd': 'Sync'}]))
|
||||
|
||||
all_weapon_location_id = []
|
||||
for weapon_location in all_weapon_slot:
|
||||
all_weapon_location_id.append(self.kh2_loc_name_to_id[weapon_location])
|
||||
self.all_weapon_location_id = set(all_weapon_location_id)
|
||||
def connect_to_game(self):
|
||||
if "KeybladeAbilities" in self.kh2slotdata.keys():
|
||||
# sora ability to slot
|
||||
self.AbilityQuantityDict.update(self.kh2slotdata["KeybladeAbilities"])
|
||||
# itemid:[slots that are available for that item]
|
||||
self.AbilityQuantityDict.update(self.kh2slotdata["StaffAbilities"])
|
||||
self.AbilityQuantityDict.update(self.kh2slotdata["ShieldAbilities"])
|
||||
|
||||
try:
|
||||
if not self.kh2:
|
||||
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||
self.get_addresses()
|
||||
self.all_weapon_location_id = {self.kh2_loc_name_to_id[loc] for loc in all_weapon_slot}
|
||||
|
||||
except Exception as e:
|
||||
if self.kh2connected:
|
||||
self.kh2connected = False
|
||||
logger.info("Game is not open.")
|
||||
self.serverconneced = True
|
||||
asyncio.create_task(self.send_msgs([{'cmd': 'Sync'}]))
|
||||
try:
|
||||
if not self.kh2:
|
||||
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
|
||||
self.get_addresses()
|
||||
|
||||
def data_package_kh2_cache(self, args):
|
||||
self.kh2_loc_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["location_name_to_id"]
|
||||
except Exception as e:
|
||||
if self.kh2connected:
|
||||
self.kh2connected = False
|
||||
logger.info("Game is not open.")
|
||||
self.serverconneced = True
|
||||
|
||||
def data_package_kh2_cache(self, loc_to_id, item_to_id):
|
||||
self.kh2_loc_name_to_id = loc_to_id
|
||||
self.lookup_id_to_location = {v: k for k, v in self.kh2_loc_name_to_id.items()}
|
||||
self.kh2_item_name_to_id = args["data"]["games"]["Kingdom Hearts 2"]["item_name_to_id"]
|
||||
self.kh2_item_name_to_id = item_to_id
|
||||
self.lookup_id_to_item = {v: k for k, v in self.kh2_item_name_to_id.items()}
|
||||
self.ability_code_list = [self.kh2_item_name_to_id[item] for item in exclusion_item_table["Ability"]]
|
||||
|
||||
@@ -742,7 +749,8 @@ class KH2Context(CommonContext):
|
||||
for item_name in master_stat:
|
||||
amount_of_items = 0
|
||||
amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][item_name]
|
||||
if self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5:
|
||||
# checking if they talked to the computer to give them these
|
||||
if self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and (self.kh2_read_byte(self.Save + 0x1D27) & 0x1 << 3) > 0:
|
||||
if item_name == ItemName.MaxHPUp:
|
||||
if self.kh2_read_byte(self.Save + 0x2498) < 3: # Non-Critical
|
||||
Bonus = 5
|
||||
@@ -808,34 +816,33 @@ class KH2Context(CommonContext):
|
||||
def get_addresses(self):
|
||||
if not self.kh2connected and self.kh2 is not None:
|
||||
if self.kh2_game_version is None:
|
||||
|
||||
if self.kh2_read_string(0x09A9830, 4) == "KH2J":
|
||||
# current verions is .10 then runs the get from github stuff
|
||||
if self.kh2_read_string(0x9A98B0, 4) == "KH2J":
|
||||
self.kh2_game_version = "STEAM"
|
||||
self.Now = 0x0717008
|
||||
self.Save = 0x09A9830
|
||||
self.Slot1 = 0x2A23518
|
||||
self.Save = 0x09A98B0
|
||||
self.Slot1 = 0x2A23598
|
||||
self.Journal = 0x7434E0
|
||||
self.Shop = 0x7435D0
|
||||
elif self.kh2_read_string(0x09A92F0, 4) == "KH2J":
|
||||
elif self.kh2_read_string(0x9A9330, 4) == "KH2J":
|
||||
self.kh2_game_version = "EGS"
|
||||
else:
|
||||
if self.game_communication_path:
|
||||
logger.info("Checking with most up to date addresses of github. If file is not found will be downloading datafiles. This might take a moment")
|
||||
logger.info("Checking with most up to date addresses from the addresses json.")
|
||||
#if mem addresses file is found then check version and if old get new one
|
||||
kh2memaddresses_path = os.path.join(self.game_communication_path, f"kh2memaddresses.json")
|
||||
kh2memaddresses_path = os.path.join(self.game_communication_path, "kh2memaddresses.json")
|
||||
if not os.path.exists(kh2memaddresses_path):
|
||||
logger.info("File is not found. Downloading json with memory addresses. This might take a moment")
|
||||
mem_resp = requests.get("https://raw.githubusercontent.com/JaredWeakStrike/KH2APMemoryValues/master/kh2memaddresses.json")
|
||||
if mem_resp.status_code == 200:
|
||||
self.mem_json = json.loads(mem_resp.content)
|
||||
with open(kh2memaddresses_path,
|
||||
'w') as f:
|
||||
with open(kh2memaddresses_path, 'w') as f:
|
||||
f.write(json.dumps(self.mem_json, indent=4))
|
||||
else:
|
||||
with open(kh2memaddresses_path, 'r') as f:
|
||||
with open(kh2memaddresses_path) as f:
|
||||
self.mem_json = json.load(f)
|
||||
if self.mem_json:
|
||||
for key in self.mem_json.keys():
|
||||
|
||||
if self.kh2_read_string(int(self.mem_json[key]["GameVersionCheck"], 0), 4) == "KH2J":
|
||||
self.Now = int(self.mem_json[key]["Now"], 0)
|
||||
self.Save = int(self.mem_json[key]["Save"], 0)
|
||||
|
||||
@@ -368,6 +368,37 @@ def patch_kh2(self, output_directory):
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'name': 'msg/us/he.bar',
|
||||
'multi': [
|
||||
{
|
||||
'name': 'msg/fr/he.bar'
|
||||
},
|
||||
{
|
||||
'name': 'msg/gr/he.bar'
|
||||
},
|
||||
{
|
||||
'name': 'msg/it/he.bar'
|
||||
},
|
||||
{
|
||||
'name': 'msg/sp/he.bar'
|
||||
}
|
||||
],
|
||||
'method': 'binarc',
|
||||
'source': [
|
||||
{
|
||||
'name': 'he',
|
||||
'type': 'list',
|
||||
'method': 'kh2msg',
|
||||
'source': [
|
||||
{
|
||||
'name': 'he.yml',
|
||||
'language': 'en'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
'title': 'Randomizer Seed'
|
||||
}
|
||||
@@ -411,6 +442,34 @@ def patch_kh2(self, output_directory):
|
||||
'en': f"Your Level Depth is {self.options.LevelDepth.current_option_name}"
|
||||
}
|
||||
]
|
||||
self.fight_and_form_text = [
|
||||
{
|
||||
'id': 15121, # poster name
|
||||
'en': f"Game Options"
|
||||
},
|
||||
{
|
||||
'id': 15122,
|
||||
'en': f"Fight Logic is {self.options.FightLogic.current_option_name}\n"
|
||||
f"Auto Form Logic is {self.options.AutoFormLogic.current_option_name}\n"
|
||||
f"Final Form Logic is {self.options.FinalFormLogic.current_option_name}"
|
||||
}
|
||||
|
||||
]
|
||||
self.cups_text = [
|
||||
{
|
||||
'id': 4043,
|
||||
'en': f"CupsToggle: {self.options.Cups.current_option_name}"
|
||||
},
|
||||
{
|
||||
'id': 4044,
|
||||
'en': f"CupsToggle: {self.options.Cups.current_option_name}"
|
||||
},
|
||||
{
|
||||
'id': 4045,
|
||||
'en': f"CupsToggle: {self.options.Cups.current_option_name}"
|
||||
},
|
||||
]
|
||||
|
||||
mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__)
|
||||
|
||||
self.mod_yml["title"] = f"Randomizer Seed {mod_name}"
|
||||
@@ -423,7 +482,8 @@ def patch_kh2(self, output_directory):
|
||||
"FmlvList.yml": yaml.dump(self.formattedFmlv, line_break="\n"),
|
||||
"mod.yml": yaml.dump(self.mod_yml, line_break="\n"),
|
||||
"po.yml": yaml.dump(self.pooh_text, line_break="\n"),
|
||||
"sys.yml": yaml.dump(self.level_depth_text, line_break="\n"),
|
||||
"sys.yml": yaml.dump(self.level_depth_text + self.fight_and_form_text, line_break="\n"),
|
||||
"he.yml": yaml.dump(self.cups_text, line_break="\n")
|
||||
}
|
||||
|
||||
mod = KH2Container(openkhmod, mod_dir, output_directory, self.player,
|
||||
|
||||
@@ -1,92 +1,266 @@
|
||||
import json
|
||||
roomAddress = 0xFFF6
|
||||
mapIdAddress = 0xFFF7
|
||||
indoorFlagAddress = 0xDBA5
|
||||
entranceRoomOffset = 0xD800
|
||||
screenCoordAddress = 0xFFFA
|
||||
import typing
|
||||
from websockets import WebSocketServerProtocol
|
||||
|
||||
mapMap = {
|
||||
0x00: 0x01,
|
||||
0x01: 0x01,
|
||||
0x02: 0x01,
|
||||
0x03: 0x01,
|
||||
0x04: 0x01,
|
||||
0x05: 0x01,
|
||||
0x06: 0x02,
|
||||
0x07: 0x02,
|
||||
0x08: 0x02,
|
||||
0x09: 0x02,
|
||||
0x0A: 0x02,
|
||||
0x0B: 0x02,
|
||||
0x0C: 0x02,
|
||||
0x0D: 0x02,
|
||||
0x0E: 0x02,
|
||||
0x0F: 0x02,
|
||||
0x10: 0x02,
|
||||
0x11: 0x02,
|
||||
0x12: 0x02,
|
||||
0x13: 0x02,
|
||||
0x14: 0x02,
|
||||
0x15: 0x02,
|
||||
0x16: 0x02,
|
||||
0x17: 0x02,
|
||||
0x18: 0x02,
|
||||
0x19: 0x02,
|
||||
0x1D: 0x01,
|
||||
0x1E: 0x01,
|
||||
0x1F: 0x01,
|
||||
0xFF: 0x03,
|
||||
}
|
||||
from . import TrackerConsts as Consts
|
||||
from .TrackerConsts import EntranceCoord
|
||||
from .LADXR.entranceInfo import ENTRANCE_INFO
|
||||
|
||||
class Entrance:
|
||||
outdoor_room: int
|
||||
indoor_map: int
|
||||
indoor_address: int
|
||||
name: str
|
||||
other_side_name: str = None
|
||||
changed: bool = False
|
||||
known_to_server: bool = False
|
||||
|
||||
def __init__(self, outdoor: int, indoor: int, name: str, indoor_address: int=None):
|
||||
self.outdoor_room = outdoor
|
||||
self.indoor_map = indoor
|
||||
self.indoor_address = indoor_address
|
||||
self.name = name
|
||||
|
||||
def map(self, other_side: str, known_to_server: bool = False):
|
||||
if other_side != self.other_side_name:
|
||||
self.changed = True
|
||||
self.known_to_server = known_to_server
|
||||
|
||||
self.other_side_name = other_side
|
||||
|
||||
class GpsTracker:
|
||||
room = None
|
||||
location_changed = False
|
||||
screenX = 0
|
||||
screenY = 0
|
||||
indoors = None
|
||||
room: int = None
|
||||
last_room: int = None
|
||||
last_different_room: int = None
|
||||
room_same_for: int = 0
|
||||
room_changed: bool = False
|
||||
screen_x: int = 0
|
||||
screen_y: int = 0
|
||||
spawn_x: int = 0
|
||||
spawn_y: int = 0
|
||||
indoors: int = None
|
||||
indoors_changed: bool = False
|
||||
spawn_map: int = None
|
||||
spawn_room: int = None
|
||||
spawn_changed: bool = False
|
||||
spawn_same_for: int = 0
|
||||
entrance_mapping: typing.Dict[str, str] = None
|
||||
entrances_by_name: typing.Dict[str, Entrance] = {}
|
||||
needs_found_entrances: bool = False
|
||||
needs_slot_data: bool = True
|
||||
|
||||
def __init__(self, gameboy) -> None:
|
||||
self.gameboy = gameboy
|
||||
|
||||
async def read_byte(self, b):
|
||||
return (await self.gameboy.async_read_memory(b))[0]
|
||||
self.gameboy.set_location_range(
|
||||
Consts.link_motion_state,
|
||||
Consts.transition_sequence - Consts.link_motion_state + 1,
|
||||
[Consts.transition_state]
|
||||
)
|
||||
|
||||
async def read_byte(self, b: int):
|
||||
return (await self.gameboy.read_memory_cache([b]))[b]
|
||||
|
||||
def load_slot_data(self, slot_data: typing.Dict[str, typing.Any]):
|
||||
if 'entrance_mapping' not in slot_data:
|
||||
return
|
||||
|
||||
# We need to know how entrances were mapped at generation before we can autotrack them
|
||||
self.entrance_mapping = {}
|
||||
|
||||
# Convert to upstream's newer format
|
||||
for outside, inside in slot_data['entrance_mapping'].items():
|
||||
new_inside = f"{inside}:inside"
|
||||
self.entrance_mapping[outside] = new_inside
|
||||
self.entrance_mapping[new_inside] = outside
|
||||
|
||||
self.entrances_by_name = {}
|
||||
|
||||
for name, info in ENTRANCE_INFO.items():
|
||||
alternate_address = (
|
||||
Consts.entrance_address_overrides[info.target]
|
||||
if info.target in Consts.entrance_address_overrides
|
||||
else None
|
||||
)
|
||||
|
||||
entrance = Entrance(info.room, info.target, name, alternate_address)
|
||||
self.entrances_by_name[name] = entrance
|
||||
|
||||
inside_entrance = Entrance(info.target, info.room, f"{name}:inside", alternate_address)
|
||||
self.entrances_by_name[f"{name}:inside"] = inside_entrance
|
||||
|
||||
self.needs_slot_data = False
|
||||
self.needs_found_entrances = True
|
||||
|
||||
async def read_location(self):
|
||||
indoors = await self.read_byte(indoorFlagAddress)
|
||||
# We need to wait for screen transitions to finish
|
||||
transition_state = await self.read_byte(Consts.transition_state)
|
||||
transition_target_x = await self.read_byte(Consts.transition_target_x)
|
||||
transition_target_y = await self.read_byte(Consts.transition_target_y)
|
||||
transition_scroll_x = await self.read_byte(Consts.transition_scroll_x)
|
||||
transition_scroll_y = await self.read_byte(Consts.transition_scroll_y)
|
||||
transition_sequence = await self.read_byte(Consts.transition_sequence)
|
||||
motion_state = await self.read_byte(Consts.link_motion_state)
|
||||
if (transition_state != 0
|
||||
or transition_target_x != transition_scroll_x
|
||||
or transition_target_y != transition_scroll_y
|
||||
or transition_sequence != 0x04):
|
||||
return
|
||||
|
||||
indoors = await self.read_byte(Consts.indoor_flag)
|
||||
|
||||
if indoors != self.indoors and self.indoors != None:
|
||||
self.indoorsChanged = True
|
||||
|
||||
self.indoors_changed = True
|
||||
|
||||
self.indoors = indoors
|
||||
|
||||
mapId = await self.read_byte(mapIdAddress)
|
||||
if mapId not in mapMap:
|
||||
print(f'Unknown map ID {hex(mapId)}')
|
||||
# We use the spawn point to know which entrance was most recently entered
|
||||
spawn_map = await self.read_byte(Consts.spawn_map)
|
||||
map_digit = Consts.map_map[spawn_map] << 8 if self.spawn_map else 0
|
||||
spawn_room = await self.read_byte(Consts.spawn_room) + map_digit
|
||||
spawn_x = await self.read_byte(Consts.spawn_x)
|
||||
spawn_y = await self.read_byte(Consts.spawn_y)
|
||||
|
||||
# The spawn point needs to be settled before we can trust location data
|
||||
if ((spawn_room != self.spawn_room and self.spawn_room != None)
|
||||
or (spawn_map != self.spawn_map and self.spawn_map != None)
|
||||
or (spawn_x != self.spawn_x and self.spawn_x != None)
|
||||
or (spawn_y != self.spawn_y and self.spawn_y != None)):
|
||||
self.spawn_changed = True
|
||||
self.spawn_same_for = 0
|
||||
else:
|
||||
self.spawn_same_for += 1
|
||||
|
||||
self.spawn_map = spawn_map
|
||||
self.spawn_room = spawn_room
|
||||
self.spawn_x = spawn_x
|
||||
self.spawn_y = spawn_y
|
||||
|
||||
# Spawn point is preferred, but doesn't work for the sidescroller entrances
|
||||
# Those can be addressed by keeping track of which room we're in
|
||||
# Also used to validate that we came from the right room for what the spawn point is mapped to
|
||||
map_id = await self.read_byte(Consts.map_id)
|
||||
if map_id not in Consts.map_map:
|
||||
print(f'Unknown map ID {hex(map_id)}')
|
||||
return
|
||||
|
||||
mapDigit = mapMap[mapId] << 8 if indoors else 0
|
||||
last_room = self.room
|
||||
self.room = await self.read_byte(roomAddress) + mapDigit
|
||||
map_digit = Consts.map_map[map_id] << 8 if indoors else 0
|
||||
self.last_room = self.room
|
||||
self.room = await self.read_byte(Consts.room) + map_digit
|
||||
|
||||
coords = await self.read_byte(screenCoordAddress)
|
||||
self.screenX = coords & 0x0F
|
||||
self.screenY = (coords & 0xF0) >> 4
|
||||
# Again, the room needs to settle before we can trust location data
|
||||
if self.last_room != self.room:
|
||||
self.room_same_for = 0
|
||||
self.room_changed = True
|
||||
self.last_different_room = self.last_room
|
||||
else:
|
||||
self.room_same_for += 1
|
||||
|
||||
if (self.room != last_room):
|
||||
self.location_changed = True
|
||||
|
||||
last_message = {}
|
||||
async def send_location(self, socket, diff=False):
|
||||
if self.room is None:
|
||||
# Only update Link's location when he's not in the air to avoid weirdness
|
||||
if motion_state in [0, 1]:
|
||||
coords = await self.read_byte(Consts.screen_coord)
|
||||
self.screen_x = coords & 0x0F
|
||||
self.screen_y = (coords & 0xF0) >> 4
|
||||
|
||||
async def read_entrances(self):
|
||||
if not self.last_different_room or not self.entrance_mapping:
|
||||
return
|
||||
|
||||
if self.spawn_changed and self.spawn_same_for > 0 and self.room_same_for > 0:
|
||||
# Use the spawn location, last room, and entrance mapping at generation to map the right entrance
|
||||
# A bit overkill for simple ER, but necessary for upstream's advanced ER
|
||||
spawn_coord = EntranceCoord(None, self.spawn_room, self.spawn_x, self.spawn_y)
|
||||
if str(spawn_coord) in Consts.entrance_lookup:
|
||||
valid_sources = {x.name for x in Consts.entrance_coords if x.room == self.last_different_room}
|
||||
dest_entrance = Consts.entrance_lookup[str(spawn_coord)].name
|
||||
source_entrance = [
|
||||
x for x in self.entrance_mapping
|
||||
if self.entrance_mapping[x] == dest_entrance and x in valid_sources
|
||||
]
|
||||
|
||||
if source_entrance:
|
||||
self.entrances_by_name[source_entrance[0]].map(dest_entrance)
|
||||
|
||||
self.spawn_changed = False
|
||||
elif self.room_changed and self.room_same_for > 0:
|
||||
# Check for the stupid sidescroller rooms that don't set your spawn point
|
||||
if self.last_different_room in Consts.sidescroller_rooms:
|
||||
source_entrance = Consts.sidescroller_rooms[self.last_different_room]
|
||||
if source_entrance in self.entrance_mapping:
|
||||
dest_entrance = self.entrance_mapping[source_entrance]
|
||||
|
||||
expected_room = self.entrances_by_name[dest_entrance].outdoor_room
|
||||
if dest_entrance.endswith(":indoor"):
|
||||
expected_room = self.entrances_by_name[dest_entrance].indoor_map
|
||||
|
||||
if expected_room == self.room:
|
||||
self.entrances_by_name[source_entrance].map(dest_entrance)
|
||||
|
||||
if self.room in Consts.sidescroller_rooms:
|
||||
valid_sources = {x.name for x in Consts.entrance_coords if x.room == self.last_different_room}
|
||||
dest_entrance = Consts.sidescroller_rooms[self.room]
|
||||
source_entrance = [
|
||||
x for x in self.entrance_mapping
|
||||
if self.entrance_mapping[x] == dest_entrance and x in valid_sources
|
||||
]
|
||||
|
||||
if source_entrance:
|
||||
self.entrances_by_name[source_entrance[0]].map(dest_entrance)
|
||||
|
||||
self.room_changed = False
|
||||
|
||||
last_location_message = {}
|
||||
async def send_location(self, socket: WebSocketServerProtocol) -> None:
|
||||
if self.room is None or self.room_same_for < 1:
|
||||
return
|
||||
|
||||
message = {
|
||||
"type":"location",
|
||||
"refresh": True,
|
||||
"version":"1.0",
|
||||
"room": f'0x{self.room:02X}',
|
||||
"x": self.screenX,
|
||||
"y": self.screenY,
|
||||
"x": self.screen_x,
|
||||
"y": self.screen_y,
|
||||
"drawFine": True,
|
||||
}
|
||||
if message != self.last_message:
|
||||
self.last_message = message
|
||||
|
||||
if message != self.last_location_message:
|
||||
self.last_location_message = message
|
||||
await socket.send(json.dumps(message))
|
||||
|
||||
async def send_entrances(self, socket: WebSocketServerProtocol, diff: bool=True) -> typing.Dict[str, str]:
|
||||
if not self.entrance_mapping:
|
||||
return
|
||||
|
||||
new_entrances = [x for x in self.entrances_by_name.values() if x.changed or (not diff and x.other_side_name)]
|
||||
|
||||
if not new_entrances:
|
||||
return
|
||||
|
||||
message = {
|
||||
"type":"entrance",
|
||||
"refresh": True,
|
||||
"diff": True,
|
||||
"entranceMap": {},
|
||||
}
|
||||
|
||||
for entrance in new_entrances:
|
||||
message['entranceMap'][entrance.name] = entrance.other_side_name
|
||||
entrance.changed = False
|
||||
|
||||
await socket.send(json.dumps(message))
|
||||
|
||||
new_to_server = {
|
||||
entrance.name: entrance.other_side_name
|
||||
for entrance in new_entrances
|
||||
if not entrance.known_to_server
|
||||
}
|
||||
|
||||
return new_to_server
|
||||
|
||||
def receive_found_entrances(self, found_entrances: typing.Dict[str, str]):
|
||||
if not found_entrances:
|
||||
return
|
||||
|
||||
for entrance, destination in found_entrances.items():
|
||||
if entrance in self.entrances_by_name:
|
||||
self.entrances_by_name[entrance].map(destination, known_to_server=True)
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import json
|
||||
gameStateAddress = 0xDB95
|
||||
validGameStates = {0x0B, 0x0C}
|
||||
gameStateResetThreshold = 0x06
|
||||
|
||||
inventorySlotCount = 16
|
||||
inventoryStartAddress = 0xDB00
|
||||
inventoryEndAddress = inventoryStartAddress + inventorySlotCount
|
||||
|
||||
rupeesHigh = 0xDB5D
|
||||
rupeesLow = 0xDB5E
|
||||
addRupeesHigh = 0xDB8F
|
||||
addRupeesLow = 0xDB90
|
||||
removeRupeesHigh = 0xDB91
|
||||
removeRupeesLow = 0xDB92
|
||||
|
||||
inventoryItemIds = {
|
||||
0x02: 'BOMB',
|
||||
0x05: 'BOW',
|
||||
@@ -98,10 +102,11 @@ dungeonItemOffsets = {
|
||||
'STONE_BEAK{}': 2,
|
||||
'NIGHTMARE_KEY{}': 3,
|
||||
'KEY{}': 4,
|
||||
'UNUSED_KEY{}': 4,
|
||||
}
|
||||
|
||||
class Item:
|
||||
def __init__(self, id, address, threshold=0, mask=None, increaseOnly=False, count=False, max=None):
|
||||
def __init__(self, id, address, threshold=0, mask=None, increaseOnly=False, count=False, max=None, encodedCount=True):
|
||||
self.id = id
|
||||
self.address = address
|
||||
self.threshold = threshold
|
||||
@@ -112,6 +117,7 @@ class Item:
|
||||
self.rawValue = 0
|
||||
self.diff = 0
|
||||
self.max = max
|
||||
self.encodedCount = encodedCount
|
||||
|
||||
def set(self, byte, extra):
|
||||
oldValue = self.value
|
||||
@@ -121,7 +127,7 @@ class Item:
|
||||
|
||||
if not self.count:
|
||||
byte = int(byte > self.threshold)
|
||||
else:
|
||||
elif self.encodedCount:
|
||||
# LADX seems to store one decimal digit per nibble
|
||||
byte = byte - (byte // 16 * 6)
|
||||
|
||||
@@ -165,6 +171,7 @@ class ItemTracker:
|
||||
Item('BOOMERANG', None),
|
||||
Item('TOADSTOOL', None),
|
||||
Item('ROOSTER', None),
|
||||
Item('RUPEE_COUNT', None, count=True, encodedCount=False),
|
||||
Item('SWORD', 0xDB4E, count=True),
|
||||
Item('POWER_BRACELET', 0xDB43, count=True),
|
||||
Item('SHIELD', 0xDB44, count=True),
|
||||
@@ -219,9 +226,9 @@ class ItemTracker:
|
||||
|
||||
self.itemDict = {item.id: item for item in self.items}
|
||||
|
||||
async def readItems(state):
|
||||
extraItems = state.extraItems
|
||||
missingItems = {x for x in state.items if x.address == None}
|
||||
async def readItems(self):
|
||||
extraItems = self.extraItems
|
||||
missingItems = {x for x in self.items if x.address == None and x.id != 'RUPEE_COUNT'}
|
||||
|
||||
# Add keys for opened key doors
|
||||
for i in range(len(dungeonKeyDoors)):
|
||||
@@ -230,16 +237,16 @@ class ItemTracker:
|
||||
|
||||
for address, masks in dungeonKeyDoors[i].items():
|
||||
for mask in masks:
|
||||
value = await state.readRamByte(address) & mask
|
||||
value = await self.readRamByte(address) & mask
|
||||
if value > 0:
|
||||
extraItems[item] += 1
|
||||
|
||||
# Main inventory items
|
||||
for i in range(inventoryStartAddress, inventoryEndAddress):
|
||||
value = await state.readRamByte(i)
|
||||
value = await self.readRamByte(i)
|
||||
|
||||
if value in inventoryItemIds:
|
||||
item = state.itemDict[inventoryItemIds[value]]
|
||||
item = self.itemDict[inventoryItemIds[value]]
|
||||
extra = extraItems[item.id] if item.id in extraItems else 0
|
||||
item.set(1, extra)
|
||||
missingItems.remove(item)
|
||||
@@ -249,9 +256,21 @@ class ItemTracker:
|
||||
item.set(0, extra)
|
||||
|
||||
# All other items
|
||||
for item in [x for x in state.items if x.address]:
|
||||
for item in [x for x in self.items if x.address]:
|
||||
extra = extraItems[item.id] if item.id in extraItems else 0
|
||||
item.set(await state.readRamByte(item.address), extra)
|
||||
item.set(await self.readRamByte(item.address), extra)
|
||||
|
||||
# The current rupee count is BCD, but the add/remove values are not
|
||||
currentRupees = self.calculateRupeeCount(await self.readRamByte(rupeesHigh), await self.readRamByte(rupeesLow))
|
||||
addingRupees = (await self.readRamByte(addRupeesHigh) << 8) + await self.readRamByte(addRupeesLow)
|
||||
removingRupees = (await self.readRamByte(removeRupeesHigh) << 8) + await self.readRamByte(removeRupeesLow)
|
||||
self.itemDict['RUPEE_COUNT'].set(currentRupees + addingRupees - removingRupees, 0)
|
||||
|
||||
def calculateRupeeCount(self, high: int, low: int) -> int:
|
||||
return (high - (high // 16 * 6)) * 100 + (low - (low // 16 * 6))
|
||||
|
||||
def setExtraItem(self, item: str, qty: int) -> None:
|
||||
self.extraItems[item] = qty
|
||||
|
||||
async def sendItems(self, socket, diff=False):
|
||||
if not self.items:
|
||||
@@ -259,7 +278,6 @@ class ItemTracker:
|
||||
message = {
|
||||
"type":"item",
|
||||
"refresh": True,
|
||||
"version":"1.0",
|
||||
"diff": diff,
|
||||
"items": [],
|
||||
}
|
||||
|
||||
@@ -7,23 +7,12 @@ from ..roomEditor import RoomEditor
|
||||
|
||||
|
||||
class StartItem(DroppedKey):
|
||||
# We need to give something here that we can use to progress.
|
||||
# FEATHER
|
||||
OPTIONS = [SWORD, SHIELD, POWER_BRACELET, OCARINA, BOOMERANG, MAGIC_ROD, TAIL_KEY, SHOVEL, HOOKSHOT, PEGASUS_BOOTS, MAGIC_POWDER, BOMB]
|
||||
MULTIWORLD = False
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(0x2A3)
|
||||
self.give_bowwow = False
|
||||
|
||||
def configure(self, options):
|
||||
if options.bowwow != 'normal':
|
||||
# When we have bowwow mode, we pretend to be a sword for logic reasons
|
||||
self.OPTIONS = [SWORD]
|
||||
self.give_bowwow = True
|
||||
if options.randomstartlocation and options.entranceshuffle != 'none':
|
||||
self.OPTIONS.append(FLIPPERS)
|
||||
|
||||
def patch(self, rom, option, *, multiworld=None):
|
||||
assert multiworld is None
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from ..roomEditor import RoomEditor
|
||||
from .. import entityData
|
||||
import os
|
||||
import bsdiff4
|
||||
import pkgutil
|
||||
|
||||
def imageTo2bpp(filename):
|
||||
import PIL.Image
|
||||
@@ -179,24 +180,9 @@ def noText(rom):
|
||||
|
||||
def reduceMessageLengths(rom, rnd):
|
||||
# Into text from Marin. Got to go fast, so less text. (This intro text is very long)
|
||||
rom.texts[0x01] = formatText(rnd.choice([
|
||||
"Let's a go!",
|
||||
"Remember, sword goes on A!",
|
||||
"Avoid the heart piece of shame!",
|
||||
"Marin? No, this is Zelda. Welcome to Hyrule",
|
||||
"Why are you in my bed?",
|
||||
"This is not a Mario game!",
|
||||
"MuffinJets was here...",
|
||||
"Remember, there are no bugs in LADX",
|
||||
"#####, #####, you got to wake up!\nDinner is ready.",
|
||||
"Go find the stepladder",
|
||||
"Pizza power!",
|
||||
"Eastmost penninsula is the secret",
|
||||
"There is no cow level",
|
||||
"You cannot lift rocks with your bear hands",
|
||||
"Thank you, daid!",
|
||||
"There, there now. Just relax. You've been asleep for almost nine hours now."
|
||||
]))
|
||||
lines = pkgutil.get_data(__name__, "marin.txt").decode("unicode_escape").splitlines()
|
||||
lines = [l for l in lines if l.strip()]
|
||||
rom.texts[0x01] = formatText(rnd.choice(lines).strip())
|
||||
|
||||
# Reduce length of a bunch of common texts
|
||||
rom.texts[0xEA] = formatText("You've got a Guardian Acorn!")
|
||||
|
||||
465
worlds/ladx/LADXR/patches/marin.txt
Normal file
465
worlds/ladx/LADXR/patches/marin.txt
Normal file
@@ -0,0 +1,465 @@
|
||||
Let's a go!
|
||||
Remember, sword goes on A!
|
||||
Remember, sword goes on B!
|
||||
It's pronounced Hydrocity Zone.
|
||||
Avoid the heart piece of shame!
|
||||
Marin? No, this is Zelda. Welcome to Hyrule
|
||||
Why are you in my bed?
|
||||
This is not a Mario game!
|
||||
Wait, I thought Daid was French!
|
||||
Is it spicefather or spaceotter?
|
||||
kbranch finally took a break!
|
||||
Baby seed ahead.
|
||||
Abandon all hope ye who enter here...
|
||||
Link... Open your eyes...\nWait, you're #####?
|
||||
Remember, there are no bugs in LADX.
|
||||
#####, #####, you got to wake up!\nDinner is ready.
|
||||
Go find the stepladder.
|
||||
Pizza power!
|
||||
Eastmost peninsula is the secret.
|
||||
There is no cow level.
|
||||
You cannot lift rocks with your bear hands.
|
||||
Don't worry, the doghouse was patched.
|
||||
The carpet whale isn't real, it can't hurt you.
|
||||
Isn't this a demake of Phantom Hourglass?
|
||||
Go try the LAS rando!
|
||||
Go try the Oracles rando!
|
||||
Go try Archipelago!
|
||||
Go try touching grass!
|
||||
Please leave my house.
|
||||
Trust me, this will be a 2 hour seed, max.
|
||||
This is still better than doing Dampe dungeons.
|
||||
They say that Marin can be found here.
|
||||
Stalfos are such boneheads.
|
||||
90 percent bug-free!
|
||||
404 Marin.personality not found.
|
||||
Idk man, works on my machine.
|
||||
Hey guys, did you know that Vaporeon
|
||||
Trans rights!
|
||||
Support gay rights!\nAnd their lefts!
|
||||
Snake? Snake?! SNAAAAKE!!!
|
||||
Oh, you chose THESE settings?
|
||||
As seen on TV!
|
||||
May contain nuts.
|
||||
Limited edition!
|
||||
May contain RNG.
|
||||
Reticulating splines!
|
||||
Keyboard compatible!
|
||||
Teetsuuuuoooo!
|
||||
Kaaneeeedaaaa!
|
||||
Learn about allyship!
|
||||
This Marin text left intentionally blank.
|
||||
'Autological' is!
|
||||
Technoblade never dies!
|
||||
Thank you, CrystalSaver!
|
||||
Wait, LADX has a rando?
|
||||
Wait, how many Pokemon are there now?
|
||||
GOOD EMU
|
||||
Good luck finding the feather.
|
||||
Good luck finding the bracelets.
|
||||
Good luck finding the boots.
|
||||
Good luck finding your swords.
|
||||
Good luck finding the flippers.
|
||||
Good luck finding the rooster.
|
||||
Good luck finding the hookshot.
|
||||
Good luck finding the magic rod.
|
||||
It's not a fire rod.\nIt's a magic rod, it shoots magic.
|
||||
You should check the Seashell Mansion.
|
||||
Mt. Tamaranch
|
||||
WIND FISH IN NAME ONLY, FOR IT IS NEITHER.
|
||||
Stuck? Try Magpie!
|
||||
Ribbit! Ribbit! I'm Marin, on vocals!
|
||||
Try this rando at ladxr.daid.eu!
|
||||
He turned himself into a carpet whale!
|
||||
Which came first, the whale or the egg?
|
||||
Glan - Known Death and Taxes appreciator.
|
||||
Pokemon number 591.
|
||||
Would you?
|
||||
Sprinkle the desert skulls.
|
||||
Please don't curse in my Christian LADXR seed.
|
||||
... ... ... \n... ...smash.
|
||||
How was bedwetting practice?
|
||||
The Oracles decomp project is going well!
|
||||
#####, how do I download RAM?
|
||||
Is this a delayed April Fool's Joke?
|
||||
Play as if your footage will go in a\nSummoning Salt video.
|
||||
I hope you prepared for our date later.
|
||||
Isn't this the game where you date a seagull?
|
||||
You look pretty good for a guy who probably drowned.
|
||||
Remember, we race on Sundays.
|
||||
This randomizer was made possible by players like you. \n \n Thank you!
|
||||
Now with real fake doors!
|
||||
Now with real fake floors!
|
||||
You could be doing something productive right now.
|
||||
No eggs were harmed in the making of this game.
|
||||
I'm helping the goat, \ncatfishing Mr. Write is kinda the goal.
|
||||
There are actually two LADX randomizers.
|
||||
You're not gonna cheat... \n ...right?
|
||||
Mamu's singing is so bad it wakes the dead.
|
||||
Don't forget the Richard picture.
|
||||
Are you sure you wanna do this? I kinda like this island.
|
||||
SJ, BT, WW, OoB, HIJKLMNOP.
|
||||
5 dollars in the swear jar. Now.
|
||||
#####, I promise this seed will be better than the last one.
|
||||
Want your name here? Contribute to LADXR!
|
||||
Kappa
|
||||
HEY! \n \n LANGUAGE!
|
||||
I sell seashells on the seashore.
|
||||
Hey! Are you even listening to me?
|
||||
Your stay will total 10,000 rupees. I hope you have good insurance.
|
||||
I have like the biggest crush on you. Will you get the hints now?
|
||||
Daid watches Matty for ideas. \nBlame her if things go wrong.
|
||||
'All of you are to blame.' -Daid
|
||||
Batman Contingency Plan: Link. Step 1: Disguise yourself as a maiden to attract the young hero.
|
||||
I have flooded Koholint with a deadly neurotoxin.
|
||||
Ahh, General #####.
|
||||
Finally, Link's Awakening!
|
||||
Is the Wind Fish dreaming that he's sleeping in an egg? Or is he dreaming that he's you?
|
||||
Save Koholint. By destroying it. Huh? Don't ask me, I'm just a kid!
|
||||
There aren't enough women in this village to sustain a civilization.
|
||||
So does this game take place before or after Oracles?
|
||||
Have you tried the critically acclaimed MMORPG FINAL FANTASY XIV that has a free trial up to level 60 including the Heavensward expansion?
|
||||
The thumbs-up sign had been used by the Galactic Federation for ages. Me, I was known for giving the thumbs-down during briefing. I had my reasons, though... Commander Adam Malkovich was normally cool and not one to joke around, but he would end all of his mission briefings by saying, 'Any objections, Lady?'
|
||||
Hot hippos are near your location!
|
||||
#####, get up! It's my turn in the bed! Tarin's smells too much...
|
||||
Have you ever had a dream\nthat\nyo wa-\nyo had\nyo\nthat\nthat you could do anything?
|
||||
Next time, try a salad.
|
||||
seagull noises
|
||||
I'm telling you, YOU HAVE UNO, it came free with your Xbox!
|
||||
I'm telling you, YOU HAVE TRENDY, it came free with your Mabe!
|
||||
LADXR - Now with even more Marin quotes!
|
||||
You guys are spending more time adding Marin quotes than actually playing the game.
|
||||
NASA faked the moon.
|
||||
Doh, I missed!
|
||||
Beginning the seed in... 100\n99\n98\n97\n96\n...\nJust Kidding.
|
||||
Consider libre software!
|
||||
Consider a GNU/Linux installation!
|
||||
Now you're gonna tell me about how you need to get some instruments or maybe shells to hatch a whale out of an egg, right? All you boys are the same...
|
||||
Oh hey #####! I made pancakes!
|
||||
Oh hey #####! I made breakfast!
|
||||
Alright Tarin, test subject number 142857 was a failure, give him the item and the memory drug and we'll try next time.
|
||||
Betcha 100 rupees that Tarin gives you a sword.
|
||||
Betcha 100 rupees that Tarin gives you the feather.
|
||||
Betcha 100 rupees that Tarin gives you a bracelet.
|
||||
Betcha 100 rupees that Tarin gives you the boots.
|
||||
Betcha 100 rupees that Tarin gives you the hookshot.
|
||||
Betcha 100 rupees that Tarin gives you the rod.
|
||||
You'd think that Madam MeowMeow would be a cat person.
|
||||
Look at you, with them dry lips.
|
||||
You are now manually breathing. Hope that doesn't throw you off for this race.
|
||||
Lemme get a number nine, a number nine large, a number six, with extra dip...
|
||||
Tarin, the red-nosed deadbeat \nHad a mushroom addiction!
|
||||
I'm using tilt controls!
|
||||
SPLASH! \n \n \n ...Wait, you meant something else by 'splash text'?
|
||||
CRACKLE-FWOOSH!
|
||||
'Logic' is a strong word.
|
||||
They say that the go-to way for fixing things is just to add another one of me.
|
||||
gl hf
|
||||
Have you considered multi-classing as a THIEF?
|
||||
Don't call me Shirley
|
||||
WHY are you buying CLOTHES at the SOUP STORE?
|
||||
Believe it or not, this won't be the last time Link gets stranded on an island.
|
||||
Is this the real life? Or is this just fantasy?
|
||||
To the owner of the white sedan, your lights are on.
|
||||
Now remade, in beautiful SD 2D!
|
||||
Animal Village in my seed \nMarin and rabbits, loop de loop.
|
||||
You seem totally entranced in Marin's appearance.
|
||||
House hippoes are very timid creatures and are rarely seen, but they will defend their territory if provoked.
|
||||
New goal! Close this seed, open the LADXR source code, and find the typo.
|
||||
All your base are belong to us
|
||||
Really? Another seed?
|
||||
This seed brought to you by: the corners in the D2 boss room.
|
||||
Hey, THIEF! Oh wait, you haven't done anything wrong... yet.
|
||||
Hello World
|
||||
With these hands, I give you life!
|
||||
I heard we're a subcommunity of FFR now.
|
||||
Try the Final Fantasy Randomizer!
|
||||
How soon should we start calling you THIEF?
|
||||
... Why do you keep doing this to yourself?
|
||||
YOUR AD HERE
|
||||
Did Matty give you this seed? Yeesh, good luck.
|
||||
Yoooo I looked ahead into the spoiler log for this one...\n...\n...\n...good luck.
|
||||
Lemme check the spoiler log...\nOkay, cool, only the normal amount of stupid.
|
||||
Oh, you're alive. Dang. Guess I won't be needing THIS anymore.
|
||||
Now you're gonna go talk to my dad. Gosh, boys are so predictable.
|
||||
Shoot, I WAS going to steal your kidneys while you were asleep. Guess I'll have to find a moment when you don't expect me.
|
||||
You caught me, mid-suavamente!
|
||||
You'll be the bedwetting champion in no time.
|
||||
Link, stop doing that, this is the fifth time this week I've had to change the sheets!
|
||||
You mind napping in Not My Bed next time?
|
||||
Why do they call it oven when you of in the cold food of out hot eat the food?
|
||||
Marin sayings will never be generated by AI. Our community really is just that unfunny.
|
||||
skibidi toilet\n...\nYes, that joke WILL age well
|
||||
WHO DARES AWAKEN ME FROM MY THOUSAND-YEAR SLUMBER
|
||||
The wind... it is... blowing...
|
||||
Have I ever told you how much I hate sand?
|
||||
explosion.gif
|
||||
It is pronounced LADXR, not LADXR.
|
||||
Stop pronouncing it lah-decks.
|
||||
Someone once suggested to add all the nag messages all at once for me.
|
||||
Accidentally playing Song 2? In front of the egg? It's more likely than you think.
|
||||
Ladies and gentlemen? We got him.
|
||||
Ladies and gentlemen? We got her.
|
||||
Ladies and gentlemen? We got 'em.
|
||||
What a wake up! I thought you'd never Marin! You were feeling a bit woozy and Zelda... What? Koholint? No, my name's relief! You must still be tossing. You are on turning Island!
|
||||
...Zelda? Oh Marin is it? My apologies, thank you for saving me. So I'm on Koholint Island? Wait, where's my sword and shield?!
|
||||
Koholint? More like kOWOlint.
|
||||
What? The Wind Fish will grant my wish literally? I forsee nothing wrong happening with this.
|
||||
Hey Marin! You woke me up from a fine nap! ... Thanks a lot! But now, I'll get my revenge! Are you ready?!
|
||||
Why bother coming up with a funny quote? You're just gonna mash through it anyway.
|
||||
something something whale something something dream something something adventure.
|
||||
Some people won't be able to see this message!
|
||||
If you're playing Archipelago and see this message, say hi to zig for me!
|
||||
I think it may be time to stop playing LADXR seeds.
|
||||
Rings do nothing unless worn!
|
||||
Thank you Link, but our Instruments are in another Dungeon.
|
||||
Are you sure you loaded the right seed?
|
||||
Is this even randomized?
|
||||
This seed brought to you by... Corners!
|
||||
To this day I still don't know if we inconvenienced the Mad Batter or not.
|
||||
Oh, hi #####
|
||||
People forgot I was playable in Hyrule Warriors
|
||||
Join our Discord. Or else.
|
||||
Also try Minecraft!
|
||||
I see you're finally awake...
|
||||
OwO
|
||||
This is Todd Howard, and today I'm pleased to announce... The Elder Scrolls V: Skyrim for the Nintendo Game Boy Color!
|
||||
Hey dummy! Need a hint? The power bracelet is... !! Whoops! There I go, talking too much again.
|
||||
Thank you for visiting Toronbo Shores featuring Mabe Village. Don't forget your complimentary gift on the way out.
|
||||
They say that sand can be found in Yarna Desert.
|
||||
I got to see a previously unreleased cut yesterday. It only cost me 200 rupees. What a deal!
|
||||
Just let him sleep
|
||||
LADXR is going to be renamed X now.
|
||||
Did you hear this chart-topping song yet? It's called Manbo's Mambo, it's so catchy! OH!
|
||||
YOU DARE BRING LIGHT INTO MY LAIR?!?! You must DIE!
|
||||
But enough talk! Have at you!
|
||||
Please input your age for optimal meme-text delivery.
|
||||
So the bear is just calling the walrus fat beecause he's projecting, right?
|
||||
Please help, #####! The Nightmare has shuffled all the items around!
|
||||
One does not simply Wake the Wind Fish.
|
||||
Nothing unusual here, just a completely normal LADX game, Mister Nintendo.
|
||||
Remember:\n1) Play Vanilla\n2) Play Solo Rando\n3) Play Multi
|
||||
Is :) a good item?
|
||||
What version do we have anyway? 0.6.9?
|
||||
So, what &newgames are coming in the next AP version?
|
||||
Is !remaining fixed yet?
|
||||
Remember the APocalypse. Never forget the rooms we lost that day.
|
||||
Have you heard of Berserker's Multiworld?
|
||||
MILF. Man I love Fangames.
|
||||
How big can the Big Async be anyway? A hundred worlds?
|
||||
Have you heard of the After Dark server?
|
||||
Try Adventure!
|
||||
Try Aquaria!
|
||||
Try Blasphemous!
|
||||
Try Bomb Rush Cyberfunk!
|
||||
Try Bumper Stickers!
|
||||
Try Castlevania 64!
|
||||
Try Celeste 64!
|
||||
Try ChecksFinder!
|
||||
Try Clique!
|
||||
Try Dark Souls III!
|
||||
Try DLCQuest!
|
||||
Try Donkey Kong Country 3!
|
||||
Try DOOM 1993!
|
||||
Try DOOM II!
|
||||
Try Factorio!
|
||||
Try Final Fantasy!
|
||||
Try Final Fantasy Mystic Quest!
|
||||
Try A Hat in Time!
|
||||
Try Heretic!
|
||||
Try Hollow Knight!
|
||||
Try Hylics 2!
|
||||
Try Kingdom Hearts 2!
|
||||
Try Kirby's Dream Land 3!
|
||||
Try Landstalker - The Treasures of King Nole!
|
||||
Try The Legend of Zelda!
|
||||
Try Lingo!
|
||||
Try A Link to the Past!
|
||||
Try Links Awakening DX!
|
||||
Try Lufia II Ancient Cave!
|
||||
Try Mario & Luigi Superstar Saga!
|
||||
Try MegaMan Battle Network 3!
|
||||
Try Meritous!
|
||||
Try The Messenger!
|
||||
Try Minecraft!
|
||||
Try Muse Dash!
|
||||
Try Noita!
|
||||
Try Ocarina of Time!
|
||||
Try Overcooked! 2!
|
||||
Try Pokemon Emerald!
|
||||
Try Pokemon Red and Blue!
|
||||
Try Raft!
|
||||
Try Risk of Rain 2!
|
||||
Try Rogue Legacy!
|
||||
Try Secret of Evermore!
|
||||
Try Shivers!
|
||||
Try A Short Hike!
|
||||
Try Slay the Spire!
|
||||
Try SMZ3!
|
||||
Try Sonic Adventure 2 Battle!
|
||||
Try Starcraft 2!
|
||||
Try Stardew Valley!
|
||||
Try Subnautica!
|
||||
Try Sudoku!
|
||||
Try Super Mario 64!
|
||||
Try Super Mario World!
|
||||
Try Super Metroid!
|
||||
Try Terraria!
|
||||
Try Timespinner!
|
||||
Try TUNIC!
|
||||
Try Undertale!
|
||||
Try VVVVVV!
|
||||
Try Wargroove!
|
||||
Try The Witness!
|
||||
Try Yoshi's Island!
|
||||
Try Yu-Gi-Oh! 2006!
|
||||
Try Zillion!
|
||||
Try Zork Grand Inquisitor!
|
||||
Try Old School Runescape!
|
||||
Try Kingdom Hearts!
|
||||
Try Mega Man 2!
|
||||
Try Yacht Dice!
|
||||
VVVVVVVVVVVVVV this should be enough V right?
|
||||
If you see this message, please open a #bug-report about it\n\n\nDon't actually though.
|
||||
This YAML is going in the bucket, isn't it?
|
||||
Oh, this is a terrible seed for a Sync
|
||||
Oh, this is a terrible seed for an Async
|
||||
What does BK stand for anyway?
|
||||
Check out the #future-game-design forum
|
||||
This is actually a Free trial of the critically acclaimed MMORPG Final Fantasy XIV, including the entirety of A Realm Reborn and the award winning Heavensward and Stormblood expansions up to level 70 with no restrictions on playtime!
|
||||
Is it April yet? Can I play ArchipIDLE again?
|
||||
https://archipelago.gg/datapackage
|
||||
Hello, Link! (Disregard message if your player sprite is not Link.)
|
||||
Go back to sleep, Outer Wilds isn't supported yet.
|
||||
:)\nWelcome back!
|
||||
Don't forget about Aginah!
|
||||
Remind your Undertale player not to warp before Mad Dummy.
|
||||
You need\n9 instruments\nor maybe not. I wouldn't know.
|
||||
Try !\n\nIt makes the game easier.
|
||||
Have you tried The Witness? If you're a fan of games about waking up on an unfamiliar island, give it a shot!
|
||||
Have you tried turning it off and on again?
|
||||
Its about time. Now go and check
|
||||
This dream is a lie. Or is it a cake?
|
||||
Don't live your dream. Dream your live.
|
||||
Only 5 more minutes. zzzZ
|
||||
Tell me, for whom do you fight?\nHmmph. How very glib. And do you believe in Koholint?
|
||||
I wonder when Undertale will be merged?\nOh wait it already has.
|
||||
Hit me up if you get stuck -\nwe could go to Burger King together.
|
||||
Post this message to delay Silksong.
|
||||
Sorry #####, but your princess is in another castle!
|
||||
You've been met with a terrible fate, haven't you?
|
||||
Hey!\nListen!\nHey! Hey!\nListen!
|
||||
I bet there's a progression item at the 980 Rupee shop check.
|
||||
Lamp oil? Rope? Bombs? You want it? It's yours, my friend. As long as you have enough rubies.
|
||||
One day I happened to be occupied with the subject of generation of waves by wind.
|
||||
(nuzzles you) uwu
|
||||
why do they call it links awakening when links awake and IN links asleep OUT the wind fish
|
||||
For many years I have been looking, searching for, but never finding, the builder of this house...
|
||||
What the heck is a Quatro?
|
||||
Have you tried The Binding of Isaac yet?
|
||||
Have you played Pong? \n I hear it's still popular nowadays.
|
||||
Five Nights at Freddy's... \n That's where I wanna be
|
||||
Setting Coinsanity to -1...
|
||||
Your Feather can be found at Mask-Shard_Grey_Mourner
|
||||
Your Sword can be found in Ganon's Tower
|
||||
Your Rooster can be found in HylemXylem
|
||||
Your Bracelet can be found at Giant Floor Puzzle
|
||||
Your Flippers can be found in Valley of Bowser
|
||||
Your Magic Rod can be found in Victory Road
|
||||
Your Hookshot can be found in Bowser in the Sky
|
||||
Have they added Among Us to AP yet?
|
||||
Every copy of LADX is personalized, David.
|
||||
Looks like you're going on A Short Hike. Bring back feathers please?
|
||||
Functioning Brain is at...\nWait. This isn't Witness. Wrong game, sorry.
|
||||
Don't forget to check your Clique!\nIf, y'know, you have one. No pressure...
|
||||
:3
|
||||
Sorry ######, but your progression item is in another world.
|
||||
&newgames\n&oldgames
|
||||
Do arrows come with turners? I'm stuck in my Bumper Stickers world.
|
||||
This seed has dexsanity enabled. Don't get stuck in Dewford!
|
||||
Please purchase the Dialogue Pack for DLC Quest: Link's Adventure to read the rest of this text.
|
||||
No hints here. Maybe ask BK Sudoku for some?
|
||||
KILNS (Yellow Middle, 5) \n REVELATION (White Low, 9)
|
||||
Push the button! When someone lets you...
|
||||
You won't believe the WEIRD thing Tarin found at the beach! Go on, ask him about it!
|
||||
When's door randomizer getting added to AP?
|
||||
Can you get my Morph Ball?
|
||||
Shoutouts to Simpleflips
|
||||
Remember, Sword goes on C!\n...you have a C button, right?
|
||||
Ask Berserker for your Progressive Power Bracelets!
|
||||
I will be taking your Burger King order now to save you some time when you inevitably need it.
|
||||
Welcome to KOHOLINT ISLAND.\nNo, we do not have a BURGER KING.
|
||||
Welcome to Burger King, may I take your order?
|
||||
Rise and shine, #####. Rise and shine.
|
||||
Well, this is\nLITTLEROOT TOWN.\nHow do you like it?
|
||||
My boy, this peace is what all true warriors strive for!
|
||||
#####, you can do it!\nSave the Princess...\nZelda is your... ... ...
|
||||
Dear Mario:\nPlease come to the castle, I've baked a cake for you. Yours truly--\nPrincess Toadstool\nPeach
|
||||
Grass-sanity mode activated. Have fun!
|
||||
Don't forget to bring rupees to the signpost maze this time.
|
||||
UP UP DOWN DOWN LEFT RIGHT LEFT RIGHT B A START
|
||||
Try LADX!\nWait a minute...
|
||||
ERROR! Unable to verify player. Please drink a verification can.
|
||||
We have been trying to reach you about your raft's extended warranty
|
||||
Are you ready for the easiest BK of your life?
|
||||
Hello, welcome to the world of Pokemon!\nMy name is Marin, and I'm--
|
||||
Alright, this is very important, I need you to listen to what I'm about to tell you--\nHey, wait, where are you going?!
|
||||
Cheques?\nSorry we don't accept cheques here
|
||||
Hi! \nMarin. \nWho...? \nHow...? \nWait... \nWhy??? \nSorry... \n...\nThanks. \nBye!
|
||||
AHHH WHY IS THERE SO MUCH GRASS? \nHOLY SH*T GRASS SNAKE AHHHH
|
||||
Could you buy some strawberries on your way home? \nHuh it's out of logic??? What??
|
||||
I heard you sleeptalking about skeletons and genocide... Your past must have been full of misery (mire)
|
||||
It's time to let go... \nIt wasn't your fault... \nYou couldn't have known your first check was going to be hardmode...
|
||||
They say that your progression is in another castle...
|
||||
A minute of silence for the failed generations due to the Fitness Gram Pacer test.
|
||||
Save an Ice Trap for me, please?
|
||||
maren
|
||||
ERROR DETECTED IN YAML\nOHKO MODE FORCED ON
|
||||
she awaken my link (extremely loud incorrect buzzer)
|
||||
Is deathlink on? If so, be careful!
|
||||
Sorry, but you're about to be BK'd.
|
||||
Did you set up cheesetracker yet?
|
||||
I've got a hint I need you to get...
|
||||
You aren't planning to destroy this island and kill everyone on it are you?
|
||||
Have you ever had a dream, that, that you um you had you'd you would you could you'd do you wi you wants you you could do so you you'd do you could you you want you want him to do you so much you could do anything?
|
||||
R R R U L L U L U R U R D R D R U U
|
||||
I'm not sure how, but I am pretty sure this is Phar's fault.
|
||||
Oh, look at that. Link's Awakened.\nYou did it, you beat the game.
|
||||
Excellent armaments, #####. Please return - \nCOVERED IN BLOOD -\n...safe and sound.
|
||||
Pray return to the Link's Awakening Sands.
|
||||
This Marin dialogue was inspired by The Witness's audiologs.
|
||||
You're awake!\n....\nYou were warned.\nI'm now going to say every word beginning with Z!\nZA\nZABAGLIONE\nZABAGLIONES\nZABAIONE\nZABAIONES\nZABAJONE\nZABAJONES\nZABETA\nZABETAS\nZABRA\nZABRAS\nZABTIEH\nZABTIEHS\nZACATON\nZACATONS\nZACK\nZACKS\nZADDICK\nZADDIK\nZADDIKIM\nZADDIKS\nZAFFAR\nzAFFARS\nZAFFER\nZAFFERS\nZAFFIR\n....\n....\n....\nI'll let you off easy.\nThis time.
|
||||
Leave me alone, I'm Marinating.
|
||||
praise be to the tungsten cube
|
||||
If you play multiple seeds in a row, you can pretend that each run is the dream you awaken from in the next.
|
||||
If this is a competitive race,\n\nyour time has already started.
|
||||
If anything goes wrong, remember.\n Blame Phar.
|
||||
Better hope your Hookshot didn't land on the Sick Kid.
|
||||
One time, I accidentally said Konoliht instead of Koholint...
|
||||
Sometimes, you must become best girl yourself...
|
||||
You just woke up! My name's #####!\nYou must be Marin, right?
|
||||
I just had the strangest dream, I was a seagull!\nI sung many songs for everybody to hear!\nHave you ever had a strange dream before?
|
||||
If you think about it, Koholint sounds suspiciously similar to Coherent...
|
||||
All I kin remember is biting into a juicy toadstool. Then I had the strangest dream... I was a Marin! Yeah, it sounds strange, but it sure was fun!
|
||||
Prepare for a 100% run!
|
||||
Prediction: 1 hour
|
||||
Prediction: 4 hours
|
||||
Prediction: 6 hours
|
||||
Prediction: 12 hours
|
||||
Prediction: Impossible seed
|
||||
Oak's parcel has arrived.
|
||||
Don't forget to like and subscribe!
|
||||
Don't BK, eat healthy!
|
||||
No omega symbols broke this seed gen? Good!
|
||||
#####...\nYou're lucky.\nLooks like my summer vacation is...\nover.
|
||||
Are you ready to send nukes to someone's Factorio game?
|
||||
You're late... Is this a Cmario game?
|
||||
At least you don't have to fight Ganon... What?
|
||||
PRAISE THE SUN!
|
||||
I'd recommend more sleep before heading out there.
|
||||
You Must Construct Additional Pylons
|
||||
#####, you lazy bum. I knew that I'd find you snoozing down here.
|
||||
This is it, #####.\nJust breathe.\nWhy are you so nervous?
|
||||
Hey, you. You're finally awake.\nYou were trying to cross the border, huh?
|
||||
Hey, you. You're finally awake.\nYou were trying to leave the island, huh?\nSwam straight into that whirlpool, same as us, and that thief over there.
|
||||
Is my Triforce locked behind your Wind Fish?
|
||||
@@ -527,6 +527,20 @@ class InGameHints(DefaultOnToggle):
|
||||
display_name = "In-game Hints"
|
||||
|
||||
|
||||
class TarinsGift(Choice):
|
||||
"""
|
||||
[Local Progression] Forces Tarin's gift to be an item that immediately opens up local checks.
|
||||
Has little effect in single player games, and isn't always necessary with randomized entrances.
|
||||
[Bush Breaker] Forces Tarin's gift to be an item that can destroy bushes.
|
||||
[Any Item] Tarin's gift can be any item for any world
|
||||
"""
|
||||
display_name = "Tarin's Gift"
|
||||
option_local_progression = 0
|
||||
option_bush_breaker = 1
|
||||
option_any_item = 2
|
||||
default = option_local_progression
|
||||
|
||||
|
||||
class StabilizeItemPool(DefaultOffToggle):
|
||||
"""
|
||||
By default, rupees in the item pool may be randomly swapped with bombs, arrows, powders, or capacity upgrades. This option disables that swapping, which is useful for plando.
|
||||
@@ -565,6 +579,7 @@ ladx_option_groups = [
|
||||
OptionGroup("Miscellaneous", [
|
||||
TradeQuest,
|
||||
Rooster,
|
||||
TarinsGift,
|
||||
Overworld,
|
||||
TrendyGame,
|
||||
InGameHints,
|
||||
@@ -638,6 +653,7 @@ class LinksAwakeningOptions(PerGameCommonOptions):
|
||||
text_mode: TextMode
|
||||
no_flash: NoFlash
|
||||
in_game_hints: InGameHints
|
||||
tarins_gift: TarinsGift
|
||||
overworld: Overworld
|
||||
stabilize_item_pool: StabilizeItemPool
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import typing
|
||||
|
||||
from worlds.ladx.GpsTracker import GpsTracker
|
||||
from .LADXR.checkMetadata import checkMetadataTable
|
||||
import json
|
||||
import logging
|
||||
@@ -10,13 +13,14 @@ logger = logging.getLogger("Tracker")
|
||||
# kbranch you're a hero
|
||||
# https://github.com/kbranch/Magpie/blob/master/autotracking/checks.py
|
||||
class Check:
|
||||
def __init__(self, id, address, mask, alternateAddress=None):
|
||||
def __init__(self, id, address, mask, alternateAddress=None, linkedItem=None):
|
||||
self.id = id
|
||||
self.address = address
|
||||
self.alternateAddress = alternateAddress
|
||||
self.mask = mask
|
||||
self.value = None
|
||||
self.diff = 0
|
||||
self.linkedItem = linkedItem
|
||||
|
||||
def set(self, bytes):
|
||||
oldValue = self.value
|
||||
@@ -86,6 +90,27 @@ class LocationTracker:
|
||||
|
||||
blacklist = {'None', '0x2A1-2'}
|
||||
|
||||
def seashellCondition(slot_data):
|
||||
return 'goal' not in slot_data or slot_data['goal'] != 'seashells'
|
||||
|
||||
linkedCheckItems = {
|
||||
'0x2E9': {'item': 'SEASHELL', 'qty': 20, 'condition': seashellCondition},
|
||||
'0x2A2': {'item': 'TOADSTOOL', 'qty': 1},
|
||||
'0x2A6-Trade': {'item': 'TRADING_ITEM_YOSHI_DOLL', 'qty': 1},
|
||||
'0x2B2-Trade': {'item': 'TRADING_ITEM_RIBBON', 'qty': 1},
|
||||
'0x2FE-Trade': {'item': 'TRADING_ITEM_DOG_FOOD', 'qty': 1},
|
||||
'0x07B-Trade': {'item': 'TRADING_ITEM_BANANAS', 'qty': 1},
|
||||
'0x087-Trade': {'item': 'TRADING_ITEM_STICK', 'qty': 1},
|
||||
'0x2D7-Trade': {'item': 'TRADING_ITEM_HONEYCOMB', 'qty': 1},
|
||||
'0x019-Trade': {'item': 'TRADING_ITEM_PINEAPPLE', 'qty': 1},
|
||||
'0x2D9-Trade': {'item': 'TRADING_ITEM_HIBISCUS', 'qty': 1},
|
||||
'0x2A8-Trade': {'item': 'TRADING_ITEM_LETTER', 'qty': 1},
|
||||
'0x0CD-Trade': {'item': 'TRADING_ITEM_BROOM', 'qty': 1},
|
||||
'0x2F5-Trade': {'item': 'TRADING_ITEM_FISHING_HOOK', 'qty': 1},
|
||||
'0x0C9-Trade': {'item': 'TRADING_ITEM_NECKLACE', 'qty': 1},
|
||||
'0x297-Trade': {'item': 'TRADING_ITEM_SCALE', 'qty': 1},
|
||||
}
|
||||
|
||||
# in no dungeons boss shuffle, the d3 boss in d7 set 0x20 in fascade's room (0x1BC)
|
||||
# after beating evil eagile in D6, 0x1BC is now 0xAC (other things may have happened in between)
|
||||
# entered d3, slime eye flag had already been set (0x15A 0x20). after killing angler fish, bits 0x0C were set
|
||||
@@ -98,6 +123,8 @@ class LocationTracker:
|
||||
address = addressOverrides[check_id] if check_id in addressOverrides else 0xD800 + int(
|
||||
room, 16)
|
||||
|
||||
linkedItem = linkedCheckItems[check_id] if check_id in linkedCheckItems else None
|
||||
|
||||
if 'Trade' in check_id or 'Owl' in check_id:
|
||||
mask = 0x20
|
||||
|
||||
@@ -111,13 +138,19 @@ class LocationTracker:
|
||||
highest_check = max(
|
||||
highest_check, alternateAddresses[check_id])
|
||||
|
||||
check = Check(check_id, address, mask,
|
||||
alternateAddresses[check_id] if check_id in alternateAddresses else None)
|
||||
check = Check(
|
||||
check_id,
|
||||
address,
|
||||
mask,
|
||||
(alternateAddresses[check_id] if check_id in alternateAddresses else None),
|
||||
linkedItem,
|
||||
)
|
||||
|
||||
if check_id == '0x2A3':
|
||||
self.start_check = check
|
||||
self.all_checks.append(check)
|
||||
self.remaining_checks = [check for check in self.all_checks]
|
||||
self.gameboy.set_cache_limits(
|
||||
self.gameboy.set_checks_range(
|
||||
lowest_check, highest_check - lowest_check + 1)
|
||||
|
||||
def has_start_item(self):
|
||||
@@ -147,10 +180,17 @@ class MagpieBridge:
|
||||
server = None
|
||||
checks = None
|
||||
item_tracker = None
|
||||
gps_tracker: GpsTracker = None
|
||||
ws = None
|
||||
features = []
|
||||
slot_data = {}
|
||||
|
||||
def use_entrance_tracker(self):
|
||||
return "entrances" in self.features \
|
||||
and self.slot_data \
|
||||
and "entrance_mapping" in self.slot_data \
|
||||
and any([k != v for k, v in self.slot_data["entrance_mapping"].items()])
|
||||
|
||||
async def handler(self, websocket):
|
||||
self.ws = websocket
|
||||
while True:
|
||||
@@ -159,14 +199,18 @@ class MagpieBridge:
|
||||
logger.info(
|
||||
f"Connected, supported features: {message['features']}")
|
||||
self.features = message["features"]
|
||||
|
||||
await self.send_handshAck()
|
||||
|
||||
if message["type"] in ("handshake", "sendFull"):
|
||||
if message["type"] == "sendFull":
|
||||
if "items" in self.features:
|
||||
await self.send_all_inventory()
|
||||
if "checks" in self.features:
|
||||
await self.send_all_checks()
|
||||
if "slot_data" in self.features:
|
||||
if "slot_data" in self.features and self.slot_data:
|
||||
await self.send_slot_data(self.slot_data)
|
||||
if self.use_entrance_tracker():
|
||||
await self.send_gps(diff=False)
|
||||
|
||||
# Translate renamed IDs back to LADXR IDs
|
||||
@staticmethod
|
||||
@@ -176,6 +220,18 @@ class MagpieBridge:
|
||||
if the_id == "0x2A7":
|
||||
return "0x2A1-1"
|
||||
return the_id
|
||||
|
||||
async def send_handshAck(self):
|
||||
if not self.ws:
|
||||
return
|
||||
|
||||
message = {
|
||||
"type": "handshAck",
|
||||
"version": "1.32",
|
||||
"name": "archipelago-ladx-client",
|
||||
}
|
||||
|
||||
await self.ws.send(json.dumps(message))
|
||||
|
||||
async def send_all_checks(self):
|
||||
while self.checks == None:
|
||||
@@ -185,7 +241,6 @@ class MagpieBridge:
|
||||
message = {
|
||||
"type": "check",
|
||||
"refresh": True,
|
||||
"version": "1.0",
|
||||
"diff": False,
|
||||
"checks": [{"id": self.fixup_id(check.id), "checked": check.value} for check in self.checks]
|
||||
}
|
||||
@@ -200,7 +255,6 @@ class MagpieBridge:
|
||||
message = {
|
||||
"type": "check",
|
||||
"refresh": True,
|
||||
"version": "1.0",
|
||||
"diff": True,
|
||||
"checks": [{"id": self.fixup_id(check), "checked": True} for check in checks]
|
||||
}
|
||||
@@ -222,10 +276,17 @@ class MagpieBridge:
|
||||
return
|
||||
await self.item_tracker.sendItems(self.ws, diff=True)
|
||||
|
||||
async def send_gps(self, gps):
|
||||
async def send_gps(self, diff: bool=True) -> typing.Dict[str, str]:
|
||||
if not self.ws:
|
||||
return
|
||||
await gps.send_location(self.ws)
|
||||
|
||||
await self.gps_tracker.send_location(self.ws)
|
||||
|
||||
if self.use_entrance_tracker():
|
||||
if self.slot_data and self.gps_tracker.needs_slot_data:
|
||||
self.gps_tracker.load_slot_data(self.slot_data)
|
||||
|
||||
return await self.gps_tracker.send_entrances(self.ws, diff)
|
||||
|
||||
async def send_slot_data(self, slot_data):
|
||||
if not self.ws:
|
||||
|
||||
291
worlds/ladx/TrackerConsts.py
Normal file
291
worlds/ladx/TrackerConsts.py
Normal file
@@ -0,0 +1,291 @@
|
||||
class EntranceCoord:
|
||||
name: str
|
||||
room: int
|
||||
x: int
|
||||
y: int
|
||||
|
||||
def __init__(self, name: str, room: int, x: int, y: int):
|
||||
self.name = name
|
||||
self.room = room
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __repr__(self):
|
||||
return EntranceCoord.coordString(self.room, self.x, self.y)
|
||||
|
||||
def coordString(room: int, x: int, y: int):
|
||||
return f"{room:#05x}, {x}, {y}"
|
||||
|
||||
storage_key = "found_entrances"
|
||||
|
||||
room = 0xFFF6
|
||||
map_id = 0xFFF7
|
||||
indoor_flag = 0xDBA5
|
||||
spawn_map = 0xDB60
|
||||
spawn_room = 0xDB61
|
||||
spawn_x = 0xDB62
|
||||
spawn_y = 0xDB63
|
||||
entrance_room_offset = 0xD800
|
||||
transition_state = 0xC124
|
||||
transition_target_x = 0xC12C
|
||||
transition_target_y = 0xC12D
|
||||
transition_scroll_x = 0xFF96
|
||||
transition_scroll_y = 0xFF97
|
||||
link_motion_state = 0xC11C
|
||||
transition_sequence = 0xC16B
|
||||
screen_coord = 0xFFFA
|
||||
|
||||
entrance_address_overrides = {
|
||||
0x312: 0xDDF2,
|
||||
}
|
||||
|
||||
map_map = {
|
||||
0x00: 0x01,
|
||||
0x01: 0x01,
|
||||
0x02: 0x01,
|
||||
0x03: 0x01,
|
||||
0x04: 0x01,
|
||||
0x05: 0x01,
|
||||
0x06: 0x02,
|
||||
0x07: 0x02,
|
||||
0x08: 0x02,
|
||||
0x09: 0x02,
|
||||
0x0A: 0x02,
|
||||
0x0B: 0x02,
|
||||
0x0C: 0x02,
|
||||
0x0D: 0x02,
|
||||
0x0E: 0x02,
|
||||
0x0F: 0x02,
|
||||
0x10: 0x02,
|
||||
0x11: 0x02,
|
||||
0x12: 0x02,
|
||||
0x13: 0x02,
|
||||
0x14: 0x02,
|
||||
0x15: 0x02,
|
||||
0x16: 0x02,
|
||||
0x17: 0x02,
|
||||
0x18: 0x02,
|
||||
0x19: 0x02,
|
||||
0x1D: 0x01,
|
||||
0x1E: 0x01,
|
||||
0x1F: 0x01,
|
||||
0xFF: 0x03,
|
||||
}
|
||||
|
||||
sidescroller_rooms = {
|
||||
0x2e9: "seashell_mansion:inside",
|
||||
0x08a: "seashell_mansion",
|
||||
0x2fd: "mambo:inside",
|
||||
0x02a: "mambo",
|
||||
0x1eb: "castle_secret_exit:inside",
|
||||
0x049: "castle_secret_exit",
|
||||
0x1ec: "castle_secret_entrance:inside",
|
||||
0x04a: "castle_secret_entrance",
|
||||
0x117: "d1:inside", # not a sidescroller, but acts weird
|
||||
}
|
||||
|
||||
entrance_coords = [
|
||||
EntranceCoord("writes_house:inside", 0x2a8, 80, 124),
|
||||
EntranceCoord("rooster_grave", 0x92, 88, 82),
|
||||
EntranceCoord("start_house:inside", 0x2a3, 80, 124),
|
||||
EntranceCoord("dream_hut", 0x83, 40, 66),
|
||||
EntranceCoord("papahl_house_right:inside", 0x2a6, 80, 124),
|
||||
EntranceCoord("papahl_house_right", 0x82, 120, 82),
|
||||
EntranceCoord("papahl_house_left:inside", 0x2a5, 80, 124),
|
||||
EntranceCoord("papahl_house_left", 0x82, 88, 82),
|
||||
EntranceCoord("d2:inside", 0x136, 80, 124),
|
||||
EntranceCoord("shop", 0x93, 72, 98),
|
||||
EntranceCoord("armos_maze_cave:inside", 0x2fc, 104, 96),
|
||||
EntranceCoord("start_house", 0xa2, 88, 82),
|
||||
EntranceCoord("animal_house3:inside", 0x2d9, 80, 124),
|
||||
EntranceCoord("trendy_shop", 0xb3, 88, 82),
|
||||
EntranceCoord("mabe_phone:inside", 0x2cb, 80, 124),
|
||||
EntranceCoord("mabe_phone", 0xb2, 88, 82),
|
||||
EntranceCoord("ulrira:inside", 0x2a9, 80, 124),
|
||||
EntranceCoord("ulrira", 0xb1, 72, 98),
|
||||
EntranceCoord("moblin_cave:inside", 0x2f0, 80, 124),
|
||||
EntranceCoord("kennel", 0xa1, 88, 66),
|
||||
EntranceCoord("madambowwow:inside", 0x2a7, 80, 124),
|
||||
EntranceCoord("madambowwow", 0xa1, 56, 66),
|
||||
EntranceCoord("library:inside", 0x1fa, 80, 124),
|
||||
EntranceCoord("library", 0xb0, 56, 50),
|
||||
EntranceCoord("d5:inside", 0x1a1, 80, 124),
|
||||
EntranceCoord("d1", 0xd3, 104, 34),
|
||||
EntranceCoord("d1:inside", 0x117, 80, 124),
|
||||
EntranceCoord("d3:inside", 0x152, 80, 124),
|
||||
EntranceCoord("d3", 0xb5, 104, 32),
|
||||
EntranceCoord("banana_seller", 0xe3, 72, 48),
|
||||
EntranceCoord("armos_temple:inside", 0x28f, 80, 124),
|
||||
EntranceCoord("boomerang_cave", 0xf4, 24, 32),
|
||||
EntranceCoord("forest_madbatter:inside", 0x1e1, 136, 80),
|
||||
EntranceCoord("ghost_house", 0xf6, 88, 66),
|
||||
EntranceCoord("prairie_low_phone:inside", 0x29d, 80, 124),
|
||||
EntranceCoord("prairie_low_phone", 0xe8, 56, 98),
|
||||
EntranceCoord("prairie_madbatter_connector_entrance:inside", 0x1f6, 136, 112),
|
||||
EntranceCoord("prairie_madbatter_connector_entrance", 0xf9, 120, 80),
|
||||
EntranceCoord("prairie_madbatter_connector_exit", 0xe7, 104, 32),
|
||||
EntranceCoord("prairie_madbatter_connector_exit:inside", 0x1e5, 40, 48),
|
||||
EntranceCoord("ghost_house:inside", 0x1e3, 80, 124),
|
||||
EntranceCoord("prairie_madbatter", 0xe6, 72, 64),
|
||||
EntranceCoord("d4:inside", 0x17a, 80, 124),
|
||||
EntranceCoord("d5", 0xd9, 88, 64),
|
||||
EntranceCoord("prairie_right_cave_bottom:inside", 0x293, 48, 124),
|
||||
EntranceCoord("prairie_right_cave_bottom", 0xc8, 40, 80),
|
||||
EntranceCoord("prairie_right_cave_high", 0xb8, 88, 48),
|
||||
EntranceCoord("prairie_right_cave_high:inside", 0x295, 112, 124),
|
||||
EntranceCoord("prairie_right_cave_top", 0xb8, 120, 96),
|
||||
EntranceCoord("prairie_right_cave_top:inside", 0x292, 48, 124),
|
||||
EntranceCoord("prairie_to_animal_connector:inside", 0x2d0, 40, 64),
|
||||
EntranceCoord("prairie_to_animal_connector", 0xaa, 136, 64),
|
||||
EntranceCoord("animal_to_prairie_connector", 0xab, 120, 80),
|
||||
EntranceCoord("animal_to_prairie_connector:inside", 0x2d1, 120, 64),
|
||||
EntranceCoord("animal_phone:inside", 0x2e3, 80, 124),
|
||||
EntranceCoord("animal_phone", 0xdb, 120, 82),
|
||||
EntranceCoord("animal_house1:inside", 0x2db, 80, 124),
|
||||
EntranceCoord("animal_house1", 0xcc, 40, 80),
|
||||
EntranceCoord("animal_house2:inside", 0x2dd, 80, 124),
|
||||
EntranceCoord("animal_house2", 0xcc, 120, 80),
|
||||
EntranceCoord("hookshot_cave:inside", 0x2b3, 80, 124),
|
||||
EntranceCoord("animal_house3", 0xcd, 40, 80),
|
||||
EntranceCoord("animal_house4:inside", 0x2da, 80, 124),
|
||||
EntranceCoord("animal_house4", 0xcd, 88, 80),
|
||||
EntranceCoord("banana_seller:inside", 0x2fe, 80, 124),
|
||||
EntranceCoord("animal_house5", 0xdd, 88, 66),
|
||||
EntranceCoord("animal_cave:inside", 0x2f7, 96, 124),
|
||||
EntranceCoord("animal_cave", 0xcd, 136, 32),
|
||||
EntranceCoord("d6", 0x8c, 56, 64),
|
||||
EntranceCoord("madbatter_taltal:inside", 0x1e2, 136, 80),
|
||||
EntranceCoord("desert_cave", 0xcf, 88, 16),
|
||||
EntranceCoord("dream_hut:inside", 0x2aa, 80, 124),
|
||||
EntranceCoord("armos_maze_cave", 0xae, 72, 112),
|
||||
EntranceCoord("shop:inside", 0x2a1, 80, 124),
|
||||
EntranceCoord("armos_temple", 0xac, 88, 64),
|
||||
EntranceCoord("d6_connector_exit:inside", 0x1f0, 56, 16),
|
||||
EntranceCoord("d6_connector_exit", 0x9c, 88, 16),
|
||||
EntranceCoord("desert_cave:inside", 0x1f9, 120, 96),
|
||||
EntranceCoord("d6_connector_entrance:inside", 0x1f1, 136, 96),
|
||||
EntranceCoord("d6_connector_entrance", 0x9d, 56, 48),
|
||||
EntranceCoord("armos_fairy:inside", 0x1ac, 80, 124),
|
||||
EntranceCoord("armos_fairy", 0x8d, 56, 32),
|
||||
EntranceCoord("raft_return_enter:inside", 0x1f7, 136, 96),
|
||||
EntranceCoord("raft_return_enter", 0x8f, 8, 32),
|
||||
EntranceCoord("raft_return_exit", 0x2f, 24, 112),
|
||||
EntranceCoord("raft_return_exit:inside", 0x1e7, 72, 16),
|
||||
EntranceCoord("raft_house:inside", 0x2b0, 80, 124),
|
||||
EntranceCoord("raft_house", 0x3f, 40, 34),
|
||||
EntranceCoord("heartpiece_swim_cave:inside", 0x1f2, 72, 124),
|
||||
EntranceCoord("heartpiece_swim_cave", 0x2e, 88, 32),
|
||||
EntranceCoord("rooster_grave:inside", 0x1f4, 88, 112),
|
||||
EntranceCoord("d4", 0x2b, 72, 34),
|
||||
EntranceCoord("castle_phone:inside", 0x2cc, 80, 124),
|
||||
EntranceCoord("castle_phone", 0x4b, 72, 34),
|
||||
EntranceCoord("castle_main_entrance:inside", 0x2d3, 80, 124),
|
||||
EntranceCoord("castle_main_entrance", 0x69, 88, 64),
|
||||
EntranceCoord("castle_upper_left", 0x59, 24, 48),
|
||||
EntranceCoord("castle_upper_left:inside", 0x2d5, 80, 124),
|
||||
EntranceCoord("witch:inside", 0x2a2, 80, 124),
|
||||
EntranceCoord("castle_upper_right", 0x59, 88, 64),
|
||||
EntranceCoord("prairie_left_cave2:inside", 0x2f4, 64, 124),
|
||||
EntranceCoord("castle_jump_cave", 0x78, 40, 112),
|
||||
EntranceCoord("prairie_left_cave1:inside", 0x2cd, 80, 124),
|
||||
EntranceCoord("seashell_mansion", 0x8a, 88, 64),
|
||||
EntranceCoord("prairie_right_phone:inside", 0x29c, 80, 124),
|
||||
EntranceCoord("prairie_right_phone", 0x88, 88, 82),
|
||||
EntranceCoord("prairie_left_fairy:inside", 0x1f3, 80, 124),
|
||||
EntranceCoord("prairie_left_fairy", 0x87, 40, 16),
|
||||
EntranceCoord("bird_cave:inside", 0x27e, 96, 124),
|
||||
EntranceCoord("prairie_left_cave2", 0x86, 24, 64),
|
||||
EntranceCoord("prairie_left_cave1", 0x84, 152, 98),
|
||||
EntranceCoord("prairie_left_phone:inside", 0x2b4, 80, 124),
|
||||
EntranceCoord("prairie_left_phone", 0xa4, 56, 66),
|
||||
EntranceCoord("mamu:inside", 0x2fb, 136, 112),
|
||||
EntranceCoord("mamu", 0xd4, 136, 48),
|
||||
EntranceCoord("richard_house:inside", 0x2c7, 80, 124),
|
||||
EntranceCoord("richard_house", 0xd6, 72, 80),
|
||||
EntranceCoord("richard_maze:inside", 0x2c9, 128, 124),
|
||||
EntranceCoord("richard_maze", 0xc6, 56, 80),
|
||||
EntranceCoord("graveyard_cave_left:inside", 0x2de, 56, 64),
|
||||
EntranceCoord("graveyard_cave_left", 0x75, 56, 64),
|
||||
EntranceCoord("graveyard_cave_right:inside", 0x2df, 56, 48),
|
||||
EntranceCoord("graveyard_cave_right", 0x76, 104, 80),
|
||||
EntranceCoord("trendy_shop:inside", 0x2a0, 80, 124),
|
||||
EntranceCoord("d0", 0x77, 120, 46),
|
||||
EntranceCoord("boomerang_cave:inside", 0x1f5, 72, 124),
|
||||
EntranceCoord("witch", 0x65, 72, 50),
|
||||
EntranceCoord("toadstool_entrance:inside", 0x2bd, 80, 124),
|
||||
EntranceCoord("toadstool_entrance", 0x62, 120, 66),
|
||||
EntranceCoord("toadstool_exit", 0x50, 136, 50),
|
||||
EntranceCoord("toadstool_exit:inside", 0x2ab, 80, 124),
|
||||
EntranceCoord("prairie_madbatter:inside", 0x1e0, 136, 112),
|
||||
EntranceCoord("hookshot_cave", 0x42, 56, 66),
|
||||
EntranceCoord("castle_upper_right:inside", 0x2d6, 80, 124),
|
||||
EntranceCoord("forest_madbatter", 0x52, 104, 48),
|
||||
EntranceCoord("writes_phone:inside", 0x29b, 80, 124),
|
||||
EntranceCoord("writes_phone", 0x31, 104, 82),
|
||||
EntranceCoord("d0:inside", 0x312, 80, 92),
|
||||
EntranceCoord("writes_house", 0x30, 120, 50),
|
||||
EntranceCoord("writes_cave_left:inside", 0x2ae, 80, 124),
|
||||
EntranceCoord("writes_cave_left", 0x20, 136, 50),
|
||||
EntranceCoord("writes_cave_right:inside", 0x2af, 80, 124),
|
||||
EntranceCoord("writes_cave_right", 0x21, 24, 50),
|
||||
EntranceCoord("d6:inside", 0x1d4, 80, 124),
|
||||
EntranceCoord("d2", 0x24, 56, 34),
|
||||
EntranceCoord("animal_house5:inside", 0x2d7, 80, 124),
|
||||
EntranceCoord("moblin_cave", 0x35, 104, 80),
|
||||
EntranceCoord("crazy_tracy:inside", 0x2ad, 80, 124),
|
||||
EntranceCoord("crazy_tracy", 0x45, 136, 66),
|
||||
EntranceCoord("photo_house:inside", 0x2b5, 80, 124),
|
||||
EntranceCoord("photo_house", 0x37, 72, 66),
|
||||
EntranceCoord("obstacle_cave_entrance:inside", 0x2b6, 80, 124),
|
||||
EntranceCoord("obstacle_cave_entrance", 0x17, 56, 50),
|
||||
EntranceCoord("left_to_right_taltalentrance:inside", 0x2ee, 120, 48),
|
||||
EntranceCoord("left_to_right_taltalentrance", 0x7, 56, 80),
|
||||
EntranceCoord("obstacle_cave_outside_chest:inside", 0x2bb, 80, 124),
|
||||
EntranceCoord("obstacle_cave_outside_chest", 0x18, 104, 18),
|
||||
EntranceCoord("obstacle_cave_exit:inside", 0x2bc, 48, 124),
|
||||
EntranceCoord("obstacle_cave_exit", 0x18, 136, 18),
|
||||
EntranceCoord("papahl_entrance:inside", 0x289, 64, 124),
|
||||
EntranceCoord("papahl_entrance", 0x19, 136, 64),
|
||||
EntranceCoord("papahl_exit:inside", 0x28b, 80, 124),
|
||||
EntranceCoord("papahl_exit", 0xa, 24, 112),
|
||||
EntranceCoord("rooster_house:inside", 0x29f, 80, 124),
|
||||
EntranceCoord("rooster_house", 0xa, 72, 34),
|
||||
EntranceCoord("d7:inside", 0x20e, 80, 124),
|
||||
EntranceCoord("bird_cave", 0xa, 120, 112),
|
||||
EntranceCoord("multichest_top:inside", 0x2f2, 80, 124),
|
||||
EntranceCoord("multichest_top", 0xd, 24, 112),
|
||||
EntranceCoord("multichest_left:inside", 0x2f9, 32, 124),
|
||||
EntranceCoord("multichest_left", 0x1d, 24, 48),
|
||||
EntranceCoord("multichest_right:inside", 0x2fa, 112, 124),
|
||||
EntranceCoord("multichest_right", 0x1d, 120, 80),
|
||||
EntranceCoord("right_taltal_connector1:inside", 0x280, 32, 124),
|
||||
EntranceCoord("right_taltal_connector1", 0x1e, 56, 16),
|
||||
EntranceCoord("right_taltal_connector3:inside", 0x283, 128, 124),
|
||||
EntranceCoord("right_taltal_connector3", 0x1e, 120, 16),
|
||||
EntranceCoord("right_taltal_connector2:inside", 0x282, 112, 124),
|
||||
EntranceCoord("right_taltal_connector2", 0x1f, 40, 16),
|
||||
EntranceCoord("right_fairy:inside", 0x1fb, 80, 124),
|
||||
EntranceCoord("right_fairy", 0x1f, 56, 80),
|
||||
EntranceCoord("right_taltal_connector4:inside", 0x287, 96, 124),
|
||||
EntranceCoord("right_taltal_connector4", 0x1f, 88, 64),
|
||||
EntranceCoord("right_taltal_connector5:inside", 0x28c, 96, 124),
|
||||
EntranceCoord("right_taltal_connector5", 0x1f, 120, 16),
|
||||
EntranceCoord("right_taltal_connector6:inside", 0x28e, 112, 124),
|
||||
EntranceCoord("right_taltal_connector6", 0xf, 72, 80),
|
||||
EntranceCoord("d7", 0x0e, 88, 48),
|
||||
EntranceCoord("left_taltal_entrance:inside", 0x2ea, 80, 124),
|
||||
EntranceCoord("left_taltal_entrance", 0x15, 136, 64),
|
||||
EntranceCoord("castle_jump_cave:inside", 0x1fd, 88, 80),
|
||||
EntranceCoord("madbatter_taltal", 0x4, 120, 112),
|
||||
EntranceCoord("fire_cave_exit:inside", 0x1ee, 24, 64),
|
||||
EntranceCoord("fire_cave_exit", 0x3, 72, 80),
|
||||
EntranceCoord("fire_cave_entrance:inside", 0x1fe, 112, 124),
|
||||
EntranceCoord("fire_cave_entrance", 0x13, 88, 16),
|
||||
EntranceCoord("phone_d8:inside", 0x299, 80, 124),
|
||||
EntranceCoord("phone_d8", 0x11, 104, 50),
|
||||
EntranceCoord("kennel:inside", 0x2b2, 80, 124),
|
||||
EntranceCoord("d8", 0x10, 88, 16),
|
||||
EntranceCoord("d8:inside", 0x25d, 80, 124),
|
||||
]
|
||||
|
||||
entrance_lookup = {str(coord): coord for coord in entrance_coords}
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
import pkgutil
|
||||
import tempfile
|
||||
import typing
|
||||
import logging
|
||||
import re
|
||||
|
||||
import bsdiff4
|
||||
@@ -178,10 +179,10 @@ class LinksAwakeningWorld(World):
|
||||
|
||||
assert(start)
|
||||
|
||||
menu_region = LinksAwakeningRegion("Menu", None, "Menu", self.player, self.multiworld)
|
||||
menu_region = LinksAwakeningRegion("Menu", None, "Menu", self.player, self.multiworld)
|
||||
menu_region.exits = [Entrance(self.player, "Start Game", menu_region)]
|
||||
menu_region.exits[0].connect(start)
|
||||
|
||||
|
||||
self.multiworld.regions.append(menu_region)
|
||||
|
||||
# Place RAFT, other access events
|
||||
@@ -189,14 +190,14 @@ class LinksAwakeningWorld(World):
|
||||
for loc in region.locations:
|
||||
if loc.address is None:
|
||||
loc.place_locked_item(self.create_event(loc.ladxr_item.event))
|
||||
|
||||
|
||||
# Connect Windfish -> Victory
|
||||
windfish = self.multiworld.get_region("Windfish", self.player)
|
||||
l = Location(self.player, "Windfish", parent=windfish)
|
||||
windfish.locations = [l]
|
||||
|
||||
|
||||
l.place_locked_item(self.create_event("An Alarm Clock"))
|
||||
|
||||
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("An Alarm Clock", player=self.player)
|
||||
|
||||
def create_item(self, item_name: str):
|
||||
@@ -206,6 +207,8 @@ class LinksAwakeningWorld(World):
|
||||
return Item(event, ItemClassification.progression, None, self.player)
|
||||
|
||||
def create_items(self) -> None:
|
||||
itempool = []
|
||||
|
||||
exclude = [item.name for item in self.multiworld.precollected_items[self.player]]
|
||||
|
||||
self.prefill_original_dungeon = [ [], [], [], [], [], [], [], [], [] ]
|
||||
@@ -265,9 +268,9 @@ class LinksAwakeningWorld(World):
|
||||
self.prefill_own_dungeons.append(item)
|
||||
self.pre_fill_items.append(item)
|
||||
else:
|
||||
self.multiworld.itempool.append(item)
|
||||
itempool.append(item)
|
||||
else:
|
||||
self.multiworld.itempool.append(item)
|
||||
itempool.append(item)
|
||||
|
||||
self.multi_key = self.generate_multi_key()
|
||||
|
||||
@@ -276,8 +279,8 @@ class LinksAwakeningWorld(World):
|
||||
event_location = Location(self.player, "Can Play Trendy Game", parent=trendy_region)
|
||||
trendy_region.locations.insert(0, event_location)
|
||||
event_location.place_locked_item(self.create_event("Can Play Trendy Game"))
|
||||
|
||||
self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []]
|
||||
|
||||
self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []]
|
||||
for r in self.multiworld.get_regions(self.player):
|
||||
# Set aside dungeon locations
|
||||
if r.dungeon_index:
|
||||
@@ -290,21 +293,52 @@ class LinksAwakeningWorld(World):
|
||||
# Properly fill locations within dungeon
|
||||
location.dungeon = r.dungeon_index
|
||||
|
||||
# For now, special case first item
|
||||
FORCE_START_ITEM = True
|
||||
if FORCE_START_ITEM:
|
||||
self.force_start_item()
|
||||
if self.options.tarins_gift != "any_item":
|
||||
self.force_start_item(itempool)
|
||||
|
||||
def force_start_item(self):
|
||||
|
||||
self.multiworld.itempool += itempool
|
||||
|
||||
def force_start_item(self, itempool):
|
||||
start_loc = self.multiworld.get_location("Tarin's Gift (Mabe Village)", self.player)
|
||||
if not start_loc.item:
|
||||
possible_start_items = [index for index, item in enumerate(self.multiworld.itempool)
|
||||
if item.player == self.player
|
||||
and item.item_data.ladxr_id in start_loc.ladxr_item.OPTIONS and not item.location]
|
||||
if possible_start_items:
|
||||
index = self.random.choice(possible_start_items)
|
||||
start_item = self.multiworld.itempool.pop(index)
|
||||
"""
|
||||
Find an item that forces progression or a bush breaker for the player, depending on settings.
|
||||
"""
|
||||
def is_possible_start_item(item):
|
||||
return item.advancement and item.name not in self.options.non_local_items
|
||||
|
||||
def opens_new_regions(item):
|
||||
collection_state = base_collection_state.copy()
|
||||
collection_state.collect(item)
|
||||
return len(collection_state.reachable_regions[self.player]) > reachable_count
|
||||
|
||||
start_items = [item for item in itempool if is_possible_start_item(item)]
|
||||
self.random.shuffle(start_items)
|
||||
|
||||
if self.options.tarins_gift == "bush_breaker":
|
||||
start_item = next((item for item in start_items if item.name in links_awakening_item_name_groups["Bush Breakers"]), None)
|
||||
|
||||
else: # local_progression
|
||||
entrance_mapping = self.ladxr_logic.world_setup.entrance_mapping
|
||||
# Tail key opens a region but not a location if d1 entrance is not mapped to d1 or d4
|
||||
# exclude it in these cases to avoid fill errors
|
||||
if entrance_mapping['d1'] not in ['d1', 'd4']:
|
||||
start_items = [item for item in start_items if item.name != 'Tail Key']
|
||||
# Exclude shovel unless starting in Mabe Village
|
||||
if entrance_mapping['start_house'] not in ['start_house', 'shop']:
|
||||
start_items = [item for item in start_items if item.name != 'Shovel']
|
||||
base_collection_state = CollectionState(self.multiworld)
|
||||
base_collection_state.update_reachable_regions(self.player)
|
||||
reachable_count = len(base_collection_state.reachable_regions[self.player])
|
||||
start_item = next((item for item in start_items if opens_new_regions(item)), None)
|
||||
|
||||
if start_item:
|
||||
itempool.remove(start_item)
|
||||
start_loc.place_locked_item(start_item)
|
||||
else:
|
||||
logging.getLogger("Link's Awakening Logger").warning(f"No {self.options.tarins_gift.current_option_name} available for Tarin's Gift.")
|
||||
|
||||
|
||||
def get_pre_fill_items(self):
|
||||
return self.pre_fill_items
|
||||
@@ -317,7 +351,7 @@ class LinksAwakeningWorld(World):
|
||||
|
||||
# set containing the list of all possible dungeon locations for the player
|
||||
all_dungeon_locs = set()
|
||||
|
||||
|
||||
# Do dungeon specific things
|
||||
for dungeon_index in range(0, 9):
|
||||
# set up allow-list for dungeon specific items
|
||||
@@ -330,7 +364,7 @@ class LinksAwakeningWorld(World):
|
||||
# ...also set the rules for the dungeon
|
||||
for location in locs:
|
||||
orig_rule = location.item_rule
|
||||
# If an item is about to be placed on a dungeon location, it can go there iff
|
||||
# If an item is about to be placed on a dungeon location, it can go there iff
|
||||
# 1. it fits the general rules for that location (probably 'return True' for most places)
|
||||
# 2. Either
|
||||
# 2a. it's not a restricted dungeon item
|
||||
@@ -382,7 +416,7 @@ class LinksAwakeningWorld(World):
|
||||
|
||||
# Sweep to pick up already placed items that are reachable with everything but the dungeon items.
|
||||
partial_all_state.sweep_for_advancements()
|
||||
|
||||
|
||||
fill_restrictive(self.multiworld, partial_all_state, all_dungeon_locs_to_fill, all_dungeon_items_to_fill, lock=True, single_player_placement=True, allow_partial=False)
|
||||
|
||||
|
||||
@@ -421,7 +455,7 @@ class LinksAwakeningWorld(World):
|
||||
for name in possibles:
|
||||
if name in self.name_cache:
|
||||
return self.name_cache[name]
|
||||
|
||||
|
||||
return "TRADING_ITEM_LETTER"
|
||||
|
||||
@classmethod
|
||||
@@ -436,7 +470,7 @@ class LinksAwakeningWorld(World):
|
||||
for loc in r.locations:
|
||||
if isinstance(loc, LinksAwakeningLocation):
|
||||
assert(loc.item)
|
||||
|
||||
|
||||
# If we're a links awakening item, just use the item
|
||||
if isinstance(loc.item, LinksAwakeningItem):
|
||||
loc.ladxr_item.item = loc.item.item_data.ladxr_id
|
||||
@@ -470,7 +504,7 @@ class LinksAwakeningWorld(World):
|
||||
args = parser.parse_args([rom_name, "-o", out_name, "--dump"])
|
||||
|
||||
rom = generator.generateRom(args, self)
|
||||
|
||||
|
||||
with open(out_path, "wb") as handle:
|
||||
rom.save(handle, name="LADXR")
|
||||
|
||||
@@ -478,7 +512,7 @@ class LinksAwakeningWorld(World):
|
||||
if self.options.ap_title_screen:
|
||||
with tempfile.NamedTemporaryFile(delete=False) as title_patch:
|
||||
title_patch.write(pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4"))
|
||||
|
||||
|
||||
bsdiff4.file_patch_inplace(out_path, title_patch.name)
|
||||
os.unlink(title_patch.name)
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ def generate_random_hints(world: "LandstalkerWorld"):
|
||||
hint_texts = list(set(hint_texts))
|
||||
random.shuffle(hint_texts)
|
||||
|
||||
hint_count = world.options.hint_count.value
|
||||
hint_count = min(world.options.hint_count.value, len(hint_texts))
|
||||
del hint_texts[hint_count:]
|
||||
|
||||
hint_source_names = [source["description"] for source in HINT_SOURCES_JSON if
|
||||
|
||||
@@ -39,7 +39,7 @@ class LandstalkerWorld(World):
|
||||
item_name_to_id = build_item_name_to_id_table()
|
||||
location_name_to_id = build_location_name_to_id_table()
|
||||
|
||||
cached_spheres: List[Set[Location]]
|
||||
cached_spheres: List[Set[Location]] = []
|
||||
|
||||
def __init__(self, multiworld, player):
|
||||
super().__init__(multiworld, player)
|
||||
@@ -48,9 +48,11 @@ class LandstalkerWorld(World):
|
||||
self.dark_region_ids = []
|
||||
self.teleport_tree_pairs = []
|
||||
self.jewel_items = []
|
||||
self.cached_spheres = []
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
if not LandstalkerWorld.cached_spheres:
|
||||
LandstalkerWorld.cached_spheres = list(self.multiworld.get_spheres())
|
||||
|
||||
# Generate hints.
|
||||
self.adjust_shop_prices()
|
||||
hints = Hints.generate_random_hints(self)
|
||||
@@ -232,18 +234,9 @@ class LandstalkerWorld(World):
|
||||
else:
|
||||
return 4
|
||||
|
||||
@classmethod
|
||||
def stage_post_fill(cls, multiworld: MultiWorld):
|
||||
# Cache spheres for hint calculation after fill completes.
|
||||
cached_spheres = list(multiworld.get_spheres())
|
||||
for world in multiworld.get_game_worlds(cls.game):
|
||||
world.cached_spheres = cached_spheres
|
||||
|
||||
@classmethod
|
||||
def stage_modify_multidata(cls, multiworld: MultiWorld, *_):
|
||||
# Clean up all references in cached spheres after generation completes.
|
||||
for world in multiworld.get_game_worlds(cls.game):
|
||||
world.cached_spheres = []
|
||||
LandstalkerWorld.cached_spheres = []
|
||||
|
||||
def adjust_shop_prices(self):
|
||||
# Calculate prices for items in shops once all items have their final position
|
||||
@@ -254,7 +247,7 @@ class LandstalkerWorld(World):
|
||||
|
||||
global_price_factor = self.options.shop_prices_factor / 100.0
|
||||
|
||||
spheres = self.cached_spheres
|
||||
spheres = LandstalkerWorld.cached_spheres
|
||||
sphere_count = len(spheres)
|
||||
for sphere_id, sphere in enumerate(spheres):
|
||||
location: LandstalkerLocation # after conditional, we guarantee it's this kind of location.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
- A compatible emulator to run the game
|
||||
- [RetroArch](https://retroarch.com?page=platforms) with the Genesis Plus GX core
|
||||
- [Bizhawk 2.9.1 (x64)](https://tasvideos.org/BizHawk/ReleaseHistory) with the Genesis Plus GX core
|
||||
- Your legally obtained Landstalker US ROM file (which can be acquired on [Steam](https://store.steampowered.com/app/71118/Landstalker_The_Treasures_of_King_Nole/))
|
||||
- A Landstalker US ROM file dumped from the original cartridge
|
||||
|
||||
## Installation Instructions
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from typing import Any, ClassVar, TextIO
|
||||
|
||||
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, MultiWorld, Tutorial
|
||||
from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, MultiWorld, Tutorial
|
||||
from Options import Accessibility
|
||||
from Utils import output_path
|
||||
from settings import FilePath, Group
|
||||
@@ -17,6 +17,7 @@ from .regions import LEVELS, MEGA_SHARDS, LOCATIONS, REGION_CONNECTIONS
|
||||
from .rules import MessengerHardRules, MessengerOOBRules, MessengerRules
|
||||
from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS, shuffle_shop_prices
|
||||
from .subclasses import MessengerEntrance, MessengerItem, MessengerRegion, MessengerShopLocation
|
||||
from .transitions import shuffle_transitions
|
||||
|
||||
components.append(
|
||||
Component("The Messenger", component_type=Type.CLIENT, func=launch_game, game_name="The Messenger", supports_uri=True)
|
||||
@@ -128,7 +129,7 @@ class MessengerWorld(World):
|
||||
spoiler_portal_mapping: dict[str, str]
|
||||
portal_mapping: list[int]
|
||||
transitions: list[Entrance]
|
||||
reachable_locs: int = 0
|
||||
reachable_locs: bool = False
|
||||
filler: dict[str, int]
|
||||
|
||||
def generate_early(self) -> None:
|
||||
@@ -145,13 +146,13 @@ class MessengerWorld(World):
|
||||
|
||||
self.shop_prices, self.figurine_prices = shuffle_shop_prices(self)
|
||||
|
||||
starting_portals = ["Autumn Hills", "Howling Grotto", "Glacial Peak", "Riviere Turquoise", "Sunken Shrine", "Searing Crags"]
|
||||
starting_portals = ["Autumn Hills", "Howling Grotto", "Glacial Peak", "Riviere Turquoise", "Sunken Shrine",
|
||||
"Searing Crags"]
|
||||
self.starting_portals = [f"{portal} Portal"
|
||||
for portal in starting_portals[:3] +
|
||||
self.random.sample(starting_portals[3:], k=self.options.available_portals - 3)]
|
||||
|
||||
# super complicated method for adding searing crags to starting portals if it wasn't chosen
|
||||
# TODO add a check for transition shuffle when that gets added back in
|
||||
if not self.options.shuffle_portals and "Searing Crags Portal" not in self.starting_portals:
|
||||
self.starting_portals.append("Searing Crags Portal")
|
||||
portals_to_strip = [portal for portal in ["Riviere Turquoise Portal", "Sunken Shrine Portal"]
|
||||
@@ -181,7 +182,7 @@ class MessengerWorld(World):
|
||||
region_name = region.name.removeprefix(f"{region.parent} - ")
|
||||
connection_data = CONNECTIONS[region.parent][region_name]
|
||||
for exit_region in connection_data:
|
||||
region.connect(self.multiworld.get_region(exit_region, self.player))
|
||||
region.connect(self.get_region(exit_region))
|
||||
|
||||
# all regions need to be created before i can do these connections so we create and connect the complex first
|
||||
for region in [level for level in simple_regions if level.name in REGION_CONNECTIONS]:
|
||||
@@ -228,7 +229,7 @@ class MessengerWorld(World):
|
||||
f"({self.options.total_seals}). Adjusting to {total_seals}"
|
||||
)
|
||||
self.total_seals = total_seals
|
||||
self.required_seals = int(self.options.percent_seals_required.value / 100 * self.total_seals)
|
||||
self.required_seals = max(1, int(self.options.percent_seals_required.value / 100 * self.total_seals))
|
||||
|
||||
seals = [self.create_item("Power Seal") for _ in range(self.total_seals)]
|
||||
itempool += seals
|
||||
@@ -256,6 +257,7 @@ class MessengerWorld(World):
|
||||
f" {logic} for {self.multiworld.get_player_name(self.player)}")
|
||||
# MessengerOOBRules(self).set_messenger_rules()
|
||||
|
||||
def connect_entrances(self) -> None:
|
||||
add_closed_portal_reqs(self)
|
||||
# i need portal shuffle to happen after rules exist so i can validate it
|
||||
attempts = 5
|
||||
@@ -271,6 +273,9 @@ class MessengerWorld(World):
|
||||
else:
|
||||
raise RuntimeError("Unable to generate valid portal output.")
|
||||
|
||||
if self.options.shuffle_transitions:
|
||||
shuffle_transitions(self)
|
||||
|
||||
def write_spoiler_header(self, spoiler_handle: TextIO) -> None:
|
||||
if self.options.available_portals < 6:
|
||||
spoiler_handle.write(f"\nStarting Portals:\n\n")
|
||||
@@ -286,9 +291,54 @@ class MessengerWorld(World):
|
||||
key=lambda portal:
|
||||
["Autumn Hills", "Riviere Turquoise",
|
||||
"Howling Grotto", "Sunken Shrine",
|
||||
"Searing Crags", "Glacial Peak"].index(portal[0]))
|
||||
"Searing Crags", "Glacial Peak"].index(portal[0])
|
||||
)
|
||||
for portal, output in portal_info:
|
||||
spoiler.set_entrance(f"{portal} Portal", output, "I can write anything I want here lmao", self.player)
|
||||
spoiler.set_entrance(f"{portal} Portal", output, "", self.player)
|
||||
|
||||
if self.options.shuffle_transitions:
|
||||
for transition in self.transitions:
|
||||
if (transition.randomization_type == EntranceType.TWO_WAY
|
||||
and (transition.connected_region.name, "both", self.player) in spoiler.entrances):
|
||||
continue
|
||||
spoiler.set_entrance(
|
||||
transition.name if "->" not in transition.name else transition.parent_region.name,
|
||||
transition.connected_region.name,
|
||||
"both" if transition.randomization_type == EntranceType.TWO_WAY
|
||||
and self.options.shuffle_transitions == ShuffleTransitions.option_coupled else "",
|
||||
self.player
|
||||
)
|
||||
|
||||
def extend_hint_information(self, hint_data: dict[int, dict[int, str]]) -> None:
|
||||
if not self.options.shuffle_transitions:
|
||||
return
|
||||
|
||||
hint_data.update({self.player: {}})
|
||||
|
||||
all_state = self.multiworld.get_all_state(True)
|
||||
# sometimes some of my regions aren't in path for some reason?
|
||||
all_state.update_reachable_regions(self.player)
|
||||
paths = all_state.path
|
||||
start = self.get_region("Tower HQ")
|
||||
start_connections = [entrance.name for entrance in start.exits if entrance not in {"Home", "Shrink Down"}]
|
||||
transition_names = [transition.name for transition in self.transitions] + start_connections
|
||||
for loc in self.get_locations():
|
||||
if (loc.parent_region.name in {"Tower HQ", "The Shop", "Music Box", "The Craftsman's Corner"}
|
||||
or loc.address is None):
|
||||
continue
|
||||
path_to_loc: list[str] = []
|
||||
name, connection = paths.get(loc.parent_region, (None, None))
|
||||
while connection != ("Menu", None) and name is not None:
|
||||
name, connection = connection
|
||||
if name in transition_names:
|
||||
if name in start_connections:
|
||||
name = f"{name} -> {self.get_entrance(name).connected_region.name}"
|
||||
path_to_loc.append(name)
|
||||
|
||||
text = " => ".join(reversed(path_to_loc))
|
||||
if not text:
|
||||
continue
|
||||
hint_data[self.player][loc.address] = text
|
||||
|
||||
def fill_slot_data(self) -> dict[str, Any]:
|
||||
slot_data = {
|
||||
@@ -308,11 +358,13 @@ class MessengerWorld(World):
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
if not getattr(self, "_filler_items", None):
|
||||
self._filler_items = [name for name in self.random.choices(
|
||||
list(self.filler),
|
||||
weights=list(self.filler.values()),
|
||||
k=20
|
||||
)]
|
||||
self._filler_items = [
|
||||
name for name in self.random.choices(
|
||||
list(self.filler),
|
||||
weights=list(self.filler.values()),
|
||||
k=20
|
||||
)
|
||||
]
|
||||
return self._filler_items.pop(0)
|
||||
|
||||
def create_item(self, name: str) -> MessengerItem:
|
||||
@@ -331,7 +383,7 @@ class MessengerWorld(World):
|
||||
self.total_shards += count
|
||||
return ItemClassification.progression_skip_balancing if count else ItemClassification.filler
|
||||
|
||||
if name == "Windmill Shuriken" and getattr(self, "multiworld", None) is not None:
|
||||
if name == "Windmill Shuriken":
|
||||
return ItemClassification.progression if self.options.logic_level else ItemClassification.filler
|
||||
|
||||
if name == "Power Seal":
|
||||
@@ -344,7 +396,7 @@ class MessengerWorld(World):
|
||||
|
||||
if name in {*USEFUL_ITEMS, *USEFUL_SHOP_ITEMS}:
|
||||
return ItemClassification.useful
|
||||
|
||||
|
||||
if name in TRAPS:
|
||||
return ItemClassification.trap
|
||||
|
||||
@@ -354,7 +406,7 @@ class MessengerWorld(World):
|
||||
def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: set[int]) -> World:
|
||||
group = super().create_group(multiworld, new_player_id, players)
|
||||
assert isinstance(group, MessengerWorld)
|
||||
|
||||
|
||||
group.filler = FILLER.copy()
|
||||
group.options.traps.value = all(multiworld.worlds[player].options.traps for player in players)
|
||||
if group.options.traps:
|
||||
|
||||
@@ -244,14 +244,12 @@ CONNECTIONS: dict[str, dict[str, list[str]]] = {
|
||||
"Bottom Left": [
|
||||
"Howling Grotto - Top",
|
||||
"Quillshroom Marsh - Sand Trap Shop",
|
||||
"Quillshroom Marsh - Bottom Right",
|
||||
],
|
||||
"Top Right": [
|
||||
"Quillshroom Marsh - Queen of Quills Shop",
|
||||
"Searing Crags - Left",
|
||||
],
|
||||
"Bottom Right": [
|
||||
"Quillshroom Marsh - Bottom Left",
|
||||
"Quillshroom Marsh - Sand Trap Shop",
|
||||
"Searing Crags - Bottom",
|
||||
],
|
||||
@@ -639,43 +637,43 @@ CONNECTIONS: dict[str, dict[str, list[str]]] = {
|
||||
}
|
||||
|
||||
RANDOMIZED_CONNECTIONS: dict[str, str] = {
|
||||
"Ninja Village - Right": "Autumn Hills - Left",
|
||||
"Autumn Hills - Left": "Ninja Village - Right",
|
||||
"Autumn Hills - Right": "Forlorn Temple - Left",
|
||||
"Autumn Hills - Bottom": "Catacombs - Bottom Left",
|
||||
"Forlorn Temple - Left": "Autumn Hills - Right",
|
||||
"Forlorn Temple - Right": "Bamboo Creek - Top Left",
|
||||
"Forlorn Temple - Bottom": "Catacombs - Top Left",
|
||||
"Catacombs - Top Left": "Forlorn Temple - Bottom",
|
||||
"Catacombs - Bottom Left": "Autumn Hills - Bottom",
|
||||
"Catacombs - Bottom": "Dark Cave - Right",
|
||||
"Catacombs - Right": "Bamboo Creek - Bottom Left",
|
||||
"Bamboo Creek - Bottom Left": "Catacombs - Right",
|
||||
"Bamboo Creek - Right": "Howling Grotto - Left",
|
||||
"Bamboo Creek - Top Left": "Forlorn Temple - Right",
|
||||
"Howling Grotto - Left": "Bamboo Creek - Right",
|
||||
"Howling Grotto - Top": "Quillshroom Marsh - Bottom Left",
|
||||
"Howling Grotto - Right": "Quillshroom Marsh - Top Left",
|
||||
"Howling Grotto - Bottom": "Sunken Shrine - Left",
|
||||
"Quillshroom Marsh - Top Left": "Howling Grotto - Right",
|
||||
"Quillshroom Marsh - Bottom Left": "Howling Grotto - Top",
|
||||
"Quillshroom Marsh - Top Right": "Searing Crags - Left",
|
||||
"Ninja Village - Right": "Autumn Hills - Left",
|
||||
"Autumn Hills - Left": "Ninja Village - Right",
|
||||
"Autumn Hills - Right": "Forlorn Temple - Left",
|
||||
"Autumn Hills - Bottom": "Catacombs - Bottom Left",
|
||||
"Forlorn Temple - Left": "Autumn Hills - Right",
|
||||
"Forlorn Temple - Right": "Bamboo Creek - Top Left",
|
||||
"Forlorn Temple - Bottom": "Catacombs - Top Left",
|
||||
"Catacombs - Top Left": "Forlorn Temple - Bottom",
|
||||
"Catacombs - Bottom Left": "Autumn Hills - Bottom",
|
||||
"Catacombs - Bottom": "Dark Cave - Right",
|
||||
"Catacombs - Right": "Bamboo Creek - Bottom Left",
|
||||
"Bamboo Creek - Bottom Left": "Catacombs - Right",
|
||||
"Bamboo Creek - Right": "Howling Grotto - Left",
|
||||
"Bamboo Creek - Top Left": "Forlorn Temple - Right",
|
||||
"Howling Grotto - Left": "Bamboo Creek - Right",
|
||||
"Howling Grotto - Top": "Quillshroom Marsh - Bottom Left",
|
||||
"Howling Grotto - Right": "Quillshroom Marsh - Top Left",
|
||||
"Howling Grotto - Bottom": "Sunken Shrine - Left",
|
||||
"Quillshroom Marsh - Top Left": "Howling Grotto - Right",
|
||||
"Quillshroom Marsh - Bottom Left": "Howling Grotto - Top",
|
||||
"Quillshroom Marsh - Top Right": "Searing Crags - Left",
|
||||
"Quillshroom Marsh - Bottom Right": "Searing Crags - Bottom",
|
||||
"Searing Crags - Left": "Quillshroom Marsh - Top Right",
|
||||
"Searing Crags - Top": "Glacial Peak - Bottom",
|
||||
"Searing Crags - Bottom": "Quillshroom Marsh - Bottom Right",
|
||||
"Searing Crags - Right": "Underworld - Left",
|
||||
"Glacial Peak - Bottom": "Searing Crags - Top",
|
||||
"Glacial Peak - Top": "Cloud Ruins - Left",
|
||||
"Glacial Peak - Left": "Elemental Skylands - Air Shmup",
|
||||
"Cloud Ruins - Left": "Glacial Peak - Top",
|
||||
"Elemental Skylands - Right": "Glacial Peak - Left",
|
||||
"Tower HQ": "Tower of Time - Left",
|
||||
"Artificer": "Corrupted Future",
|
||||
"Underworld - Left": "Searing Crags - Right",
|
||||
"Dark Cave - Right": "Catacombs - Bottom",
|
||||
"Dark Cave - Left": "Riviere Turquoise - Right",
|
||||
"Sunken Shrine - Left": "Howling Grotto - Bottom",
|
||||
"Searing Crags - Left": "Quillshroom Marsh - Top Right",
|
||||
"Searing Crags - Top": "Glacial Peak - Bottom",
|
||||
"Searing Crags - Bottom": "Quillshroom Marsh - Bottom Right",
|
||||
"Searing Crags - Right": "Underworld - Left",
|
||||
"Glacial Peak - Bottom": "Searing Crags - Top",
|
||||
"Glacial Peak - Top": "Cloud Ruins - Left",
|
||||
"Glacial Peak - Left": "Elemental Skylands - Air Shmup",
|
||||
"Cloud Ruins - Left": "Glacial Peak - Top",
|
||||
"Elemental Skylands - Right": "Glacial Peak - Left",
|
||||
"Tower HQ": "Tower of Time - Left",
|
||||
"Artificer": "Corrupted Future",
|
||||
"Underworld - Left": "Searing Crags - Right",
|
||||
"Dark Cave - Right": "Catacombs - Bottom",
|
||||
"Dark Cave - Left": "Riviere Turquoise - Right",
|
||||
"Sunken Shrine - Left": "Howling Grotto - Bottom",
|
||||
}
|
||||
|
||||
TRANSITIONS: list[str] = [
|
||||
|
||||
@@ -3,7 +3,8 @@ from dataclasses import dataclass
|
||||
from schema import And, Optional, Or, Schema
|
||||
|
||||
from Options import Choice, DeathLinkMixin, DefaultOnToggle, ItemsAccessibility, OptionDict, PerGameCommonOptions, \
|
||||
PlandoConnections, Range, StartInventoryPool, Toggle, Visibility
|
||||
PlandoConnections, Range, StartInventoryPool, Toggle
|
||||
from . import RANDOMIZED_CONNECTIONS
|
||||
from .portals import CHECKPOINTS, PORTALS, SHOP_POINTS
|
||||
|
||||
|
||||
@@ -30,17 +31,31 @@ class PortalPlando(PlandoConnections):
|
||||
portals = [f"{portal} Portal" for portal in PORTALS]
|
||||
shop_points = [point for points in SHOP_POINTS.values() for point in points]
|
||||
checkpoints = [point for points in CHECKPOINTS.values() for point in points]
|
||||
portal_entrances = PORTALS
|
||||
portal_exits = portals + shop_points + checkpoints
|
||||
entrances = portal_entrances
|
||||
exits = portal_exits
|
||||
|
||||
entrances = frozenset(PORTALS)
|
||||
exits = frozenset(portals + shop_points + checkpoints)
|
||||
|
||||
|
||||
# for back compatibility. To later be replaced with transition plando
|
||||
class HiddenPortalPlando(PortalPlando):
|
||||
visibility = Visibility.none
|
||||
entrances = PortalPlando.entrances
|
||||
exits = PortalPlando.exits
|
||||
class TransitionPlando(PlandoConnections):
|
||||
"""
|
||||
Plando connections to be used with transition shuffle.
|
||||
List of valid connections can be found at https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/messenger/connections.py#L641.
|
||||
Dictionary keys (left) are entrances and values (right) are exits. If transition shuffle is on coupled all plando
|
||||
connections will be coupled. If on decoupled, "entrance" and "exit" will be treated the same, simply making the
|
||||
plando connection one-way from entrance to exit.
|
||||
Example:
|
||||
- entrance: Searing Crags - Top
|
||||
exit: Dark Cave - Right
|
||||
direction: both
|
||||
"""
|
||||
entrances = frozenset(RANDOMIZED_CONNECTIONS.keys())
|
||||
exits = frozenset(RANDOMIZED_CONNECTIONS.values())
|
||||
|
||||
@classmethod
|
||||
def can_connect(cls, entrance: str, exit: str) -> bool:
|
||||
if entrance != "Glacial Peak - Left" and entrance.lower() in cls.exits:
|
||||
return exit.lower() in cls.entrances
|
||||
return exit.lower() not in cls.entrances
|
||||
|
||||
|
||||
class Logic(Choice):
|
||||
@@ -226,7 +241,7 @@ class MessengerOptions(DeathLinkMixin, PerGameCommonOptions):
|
||||
early_meditation: EarlyMed
|
||||
available_portals: AvailablePortals
|
||||
shuffle_portals: ShufflePortals
|
||||
# shuffle_transitions: ShuffleTransitions
|
||||
shuffle_transitions: ShuffleTransitions
|
||||
goal: Goal
|
||||
music_box: MusicBox
|
||||
notes_needed: NotesNeeded
|
||||
@@ -236,4 +251,4 @@ class MessengerOptions(DeathLinkMixin, PerGameCommonOptions):
|
||||
shop_price: ShopPrices
|
||||
shop_price_plan: PlannedShopPrices
|
||||
portal_plando: PortalPlando
|
||||
plando_connections: HiddenPortalPlando
|
||||
plando_connections: TransitionPlando
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from copy import deepcopy
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState, PlandoOptions
|
||||
from BaseClasses import CollectionState
|
||||
from Options import PlandoConnection
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -252,9 +252,7 @@ def shuffle_portals(world: "MessengerWorld") -> None:
|
||||
world.random.shuffle(available_portals)
|
||||
|
||||
plando = world.options.portal_plando.value
|
||||
if not plando:
|
||||
plando = world.options.plando_connections.value
|
||||
if plando and world.multiworld.plando_options & PlandoOptions.connections and not world.plando_portals:
|
||||
if plando and not world.plando_portals:
|
||||
try:
|
||||
handle_planned_portals(plando)
|
||||
# any failure i expect will trigger on available_portals.remove
|
||||
@@ -294,8 +292,8 @@ def disconnect_portals(world: "MessengerWorld") -> None:
|
||||
|
||||
|
||||
def validate_portals(world: "MessengerWorld") -> bool:
|
||||
# if world.options.shuffle_transitions:
|
||||
# return True
|
||||
if world.options.shuffle_transitions:
|
||||
return True
|
||||
new_state = CollectionState(world.multiworld)
|
||||
new_state.update_reachable_regions(world.player)
|
||||
reachable_locs = 0
|
||||
|
||||
@@ -26,7 +26,7 @@ class MessengerRules:
|
||||
maximum_price = (world.multiworld.get_location("The Shop - Demon's Bane", self.player).cost +
|
||||
world.multiworld.get_location("The Shop - Focused Power Sense", self.player).cost)
|
||||
self.maximum_price = min(maximum_price, world.total_shards)
|
||||
self.required_seals = max(1, world.required_seals)
|
||||
self.required_seals = world.required_seals
|
||||
|
||||
# dict of connection names and requirements to traverse the exit
|
||||
self.connection_rules = {
|
||||
@@ -34,7 +34,7 @@ class MessengerRules:
|
||||
"Artificer's Portal":
|
||||
lambda state: state.has_all({"Demon King Crown", "Magic Firefly"}, self.player),
|
||||
"Shrink Down":
|
||||
lambda state: state.has_all(NOTES, self.player) or self.has_enough_seals(state),
|
||||
lambda state: state.has_all(NOTES, self.player),
|
||||
# the shop
|
||||
"Money Sink":
|
||||
lambda state: state.has("Money Wrench", self.player) and self.can_shop(state),
|
||||
@@ -314,6 +314,9 @@ class MessengerRules:
|
||||
self.has_dart,
|
||||
}
|
||||
|
||||
if self.required_seals:
|
||||
self.connection_rules["Shrink Down"] = self.has_enough_seals
|
||||
|
||||
def has_wingsuit(self, state: CollectionState) -> bool:
|
||||
return state.has("Wingsuit", self.player)
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from functools import cached_property
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Region
|
||||
from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, Location, Region
|
||||
from entrance_rando import ERPlacementState
|
||||
from .regions import LOCATIONS, MEGA_SHARDS
|
||||
from .shop import FIGURINES, SHOP_ITEMS
|
||||
|
||||
@@ -12,9 +13,21 @@ if TYPE_CHECKING:
|
||||
class MessengerEntrance(Entrance):
|
||||
world: "MessengerWorld | None" = None
|
||||
|
||||
def can_connect_to(self, other: Entrance, dead_end: bool, state: "ERPlacementState") -> bool:
|
||||
can_connect = super().can_connect_to(other, dead_end, state)
|
||||
world: MessengerWorld = getattr(self, "world", None)
|
||||
if not world or world.reachable_locs or not can_connect:
|
||||
return can_connect
|
||||
empty_state = CollectionState(world.multiworld, True)
|
||||
self.connected_region = other.connected_region
|
||||
empty_state.update_reachable_regions(world.player)
|
||||
world.reachable_locs = any(loc.can_reach(empty_state) and not loc.is_event for loc in world.get_locations())
|
||||
self.connected_region = None
|
||||
return world.reachable_locs and (not state.coupled or self.name != other.name)
|
||||
|
||||
|
||||
class MessengerRegion(Region):
|
||||
parent: str
|
||||
parent: str | None
|
||||
entrance_type = MessengerEntrance
|
||||
|
||||
def __init__(self, name: str, world: "MessengerWorld", parent: str | None = None) -> None:
|
||||
@@ -32,8 +45,9 @@ class MessengerRegion(Region):
|
||||
for shop_loc in SHOP_ITEMS}
|
||||
self.add_locations(shop_locations, MessengerShopLocation)
|
||||
elif name == "The Craftsman's Corner":
|
||||
self.add_locations({figurine: world.location_name_to_id[figurine] for figurine in FIGURINES},
|
||||
MessengerLocation)
|
||||
self.add_locations(
|
||||
{figurine: world.location_name_to_id[figurine] for figurine in FIGURINES},
|
||||
MessengerLocation)
|
||||
elif name == "Tower HQ":
|
||||
locations.append("Money Wrench")
|
||||
|
||||
@@ -57,6 +71,7 @@ class MessengerLocation(Location):
|
||||
|
||||
|
||||
class MessengerShopLocation(MessengerLocation):
|
||||
|
||||
@cached_property
|
||||
def cost(self) -> int:
|
||||
name = self.name.removeprefix("The Shop - ")
|
||||
|
||||
19
worlds/messenger/test/test_entrance_randomization.py
Normal file
19
worlds/messenger/test/test_entrance_randomization.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import unittest
|
||||
|
||||
from . import MessengerTestBase
|
||||
|
||||
|
||||
class StrictEntranceRandoTest(MessengerTestBase):
|
||||
"""Bare-bones world that tests the strictest possible settings to ensure it doesn't crash"""
|
||||
auto_construct = True
|
||||
options = {
|
||||
"limited_movement": 1,
|
||||
"available_portals": 3,
|
||||
"shuffle_portals": 1,
|
||||
"shuffle_transitions": 1,
|
||||
}
|
||||
|
||||
@unittest.skip
|
||||
def test_all_state_can_reach_everything(self) -> None:
|
||||
"""It's not possible to reach everything with these options so skip this test."""
|
||||
pass
|
||||
@@ -1,4 +1,4 @@
|
||||
from BaseClasses import ItemClassification, CollectionState
|
||||
from BaseClasses import CollectionState, ItemClassification
|
||||
from . import MessengerTestBase
|
||||
|
||||
|
||||
@@ -10,8 +10,9 @@ class AllSealsRequired(MessengerTestBase):
|
||||
def test_chest_access(self) -> None:
|
||||
"""Defaults to a total of 45 power seals in the pool and required."""
|
||||
with self.subTest("Access Dependency"):
|
||||
self.assertEqual(len([seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]),
|
||||
self.world.options.total_seals)
|
||||
self.assertEqual(
|
||||
len([seal for seal in self.multiworld.itempool if seal.name == "Power Seal"]),
|
||||
self.world.options.total_seals)
|
||||
locations = ["Rescue Phantom"]
|
||||
items = [["Power Seal"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
@@ -93,3 +94,22 @@ class MaxSealsWithShards(MessengerTestBase):
|
||||
if seal.classification == ItemClassification.progression_skip_balancing]
|
||||
self.assertEqual(len(total_seals), 85)
|
||||
self.assertEqual(len(required_seals), 85)
|
||||
|
||||
|
||||
class NoSealsRequired(MessengerTestBase):
|
||||
options = {
|
||||
"goal": "power_seal_hunt",
|
||||
"total_seals": 1,
|
||||
"percent_seals_required": 10, # percentage
|
||||
}
|
||||
|
||||
def test_seals_amount(self) -> None:
|
||||
"""Should be 1 seal and it should be progression."""
|
||||
self.assertEqual(self.world.options.total_seals, 1)
|
||||
self.assertEqual(self.world.total_seals, 1)
|
||||
self.assertEqual(self.world.required_seals, 1)
|
||||
total_seals = [item for item in self.multiworld.itempool if item.name == "Power Seal"]
|
||||
required_seals = [item for item in self.multiworld.itempool if
|
||||
item.advancement and item.name == "Power Seal"]
|
||||
self.assertEqual(len(total_seals), 1)
|
||||
self.assertEqual(len(required_seals), 1)
|
||||
|
||||
101
worlds/messenger/transitions.py
Normal file
101
worlds/messenger/transitions.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import Region
|
||||
from entrance_rando import EntranceType, randomize_entrances
|
||||
from .connections import RANDOMIZED_CONNECTIONS, TRANSITIONS
|
||||
from .options import ShuffleTransitions, TransitionPlando
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MessengerWorld
|
||||
|
||||
|
||||
def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando) -> None:
|
||||
def remove_dangling_exit(region: Region) -> None:
|
||||
# find the disconnected exit and remove references to it
|
||||
for _exit in region.exits:
|
||||
if not _exit.connected_region:
|
||||
break
|
||||
else:
|
||||
raise ValueError(f"Unable to find randomized transition for {plando_connection}")
|
||||
region.exits.remove(_exit)
|
||||
|
||||
def remove_dangling_entrance(region: Region) -> None:
|
||||
# find the disconnected entrance and remove references to it
|
||||
for _entrance in region.entrances:
|
||||
if not _entrance.parent_region:
|
||||
break
|
||||
else:
|
||||
raise ValueError(f"Invalid target region for {plando_connection}")
|
||||
region.entrances.remove(_entrance)
|
||||
|
||||
for plando_connection in plando_connections:
|
||||
# get the connecting regions
|
||||
# need to handle these special because the names are unique but have the same parent region
|
||||
if plando_connection.entrance in ("Artificer", "Tower HQ"):
|
||||
reg1 = world.get_region("Tower HQ")
|
||||
if plando_connection.entrance == "Artificer":
|
||||
dangling_exit = world.get_entrance("Artificer's Portal")
|
||||
else:
|
||||
dangling_exit = world.get_entrance("Artificer's Challenge")
|
||||
reg1.exits.remove(dangling_exit)
|
||||
else:
|
||||
reg1 = world.get_region(plando_connection.entrance)
|
||||
remove_dangling_exit(reg1)
|
||||
|
||||
reg2 = world.get_region(plando_connection.exit)
|
||||
remove_dangling_entrance(reg2)
|
||||
# connect the regions
|
||||
reg1.connect(reg2)
|
||||
|
||||
# pretend the user set the plando direction as "both" regardless of what they actually put on coupled
|
||||
if ((world.options.shuffle_transitions == ShuffleTransitions.option_coupled
|
||||
or plando_connection.direction == "both")
|
||||
and plando_connection.exit in RANDOMIZED_CONNECTIONS):
|
||||
remove_dangling_exit(reg2)
|
||||
remove_dangling_entrance(reg1)
|
||||
reg2.connect(reg1)
|
||||
|
||||
|
||||
def shuffle_transitions(world: "MessengerWorld") -> None:
|
||||
coupled = world.options.shuffle_transitions == ShuffleTransitions.option_coupled
|
||||
|
||||
def disconnect_entrance() -> None:
|
||||
child_region.entrances.remove(entrance)
|
||||
entrance.connected_region = None
|
||||
|
||||
er_type = EntranceType.ONE_WAY if child == "Glacial Peak - Left" else \
|
||||
EntranceType.TWO_WAY if child in RANDOMIZED_CONNECTIONS else EntranceType.ONE_WAY
|
||||
if er_type == EntranceType.TWO_WAY:
|
||||
mock_entrance = parent_region.create_er_target(entrance.name)
|
||||
else:
|
||||
mock_entrance = child_region.create_er_target(child)
|
||||
|
||||
entrance.randomization_type = er_type
|
||||
mock_entrance.randomization_type = er_type
|
||||
|
||||
for parent, child in RANDOMIZED_CONNECTIONS.items():
|
||||
if child == "Corrupted Future":
|
||||
entrance = world.get_entrance("Artificer's Portal")
|
||||
elif child == "Tower of Time - Left":
|
||||
entrance = world.get_entrance("Artificer's Challenge")
|
||||
else:
|
||||
entrance = world.get_entrance(f"{parent} -> {child}")
|
||||
parent_region = entrance.parent_region
|
||||
child_region = entrance.connected_region
|
||||
entrance.world = world
|
||||
disconnect_entrance()
|
||||
|
||||
plando = world.options.plando_connections
|
||||
if plando:
|
||||
connect_plando(world, plando)
|
||||
|
||||
result = randomize_entrances(world, coupled, {0: [0]})
|
||||
|
||||
world.transitions = sorted(result.placements, key=lambda entrance: TRANSITIONS.index(entrance.parent_region.name))
|
||||
|
||||
for transition in world.transitions:
|
||||
if "->" not in transition.name:
|
||||
continue
|
||||
transition.parent_region.exits.remove(transition)
|
||||
transition.name = f"{transition.parent_region.name} -> {transition.connected_region.name}"
|
||||
transition.parent_region.exits.append(transition)
|
||||
@@ -269,7 +269,7 @@ class MLSSClient(BizHawkClient):
|
||||
self.local_checked_locations = locs_to_send
|
||||
|
||||
if locs_to_send is not None:
|
||||
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": list(locs_to_send)}])
|
||||
await ctx.check_locations(locs_to_send)
|
||||
|
||||
except bizhawk.RequestFailedError:
|
||||
# Exit handler and return to main loop to reconnect.
|
||||
|
||||
@@ -153,7 +153,6 @@ enemies = [
|
||||
0x50458C,
|
||||
0x5045AC,
|
||||
0x50468C,
|
||||
# 0x5046CC, 6 enemy formation
|
||||
0x5046EC,
|
||||
0x50470C
|
||||
]
|
||||
@@ -166,6 +165,7 @@ bosses = [
|
||||
0x50360C,
|
||||
0x5037AC,
|
||||
0x5037CC,
|
||||
0x50396C,
|
||||
0x503A8C,
|
||||
0x503D6C,
|
||||
0x503F0C,
|
||||
|
||||
@@ -160,6 +160,7 @@ itemList: typing.List[ItemData] = [
|
||||
ItemData(77771142, "Game Boy Horror SP", ItemClassification.useful, 0xFE),
|
||||
ItemData(77771143, "Woo Bean", ItemClassification.skip_balancing, 0x1C),
|
||||
ItemData(77771144, "Hee Bean", ItemClassification.skip_balancing, 0x1F),
|
||||
ItemData(77771145, "Beanstar Emblem", ItemClassification.progression, 0x3E),
|
||||
]
|
||||
|
||||
item_frequencies: typing.Dict[str, int] = {
|
||||
@@ -186,5 +187,12 @@ item_frequencies: typing.Dict[str, int] = {
|
||||
"Hammers": 3,
|
||||
}
|
||||
|
||||
mlss_item_name_groups = {
|
||||
"Beanstar Piece": { "Beanstar Piece 1", "Beanstar Piece 2", "Beanstar Piece 3", "Beanstar Piece 4"},
|
||||
"Beanfruit": { "Bean Fruit 1", "Bean Fruit 2", "Bean Fruit 3", "Bean Fruit 4", "Bean Fruit 5", "Bean Fruit 6", "Bean Fruit 7"},
|
||||
"Neon Egg": { "Blue Neon Egg", "Red Neon Egg", "Green Neon Egg", "Yellow Neon Egg", "Purple Neon Egg", "Orange Neon Egg", "Azure Neon Egg"},
|
||||
"Chuckola Fruit": { "Red Chuckola Fruit", "Purple Chuckola Fruit", "White Chuckola Fruit"}
|
||||
}
|
||||
|
||||
item_table: typing.Dict[str, ItemData] = {item.itemName: item for item in itemList}
|
||||
items_by_id: typing.Dict[int, ItemData] = {item.code: item for item in itemList}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user