mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-10 09:33:46 -07:00
Compare commits
29 Commits
NewSoupVi-
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80e89db812 | ||
|
|
8d0f1be791 | ||
|
|
febcd84995 | ||
|
|
d0a555a185 | ||
|
|
da72d40f14 | ||
|
|
f78f906ced | ||
|
|
245a66828d | ||
|
|
798556fd3f | ||
|
|
14609100cf | ||
|
|
fe366e2985 | ||
|
|
819710ff61 | ||
|
|
c0cd03f436 | ||
|
|
e694804509 | ||
|
|
c21c10a602 | ||
|
|
54ab181514 | ||
|
|
88de34dfd7 | ||
|
|
555b7211ef | ||
|
|
7b097c918e | ||
|
|
4eeac945fb | ||
|
|
db577ad899 | ||
|
|
8aed3efe97 | ||
|
|
f7d6f689fb | ||
|
|
910d4b93c9 | ||
|
|
584dd8fe75 | ||
|
|
259d010a86 | ||
|
|
98d808bb87 | ||
|
|
f9108f4331 | ||
|
|
4b6ad12192 | ||
|
|
a08fbc66a8 |
@@ -720,7 +720,7 @@ class CollectionState():
|
||||
if new_region in reachable_regions:
|
||||
blocked_connections.remove(connection)
|
||||
elif connection.can_reach(self):
|
||||
assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region"
|
||||
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
|
||||
reachable_regions.add(new_region)
|
||||
blocked_connections.remove(connection)
|
||||
blocked_connections.update(new_region.exits)
|
||||
@@ -946,7 +946,6 @@ class Entrance:
|
||||
self.player = player
|
||||
|
||||
def can_reach(self, state: CollectionState) -> bool:
|
||||
assert self.parent_region, f"called can_reach on an Entrance \"{self}\" with no parent_region"
|
||||
if self.parent_region.can_reach(state) and self.access_rule(state):
|
||||
if not self.hide_path and not self in state.path:
|
||||
state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
|
||||
@@ -1167,7 +1166,7 @@ class Location:
|
||||
|
||||
def can_reach(self, state: CollectionState) -> bool:
|
||||
# Region.can_reach is just a cache lookup, so placing it first for faster abort on average
|
||||
assert self.parent_region, f"called can_reach on a Location \"{self}\" with no parent_region"
|
||||
assert self.parent_region, "Can't reach location without region"
|
||||
return self.parent_region.can_reach(state) and self.access_rule(state)
|
||||
|
||||
def place_locked_item(self, item: Item):
|
||||
|
||||
@@ -267,7 +267,9 @@ class WargrooveContext(CommonContext):
|
||||
|
||||
def build(self):
|
||||
container = super().build()
|
||||
self.add_client_tab("Wargroove", self.build_tracker())
|
||||
panel = TabbedPanelItem(text="Wargroove")
|
||||
panel.content = self.build_tracker()
|
||||
self.tabs.add_widget(panel)
|
||||
return container
|
||||
|
||||
def build_tracker(self) -> TrackerLayout:
|
||||
|
||||
@@ -99,18 +99,14 @@
|
||||
{% if hint.finding_player == player %}
|
||||
<b>{{ player_names_with_alias[(team, hint.finding_player)] }}</b>
|
||||
{% else %}
|
||||
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.finding_player) }}">
|
||||
{{ player_names_with_alias[(team, hint.finding_player)] }}
|
||||
</a>
|
||||
{{ player_names_with_alias[(team, hint.finding_player)] }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if hint.receiving_player == player %}
|
||||
<b>{{ player_names_with_alias[(team, hint.receiving_player)] }}</b>
|
||||
{% else %}
|
||||
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.receiving_player) }}">
|
||||
{{ player_names_with_alias[(team, hint.receiving_player)] }}
|
||||
</a>
|
||||
{{ player_names_with_alias[(team, hint.receiving_player)] }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}</td>
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
{% extends 'tablepage.html' %}
|
||||
|
||||
{%- macro games(slots) -%}
|
||||
{%- set gameList = [] -%}
|
||||
{%- set maxGamesToShow = 10 -%}
|
||||
|
||||
{%- for slot in (slots|list|sort(attribute="player_id"))[:maxGamesToShow] -%}
|
||||
{% set player = "#" + slot["player_id"]|string + " " + slot["player_name"] + " : " + slot["game"] -%}
|
||||
{% set _ = gameList.append(player) -%}
|
||||
{%- endfor -%}
|
||||
|
||||
{%- if slots|length > maxGamesToShow -%}
|
||||
{% set _ = gameList.append("... and " + (slots|length - maxGamesToShow)|string + " more") -%}
|
||||
{%- endif -%}
|
||||
|
||||
{{ gameList|join('\n') }}
|
||||
{%- endmacro -%}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>User Content</title>
|
||||
@@ -49,12 +33,10 @@
|
||||
<tr>
|
||||
<td><a href="{{ url_for("view_seed", seed=room.seed.id) }}">{{ room.seed.id|suuid }}</a></td>
|
||||
<td><a href="{{ url_for("host_room", room=room.id) }}">{{ room.id|suuid }}</a></td>
|
||||
<td title="{{ games(room.seed.slots) }}">
|
||||
{{ room.seed.slots|length }}
|
||||
</td>
|
||||
<td>{{ room.seed.slots|length }}</td>
|
||||
<td>{{ room.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||
<td>{{ room.last_activity.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||
<td><a href="{{ url_for("disown_room", room=room.id) }}">Delete next maintenance.</a></td>
|
||||
<td><a href="{{ url_for("disown_room", room=room.id) }}">Delete next maintenance.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -78,15 +60,10 @@
|
||||
{% for seed in seeds %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for("view_seed", seed=seed.id) }}">{{ seed.id|suuid }}</a></td>
|
||||
<td title="{{ games(seed.slots) }}">
|
||||
{% if seed.multidata %}
|
||||
{{ seed.slots|length }}
|
||||
{% else %}
|
||||
1
|
||||
{% endif %}
|
||||
<td>{% if seed.multidata %}{{ seed.slots|length }}{% else %}1{% endif %}
|
||||
</td>
|
||||
<td>{{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||
<td><a href="{{ url_for("disown_seed", seed=seed.id) }}">Delete next maintenance.</a></td>
|
||||
<td><a href="{{ url_for("disown_seed", seed=seed.id) }}">Delete next maintenance.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -696,9 +696,92 @@ When importing a file that defines a class that inherits from `worlds.AutoWorld.
|
||||
is automatically extended by the mixin's members. These members should be prefixed with the name of the implementing
|
||||
world since the namespace is shared with all other logic mixins.
|
||||
|
||||
Some uses could be to add additional variables to the state object, or to have a custom state machine that gets modified
|
||||
with the state.
|
||||
Please do this with caution and only when necessary.
|
||||
LogicMixin is handy when your logic is more complex than one-to-one location-item relationships.
|
||||
A game in which "The red key opens the red door" can just express this relationship through a one-line access rule.
|
||||
But now, consider a game with a heavy focus on combat, where the main logical consideration is which enemies you can
|
||||
defeat with your current items.
|
||||
There could be dozens of weapons, armor pieces, or consumables that each improve your ability to defeat
|
||||
specific enemies to varying degrees. It would be useful to be able to keep track of "defeatable enemies" as a state variable,
|
||||
and have this variable be recalculated as necessary based on newly collected/removed items.
|
||||
This is the capability of LogicMixin: Adding custom variables to state that get recalculated as necessary.
|
||||
|
||||
In general, a LogicMixin class should have at least one mutable variable that is tracking some custom state per player,
|
||||
as well as `init_mixin` and `copy_mixin` functions so that this variable gets initialized and copied correctly when
|
||||
`CollectionState()` and `CollectionState.copy()` are called respectively.
|
||||
|
||||
```python
|
||||
from BaseClasses import CollectionState, MultiWorld
|
||||
from worlds.AutoWorld import LogicMixin
|
||||
|
||||
class MyGameState(LogicMixin):
|
||||
mygame_defeatable_enemies: Dict[int, Set[str]] # per player
|
||||
|
||||
def init_mixin(self, multiworld: MultiWorld) -> None:
|
||||
# Initialize per player with the corresponding "nothing" value, such as 0 or an empty set.
|
||||
# You can also use something like Collections.defaultdict
|
||||
self.mygame_defeatable_enemies = {
|
||||
player: set() for player in multiworld.get_game_players("My Game")
|
||||
}
|
||||
|
||||
def copy_mixin(self, new_state: CollectionState) -> CollectionState:
|
||||
# Be careful to make a "deep enough" copy here!
|
||||
new_state.mygame_defeatable_enemies = {
|
||||
player: enemies.copy() for player, enemies in self.mygame_defeatable_enemies.items()
|
||||
}
|
||||
```
|
||||
|
||||
After doing this, you can now access `state.mygame_defeatable_enemies[player]` from your access rules.
|
||||
|
||||
Usually, doing this coincides with an override of `World.collect` and `World.remove`, where the custom state variable
|
||||
gets recalculated when a relevant item is collected or removed.
|
||||
|
||||
```python
|
||||
# __init__.py
|
||||
|
||||
def collect(self, state: CollectionState, item: Item) -> bool:
|
||||
change = super().collect(state, item)
|
||||
if change and item in COMBAT_ITEMS:
|
||||
state.mygame_defeatable_enemies[self.player] |= get_newly_unlocked_enemies(state)
|
||||
return change
|
||||
|
||||
def remove(self, state: CollectionState, item: Item) -> bool:
|
||||
change = super().remove(state, item)
|
||||
if change and item in COMBAT_ITEMS:
|
||||
state.mygame_defeatable_enemies[self.player] -= get_newly_locked_enemies(state)
|
||||
return change
|
||||
```
|
||||
|
||||
Using LogicMixin can greatly slow down your code if you don't use it intelligently. This is because `collect`
|
||||
and `remove` are called very frequently during fill. If your `collect` & `remove` cause a heavy calculation
|
||||
every time, your code might end up being *slower* than just doing calculations in your access rules.
|
||||
|
||||
One way to optimise recalculations is to make use of the fact that `collect` should only unlock things,
|
||||
and `remove` should only lock things.
|
||||
In our example, we have two different functions: `get_newly_unlocked_enemies` and `get_newly_locked_enemies`.
|
||||
`get_newly_unlocked_enemies` should only consider enemies that are *not already in the set*
|
||||
and check whether they were **unlocked**.
|
||||
`get_newly_locked_enemies` should only consider enemies that are *already in the set*
|
||||
and check whether they **became locked**.
|
||||
|
||||
Another impactful way to optimise LogicMixin is to use caching.
|
||||
Your custom state variables don't actually need to be recalculated on every `collect` / `remove`, because there are
|
||||
often multiple calls to `collect` / `remove` between access rule calls. Thus, it would be much more efficient to hold
|
||||
off on recaculating until the an actual access rule call happens.
|
||||
A common way to realize this is to define a `mygame_state_is_stale` variable that is set to True in `collect`, `remove`,
|
||||
and `init_mixin`. The calls to the actual recalculating functions are then moved to the start of the relevant
|
||||
access rules like this:
|
||||
|
||||
```python
|
||||
def can_defeat_enemy(state: CollectionState, player: int, enemy: str) -> bool:
|
||||
if state.mygame_state_is_stale[player]:
|
||||
state.mygame_defeatable_enemies[player] = recalculate_defeatable_enemies(state)
|
||||
state.mygame_state_is_stale[player] = False
|
||||
|
||||
return enemy in state.mygame_defeatable_enemies[player]
|
||||
```
|
||||
|
||||
Only use LogicMixin if necessary. There are often other ways to achieve what it does, like making clever use of
|
||||
`state.prog_items`, using event items, pseudo-regions, etc.
|
||||
|
||||
#### pre_fill
|
||||
|
||||
|
||||
13
kvui.py
13
kvui.py
@@ -536,8 +536,9 @@ class GameManager(App):
|
||||
# show Archipelago tab if other logging is present
|
||||
self.tabs.add_widget(panel)
|
||||
|
||||
hint_panel = self.add_client_tab("Hints", HintLog(self.json_to_kivy_parser))
|
||||
self.log_panels["Hints"] = hint_panel.content
|
||||
hint_panel = TabbedPanelItem(text="Hints")
|
||||
self.log_panels["Hints"] = hint_panel.content = HintLog(self.json_to_kivy_parser)
|
||||
self.tabs.add_widget(hint_panel)
|
||||
|
||||
if len(self.logging_pairs) == 1:
|
||||
self.tabs.default_tab_text = "Archipelago"
|
||||
@@ -571,14 +572,6 @@ class GameManager(App):
|
||||
|
||||
return self.container
|
||||
|
||||
def add_client_tab(self, title: str, content: Widget) -> Widget:
|
||||
"""Adds a new tab to the client window with a given title, and provides a given Widget as its content.
|
||||
Returns the new tab widget, with the provided content being placed on the tab as content."""
|
||||
new_tab = TabbedPanelItem(text=title)
|
||||
new_tab.content = content
|
||||
self.tabs.add_widget(new_tab)
|
||||
return new_tab
|
||||
|
||||
def update_texts(self, dt):
|
||||
if hasattr(self.tabs.content.children[0], "fix_heights"):
|
||||
self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream
|
||||
|
||||
@@ -4,7 +4,7 @@ import websockets
|
||||
import functools
|
||||
from copy import deepcopy
|
||||
from typing import List, Any, Iterable
|
||||
from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem, NetworkPlayer
|
||||
from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem
|
||||
from MultiServer import Endpoint
|
||||
from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser
|
||||
|
||||
@@ -101,35 +101,12 @@ class AHITContext(CommonContext):
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == "Connected":
|
||||
json = args
|
||||
# This data is not needed and causes the game to freeze for long periods of time in large asyncs.
|
||||
if "slot_info" in json.keys():
|
||||
json["slot_info"] = {}
|
||||
if "players" in json.keys():
|
||||
me: NetworkPlayer
|
||||
for n in json["players"]:
|
||||
if n.slot == json["slot"] and n.team == json["team"]:
|
||||
me = n
|
||||
break
|
||||
|
||||
# Only put our player info in there as we actually need it
|
||||
json["players"] = [me]
|
||||
if DEBUG:
|
||||
print(json)
|
||||
self.connected_msg = encode([json])
|
||||
self.connected_msg = encode([args])
|
||||
if self.awaiting_info:
|
||||
self.server_msgs.append(self.room_info)
|
||||
self.update_items()
|
||||
self.awaiting_info = False
|
||||
|
||||
elif cmd == "RoomUpdate":
|
||||
# Same story as above
|
||||
json = args
|
||||
if "players" in json.keys():
|
||||
json["players"] = []
|
||||
|
||||
self.server_msgs.append(encode(json))
|
||||
|
||||
elif cmd == "ReceivedItems":
|
||||
if args["index"] == 0:
|
||||
self.full_inventory.clear()
|
||||
@@ -189,17 +166,6 @@ async def proxy(websocket, path: str = "/", ctx: AHITContext = None):
|
||||
await ctx.disconnect_proxy()
|
||||
break
|
||||
|
||||
if ctx.auth:
|
||||
name = msg.get("name", "")
|
||||
if name != "" and name != ctx.auth:
|
||||
logger.info("Aborting proxy connection: player name mismatch from save file")
|
||||
logger.info(f"Expected: {ctx.auth}, got: {name}")
|
||||
text = encode([{"cmd": "PrintJSON",
|
||||
"data": [{"text": "Connection aborted - player name mismatch"}]}])
|
||||
await ctx.send_msgs_proxy(text)
|
||||
await ctx.disconnect_proxy()
|
||||
break
|
||||
|
||||
if ctx.connected_msg and ctx.is_connected():
|
||||
await ctx.send_msgs_proxy(ctx.connected_msg)
|
||||
ctx.update_items()
|
||||
|
||||
@@ -152,10 +152,10 @@ def create_dw_regions(world: "HatInTimeWorld"):
|
||||
for name in annoying_dws:
|
||||
world.excluded_dws.append(name)
|
||||
|
||||
if not world.options.DWEnableBonus and world.options.DWAutoCompleteBonuses:
|
||||
if not world.options.DWEnableBonus or world.options.DWAutoCompleteBonuses:
|
||||
for name in death_wishes:
|
||||
world.excluded_bonuses.append(name)
|
||||
if world.options.DWExcludeAnnoyingBonuses and not world.options.DWAutoCompleteBonuses:
|
||||
elif world.options.DWExcludeAnnoyingBonuses:
|
||||
for name in annoying_bonuses:
|
||||
world.excluded_bonuses.append(name)
|
||||
|
||||
|
||||
@@ -253,8 +253,7 @@ class HatInTimeWorld(World):
|
||||
else:
|
||||
item_name = loc.item.name
|
||||
|
||||
shop_item_names.setdefault(str(loc.address),
|
||||
f"{item_name} ({self.multiworld.get_player_name(loc.item.player)})")
|
||||
shop_item_names.setdefault(str(loc.address), item_name)
|
||||
|
||||
slot_data["ShopItemNames"] = shop_item_names
|
||||
|
||||
|
||||
@@ -2214,13 +2214,13 @@ location_table: Dict[int, LocationDict] = {
|
||||
'map': 2,
|
||||
'index': 217,
|
||||
'doom_type': 2006,
|
||||
'region': "Perfect Hatred (E4M2) Upper"},
|
||||
'region': "Perfect Hatred (E4M2) Blue"},
|
||||
351367: {'name': 'Perfect Hatred (E4M2) - Exit',
|
||||
'episode': 4,
|
||||
'map': 2,
|
||||
'index': -1,
|
||||
'doom_type': -1,
|
||||
'region': "Perfect Hatred (E4M2) Upper"},
|
||||
'region': "Perfect Hatred (E4M2) Blue"},
|
||||
351368: {'name': 'Sever the Wicked (E4M3) - Invulnerability',
|
||||
'episode': 4,
|
||||
'map': 3,
|
||||
|
||||
@@ -502,12 +502,13 @@ regions:List[RegionDict] = [
|
||||
"episode":4,
|
||||
"connections":[
|
||||
{"target":"Perfect Hatred (E4M2) Blue","pro":False},
|
||||
{"target":"Perfect Hatred (E4M2) Yellow","pro":False},
|
||||
{"target":"Perfect Hatred (E4M2) Upper","pro":True}]},
|
||||
{"target":"Perfect Hatred (E4M2) Yellow","pro":False}]},
|
||||
{"name":"Perfect Hatred (E4M2) Blue",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[{"target":"Perfect Hatred (E4M2) Upper","pro":False}]},
|
||||
"connections":[
|
||||
{"target":"Perfect Hatred (E4M2) Main","pro":False},
|
||||
{"target":"Perfect Hatred (E4M2) Cave","pro":False}]},
|
||||
{"name":"Perfect Hatred (E4M2) Yellow",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
@@ -517,13 +518,7 @@ regions:List[RegionDict] = [
|
||||
{"name":"Perfect Hatred (E4M2) Cave",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[{"target":"Perfect Hatred (E4M2) Main","pro":False}]},
|
||||
{"name":"Perfect Hatred (E4M2) Upper",
|
||||
"connects_to_hub":False,
|
||||
"episode":4,
|
||||
"connections":[
|
||||
{"target":"Perfect Hatred (E4M2) Cave","pro":False},
|
||||
{"target":"Perfect Hatred (E4M2) Main","pro":False}]},
|
||||
"connections":[]},
|
||||
|
||||
# Sever the Wicked (E4M3)
|
||||
{"name":"Sever the Wicked (E4M3) Main",
|
||||
|
||||
@@ -403,8 +403,9 @@ def set_episode4_rules(player, multiworld, pro):
|
||||
state.has("Chaingun", player, 1)) and (state.has("Plasma gun", player, 1) or
|
||||
state.has("BFG9000", player, 1)))
|
||||
set_rule(multiworld.get_entrance("Hell Beneath (E4M1) Main -> Hell Beneath (E4M1) Blue", player), lambda state:
|
||||
(state.has("Hell Beneath (E4M1) - Blue skull key", player, 1)) and (state.has("Shotgun", player, 1) or
|
||||
state.has("Chaingun", player, 1)))
|
||||
state.has("Shotgun", player, 1) or
|
||||
state.has("Chaingun", player, 1) or
|
||||
state.has("Hell Beneath (E4M1) - Blue skull key", player, 1))
|
||||
|
||||
# Perfect Hatred (E4M2)
|
||||
set_rule(multiworld.get_entrance("Hub -> Perfect Hatred (E4M2) Main", player), lambda state:
|
||||
|
||||
@@ -111,10 +111,13 @@ class SC2Manager(GameManager):
|
||||
def build(self):
|
||||
container = super().build()
|
||||
|
||||
panel = self.add_client_tab("Starcraft 2 Launcher", CampaignScroll())
|
||||
panel = TabbedPanelItem(text="Starcraft 2 Launcher")
|
||||
panel.content = CampaignScroll()
|
||||
self.campaign_panel = MultiCampaignLayout()
|
||||
panel.content.add_widget(self.campaign_panel)
|
||||
|
||||
self.tabs.add_widget(panel)
|
||||
|
||||
Clock.schedule_interval(self.build_mission_table, 0.5)
|
||||
|
||||
return container
|
||||
|
||||
@@ -2,7 +2,7 @@ from ..game_content import ContentPack
|
||||
from ...data import villagers_data, fish_data
|
||||
from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource, CompoundSource
|
||||
from ...data.harvest import ForagingSource, SeasonalForagingSource, ArtifactSpotSource
|
||||
from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement
|
||||
from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement, SeasonRequirement
|
||||
from ...data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource
|
||||
from ...strings.book_names import Book
|
||||
from ...strings.crop_names import Fruit
|
||||
@@ -250,7 +250,10 @@ pelican_town = ContentPack(
|
||||
ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
|
||||
Book.the_art_o_crabbing: (
|
||||
Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
|
||||
CustomRuleSource(create_rule=lambda logic: logic.festival.has_squidfest_day_1_iridium_reward()),
|
||||
GenericSource(regions=(Region.beach,),
|
||||
other_requirements=(ToolRequirement(Tool.fishing_rod, ToolMaterial.iridium),
|
||||
SkillRequirement(Skill.fishing, 6),
|
||||
SeasonRequirement(Season.winter))),
|
||||
ShopSource(money_price=20000, shop_region=LogicRegion.bookseller_3),),
|
||||
Book.treasure_appraisal_guide: (
|
||||
Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
|
||||
|
||||
@@ -474,7 +474,7 @@ id,name,classification,groups,mod_name
|
||||
507,Resource Pack: 40 Calico Egg,useful,"FESTIVAL",
|
||||
508,Resource Pack: 35 Calico Egg,useful,"FESTIVAL",
|
||||
509,Resource Pack: 30 Calico Egg,useful,"FESTIVAL",
|
||||
510,Book: The Art O' Crabbing,progression,"FESTIVAL",
|
||||
510,Book: The Art O' Crabbing,useful,"FESTIVAL",
|
||||
511,Mr Qi's Plane Ride,progression,,
|
||||
521,Power: Price Catalogue,useful,"BOOK_POWER",
|
||||
522,Power: Mapping Cave Systems,useful,"BOOK_POWER",
|
||||
|
||||
|
@@ -313,14 +313,14 @@ id,region,name,tags,mod_name
|
||||
611,JotPK World 2,JotPK: Cowboy 2,"ARCADE_MACHINE,JOTPK",
|
||||
612,Junimo Kart 1,Junimo Kart: Crumble Cavern,"ARCADE_MACHINE,JUNIMO_KART",
|
||||
613,Junimo Kart 1,Junimo Kart: Slippery Slopes,"ARCADE_MACHINE,JUNIMO_KART",
|
||||
614,Junimo Kart 4,Junimo Kart: Secret Level,"ARCADE_MACHINE,JUNIMO_KART",
|
||||
614,Junimo Kart 2,Junimo Kart: Secret Level,"ARCADE_MACHINE,JUNIMO_KART",
|
||||
615,Junimo Kart 2,Junimo Kart: The Gem Sea Giant,"ARCADE_MACHINE,JUNIMO_KART",
|
||||
616,Junimo Kart 2,Junimo Kart: Slomp's Stomp,"ARCADE_MACHINE,JUNIMO_KART",
|
||||
617,Junimo Kart 2,Junimo Kart: Ghastly Galleon,"ARCADE_MACHINE,JUNIMO_KART",
|
||||
618,Junimo Kart 3,Junimo Kart: Glowshroom Grotto,"ARCADE_MACHINE,JUNIMO_KART",
|
||||
619,Junimo Kart 3,Junimo Kart: Red Hot Rollercoaster,"ARCADE_MACHINE,JUNIMO_KART",
|
||||
620,JotPK World 3,Journey of the Prairie King Victory,"ARCADE_MACHINE_VICTORY,JOTPK",
|
||||
621,Junimo Kart 4,Junimo Kart: Sunset Speedway (Victory),"ARCADE_MACHINE_VICTORY,JUNIMO_KART",
|
||||
621,Junimo Kart 3,Junimo Kart: Sunset Speedway (Victory),"ARCADE_MACHINE_VICTORY,JUNIMO_KART",
|
||||
701,Secret Woods,Old Master Cannoli,MANDATORY,
|
||||
702,Beach,Beach Bridge Repair,MANDATORY,
|
||||
703,Desert,Galaxy Sword Shrine,MANDATORY,
|
||||
|
||||
|
@@ -1,186 +0,0 @@
|
||||
from typing import Union
|
||||
|
||||
from .action_logic import ActionLogicMixin
|
||||
from .animal_logic import AnimalLogicMixin
|
||||
from .artisan_logic import ArtisanLogicMixin
|
||||
from .base_logic import BaseLogicMixin, BaseLogic
|
||||
from .fishing_logic import FishingLogicMixin
|
||||
from .gift_logic import GiftLogicMixin
|
||||
from .has_logic import HasLogicMixin
|
||||
from .money_logic import MoneyLogicMixin
|
||||
from .monster_logic import MonsterLogicMixin
|
||||
from .museum_logic import MuseumLogicMixin
|
||||
from .received_logic import ReceivedLogicMixin
|
||||
from .region_logic import RegionLogicMixin
|
||||
from .relationship_logic import RelationshipLogicMixin
|
||||
from .skill_logic import SkillLogicMixin
|
||||
from .time_logic import TimeLogicMixin
|
||||
from ..options import FestivalLocations
|
||||
from ..stardew_rule import StardewRule
|
||||
from ..strings.book_names import Book
|
||||
from ..strings.craftable_names import Fishing
|
||||
from ..strings.crop_names import Fruit, Vegetable
|
||||
from ..strings.festival_check_names import FestivalCheck
|
||||
from ..strings.fish_names import Fish
|
||||
from ..strings.forageable_names import Forageable
|
||||
from ..strings.generic_names import Generic
|
||||
from ..strings.machine_names import Machine
|
||||
from ..strings.monster_names import Monster
|
||||
from ..strings.region_names import Region
|
||||
|
||||
|
||||
class FestivalLogicMixin(BaseLogicMixin):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.festival = FestivalLogic(*args, **kwargs)
|
||||
|
||||
|
||||
class FestivalLogic(BaseLogic[Union[HasLogicMixin, ReceivedLogicMixin, FestivalLogicMixin, ArtisanLogicMixin, AnimalLogicMixin, MoneyLogicMixin, TimeLogicMixin,
|
||||
SkillLogicMixin, RegionLogicMixin, ActionLogicMixin, MonsterLogicMixin, RelationshipLogicMixin, FishingLogicMixin, MuseumLogicMixin, GiftLogicMixin]]):
|
||||
|
||||
def initialize_rules(self):
|
||||
self.registry.festival_rules.update({
|
||||
FestivalCheck.egg_hunt: self.logic.festival.can_win_egg_hunt(),
|
||||
FestivalCheck.strawberry_seeds: self.logic.money.can_spend(1000),
|
||||
FestivalCheck.dance: self.logic.relationship.has_hearts_with_any_bachelor(4),
|
||||
FestivalCheck.tub_o_flowers: self.logic.money.can_spend(2000),
|
||||
FestivalCheck.rarecrow_5: self.logic.money.can_spend(2500),
|
||||
FestivalCheck.luau_soup: self.logic.festival.can_succeed_luau_soup(),
|
||||
FestivalCheck.moonlight_jellies: self.logic.true_,
|
||||
FestivalCheck.moonlight_jellies_banner: self.logic.money.can_spend(800),
|
||||
FestivalCheck.starport_decal: self.logic.money.can_spend(1000),
|
||||
FestivalCheck.smashing_stone: self.logic.true_,
|
||||
FestivalCheck.grange_display: self.logic.festival.can_succeed_grange_display(),
|
||||
FestivalCheck.rarecrow_1: self.logic.true_, # only cost star tokens
|
||||
FestivalCheck.fair_stardrop: self.logic.true_, # only cost star tokens
|
||||
FestivalCheck.spirit_eve_maze: self.logic.true_,
|
||||
FestivalCheck.jack_o_lantern: self.logic.money.can_spend(2000),
|
||||
FestivalCheck.rarecrow_2: self.logic.money.can_spend(5000),
|
||||
FestivalCheck.fishing_competition: self.logic.festival.can_win_fishing_competition(),
|
||||
FestivalCheck.rarecrow_4: self.logic.money.can_spend(5000),
|
||||
FestivalCheck.mermaid_pearl: self.logic.has(Forageable.secret_note),
|
||||
FestivalCheck.cone_hat: self.logic.money.can_spend(2500),
|
||||
FestivalCheck.iridium_fireplace: self.logic.money.can_spend(15000),
|
||||
FestivalCheck.rarecrow_7: self.logic.money.can_spend(5000) & self.logic.museum.can_donate_museum_artifacts(20),
|
||||
FestivalCheck.rarecrow_8: self.logic.money.can_spend(5000) & self.logic.museum.can_donate_museum_items(40),
|
||||
FestivalCheck.lupini_red_eagle: self.logic.money.can_spend(1200),
|
||||
FestivalCheck.lupini_portrait_mermaid: self.logic.money.can_spend(1200),
|
||||
FestivalCheck.lupini_solar_kingdom: self.logic.money.can_spend(1200),
|
||||
FestivalCheck.lupini_clouds: self.logic.time.has_year_two & self.logic.money.can_spend(1200),
|
||||
FestivalCheck.lupini_1000_years: self.logic.time.has_year_two & self.logic.money.can_spend(1200),
|
||||
FestivalCheck.lupini_three_trees: self.logic.time.has_year_two & self.logic.money.can_spend(1200),
|
||||
FestivalCheck.lupini_the_serpent: self.logic.time.has_year_three & self.logic.money.can_spend(1200),
|
||||
FestivalCheck.lupini_tropical_fish: self.logic.time.has_year_three & self.logic.money.can_spend(1200),
|
||||
FestivalCheck.lupini_land_of_clay: self.logic.time.has_year_three & self.logic.money.can_spend(1200),
|
||||
FestivalCheck.secret_santa: self.logic.gifts.has_any_universal_love,
|
||||
FestivalCheck.legend_of_the_winter_star: self.logic.true_,
|
||||
FestivalCheck.rarecrow_3: self.logic.true_,
|
||||
FestivalCheck.all_rarecrows: self.logic.region.can_reach(Region.farm) & self.logic.festival.has_all_rarecrows(),
|
||||
FestivalCheck.calico_race: self.logic.true_,
|
||||
FestivalCheck.mummy_mask: self.logic.true_,
|
||||
FestivalCheck.calico_statue: self.logic.true_,
|
||||
FestivalCheck.emily_outfit_service: self.logic.true_,
|
||||
FestivalCheck.earthy_mousse: self.logic.true_,
|
||||
FestivalCheck.sweet_bean_cake: self.logic.true_,
|
||||
FestivalCheck.skull_cave_casserole: self.logic.true_,
|
||||
FestivalCheck.spicy_tacos: self.logic.true_,
|
||||
FestivalCheck.mountain_chili: self.logic.true_,
|
||||
FestivalCheck.crystal_cake: self.logic.true_,
|
||||
FestivalCheck.cave_kebab: self.logic.true_,
|
||||
FestivalCheck.hot_log: self.logic.true_,
|
||||
FestivalCheck.sour_salad: self.logic.true_,
|
||||
FestivalCheck.superfood_cake: self.logic.true_,
|
||||
FestivalCheck.warrior_smoothie: self.logic.true_,
|
||||
FestivalCheck.rumpled_fruit_skin: self.logic.true_,
|
||||
FestivalCheck.calico_pizza: self.logic.true_,
|
||||
FestivalCheck.stuffed_mushrooms: self.logic.true_,
|
||||
FestivalCheck.elf_quesadilla: self.logic.true_,
|
||||
FestivalCheck.nachos_of_the_desert: self.logic.true_,
|
||||
FestivalCheck.cloppino: self.logic.true_,
|
||||
FestivalCheck.rainforest_shrimp: self.logic.true_,
|
||||
FestivalCheck.shrimp_donut: self.logic.true_,
|
||||
FestivalCheck.smell_of_the_sea: self.logic.true_,
|
||||
FestivalCheck.desert_gumbo: self.logic.true_,
|
||||
FestivalCheck.free_cactis: self.logic.true_,
|
||||
FestivalCheck.monster_hunt: self.logic.monster.can_kill(Monster.serpent),
|
||||
FestivalCheck.deep_dive: self.logic.region.can_reach(Region.skull_cavern_50),
|
||||
FestivalCheck.treasure_hunt: self.logic.region.can_reach(Region.skull_cavern_25),
|
||||
FestivalCheck.touch_calico_statue: self.logic.region.can_reach(Region.skull_cavern_25),
|
||||
FestivalCheck.real_calico_egg_hunter: self.logic.region.can_reach(Region.skull_cavern_100),
|
||||
FestivalCheck.willy_challenge: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.scorpion_carp]),
|
||||
FestivalCheck.desert_scholar: self.logic.true_,
|
||||
FestivalCheck.squidfest_day_1_copper: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]),
|
||||
FestivalCheck.squidfest_day_1_iron: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.has(Fishing.bait),
|
||||
FestivalCheck.squidfest_day_1_gold: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.has(Fishing.deluxe_bait),
|
||||
FestivalCheck.squidfest_day_1_iridium: self.logic.festival.can_squidfest_day_1_iridium_reward(),
|
||||
FestivalCheck.squidfest_day_2_copper: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]),
|
||||
FestivalCheck.squidfest_day_2_iron: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.has(Fishing.bait),
|
||||
FestivalCheck.squidfest_day_2_gold: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.has(Fishing.deluxe_bait),
|
||||
FestivalCheck.squidfest_day_2_iridium: self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) &
|
||||
self.logic.fishing.has_specific_bait(self.content.fishes[Fish.squid]),
|
||||
})
|
||||
for i in range(1, 11):
|
||||
check_name = f"{FestivalCheck.trout_derby_reward_pattern}{i}"
|
||||
self.registry.festival_rules[check_name] = self.logic.fishing.can_catch_fish(self.content.fishes[Fish.rainbow_trout])
|
||||
|
||||
def can_squidfest_day_1_iridium_reward(self) -> StardewRule:
|
||||
return self.logic.fishing.can_catch_fish(self.content.fishes[Fish.squid]) & self.logic.fishing.has_specific_bait(self.content.fishes[Fish.squid])
|
||||
|
||||
def has_squidfest_day_1_iridium_reward(self) -> StardewRule:
|
||||
if self.options.festival_locations == FestivalLocations.option_disabled:
|
||||
return self.logic.festival.can_squidfest_day_1_iridium_reward()
|
||||
else:
|
||||
return self.logic.received(f"Book: {Book.the_art_o_crabbing}")
|
||||
|
||||
def can_win_egg_hunt(self) -> StardewRule:
|
||||
return self.logic.true_
|
||||
|
||||
def can_succeed_luau_soup(self) -> StardewRule:
|
||||
if self.options.festival_locations != FestivalLocations.option_hard:
|
||||
return self.logic.true_
|
||||
eligible_fish = (Fish.blobfish, Fish.crimsonfish, Fish.ice_pip, Fish.lava_eel, Fish.legend, Fish.angler, Fish.catfish, Fish.glacierfish,
|
||||
Fish.mutant_carp, Fish.spookfish, Fish.stingray, Fish.sturgeon, Fish.super_cucumber)
|
||||
fish_rule = self.logic.has_any(*(f for f in eligible_fish if f in self.content.fishes)) # To filter stingray
|
||||
eligible_kegables = (Fruit.ancient_fruit, Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.melon,
|
||||
Fruit.orange, Fruit.peach, Fruit.pineapple, Fruit.pomegranate, Fruit.rhubarb, Fruit.starfruit, Fruit.strawberry,
|
||||
Forageable.cactus_fruit, Fruit.cherry, Fruit.cranberries, Fruit.grape, Forageable.spice_berry, Forageable.wild_plum,
|
||||
Vegetable.hops, Vegetable.wheat)
|
||||
keg_rules = [self.logic.artisan.can_keg(kegable) for kegable in eligible_kegables if kegable in self.content.game_items]
|
||||
aged_rule = self.logic.has(Machine.cask) & self.logic.or_(*keg_rules)
|
||||
# There are a few other valid items, but I don't feel like coding them all
|
||||
return fish_rule | aged_rule
|
||||
|
||||
def can_succeed_grange_display(self) -> StardewRule:
|
||||
if self.options.festival_locations != FestivalLocations.option_hard:
|
||||
return self.logic.true_
|
||||
|
||||
animal_rule = self.logic.animal.has_animal(Generic.any)
|
||||
artisan_rule = self.logic.artisan.can_keg(Generic.any) | self.logic.artisan.can_preserves_jar(Generic.any)
|
||||
cooking_rule = self.logic.money.can_spend_at(Region.saloon, 220) # Salads at the bar are good enough
|
||||
fish_rule = self.logic.skill.can_fish(difficulty=50)
|
||||
forage_rule = self.logic.region.can_reach_any((Region.forest, Region.backwoods)) # Hazelnut always available since the grange display is in fall
|
||||
mineral_rule = self.logic.action.can_open_geode(Generic.any) # More than half the minerals are good enough
|
||||
good_fruits = (fruit
|
||||
for fruit in
|
||||
(Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.orange, Fruit.peach, Fruit.pomegranate,
|
||||
Fruit.strawberry, Fruit.melon, Fruit.rhubarb, Fruit.pineapple, Fruit.ancient_fruit, Fruit.starfruit)
|
||||
if fruit in self.content.game_items)
|
||||
fruit_rule = self.logic.has_any(*good_fruits)
|
||||
good_vegetables = (vegeteable
|
||||
for vegeteable in
|
||||
(Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.cauliflower, Forageable.fiddlehead_fern, Vegetable.kale,
|
||||
Vegetable.radish, Vegetable.taro_root, Vegetable.yam, Vegetable.red_cabbage, Vegetable.pumpkin)
|
||||
if vegeteable in self.content.game_items)
|
||||
vegetable_rule = self.logic.has_any(*good_vegetables)
|
||||
|
||||
return animal_rule & artisan_rule & cooking_rule & fish_rule & \
|
||||
forage_rule & fruit_rule & mineral_rule & vegetable_rule
|
||||
|
||||
def can_win_fishing_competition(self) -> StardewRule:
|
||||
return self.logic.skill.can_fish(difficulty=60)
|
||||
|
||||
def has_all_rarecrows(self) -> StardewRule:
|
||||
rules = []
|
||||
for rarecrow_number in range(1, 9):
|
||||
rules.append(self.logic.received(f"Rarecrow #{rarecrow_number}"))
|
||||
return self.logic.and_(*rules)
|
||||
@@ -16,7 +16,6 @@ from .combat_logic import CombatLogicMixin
|
||||
from .cooking_logic import CookingLogicMixin
|
||||
from .crafting_logic import CraftingLogicMixin
|
||||
from .farming_logic import FarmingLogicMixin
|
||||
from .festival_logic import FestivalLogicMixin
|
||||
from .fishing_logic import FishingLogicMixin
|
||||
from .gift_logic import GiftLogicMixin
|
||||
from .grind_logic import GrindLogicMixin
|
||||
@@ -63,6 +62,7 @@ from ..strings.crop_names import Fruit, Vegetable
|
||||
from ..strings.currency_names import Currency
|
||||
from ..strings.decoration_names import Decoration
|
||||
from ..strings.fertilizer_names import Fertilizer, SpeedGro, RetainingSoil
|
||||
from ..strings.festival_check_names import FestivalCheck
|
||||
from ..strings.fish_names import Fish, Trash, WaterItem, WaterChest
|
||||
from ..strings.flower_names import Flower
|
||||
from ..strings.food_names import Meal, Beverage
|
||||
@@ -94,7 +94,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
CombatLogicMixin, MagicLogicMixin, MonsterLogicMixin, ToolLogicMixin, PetLogicMixin, QualityLogicMixin,
|
||||
SkillLogicMixin, FarmingLogicMixin, BundleLogicMixin, FishingLogicMixin, MineLogicMixin, CookingLogicMixin, AbilityLogicMixin,
|
||||
SpecialOrderLogicMixin, QuestLogicMixin, CraftingLogicMixin, ModLogicMixin, HarvestingLogicMixin, SourceLogicMixin,
|
||||
RequirementLogicMixin, BookLogicMixin, GrindLogicMixin, FestivalLogicMixin, WalnutLogicMixin):
|
||||
RequirementLogicMixin, BookLogicMixin, GrindLogicMixin, WalnutLogicMixin):
|
||||
player: int
|
||||
options: StardewValleyOptions
|
||||
content: StardewContent
|
||||
@@ -363,7 +363,89 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
self.quest.initialize_rules()
|
||||
self.quest.update_rules(self.mod.quest.get_modded_quest_rules())
|
||||
|
||||
self.festival.initialize_rules()
|
||||
self.registry.festival_rules.update({
|
||||
FestivalCheck.egg_hunt: self.can_win_egg_hunt(),
|
||||
FestivalCheck.strawberry_seeds: self.money.can_spend(1000),
|
||||
FestivalCheck.dance: self.relationship.has_hearts_with_any_bachelor(4),
|
||||
FestivalCheck.tub_o_flowers: self.money.can_spend(2000),
|
||||
FestivalCheck.rarecrow_5: self.money.can_spend(2500),
|
||||
FestivalCheck.luau_soup: self.can_succeed_luau_soup(),
|
||||
FestivalCheck.moonlight_jellies: True_(),
|
||||
FestivalCheck.moonlight_jellies_banner: self.money.can_spend(800),
|
||||
FestivalCheck.starport_decal: self.money.can_spend(1000),
|
||||
FestivalCheck.smashing_stone: True_(),
|
||||
FestivalCheck.grange_display: self.can_succeed_grange_display(),
|
||||
FestivalCheck.rarecrow_1: True_(), # only cost star tokens
|
||||
FestivalCheck.fair_stardrop: True_(), # only cost star tokens
|
||||
FestivalCheck.spirit_eve_maze: True_(),
|
||||
FestivalCheck.jack_o_lantern: self.money.can_spend(2000),
|
||||
FestivalCheck.rarecrow_2: self.money.can_spend(5000),
|
||||
FestivalCheck.fishing_competition: self.can_win_fishing_competition(),
|
||||
FestivalCheck.rarecrow_4: self.money.can_spend(5000),
|
||||
FestivalCheck.mermaid_pearl: self.has(Forageable.secret_note),
|
||||
FestivalCheck.cone_hat: self.money.can_spend(2500),
|
||||
FestivalCheck.iridium_fireplace: self.money.can_spend(15000),
|
||||
FestivalCheck.rarecrow_7: self.money.can_spend(5000) & self.museum.can_donate_museum_artifacts(20),
|
||||
FestivalCheck.rarecrow_8: self.money.can_spend(5000) & self.museum.can_donate_museum_items(40),
|
||||
FestivalCheck.lupini_red_eagle: self.money.can_spend(1200),
|
||||
FestivalCheck.lupini_portrait_mermaid: self.money.can_spend(1200),
|
||||
FestivalCheck.lupini_solar_kingdom: self.money.can_spend(1200),
|
||||
FestivalCheck.lupini_clouds: self.time.has_year_two & self.money.can_spend(1200),
|
||||
FestivalCheck.lupini_1000_years: self.time.has_year_two & self.money.can_spend(1200),
|
||||
FestivalCheck.lupini_three_trees: self.time.has_year_two & self.money.can_spend(1200),
|
||||
FestivalCheck.lupini_the_serpent: self.time.has_year_three & self.money.can_spend(1200),
|
||||
FestivalCheck.lupini_tropical_fish: self.time.has_year_three & self.money.can_spend(1200),
|
||||
FestivalCheck.lupini_land_of_clay: self.time.has_year_three & self.money.can_spend(1200),
|
||||
FestivalCheck.secret_santa: self.gifts.has_any_universal_love,
|
||||
FestivalCheck.legend_of_the_winter_star: True_(),
|
||||
FestivalCheck.rarecrow_3: True_(),
|
||||
FestivalCheck.all_rarecrows: self.region.can_reach(Region.farm) & self.has_all_rarecrows(),
|
||||
FestivalCheck.calico_race: True_(),
|
||||
FestivalCheck.mummy_mask: True_(),
|
||||
FestivalCheck.calico_statue: True_(),
|
||||
FestivalCheck.emily_outfit_service: True_(),
|
||||
FestivalCheck.earthy_mousse: True_(),
|
||||
FestivalCheck.sweet_bean_cake: True_(),
|
||||
FestivalCheck.skull_cave_casserole: True_(),
|
||||
FestivalCheck.spicy_tacos: True_(),
|
||||
FestivalCheck.mountain_chili: True_(),
|
||||
FestivalCheck.crystal_cake: True_(),
|
||||
FestivalCheck.cave_kebab: True_(),
|
||||
FestivalCheck.hot_log: True_(),
|
||||
FestivalCheck.sour_salad: True_(),
|
||||
FestivalCheck.superfood_cake: True_(),
|
||||
FestivalCheck.warrior_smoothie: True_(),
|
||||
FestivalCheck.rumpled_fruit_skin: True_(),
|
||||
FestivalCheck.calico_pizza: True_(),
|
||||
FestivalCheck.stuffed_mushrooms: True_(),
|
||||
FestivalCheck.elf_quesadilla: True_(),
|
||||
FestivalCheck.nachos_of_the_desert: True_(),
|
||||
FestivalCheck.cloppino: True_(),
|
||||
FestivalCheck.rainforest_shrimp: True_(),
|
||||
FestivalCheck.shrimp_donut: True_(),
|
||||
FestivalCheck.smell_of_the_sea: True_(),
|
||||
FestivalCheck.desert_gumbo: True_(),
|
||||
FestivalCheck.free_cactis: True_(),
|
||||
FestivalCheck.monster_hunt: self.monster.can_kill(Monster.serpent),
|
||||
FestivalCheck.deep_dive: self.region.can_reach(Region.skull_cavern_50),
|
||||
FestivalCheck.treasure_hunt: self.region.can_reach(Region.skull_cavern_25),
|
||||
FestivalCheck.touch_calico_statue: self.region.can_reach(Region.skull_cavern_25),
|
||||
FestivalCheck.real_calico_egg_hunter: self.region.can_reach(Region.skull_cavern_100),
|
||||
FestivalCheck.willy_challenge: self.fishing.can_catch_fish(content.fishes[Fish.scorpion_carp]),
|
||||
FestivalCheck.desert_scholar: True_(),
|
||||
FestivalCheck.squidfest_day_1_copper: self.fishing.can_catch_fish(content.fishes[Fish.squid]),
|
||||
FestivalCheck.squidfest_day_1_iron: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.bait),
|
||||
FestivalCheck.squidfest_day_1_gold: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.deluxe_bait),
|
||||
FestivalCheck.squidfest_day_1_iridium: self.fishing.can_catch_fish(content.fishes[Fish.squid]) &
|
||||
self.fishing.has_specific_bait(content.fishes[Fish.squid]),
|
||||
FestivalCheck.squidfest_day_2_copper: self.fishing.can_catch_fish(content.fishes[Fish.squid]),
|
||||
FestivalCheck.squidfest_day_2_iron: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.bait),
|
||||
FestivalCheck.squidfest_day_2_gold: self.fishing.can_catch_fish(content.fishes[Fish.squid]) & self.has(Fishing.deluxe_bait),
|
||||
FestivalCheck.squidfest_day_2_iridium: self.fishing.can_catch_fish(content.fishes[Fish.squid]) &
|
||||
self.fishing.has_specific_bait(content.fishes[Fish.squid]),
|
||||
})
|
||||
for i in range(1, 11):
|
||||
self.registry.festival_rules[f"{FestivalCheck.trout_derby_reward_pattern}{i}"] = self.fishing.can_catch_fish(content.fishes[Fish.rainbow_trout])
|
||||
|
||||
self.special_order.initialize_rules()
|
||||
self.special_order.update_rules(self.mod.special_order.get_modded_special_orders_rules())
|
||||
@@ -404,6 +486,53 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
]
|
||||
return self.count(12, *rules_worth_a_point)
|
||||
|
||||
def can_win_egg_hunt(self) -> StardewRule:
|
||||
return True_()
|
||||
|
||||
def can_succeed_luau_soup(self) -> StardewRule:
|
||||
if self.options.festival_locations != FestivalLocations.option_hard:
|
||||
return True_()
|
||||
eligible_fish = (Fish.blobfish, Fish.crimsonfish, Fish.ice_pip, Fish.lava_eel, Fish.legend, Fish.angler, Fish.catfish, Fish.glacierfish,
|
||||
Fish.mutant_carp, Fish.spookfish, Fish.stingray, Fish.sturgeon, Fish.super_cucumber)
|
||||
fish_rule = self.has_any(*(f for f in eligible_fish if f in self.content.fishes)) # To filter stingray
|
||||
eligible_kegables = (Fruit.ancient_fruit, Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.melon,
|
||||
Fruit.orange, Fruit.peach, Fruit.pineapple, Fruit.pomegranate, Fruit.rhubarb, Fruit.starfruit, Fruit.strawberry,
|
||||
Forageable.cactus_fruit, Fruit.cherry, Fruit.cranberries, Fruit.grape, Forageable.spice_berry, Forageable.wild_plum,
|
||||
Vegetable.hops, Vegetable.wheat)
|
||||
keg_rules = [self.artisan.can_keg(kegable) for kegable in eligible_kegables if kegable in self.content.game_items]
|
||||
aged_rule = self.has(Machine.cask) & self.logic.or_(*keg_rules)
|
||||
# There are a few other valid items, but I don't feel like coding them all
|
||||
return fish_rule | aged_rule
|
||||
|
||||
def can_succeed_grange_display(self) -> StardewRule:
|
||||
if self.options.festival_locations != FestivalLocations.option_hard:
|
||||
return True_()
|
||||
|
||||
animal_rule = self.animal.has_animal(Generic.any)
|
||||
artisan_rule = self.artisan.can_keg(Generic.any) | self.artisan.can_preserves_jar(Generic.any)
|
||||
cooking_rule = self.money.can_spend_at(Region.saloon, 220) # Salads at the bar are good enough
|
||||
fish_rule = self.skill.can_fish(difficulty=50)
|
||||
forage_rule = self.region.can_reach_any((Region.forest, Region.backwoods)) # Hazelnut always available since the grange display is in fall
|
||||
mineral_rule = self.action.can_open_geode(Generic.any) # More than half the minerals are good enough
|
||||
good_fruits = (fruit
|
||||
for fruit in
|
||||
(Fruit.apple, Fruit.banana, Forageable.coconut, Forageable.crystal_fruit, Fruit.mango, Fruit.orange, Fruit.peach, Fruit.pomegranate,
|
||||
Fruit.strawberry, Fruit.melon, Fruit.rhubarb, Fruit.pineapple, Fruit.ancient_fruit, Fruit.starfruit)
|
||||
if fruit in self.content.game_items)
|
||||
fruit_rule = self.has_any(*good_fruits)
|
||||
good_vegetables = (vegeteable
|
||||
for vegeteable in
|
||||
(Vegetable.amaranth, Vegetable.artichoke, Vegetable.beet, Vegetable.cauliflower, Forageable.fiddlehead_fern, Vegetable.kale,
|
||||
Vegetable.radish, Vegetable.taro_root, Vegetable.yam, Vegetable.red_cabbage, Vegetable.pumpkin)
|
||||
if vegeteable in self.content.game_items)
|
||||
vegetable_rule = self.has_any(*good_vegetables)
|
||||
|
||||
return animal_rule & artisan_rule & cooking_rule & fish_rule & \
|
||||
forage_rule & fruit_rule & mineral_rule & vegetable_rule
|
||||
|
||||
def can_win_fishing_competition(self) -> StardewRule:
|
||||
return self.skill.can_fish(difficulty=60)
|
||||
|
||||
def has_island_trader(self) -> StardewRule:
|
||||
if self.options.exclude_ginger_island == ExcludeGingerIsland.option_true:
|
||||
return False_()
|
||||
@@ -442,6 +571,12 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, Travelin
|
||||
|
||||
return self.received("Stardrop", number_of_stardrops_to_receive) & self.logic.and_(*other_rules)
|
||||
|
||||
def has_all_rarecrows(self) -> StardewRule:
|
||||
rules = []
|
||||
for rarecrow_number in range(1, 9):
|
||||
rules.append(self.received(f"Rarecrow #{rarecrow_number}"))
|
||||
return self.logic.and_(*rules)
|
||||
|
||||
def has_abandoned_jojamart(self) -> StardewRule:
|
||||
return self.received(CommunityUpgrade.movie_theater, 1)
|
||||
|
||||
|
||||
@@ -87,8 +87,7 @@ vanilla_regions = [
|
||||
RegionData(Region.jotpk_world_3),
|
||||
RegionData(Region.junimo_kart_1, [Entrance.reach_junimo_kart_2]),
|
||||
RegionData(Region.junimo_kart_2, [Entrance.reach_junimo_kart_3]),
|
||||
RegionData(Region.junimo_kart_3, [Entrance.reach_junimo_kart_4]),
|
||||
RegionData(Region.junimo_kart_4),
|
||||
RegionData(Region.junimo_kart_3),
|
||||
RegionData(Region.alex_house),
|
||||
RegionData(Region.trailer),
|
||||
RegionData(Region.mayor_house),
|
||||
@@ -331,7 +330,6 @@ vanilla_connections = [
|
||||
ConnectionData(Entrance.play_junimo_kart, Region.junimo_kart_1),
|
||||
ConnectionData(Entrance.reach_junimo_kart_2, Region.junimo_kart_2),
|
||||
ConnectionData(Entrance.reach_junimo_kart_3, Region.junimo_kart_3),
|
||||
ConnectionData(Entrance.reach_junimo_kart_4, Region.junimo_kart_4),
|
||||
ConnectionData(Entrance.town_to_sam_house, Region.sam_house,
|
||||
flag=RandomizationFlag.PELICAN_TOWN | RandomizationFlag.LEAD_TO_OPEN_AREA),
|
||||
ConnectionData(Entrance.town_to_haley_house, Region.haley_house,
|
||||
|
||||
@@ -891,7 +891,7 @@ def set_arcade_machine_rules(logic: StardewLogic, multiworld: MultiWorld, player
|
||||
logic.has("Junimo Kart Medium Buff"))
|
||||
MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_3, player),
|
||||
logic.has("Junimo Kart Big Buff"))
|
||||
MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_4, player),
|
||||
MultiWorldRules.add_rule(multiworld.get_location("Junimo Kart: Sunset Speedway (Victory)", player),
|
||||
logic.has("Junimo Kart Max Buff"))
|
||||
MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_journey_of_the_prairie_king, player),
|
||||
logic.has("JotPK Small Buff"))
|
||||
|
||||
@@ -94,7 +94,6 @@ class Entrance:
|
||||
play_junimo_kart = "Play Junimo Kart"
|
||||
reach_junimo_kart_2 = "Reach Junimo Kart 2"
|
||||
reach_junimo_kart_3 = "Reach Junimo Kart 3"
|
||||
reach_junimo_kart_4 = "Reach Junimo Kart 4"
|
||||
enter_locker_room = "Bathhouse Entrance to Locker Room"
|
||||
enter_public_bath = "Locker Room to Public Bath"
|
||||
enter_witch_swamp = "Witch Warp Cave to Witch's Swamp"
|
||||
|
||||
@@ -114,7 +114,6 @@ class Region:
|
||||
junimo_kart_1 = "Junimo Kart 1"
|
||||
junimo_kart_2 = "Junimo Kart 2"
|
||||
junimo_kart_3 = "Junimo Kart 3"
|
||||
junimo_kart_4 = "Junimo Kart 4"
|
||||
mines_floor_5 = "The Mines - Floor 5"
|
||||
mines_floor_10 = "The Mines - Floor 10"
|
||||
mines_floor_15 = "The Mines - Floor 15"
|
||||
|
||||
@@ -762,10 +762,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
regions["Beneath the Vault Ladder Exit"].connect(
|
||||
connecting_region=regions["Beneath the Vault Main"],
|
||||
rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world)
|
||||
and has_lantern(state, world)
|
||||
# there's some boxes in the way
|
||||
and (has_stick(state, player) or state.has_any((gun, grapple, fire_wand, laurels), player)))
|
||||
# on the reverse trip, you can lure an enemy over to break the boxes if needed
|
||||
and has_lantern(state, world))
|
||||
regions["Beneath the Vault Main"].connect(
|
||||
connecting_region=regions["Beneath the Vault Ladder Exit"],
|
||||
rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world))
|
||||
|
||||
@@ -114,9 +114,7 @@ def set_region_rules(world: "TunicWorld") -> None:
|
||||
or can_ladder_storage(state, world)
|
||||
# using laurels or ls to get in is covered by the -> Eastern Vault Fortress rules
|
||||
world.get_entrance("Overworld -> Beneath the Vault").access_rule = \
|
||||
lambda state: (has_lantern(state, world) and has_ability(prayer, state, world)
|
||||
# there's some boxes in the way
|
||||
and (has_stick(state, player) or state.has_any((gun, grapple, fire_wand), player)))
|
||||
lambda state: has_lantern(state, world) and has_ability(prayer, state, world)
|
||||
world.get_entrance("Ruined Atoll -> Library").access_rule = \
|
||||
lambda state: state.has_any({grapple, laurels}, player) and has_ability(prayer, state, world)
|
||||
world.get_entrance("Overworld -> Quarry").access_rule = \
|
||||
|
||||
Reference in New Issue
Block a user