Compare commits

...

19 Commits

Author SHA1 Message Date
NewSoupVi
ad8223998e Update components.py 2025-12-18 22:20:21 +01:00
NewSoupVi
b6e2c8129f Update components.py 2025-12-18 22:19:35 +01:00
NewSoupVi
fd4e47efab APQuest: Explain game_name and supports_uri more in components.py
Hopefully this can lead to more games implementing support for the "click on slot name -> everything launches automatically" functionality.
2025-12-18 22:05:55 +01:00
Alchav
b42fb77451 Factorio: Craftsanity (#5529) 2025-12-18 07:52:15 +01:00
Ziktofel
5a8e166289 SC2: New maintainership (#5752)
I (Ziktofel) stepped down but will remain as a mentor
2025-12-18 00:06:49 +01:00
Rosalie
5fa719143c TLOZ: Add manifest file (#5755)
* Added manifest file.

* Update archipelago.json

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2025-12-18 00:06:06 +01:00
Duck
a906f139c3 APQuest: Fix ValueError on typing numbers/backspace #5757 2025-12-18 00:02:11 +01:00
Katelyn Gigante
56363ea7e7 OptionsCreator: Respect World.hidden flag (#5754) 2025-12-17 20:09:35 +01:00
Fabian Dill
01e1e1fe11 WebHost: increase form upload limit (#5756) 2025-12-17 19:12:10 +01:00
Fabian Dill
4477dc7a66 Core: Bump version from 0.6.5 to 0.6.6 (#5753) 2025-12-17 03:33:29 +01:00
Silvris
45994e344e Tests: test that every option in a preset is visible in either simple or complex UI (#5750) 2025-12-16 19:27:02 +01:00
Silvris
51d5e1afae Launcher: fix shortcuts on the AppImage (#5726)
* fix appimage executable reference

* adjust working dir

* use argv0 instead of appimage directly

* set noexe on frozen
2025-12-15 03:30:07 +01:00
Ziktofel
577b958c4d SC2: Fix Kerrigan logic for active spells (#5746) 2025-12-15 00:56:54 +01:00
Benny D
ce38d8ced6 Docs: Add 'silasary' to Mac tutorial contributors (#5745) 2025-12-14 17:01:32 +01:00
BeeFox-sys
d65fcf286d Launcher: Add workaround for kivy bug for linux touchpad devices (#5737)
* add code to fix touchpad on linux, courtesy of Snu of the kivy community

* Launcher: Update workaround to follow styleguide
2025-12-12 02:44:22 +01:00
Phaneros
5a6a0b37d6 sc2: Fixing typos in item descriptions (#5739) 2025-12-11 22:43:06 +01:00
Fabian Dill
4a0a65d604 WebHost: add played game to static tracker (#5731) 2025-12-09 00:45:02 +01:00
Emily
d25abfc305 Docs: update apsudoku docs / add links to web build (#5720) 2025-12-05 01:09:56 +01:00
Duck
0905e3ce32 WebHost/Game Guides: Change links to stay on current instance (#5699)
* Remove absolute links to archipelago.gg

* Fix other link issues
2025-12-02 00:40:05 +01:00
35 changed files with 304 additions and 88 deletions

View File

@@ -218,12 +218,17 @@ def launch(exe, in_terminal=False):
def create_shortcut(button: Any, component: Component) -> None: def create_shortcut(button: Any, component: Component) -> None:
from pyshortcuts import make_shortcut from pyshortcuts import make_shortcut
script = sys.argv[0] env = os.environ
wkdir = Utils.local_path() if "APPIMAGE" in env:
script = env["ARGV0"]
wkdir = None # defaults to ~ on Linux
else:
script = sys.argv[0]
wkdir = Utils.local_path()
script = f"{script} \"{component.display_name}\"" script = f"{script} \"{component.display_name}\""
make_shortcut(script, name=f"Archipelago {component.display_name}", icon=local_path("data", "icon.ico"), make_shortcut(script, name=f"Archipelago {component.display_name}", icon=local_path("data", "icon.ico"),
startmenu=False, terminal=False, working_dir=wkdir) startmenu=False, terminal=False, working_dir=wkdir, noexe=Utils.is_frozen())
button.menu.dismiss() button.menu.dismiss()

View File

@@ -632,7 +632,7 @@ class OptionsCreator(ThemedApp):
self.create_options_panel(world_btn) self.create_options_panel(world_btn)
for world, cls in sorted(AutoWorldRegister.world_types.items(), key=lambda x: x[0]): for world, cls in sorted(AutoWorldRegister.world_types.items(), key=lambda x: x[0]):
if world == "Archipelago": if cls.hidden:
continue continue
world_text = MDButtonText(text=world, size_hint_y=None, width=dp(150), world_text = MDButtonText(text=world, size_hint_y=None, width=dp(150),
pos_hint={"x": 0.03, "center_y": 0.5}) pos_hint={"x": 0.03, "center_y": 0.5})

View File

@@ -48,7 +48,7 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self) return ".".join(str(item) for item in self)
__version__ = "0.6.5" __version__ = "0.6.6"
version_tuple = tuplize_version(__version__) version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux") is_linux = sys.platform.startswith("linux")

View File

@@ -23,6 +23,17 @@ app.jinja_env.filters['any'] = any
app.jinja_env.filters['all'] = all app.jinja_env.filters['all'] = all
app.jinja_env.filters['get_file_safe_name'] = get_file_safe_name app.jinja_env.filters['get_file_safe_name'] = get_file_safe_name
# overwrites of flask default config
app.config["DEBUG"] = False
app.config["PORT"] = 80
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
app.config["MAX_CONTENT_LENGTH"] = 64 * 1024 * 1024 # 64 megabyte limit
# if you want to deploy, make sure you have a non-guessable secret key
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
app.config["SESSION_PERMANENT"] = True
app.config["MAX_FORM_MEMORY_SIZE"] = 2 * 1024 * 1024 # 2 MB, needed for large option pages such as SC2
# custom config
app.config["SELFHOST"] = True # application process is in charge of running the websites app.config["SELFHOST"] = True # application process is in charge of running the websites
app.config["GENERATORS"] = 8 # maximum concurrent world gens app.config["GENERATORS"] = 8 # maximum concurrent world gens
app.config["HOSTERS"] = 8 # maximum concurrent room hosters app.config["HOSTERS"] = 8 # maximum concurrent room hosters
@@ -30,19 +41,12 @@ app.config["SELFLAUNCH"] = True # application process is in charge of launching
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations. app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
app.config["DEBUG"] = False
app.config["PORT"] = 80
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 64 * 1024 * 1024 # 64 megabyte limit
# if you want to deploy, make sure you have a non-guessable secret key
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
# at what amount of worlds should scheduling be used, instead of rolling in the web-thread # at what amount of worlds should scheduling be used, instead of rolling in the web-thread
app.config["JOB_THRESHOLD"] = 1 app.config["JOB_THRESHOLD"] = 1
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable. # after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
app.config["JOB_TIME"] = 600 app.config["JOB_TIME"] = 600
# memory limit for generator processes in bytes # memory limit for generator processes in bytes
app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296 app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296
app.config['SESSION_PERMANENT'] = True
# waitress uses one thread for I/O, these are for processing of views that then get sent # waitress uses one thread for I/O, these are for processing of views that then get sent
# archipelago.gg uses gunicorn + nginx; ignoring this option # archipelago.gg uses gunicorn + nginx; ignoring this option

View File

@@ -58,6 +58,12 @@ class PlayerLocationsTotal(TypedDict):
total_locations: int total_locations: int
class PlayerGame(TypedDict):
team: int
player: int
game: str
@api_endpoints.route("/tracker/<suuid:tracker>") @api_endpoints.route("/tracker/<suuid:tracker>")
@cache.memoize(timeout=60) @cache.memoize(timeout=60)
def tracker_data(tracker: UUID) -> dict[str, Any]: def tracker_data(tracker: UUID) -> dict[str, Any]:
@@ -80,7 +86,8 @@ def tracker_data(tracker: UUID) -> dict[str, Any]:
"""Slot aliases of all players.""" """Slot aliases of all players."""
for team, players in all_players.items(): for team, players in all_players.items():
for player in players: for player in players:
player_aliases.append({"team": team, "player": player, "alias": tracker_data.get_player_alias(team, player)}) player_aliases.append(
{"team": team, "player": player, "alias": tracker_data.get_player_alias(team, player)})
player_items_received: list[PlayerItemsReceived] = [] player_items_received: list[PlayerItemsReceived] = []
"""Items received by each player.""" """Items received by each player."""
@@ -94,7 +101,8 @@ def tracker_data(tracker: UUID) -> dict[str, Any]:
for team, players in all_players.items(): for team, players in all_players.items():
for player in players: for player in players:
player_checks_done.append( player_checks_done.append(
{"team": team, "player": player, "locations": sorted(tracker_data.get_player_checked_locations(team, player))}) {"team": team, "player": player,
"locations": sorted(tracker_data.get_player_checked_locations(team, player))})
total_checks_done: list[TeamTotalChecks] = [ total_checks_done: list[TeamTotalChecks] = [
{"team": team, "checks_done": checks_done} {"team": team, "checks_done": checks_done}
@@ -144,7 +152,8 @@ def tracker_data(tracker: UUID) -> dict[str, Any]:
"""The current client status for each player.""" """The current client status for each player."""
for team, players in all_players.items(): for team, players in all_players.items():
for player in players: for player in players:
player_status.append({"team": team, "player": player, "status": tracker_data.get_player_client_status(team, player)}) player_status.append(
{"team": team, "player": player, "status": tracker_data.get_player_client_status(team, player)})
return { return {
"aliases": player_aliases, "aliases": player_aliases,
@@ -207,12 +216,20 @@ def static_tracker_data(tracker: UUID) -> dict[str, Any]:
player_locations_total.append( player_locations_total.append(
{"team": team, "player": player, "total_locations": len(tracker_data.get_player_locations(player))}) {"team": team, "player": player, "total_locations": len(tracker_data.get_player_locations(player))})
player_game: list[PlayerGame] = []
"""The played game per player slot."""
for team, players in all_players.items():
for player in players:
player_game.append({"team": team, "player": player, "game": tracker_data.get_player_game(player)})
return { return {
"groups": groups, "groups": groups,
"datapackage": tracker_data._multidata["datapackage"], "datapackage": tracker_data._multidata["datapackage"],
"player_locations_total": player_locations_total, "player_locations_total": player_locations_total,
"player_game": player_game,
} }
# It should be exceedingly rare that slot data is needed, so it's separated out. # It should be exceedingly rare that slot data is needed, so it's separated out.
@api_endpoints.route("/slot_data_tracker/<suuid:tracker>") @api_endpoints.route("/slot_data_tracker/<suuid:tracker>")
@cache.memoize(timeout=300) @cache.memoize(timeout=300)

View File

@@ -23,7 +23,7 @@ players to rely upon each other to complete their game.
While a multiworld game traditionally requires all players to be playing the same game, a multi-game multiworld allows While a multiworld game traditionally requires all players to be playing the same game, a multi-game multiworld allows
players to randomize any of the supported games, and send items between them. This allows players of different players to randomize any of the supported games, and send items between them. This allows players of different
games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworlds. games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworlds.
Here is a list of our [Supported Games](https://archipelago.gg/games). Here is a list of our [Supported Games](/games).
## Can I generate a single-player game with Archipelago? ## Can I generate a single-player game with Archipelago?
@@ -33,7 +33,7 @@ play, open the Settings Page, pick your settings, and click Generate Game.
## How do I get started? ## How do I get started?
We have a [Getting Started](https://archipelago.gg/tutorial/Archipelago/setup/en) guide that will help you get the We have a [Getting Started](/tutorial/Archipelago/setup/en) guide that will help you get the
software set up. You can use that guide to learn how to generate multiworlds. There are also basic instructions for software set up. You can use that guide to learn how to generate multiworlds. There are also basic instructions for
including multiple games, and hosting multiworlds on the website for ease and convenience. including multiple games, and hosting multiworlds on the website for ease and convenience.
@@ -57,7 +57,7 @@ their multiworld.
If a player must leave early, they can use Archipelago's release system. When a player releases their game, all items If a player must leave early, they can use Archipelago's release system. When a player releases their game, all items
in that game belonging to other players are sent out automatically. This allows other players to continue to play in that game belonging to other players are sent out automatically. This allows other players to continue to play
uninterrupted. Here is a list of all of our [Server Commands](https://archipelago.gg/tutorial/Archipelago/commands/en). uninterrupted. Here is a list of all of our [Server Commands](/tutorial/Archipelago/commands/en).
## What happens if an item is placed somewhere it is impossible to get? ## What happens if an item is placed somewhere it is impossible to get?

View File

@@ -177,7 +177,8 @@
/worlds/sa2b/ @PoryGone @RaspberrySpace /worlds/sa2b/ @PoryGone @RaspberrySpace
# Starcraft 2 # Starcraft 2
/worlds/sc2/ @Ziktofel # Note: @Ziktofel acts as a mentor
/worlds/sc2/ @MatthewMarinets @Snarkie @SirChuckOfTheChuckles
# Super Metroid # Super Metroid
/worlds/sm/ @lordlou /worlds/sm/ @lordlou

View File

@@ -385,6 +385,8 @@ Will provide a dict of static tracker data with the following keys:
- This hash can then be sent to the datapackage API to receive the appropriate datapackage as necessary - This hash can then be sent to the datapackage API to receive the appropriate datapackage as necessary
- The number of checks found vs. total checks available per player (`player_locations_total`) - The number of checks found vs. total checks available per player (`player_locations_total`)
- Same logic as the multitracker template: found = len(player_checks_done.locations) / total = player_locations_total.total_locations (all available checks). - Same logic as the multitracker template: found = len(player_checks_done.locations) / total = player_locations_total.total_locations (all available checks).
- The game each player is playing (`player_game`)
- Provided as a list of objects with `team`, `player`, and `game`.
Example: Example:
```json ```json
@@ -409,10 +411,10 @@ Example:
], ],
"datapackage": { "datapackage": {
"Archipelago": { "Archipelago": {
"checksum": "ac9141e9ad0318df2fa27da5f20c50a842afeecb", "checksum": "ac9141e9ad0318df2fa27da5f20c50a842afeecb"
}, },
"The Messenger": { "The Messenger": {
"checksum": "6991cbcda7316b65bcb072667f3ee4c4cae71c0b", "checksum": "6991cbcda7316b65bcb072667f3ee4c4cae71c0b"
} }
}, },
"player_locations_total": [ "player_locations_total": [
@@ -427,6 +429,18 @@ Example:
"total_locations": 20 "total_locations": 20
} }
], ],
"player_game": [
{
"team": 0,
"player": 1,
"game": "Archipelago"
},
{
"team": 0,
"player": 2,
"game": "The Messenger"
}
]
} }
``` ```

11
kvui.py
View File

@@ -35,6 +35,17 @@ Config.set("input", "mouse", "mouse,disable_multitouch")
Config.set("kivy", "exit_on_escape", "0") Config.set("kivy", "exit_on_escape", "0")
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
# Workaround for Kivy issue #9226.
# caused by kivy by default using probesysfs,
# which assumes all multi touch deviecs are touch screens.
# workaround provided by Snu of the kivy commmunity c:
from kivy.utils import platform
if platform == "linux":
options = Config.options("input")
for option in options:
if Config.get("input", option) == "probesysfs":
Config.remove_option("input", option)
# Workaround for an issue where importing kivy.core.window before loading sounds # Workaround for an issue where importing kivy.core.window before loading sounds
# will hang the whole application on Linux once the first sound is loaded. # will hang the whole application on Linux once the first sound is loaded.
# kivymd imports kivy.core.window, so we have to do this before the first kivymd import. # kivymd imports kivy.core.window, so we have to do this before the first kivymd import.

View File

@@ -2,7 +2,7 @@ import unittest
from BaseClasses import PlandoOptions from BaseClasses import PlandoOptions
from worlds import AutoWorldRegister from worlds import AutoWorldRegister
from Options import OptionCounter, NamedRange, NumericOption, OptionList, OptionSet from Options import OptionCounter, NamedRange, NumericOption, OptionList, OptionSet, Visibility
class TestOptionPresets(unittest.TestCase): class TestOptionPresets(unittest.TestCase):
@@ -19,6 +19,9 @@ class TestOptionPresets(unittest.TestCase):
# pass in all plando options in case a preset wants to require certain plando options # pass in all plando options in case a preset wants to require certain plando options
# for some reason # for some reason
option.verify(world_type, "Test Player", PlandoOptions(sum(PlandoOptions))) option.verify(world_type, "Test Player", PlandoOptions(sum(PlandoOptions)))
if not (Visibility.complex_ui in option.visibility or Visibility.simple_ui in option.visibility):
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' is not "
f"visible in any supported UI.")
supported_types = [NumericOption, OptionSet, OptionList, OptionCounter] supported_types = [NumericOption, OptionSet, OptionList, OptionCounter]
if not any([issubclass(option.__class__, t) for t in supported_types]): if not any([issubclass(option.__class__, t) for t in supported_types]):
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' " self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' "

View File

@@ -31,3 +31,21 @@ components.append(
supports_uri=True, supports_uri=True,
) )
) )
# There are two optional parameters that are worth drawing attention to here: "game_name" and "supports_uri".
# As you might know, on a room page on WebHost, clicking a slot name opens your locally installed Launcher
# and asks you if you want to open a Text Client.
# If you have "game_name" set on your Component, your user also gets the option to open that instead.
# Furthermore, if you have "supports_uri" set to True, your Component will be passed a uri as an arg.
# This uri contains the room url + port, the slot name, and the password.
# You can process this uri arg to automatically connect the user to their slot without having to type anything.
# As you can see above, the APQuest client has both of these parameters set.
# This means a user can click on the slot name of an APQuest slot on WebHost,
# then click "APQuest Client" instead of "Text Client" in the Launcher popup, and after a few seconds,
# they will be connected and playing the game without having to touch their keyboard once.
# Since a Component is just Python code, this doesn't just work with CommonClient-derived clients.
# You could forward this uri arg to your standalone C++/Java/.NET/whatever client as well,
# meaning just about every client can support this "Click on slot name -> Everything happens automatically" action.
# The author would like to see more clients be aware of this feature and try to support it.

View File

@@ -2,7 +2,7 @@
## Benötigte Software ## Benötigte Software
- [Archipelago](github.com/ArchipelagoMW/Archipelago/releases/latest) - [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
- Die [APQuest-apworld](https://github.com/NewSoupVi/Archipelago/releases), - Die [APQuest-apworld](https://github.com/NewSoupVi/Archipelago/releases),
falls diese nicht mit deiner Version von Archipelago gebündelt ist. falls diese nicht mit deiner Version von Archipelago gebündelt ist.
@@ -11,16 +11,16 @@
Zuerst brauchst du einen Raum, mit dem du dich verbinden kannst. Zuerst brauchst du einen Raum, mit dem du dich verbinden kannst.
Dafür musst du oder jemand den du kennst ein Spiel generieren. Dafür musst du oder jemand den du kennst ein Spiel generieren.
Dieser Schritt wird hier nicht erklärt, aber du kannst den Dieser Schritt wird hier nicht erklärt, aber du kannst den
[Archipelago Setup Guide](https://archipelago.gg/tutorial/Archipelago/setup_en#generating-a-game) lesen. [Archipelago Setup Guide](/tutorial/Archipelago/setup_en#generating-a-game) lesen.
Du musst außerdem [Archipelago](github.com/ArchipelagoMW/Archipelago/releases/latest) installiert haben Du musst außerdem [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest) installiert haben
und die [APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases) darin installieren. und die [APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases) darin installieren.
Von hier ist es einfach, dich mit deinem Slot zu verbinden. Von hier ist es einfach, dich mit deinem Slot zu verbinden.
### Webhost-Raum ### Webhost-Raum
Wenn dein Raum auf einem WebHost läuft (z.B. [archipelago.gg](archipelago.gg)) Wenn dein Raum auf einem WebHost läuft (z.B. [archipelago.gg](https://archipelago.gg))
kannst du einfach auf deinen Namen in der Spielerliste klicken. kannst du einfach auf deinen Namen in der Spielerliste klicken.
Dies öffnet den Archipelago Launcher, welcher dich dann fragt, Dies öffnet den Archipelago Launcher, welcher dich dann fragt,
ob du den Text Client oder den APQuest Client öffnen willst. ob du den Text Client oder den APQuest Client öffnen willst.

View File

@@ -2,7 +2,7 @@
## Required Software ## Required Software
- [Archipelago](github.com/ArchipelagoMW/Archipelago/releases/latest) - [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
- [The APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases), - [The APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases),
if not bundled with your version of Archipelago if not bundled with your version of Archipelago
@@ -10,16 +10,16 @@
First, you need a room to connect to. For this, you or someone you know has to generate a game. First, you need a room to connect to. For this, you or someone you know has to generate a game.
This will not be explained here, This will not be explained here,
but you can check the [Archipelago Setup Guide](https://archipelago.gg/tutorial/Archipelago/setup_en#generating-a-game). but you can check the [Archipelago Setup Guide](/tutorial/Archipelago/setup_en#generating-a-game).
You also need to have [Archipelago](github.com/ArchipelagoMW/Archipelago/releases/latest) installed You also need to have [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest) installed
and the [The APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases) installed into Archipelago. and the [The APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases) installed into Archipelago.
From here, connecting to your APQuest slot is easy. There are two scenarios. From here, connecting to your APQuest slot is easy. There are two scenarios.
### Webhost Room ### Webhost Room
If your room is hosted on a WebHost (e.g. [archipelago.gg](archipelago.gg)), If your room is hosted on a WebHost (e.g. [archipelago.gg](https://archipelago.gg)),
you should be able to simply click on your name in the player list. you should be able to simply click on your name in the player list.
This will open the Archipelago Launcher This will open the Archipelago Launcher
and ask you whether you want to connect with the Text Client or the APQuest Client. and ask you whether you want to connect with the Text Client or the APQuest Client.

View File

@@ -158,11 +158,11 @@ class Game:
if not self.gameboard.ready: if not self.gameboard.ready:
return return
if self.active_math_problem is not None: if input_key in DIGIT_INPUTS_TO_DIGITS:
if input_key in DIGIT_INPUTS_TO_DIGITS: self.math_problem_input(DIGIT_INPUTS_TO_DIGITS[input_key])
self.math_problem_input(DIGIT_INPUTS_TO_DIGITS[input_key]) return
if input_key == Input.BACKSPACE: if input_key == Input.BACKSPACE:
self.math_problem_delete() self.math_problem_delete()
return return
if input_key == Input.LEFT: if input_key == Input.LEFT:

View File

@@ -11,3 +11,5 @@ Play Sudoku puzzles of varying difficulties, earning a hint for each puzzle corr
## Where is the options page? ## Where is the options page?
There is no options page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld. There is no options page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld.
By using the connected room's Admin Password on the Admin Panel tab, you can configure some settings at any time to affect the entire room. This allows disabling hints entirely, as well as altering the hint odds for each difficulty.

View File

@@ -11,7 +11,11 @@ Does not need to be added at the start of a seed, as it does not create any slot
## Installation Procedures ## Installation Procedures
Go to the latest release from the [APSudoku Releases page](https://github.com/APSudoku/APSudoku/releases/latest). Download and extract the appropriate file for your platform. ### Windows / Linux
Go to the latest release from the [github APSudoku Releases page](https://github.com/APSudoku/APSudoku/releases/latest). Download and extract the appropriate file for your platform.
### Web
Go to the [github pages](apsudoku.github.io) or [itch.io](https://emilyv99.itch.io/apsudoku) site, and play in the browser.
## Joining a MultiWorld Game ## Joining a MultiWorld Game
@@ -35,6 +39,14 @@ Info:
- You can also use the `Tracking` tab to view either a basic tracker or a valid [GodotAP tracker pack](https://github.com/EmilyV99/GodotAP/blob/main/tracker_packs/GET_PACKS.md) - You can also use the `Tracking` tab to view either a basic tracker or a valid [GodotAP tracker pack](https://github.com/EmilyV99/GodotAP/blob/main/tracker_packs/GET_PACKS.md)
- While connected, the number of "unhinted" locations for your slot is shown in the upper-left of the the `Sudoku` tab. (If this reads 0, no further hints can be earned for this slot, as every locations is already hinted) - While connected, the number of "unhinted" locations for your slot is shown in the upper-left of the the `Sudoku` tab. (If this reads 0, no further hints can be earned for this slot, as every locations is already hinted)
- Click the various `?` buttons for information on controls/how to play - Click the various `?` buttons for information on controls/how to play
## Admin Settings
By using the connected room's Admin Password on the Admin Panel tab, you can configure some settings at any time to affect the entire room.
- You can disable APSudoku for the entire room, preventing any hints from being granted.
- You can customize the reward weights for each difficulty, making progression hints more or less likely, and/or adding a chance to get "no hint" after a solve.
## DeathLink Support ## DeathLink Support
If `DeathLink` is enabled when you click `Connect`: If `DeathLink` is enabled when you click `Connect`:

View File

@@ -19,7 +19,7 @@ The Mod can be installed and played by following these steps (see the [Mod Downl
2. Launch the game, if "OFFLINE" is visible in the upper-right corner of the screen, the Mod is working 2. Launch the game, if "OFFLINE" is visible in the upper-right corner of the screen, the Mod is working
### Create a Config (.yaml) File ### Create a Config (.yaml) File
The purpose of a YAML file is described in the [Basic Multiworld Setup Guide](https://archipelago.gg/tutorial/Archipelago/setup/en#generating-a-game). The purpose of a YAML file is described in the [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en#generating-a-game).
The [Player Options page](/games/Choo-Choo%20Charles/player-options) allows to configure personal options and export a config YAML file. The [Player Options page](/games/Choo-Choo%20Charles/player-options) allows to configure personal options and export a config YAML file.
@@ -38,7 +38,7 @@ Follow these steps to host a remote multiplayer or a local single-player session
1. Double-click the **cccharles.apworld** to automatically install the world randomization logic 1. Double-click the **cccharles.apworld** to automatically install the world randomization logic
2. Put the **CCCharles.yaml** to **Archipelago/Players/** with the YAML of each player to host 2. Put the **CCCharles.yaml** to **Archipelago/Players/** with the YAML of each player to host
3. Launch the Archipelago launcher and click "Generate" to configure a game with the YAMLs in **Archipelago/output/** 3. Launch the Archipelago launcher and click "Generate" to configure a game with the YAMLs in **Archipelago/output/**
4. For a multiplayer session, go to the [Archipelago HOST GAME page](https://archipelago.gg/uploads) 4. For a multiplayer session, go to the [Archipelago HOST GAME page](/uploads)
5. Click "Upload File" and select the generated **AP_\<seed\>.zip** in **Archipelago/output/** 5. Click "Upload File" and select the generated **AP_\<seed\>.zip** in **Archipelago/output/**
6. Send the generated room page to each player 6. Send the generated room page to each player

View File

@@ -19,7 +19,7 @@ Le Mod peut être installé et joué en suivant les étapes suivantes (voir la s
2. Lancer le jeu, si "OFFLINE" est visible dans le coin en haut à droite de l'écran, le Mod est actif 2. Lancer le jeu, si "OFFLINE" est visible dans le coin en haut à droite de l'écran, le Mod est actif
### Créer un Fichier de Configuration (.yaml) ### Créer un Fichier de Configuration (.yaml)
L'objectif d'un fichier YAML est décrit dans le [Guide d'Installation Basique du Multiworld](https://archipelago.gg/tutorial/Archipelago/setup/en#generating-a-game) (en anglais). L'objectif d'un fichier YAML est décrit dans le [Guide d'Installation Basique du Multiworld](/tutorial/Archipelago/setup/en#generating-a-game) (en anglais).
La [page d'Options Joueur](/games/Choo-Choo%20Charles/player-options) permet de configurer des options personnelles et exporter un fichier de configuration YAML. La [page d'Options Joueur](/games/Choo-Choo%20Charles/player-options) permet de configurer des options personnelles et exporter un fichier de configuration YAML.
@@ -38,7 +38,7 @@ Suivre ces étapes pour héberger une session multijoueur à distance ou locale
1. Double-cliquer sur **cccharles.apworld** pour installer automatiquement la logique de randomisation du monde 1. Double-cliquer sur **cccharles.apworld** pour installer automatiquement la logique de randomisation du monde
2. Placer le **CCCharles.yaml** dans **Archipelago/Players/** avec le YAML de chaque joueur à héberger 2. Placer le **CCCharles.yaml** dans **Archipelago/Players/** avec le YAML de chaque joueur à héberger
3. Exécuter le lanceur Archipelago et cliquer sur "Generate" pour configurer une partie avec les YAML dans **Archipelago/output/** 3. Exécuter le lanceur Archipelago et cliquer sur "Generate" pour configurer une partie avec les YAML dans **Archipelago/output/**
4. Pour une session multijoueur, aller à la [page Archipelago HOST GAME](https://archipelago.gg/uploads) 4. Pour une session multijoueur, aller à la [page Archipelago HOST GAME](/uploads)
5. Cliquer sur "Upload File" et sélectionner le **AP_\<seed\>.zip** généré dans **Archipelago/output/** 5. Cliquer sur "Upload File" et sélectionner le **AP_\<seed\>.zip** généré dans **Archipelago/output/**
6. Envoyer la page de la partie générée à chaque joueur 6. Envoyer la page de la partie générée à chaque joueur

View File

@@ -1,6 +1,6 @@
from typing import Dict, List from typing import Dict, List
from .Technologies import factorio_base_id from .Technologies import factorio_base_id, recipes
from .Options import MaxSciencePack from .Options import MaxSciencePack
@@ -21,5 +21,18 @@ for pool in location_pools.values():
location_table.update({name: ap_id for ap_id, name in enumerate(pool, start=end_id)}) location_table.update({name: ap_id for ap_id, name in enumerate(pool, start=end_id)})
end_id += len(pool) end_id += len(pool)
craftsanity_locations = []
valid_items = []
item_category = {}
for recipe_name, recipe in recipes.items():
if not recipe_name.endswith(("-barrel", "-science-pack")):
for result in recipe.products:
if result not in valid_items:
valid_items.append(result)
for i, item in enumerate(valid_items, start=end_id):
location_table[f"Craft {item}"] = i
craftsanity_locations.append(f"Craft {item}")
end_id += 1
assert end_id - len(location_table) == factorio_base_id assert end_id - len(location_table) == factorio_base_id
del pool del pool

View File

@@ -112,7 +112,7 @@ def generate_mod(world: "Factorio", output_directory: str):
settings_template = template_env.get_template("settings.lua") settings_template = template_env.get_template("settings.lua")
# get data for templates # get data for templates
locations = [(location, location.item) locations = [(location, location.item)
for location in world.science_locations] for location in world.science_locations + world.craftsanity_locations]
mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}" mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}"
versioned_mod_name = mod_name + "_" + Utils.__version__ versioned_mod_name = mod_name + "_" + Utils.__version__

View File

@@ -6,7 +6,7 @@ import typing
from schema import Schema, Optional, And, Or, SchemaError from schema import Schema, Optional, And, Or, SchemaError
from Options import Choice, OptionDict, OptionSet, DefaultOnToggle, Range, DeathLink, Toggle, \ from Options import Choice, OptionDict, OptionSet, DefaultOnToggle, Range, DeathLink, Toggle, \
StartInventoryPool, PerGameCommonOptions, OptionGroup StartInventoryPool, PerGameCommonOptions, OptionGroup, NamedRange
# schema helpers # schema helpers
@@ -60,6 +60,20 @@ class Goal(Choice):
default = 0 default = 0
class CraftSanity(NamedRange):
"""Choose a number of researches to require crafting a specific item rather than with science packs.
May be capped based on the total number of locations.
There will always be at least 2 Science Pack research locations for automation and logistics, and 1 for rocket-silo
if the Rocket Silo option is not set to Spawn."""
display_name = "CraftSanity"
default = 0
range_start = 0
range_end = 183
special_range_names = {
"disabled": 0
}
class TechCost(Range): class TechCost(Range):
range_start = 1 range_start = 1
range_end = 10000 range_end = 10000
@@ -475,6 +489,7 @@ class EnergyLink(Toggle):
class FactorioOptions(PerGameCommonOptions): class FactorioOptions(PerGameCommonOptions):
max_science_pack: MaxSciencePack max_science_pack: MaxSciencePack
goal: Goal goal: Goal
craftsanity: CraftSanity
tech_tree_layout: TechTreeLayout tech_tree_layout: TechTreeLayout
min_tech_cost: MinTechCost min_tech_cost: MinTechCost
max_tech_cost: MaxTechCost max_tech_cost: MaxTechCost

View File

@@ -334,14 +334,15 @@ required_technologies: Dict[str, FrozenSet[Technology]] = Utils.KeyedDefaultDict
recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock))) recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock)))
def get_rocket_requirements(silo_recipe: Optional[Recipe], part_recipe: Recipe, def get_rocket_requirements(silo_recipe: Optional[Recipe], part_recipe: Optional[Recipe],
satellite_recipe: Optional[Recipe], cargo_landing_pad_recipe: Optional[Recipe]) -> Set[str]: satellite_recipe: Optional[Recipe], cargo_landing_pad_recipe: Optional[Recipe]) -> Set[str]:
techs = set() techs = set()
if silo_recipe: if silo_recipe:
for ingredient in silo_recipe.ingredients: for ingredient in silo_recipe.ingredients:
techs |= recursively_get_unlocking_technologies(ingredient) techs |= recursively_get_unlocking_technologies(ingredient)
for ingredient in part_recipe.ingredients: if part_recipe:
techs |= recursively_get_unlocking_technologies(ingredient) for ingredient in part_recipe.ingredients:
techs |= recursively_get_unlocking_technologies(ingredient)
if cargo_landing_pad_recipe: if cargo_landing_pad_recipe:
for ingredient in cargo_landing_pad_recipe.ingredients: for ingredient in cargo_landing_pad_recipe.ingredients:
techs |= recursively_get_unlocking_technologies(ingredient) techs |= recursively_get_unlocking_technologies(ingredient)

View File

@@ -9,7 +9,7 @@ from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
from worlds.AutoWorld import World, WebWorld from worlds.AutoWorld import World, WebWorld
from worlds.LauncherComponents import Component, components, Type, launch as launch_component from worlds.LauncherComponents import Component, components, Type, launch as launch_component
from worlds.generic import Rules from worlds.generic import Rules
from .Locations import location_pools, location_table from .Locations import location_pools, location_table, craftsanity_locations
from .Mod import generate_mod from .Mod import generate_mod
from .Options import (FactorioOptions, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, from .Options import (FactorioOptions, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal,
TechCostDistribution, option_groups) TechCostDistribution, option_groups)
@@ -88,6 +88,7 @@ class Factorio(World):
skip_silo: bool = False skip_silo: bool = False
origin_region_name = "Nauvis" origin_region_name = "Nauvis"
science_locations: typing.List[FactorioScienceLocation] science_locations: typing.List[FactorioScienceLocation]
craftsanity_locations: typing.List[FactorioCraftsanityLocation]
removed_technologies: typing.Set[str] removed_technologies: typing.Set[str]
settings: typing.ClassVar[FactorioSettings] settings: typing.ClassVar[FactorioSettings]
trap_names: tuple[str] = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", trap_names: tuple[str] = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery",
@@ -100,6 +101,7 @@ class Factorio(World):
self.advancement_technologies = set() self.advancement_technologies = set()
self.custom_recipes = {} self.custom_recipes = {}
self.science_locations = [] self.science_locations = []
self.craftsanity_locations = []
self.tech_tree_layout_prerequisites = {} self.tech_tree_layout_prerequisites = {}
generate_output = generate_mod generate_output = generate_mod
@@ -127,17 +129,42 @@ class Factorio(World):
location_pool = [] location_pool = []
craftsanity_pool = [craft for craft in craftsanity_locations
if self.options.silo != Silo.option_spawn
or craft not in ["Craft rocket-silo", "Craft cargo-landing-pad"]]
# Ensure at least 2 science pack locations for automation and logistics, and 1 more for rocket-silo
# if it is not pre-spawned
craftsanity_count = min(self.options.craftsanity.value, len(craftsanity_pool),
location_count - (2 if self.options.silo == Silo.option_spawn else 3))
location_count -= craftsanity_count
for pack in sorted(self.options.max_science_pack.get_allowed_packs()): for pack in sorted(self.options.max_science_pack.get_allowed_packs()):
location_pool.extend(location_pools[pack]) location_pool.extend(location_pools[pack])
try: try:
location_names = random.sample(location_pool, location_count) # Ensure there are two "AP-1-" locations for automation and logistics, and one max science pack location
# for rocket-silo if it is not pre-spawned
max_science_pack_number = len(self.options.max_science_pack.get_allowed_packs())
science_location_names = None
while (not science_location_names or
len([location for location in science_location_names if location.startswith("AP-1-")]) < 2
or (self.options.silo != Silo.option_spawn and len([location for location in science_location_names
if location.startswith(f"AP-{max_science_pack_number}")]) < 1)):
science_location_names = random.sample(location_pool, location_count)
craftsanity_location_names = random.sample(craftsanity_pool, craftsanity_count)
except ValueError as e: except ValueError as e:
# should be "ValueError: Sample larger than population or is negative" # should be "ValueError: Sample larger than population or is negative"
raise Exception("Too many traps for too few locations. Either decrease the trap count, " raise Exception("Too many traps for too few locations. Either decrease the trap count, "
f"or increase the location count (higher max science pack). (Player {self.player})") from e f"or increase the location count (higher max science pack). (Player {self.player})") from e
self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis) self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis)
for loc_name in location_names] for loc_name in science_location_names]
self.craftsanity_locations = [FactorioCraftsanityLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis)
for loc_name in craftsanity_location_names]
distribution: TechCostDistribution = self.options.tech_cost_distribution distribution: TechCostDistribution = self.options.tech_cost_distribution
min_cost = self.options.min_tech_cost.value min_cost = self.options.min_tech_cost.value
max_cost = self.options.max_tech_cost.value max_cost = self.options.max_tech_cost.value
@@ -159,6 +186,7 @@ class Factorio(World):
location.count = rand_values[i] location.count = rand_values[i]
del rand_values del rand_values
nauvis.locations.extend(self.science_locations) nauvis.locations.extend(self.science_locations)
nauvis.locations.extend(self.craftsanity_locations)
location = FactorioLocation(player, "Rocket Launch", None, nauvis) location = FactorioLocation(player, "Rocket Launch", None, nauvis)
nauvis.locations.append(location) nauvis.locations.append(location)
event = FactorioItem("Victory", ItemClassification.progression, None, player) event = FactorioItem("Victory", ItemClassification.progression, None, player)
@@ -188,7 +216,7 @@ class Factorio(World):
loc: FactorioScienceLocation loc: FactorioScienceLocation
if self.options.tech_tree_information == TechTreeInformation.option_full: if self.options.tech_tree_information == TechTreeInformation.option_full:
# mark all locations as pre-hinted # mark all locations as pre-hinted
for loc in self.science_locations: for loc in self.science_locations + self.craftsanity_locations:
loc.revealed = True loc.revealed = True
if self.skip_silo: if self.skip_silo:
self.removed_technologies |= {"rocket-silo"} self.removed_technologies |= {"rocket-silo"}
@@ -236,6 +264,23 @@ class Factorio(World):
location.access_rule = lambda state, ingredient=ingredient: \ location.access_rule = lambda state, ingredient=ingredient: \
all(state.has(technology.name, player) for technology in required_technologies[ingredient]) all(state.has(technology.name, player) for technology in required_technologies[ingredient])
for location in self.craftsanity_locations:
if location.crafted_item == "crude-oil":
recipe = recipes["pumpjack"]
elif location.crafted_item in recipes:
recipe = recipes[location.crafted_item]
else:
for recipe_name, recipe in recipes.items():
if recipe_name.endswith("-barrel"):
continue
if location.crafted_item in recipe.products:
break
else:
raise Exception(
f"No recipe found for {location.crafted_item} for Craftsanity for player {self.player}")
location.access_rule = lambda state, recipe=recipe: \
state.has_all({technology.name for technology in recipe.recursive_unlocking_technologies}, player)
for location in self.science_locations: for location in self.science_locations:
Rules.set_rule(location, lambda state, ingredients=frozenset(location.ingredients): Rules.set_rule(location, lambda state, ingredients=frozenset(location.ingredients):
all(state.has(f"Automated {ingredient}", player) for ingredient in ingredients)) all(state.has(f"Automated {ingredient}", player) for ingredient in ingredients))
@@ -250,10 +295,11 @@ class Factorio(World):
silo_recipe = self.get_recipe("rocket-silo") silo_recipe = self.get_recipe("rocket-silo")
cargo_pad_recipe = self.get_recipe("cargo-landing-pad") cargo_pad_recipe = self.get_recipe("cargo-landing-pad")
part_recipe = self.custom_recipes["rocket-part"] part_recipe = self.custom_recipes["rocket-part"]
satellite_recipe = None satellite_recipe = self.get_recipe("satellite")
if self.options.goal == Goal.option_satellite: victory_tech_names = get_rocket_requirements(
satellite_recipe = self.get_recipe("satellite") silo_recipe, part_recipe,
victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe, cargo_pad_recipe) satellite_recipe if self.options.goal == Goal.option_satellite else None,
cargo_pad_recipe)
if self.options.silo == Silo.option_spawn: if self.options.silo == Silo.option_spawn:
victory_tech_names -= {"rocket-silo"} victory_tech_names -= {"rocket-silo"}
else: else:
@@ -263,6 +309,46 @@ class Factorio(World):
victory_tech_names) victory_tech_names)
self.multiworld.completion_condition[player] = lambda state: state.has('Victory', player) self.multiworld.completion_condition[player] = lambda state: state.has('Victory', player)
if "Craft rocket-silo" in self.multiworld.regions.location_cache[self.player]:
victory_tech_names_r = get_rocket_requirements(silo_recipe, None, None, None)
if self.options.silo == Silo.option_spawn:
victory_tech_names_r -= {"rocket-silo"}
else:
victory_tech_names_r |= {"rocket-silo"}
self.get_location("Craft rocket-silo").access_rule = lambda state: all(state.has(technology, player)
for technology in
victory_tech_names_r)
if "Craft rocket-part" in self.multiworld.regions.location_cache[self.player]:
victory_tech_names_p = get_rocket_requirements(silo_recipe, part_recipe, None, None)
if self.options.silo == Silo.option_spawn:
victory_tech_names_p -= {"rocket-silo"}
else:
victory_tech_names_p |= {"rocket-silo"}
self.get_location("Craft rocket-part").access_rule = lambda state: all(state.has(technology, player)
for technology in
victory_tech_names_p)
if "Craft satellite" in self.multiworld.regions.location_cache[self.player]:
victory_tech_names_s = get_rocket_requirements(None, None, satellite_recipe, None)
if self.options.silo == Silo.option_spawn:
victory_tech_names_s -= {"rocket-silo"}
else:
victory_tech_names_s |= {"rocket-silo"}
self.get_location("Craft satellite").access_rule = lambda state: all(state.has(technology, player)
for technology in
victory_tech_names_s)
if "Craft cargo-landing-pad" in self.multiworld.regions.location_cache[self.player]:
victory_tech_names_c = get_rocket_requirements(None, None, None, cargo_pad_recipe)
if self.options.silo == Silo.option_spawn:
victory_tech_names_c -= {"rocket-silo"}
else:
victory_tech_names_c |= {"rocket-silo"}
self.get_location("Craft cargo-landing-pad").access_rule = lambda state: all(state.has(technology, player)
for technology in
victory_tech_names_c)
def get_recipe(self, name: str) -> Recipe: def get_recipe(self, name: str) -> Recipe:
return self.custom_recipes[name] if name in self.custom_recipes \ return self.custom_recipes[name] if name in self.custom_recipes \
else next(iter(all_product_sources.get(name))) else next(iter(all_product_sources.get(name)))
@@ -486,9 +572,17 @@ class Factorio(World):
needed_recipes = self.options.max_science_pack.get_allowed_packs() | {"rocket-part"} needed_recipes = self.options.max_science_pack.get_allowed_packs() | {"rocket-part"}
if self.options.silo != Silo.option_spawn: if self.options.silo != Silo.option_spawn:
needed_recipes |= {"rocket-silo", "cargo-landing-pad"} needed_recipes |= {"rocket-silo", "cargo-landing-pad"}
if self.options.goal.value == Goal.option_satellite: if (self.options.goal.value == Goal.option_satellite
or "Craft satellite" in self.multiworld.regions.location_cache[self.player]):
needed_recipes |= {"satellite"} needed_recipes |= {"satellite"}
needed_items = {location.crafted_item for location in self.craftsanity_locations}
for recipe_name, recipe in recipes.items():
for product in recipe.products:
if product in needed_items:
self.advancement_technologies |= {tech.name for tech in recipe.recursive_unlocking_technologies}
break
for recipe in needed_recipes: for recipe in needed_recipes:
recipe = self.custom_recipes.get(recipe, recipes[recipe]) recipe = self.custom_recipes.get(recipe, recipes[recipe])
self.advancement_technologies |= {tech.name for tech in recipe.recursive_unlocking_technologies} self.advancement_technologies |= {tech.name for tech in recipe.recursive_unlocking_technologies}
@@ -520,9 +614,23 @@ class FactorioLocation(Location):
game: str = Factorio.game game: str = Factorio.game
class FactorioCraftsanityLocation(FactorioLocation):
ingredients = {}
count = 0
revealed = False
def __init__(self, player: int, name: str, address: int, parent: Region):
super(FactorioCraftsanityLocation, self).__init__(player, name, address, parent)
@property
def crafted_item(self):
return " ".join(self.name.split(" ")[1:])
class FactorioScienceLocation(FactorioLocation): class FactorioScienceLocation(FactorioLocation):
complexity: int complexity: int
revealed: bool = False revealed: bool = False
crafted_item = None
# Factorio technology properties: # Factorio technology properties:
ingredients: typing.Dict[str, int] ingredients: typing.Dict[str, int]

View File

@@ -63,22 +63,6 @@ template_tech.upgrade = false
template_tech.effects = {} template_tech.effects = {}
template_tech.prerequisites = {} template_tech.prerequisites = {}
{%- if max_science_pack < 6 %}
technologies["space-science-pack"].effects = {}
{%- if max_science_pack == 0 %}
table.insert (technologies["automation"].effects, {type = "unlock-recipe", recipe = "satellite"})
{%- elif max_science_pack == 1 %}
table.insert (technologies["logistic-science-pack"].effects, {type = "unlock-recipe", recipe = "satellite"})
{%- elif max_science_pack == 2 %}
table.insert (technologies["military-science-pack"].effects, {type = "unlock-recipe", recipe = "satellite"})
{%- elif max_science_pack == 3 %}
table.insert (technologies["chemical-science-pack"].effects, {type = "unlock-recipe", recipe = "satellite"})
{%- elif max_science_pack == 4 %}
table.insert (technologies["production-science-pack"].effects, {type = "unlock-recipe", recipe = "satellite"})
{%- elif max_science_pack == 5 %}
table.insert (technologies["utility-science-pack"].effects, {type = "unlock-recipe", recipe = "satellite"})
{% endif %}
{% endif %}
{%- if silo == 2 %} {%- if silo == 2 %}
data.raw["recipe"]["rocket-silo"].enabled = true data.raw["recipe"]["rocket-silo"].enabled = true
{% endif %} {% endif %}
@@ -169,9 +153,16 @@ technologies["{{ original_tech_name }}"].hidden_in_factoriopedia = true
{#- the tech researched by the local player #} {#- the tech researched by the local player #}
new_tree_copy = table.deepcopy(template_tech) new_tree_copy = table.deepcopy(template_tech)
new_tree_copy.name = "ap-{{ location.address }}-"{# use AP ID #} new_tree_copy.name = "ap-{{ location.address }}-"{# use AP ID #}
{% if location.crafted_item is not none %}
new_tree_copy.research_trigger = {
type = "{{ 'craft-fluid' if location.crafted_item in liquids else 'craft-item' }}",
{{ 'fluid' if location.crafted_item in liquids else 'item' }} = {{ variable_to_lua(location.crafted_item) }}
}
new_tree_copy.unit = nil
{% else %}
new_tree_copy.unit.count = {{ location.count }} new_tree_copy.unit.count = {{ location.count }}
new_tree_copy.unit.ingredients = {{ variable_to_lua(location.factorio_ingredients) }} new_tree_copy.unit.ingredients = {{ variable_to_lua(location.factorio_ingredients) }}
{% endif %}
{%- if location.revealed and item.name in base_tech_table -%} {%- if location.revealed and item.name in base_tech_table -%}
{#- copy Factorio Technology Icon #} {#- copy Factorio Technology Icon #}
copy_factorio_icon(new_tree_copy, "{{ item.name }}") copy_factorio_icon(new_tree_copy, "{{ item.name }}")

View File

@@ -17,7 +17,7 @@ class GenericWeb(WebWorld):
'A guide detailing the commands available to the user when participating in an Archipelago session.', 'A guide detailing the commands available to the user when participating in an Archipelago session.',
'English', 'commands_en.md', 'commands/en', ['jat2980', 'Ijwu']) 'English', 'commands_en.md', 'commands/en', ['jat2980', 'Ijwu'])
mac = Tutorial('Archipelago Setup Guide for Mac', 'A guide detailing how to run Archipelago clients on macOS.', mac = Tutorial('Archipelago Setup Guide for Mac', 'A guide detailing how to run Archipelago clients on macOS.',
'English', 'mac_en.md','mac/en', ['Bicoloursnake']) 'English', 'mac_en.md','mac/en', ['Bicoloursnake', 'silasary'])
plando = Tutorial('Archipelago Plando Guide', 'A guide to understanding and using plando for your game.', plando = Tutorial('Archipelago Plando Guide', 'A guide to understanding and using plando for your game.',
'English', 'plando_en.md', 'plando/en', ['alwaysintreble', 'Alchav']) 'English', 'plando_en.md', 'plando/en', ['alwaysintreble', 'Alchav'])
setup = Tutorial('Getting Started', setup = Tutorial('Getting Started',

View File

@@ -29,7 +29,7 @@ It is generally recommended that you use a virtual environment to run python bas
4. If the patching process needs a rom, but cannot find it, it will ask you to navigate to your legally obtained rom. 4. If the patching process needs a rom, but cannot find it, it will ask you to navigate to your legally obtained rom.
5. Your client should now be running and rom created (where applicable). 5. Your client should now be running and rom created (where applicable).
## Additional Steps for SNES Games ## Additional Steps for SNES Games
1. If using RetroArch, the instructions to set up your emulator [here in the Link to the Past setup guide](https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/multiworld/en) also work on the macOS version of RetroArch. 1. If using RetroArch, the instructions to set up your emulator [here in the Link to the Past setup guide](/tutorial/A%20Link%20to%20the%20Past/multiworld/en) also work on the macOS version of RetroArch.
2. Double click on the SNI tar.gz download to extract the files to an SNI directory. If it isn't already, rename this directory to SNI to make some steps easier. 2. Double click on the SNI tar.gz download to extract the files to an SNI directory. If it isn't already, rename this directory to SNI to make some steps easier.
3. Move the SNI directory out of the downloads directory, preferably into the Archipelago directory created earlier. 3. Move the SNI directory out of the downloads directory, preferably into the Archipelago directory created earlier.
4. If the SNI directory is correctly named and moved into the Archipelago directory, it should auto run with the SNI client. If it doesn't automatically run, open up the SNI directory and run the SNI executable file manually. 4. If the SNI directory is correctly named and moved into the Archipelago directory, it should auto run with the SNI client. If it doesn't automatically run, open up the SNI directory and run the SNI executable file manually.

View File

@@ -7,7 +7,7 @@ all_random = {
"game_language": "random", "game_language": "random",
"goal": "random", "goal": "random",
"goal_speed": "random", "goal_speed": "random",
"total_heart_stars": "random", "max_heart_stars": "random",
"heart_stars_required": "random", "heart_stars_required": "random",
"filler_percentage": "random", "filler_percentage": "random",
"trap_percentage": "random", "trap_percentage": "random",
@@ -34,7 +34,7 @@ all_random = {
beginner = { beginner = {
"goal": "zero", "goal": "zero",
"goal_speed": "normal", "goal_speed": "normal",
"total_heart_stars": 50, "max_heart_stars": 50,
"heart_stars_required": 30, "heart_stars_required": 30,
"filler_percentage": 25, "filler_percentage": 25,
"trap_percentage": 0, "trap_percentage": 0,

View File

@@ -84,7 +84,7 @@ Enter The room's port number into the top box <b> where the x's are</b> and pres
- To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Panacea and Lua Backend Steps. - To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Panacea and Lua Backend Steps.
- Using a seed from the standalone KH2 Randomizer Seed Generator. - Using a seed from the standalone KH2 Randomizer Seed Generator.
- The Archipelago version of the KH2 Randomizer does not use this Seed Generator; refer to the [Archipelago Setup](https://archipelago.gg/tutorial/Archipelago/setup/en) to learn how to generate and play a seed through Archipelago. - The Archipelago version of the KH2 Randomizer does not use this Seed Generator; refer to the [Archipelago Setup](/tutorial/Archipelago/setup/en) to learn how to generate and play a seed through Archipelago.
## Best Practices ## Best Practices

View File

@@ -135,7 +135,7 @@ Additionally, if you get an item while already having the max for that item (for
It is likely that you do not have release or collect permissions, or that there is nothing to release or collect. It is likely that you do not have release or collect permissions, or that there is nothing to release or collect.
Another option is that your connection was interrupted. Another option is that your connection was interrupted.
If you would still like to use release or collect, refer to [this section of the server commands page](https://archipelago.gg/tutorial/Archipelago/commands/en#collect/release). If you would still like to use release or collect, refer to [this section of the server commands page](/tutorial/Archipelago/commands/en#collectrelease).
You may use the in-game console to execute the commands, if your slot has permissions to do so. You may use the in-game console to execute the commands, if your slot has permissions to do so.

View File

@@ -60,7 +60,7 @@
This is usage documentation for the `custom_mission_order` YAML option for Starcraft 2. You can enable Custom Mission Orders by setting `mission_order: custom` in your YAML. This is usage documentation for the `custom_mission_order` YAML option for Starcraft 2. You can enable Custom Mission Orders by setting `mission_order: custom` in your YAML.
You will need to know how to write a YAML before engaging with this feature, and should read the [Archipelago YAML documentation](https://archipelago.gg/tutorial/Archipelago/advanced_settings/en) before continuing here. You will need to know how to write a YAML before engaging with this feature, and should read the [Archipelago YAML documentation](/tutorial/Archipelago/advanced_settings/en) before continuing here.
Every example in this document should be valid to generate. Every example in this document should be valid to generate.

View File

@@ -951,14 +951,14 @@ item_descriptions = {
item_names.TEMPEST_GRAVITY_SLING: "Tempests gain +8 range against air targets and +8 cast range.", item_names.TEMPEST_GRAVITY_SLING: "Tempests gain +8 range against air targets and +8 cast range.",
item_names.TEMPEST_INTERPLANETARY_RANGE: "Tempests gain +8 weapon range against all targets.", item_names.TEMPEST_INTERPLANETARY_RANGE: "Tempests gain +8 weapon range against all targets.",
item_names.PHOENIX_CLASS_IONIC_WAVELENGTH_FLUX: "Increases Phoenix, Mirage, and Skirmisher weapon damage by +2.", item_names.PHOENIX_CLASS_IONIC_WAVELENGTH_FLUX: "Increases Phoenix, Mirage, and Skirmisher weapon damage by +2.",
item_names.PHOENIX_CLASS_ANION_PULSE_CRYSTALS: "Increases Phoenix, Mirage, and Skirmiser range by +2.", item_names.PHOENIX_CLASS_ANION_PULSE_CRYSTALS: "Increases Phoenix, Mirage, and Skirmisher range by +2.",
item_names.CORSAIR_STEALTH_DRIVE: "Corsairs become permanently cloaked.", item_names.CORSAIR_STEALTH_DRIVE: "Corsairs become permanently cloaked.",
item_names.CORSAIR_ARGUS_JEWEL: "Corsairs can store 2 charges of disruption web.", item_names.CORSAIR_ARGUS_JEWEL: "Corsairs can store 2 charges of disruption web.",
item_names.CORSAIR_SUSTAINING_DISRUPTION: "Corsair disruption webs last longer.", item_names.CORSAIR_SUSTAINING_DISRUPTION: "Corsair disruption webs last longer.",
item_names.CORSAIR_NEUTRON_SHIELDS: "Increases corsair maximum shields by +20.", item_names.CORSAIR_NEUTRON_SHIELDS: "Increases corsair maximum shields by +20.",
item_names.ORACLE_STEALTH_DRIVE: "Oracles become permanently cloaked.", item_names.ORACLE_STEALTH_DRIVE: "Oracles become permanently cloaked.",
item_names.ORACLE_SKYWARD_CHRONOANOMALY: "The Oracle's Stasis Ward can affect air units.", item_names.ORACLE_SKYWARD_CHRONOANOMALY: "The Oracle's Stasis Ward can affect air units.",
item_names.ORACLE_TEMPORAL_ACCELERATION_BEAM: "Oracles no longer need to to spend energy to attack.", item_names.ORACLE_TEMPORAL_ACCELERATION_BEAM: "Oracles no longer need to spend energy to attack.",
item_names.ORACLE_BOSONIC_CORE: "Increases starting energy by 150 and maximum energy by 50.", item_names.ORACLE_BOSONIC_CORE: "Increases starting energy by 150 and maximum energy by 50.",
item_names.ARBITER_CHRONOSTATIC_REINFORCEMENT: "Arbiters gain +50 maximum life and +1 armor.", item_names.ARBITER_CHRONOSTATIC_REINFORCEMENT: "Arbiters gain +50 maximum life and +1 armor.",
item_names.ARBITER_KHAYDARIN_CORE: _get_start_and_max_energy_desc("Arbiters"), item_names.ARBITER_KHAYDARIN_CORE: _get_start_and_max_energy_desc("Arbiters"),

View File

@@ -1309,7 +1309,7 @@ class MaximumSupplyReductionPerItem(Range):
class LowestMaximumSupply(Range): class LowestMaximumSupply(Range):
"""Controls how far max supply reduction traps can reduce maximum supply.""" """Controls how far max supply reduction traps can reduce maximum supply."""
display_name = "Lowest Maximum Supply" display_name = "Lowest Maximum Supply"
range_start = 100 range_start = 50
range_end = 200 range_end = 200
default = 180 default = 180

View File

@@ -1168,11 +1168,7 @@ class SC2Logic:
def two_kerrigan_actives(self, state: CollectionState, story_tech_available=True) -> bool: def two_kerrigan_actives(self, state: CollectionState, story_tech_available=True) -> bool:
if story_tech_available and self.grant_story_tech == GrantStoryTech.option_grant: if story_tech_available and self.grant_story_tech == GrantStoryTech.option_grant:
return True return True
count = 0 return state.count_from_list(item_groups.kerrigan_logic_active_abilities, self.player) >= 2
for i in range(7):
if state.has_any(kerrigan_logic_active_abilities, self.player):
count += 1
return count >= 2
# Global Protoss # Global Protoss
def protoss_power_rating(self, state: CollectionState) -> int: def protoss_power_rating(self, state: CollectionState) -> int:

View File

@@ -42,7 +42,7 @@ Weitere Informationen zum Randomizer findest du hier: [ReadMe](https://github.co
## Woher bekomme ich eine Konfigurationsdatei? ## Woher bekomme ich eine Konfigurationsdatei?
Die [Player Options](https://archipelago.gg/games/Timespinner/player-options) Seite auf der Website erlaubt dir, Die [Player Options](/games/Timespinner/player-options) Seite auf der Website erlaubt dir,
persönliche Einstellungen zu definieren und diese in eine Konfigurationsdatei zu exportieren persönliche Einstellungen zu definieren und diese in eine Konfigurationsdatei zu exportieren
* Die Timespinner Randomizer Option "StinkyMaw" ist in Archipelago Seeds aktuell immer an * Die Timespinner Randomizer Option "StinkyMaw" ist in Archipelago Seeds aktuell immer an

View File

@@ -0,0 +1,5 @@
{
"game": "The Legend of Zelda",
"world_version": "1.0.0",
"authors": ["Rosalie"]
}