forked from ArchipelagoMW/Archipelago
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
TUNIC: Implement New Game (ArchipelagoMW#2172)
- Loading branch information
1 parent
951cffc
commit cae2adb
Showing
15 changed files
with
3,991 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,279 @@ | ||
from typing import Dict, List, Any | ||
|
||
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification | ||
from .items import item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names | ||
from .locations import location_table, location_name_groups, location_name_to_id, hexagon_locations | ||
from .rules import set_location_rules, set_region_rules, randomize_ability_unlocks, gold_hexagon | ||
from .er_rules import set_er_location_rules | ||
from .regions import tunic_regions | ||
from .er_scripts import create_er_regions | ||
from .options import TunicOptions | ||
from worlds.AutoWorld import WebWorld, World | ||
from decimal import Decimal, ROUND_HALF_UP | ||
|
||
|
||
class TunicWeb(WebWorld): | ||
tutorials = [ | ||
Tutorial( | ||
tutorial_name="Multiworld Setup Guide", | ||
description="A guide to setting up the TUNIC Randomizer for Archipelago multiworld games.", | ||
language="English", | ||
file_name="setup_en.md", | ||
link="setup/en", | ||
authors=["SilentDestroyer"] | ||
) | ||
] | ||
theme = "grassFlowers" | ||
game = "Tunic" | ||
|
||
|
||
class TunicItem(Item): | ||
game: str = "Tunic" | ||
|
||
|
||
class TunicLocation(Location): | ||
game: str = "Tunic" | ||
|
||
|
||
class TunicWorld(World): | ||
""" | ||
Explore a land filled with lost legends, ancient powers, and ferocious monsters in TUNIC, an isometric action game | ||
about a small fox on a big adventure. Stranded on a mysterious beach, armed with only your own curiosity, you will | ||
confront colossal beasts, collect strange and powerful items, and unravel long-lost secrets. Be brave, tiny fox! | ||
""" | ||
game = "Tunic" | ||
web = TunicWeb() | ||
|
||
data_version = 2 | ||
options: TunicOptions | ||
options_dataclass = TunicOptions | ||
item_name_groups = item_name_groups | ||
location_name_groups = location_name_groups | ||
|
||
item_name_to_id = item_name_to_id | ||
location_name_to_id = location_name_to_id | ||
|
||
ability_unlocks: Dict[str, int] | ||
slot_data_items: List[TunicItem] | ||
tunic_portal_pairs: Dict[str, str] | ||
er_portal_hints: Dict[int, str] | ||
|
||
def generate_early(self) -> None: | ||
if self.options.start_with_sword and "Sword" not in self.options.start_inventory: | ||
self.options.start_inventory.value["Sword"] = 1 | ||
|
||
def create_item(self, name: str) -> TunicItem: | ||
item_data = item_table[name] | ||
return TunicItem(name, item_data.classification, self.item_name_to_id[name], self.player) | ||
|
||
def create_items(self) -> None: | ||
keys_behind_bosses = self.options.keys_behind_bosses | ||
hexagon_quest = self.options.hexagon_quest | ||
sword_progression = self.options.sword_progression | ||
|
||
tunic_items: List[TunicItem] = [] | ||
self.slot_data_items = [] | ||
|
||
items_to_create: Dict[str, int] = {item: data.quantity_in_item_pool for item, data in item_table.items()} | ||
|
||
for money_fool in fool_tiers[self.options.fool_traps]: | ||
items_to_create["Fool Trap"] += items_to_create[money_fool] | ||
items_to_create[money_fool] = 0 | ||
|
||
if sword_progression: | ||
items_to_create["Stick"] = 0 | ||
items_to_create["Sword"] = 0 | ||
else: | ||
items_to_create["Sword Upgrade"] = 0 | ||
|
||
if self.options.laurels_location: | ||
laurels = self.create_item("Hero's Laurels") | ||
if self.options.laurels_location == "6_coins": | ||
self.multiworld.get_location("Coins in the Well - 6 Coins", self.player).place_locked_item(laurels) | ||
elif self.options.laurels_location == "10_coins": | ||
self.multiworld.get_location("Coins in the Well - 10 Coins", self.player).place_locked_item(laurels) | ||
elif self.options.laurels_location == "10_fairies": | ||
self.multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", self.player).place_locked_item(laurels) | ||
self.slot_data_items.append(laurels) | ||
items_to_create["Hero's Laurels"] = 0 | ||
|
||
if keys_behind_bosses: | ||
for rgb_hexagon, location in hexagon_locations.items(): | ||
hex_item = self.create_item(gold_hexagon if hexagon_quest else rgb_hexagon) | ||
self.multiworld.get_location(location, self.player).place_locked_item(hex_item) | ||
self.slot_data_items.append(hex_item) | ||
items_to_create[rgb_hexagon] = 0 | ||
items_to_create[gold_hexagon] -= 3 | ||
|
||
if hexagon_quest: | ||
# Calculate number of hexagons in item pool | ||
hexagon_goal = self.options.hexagon_goal | ||
extra_hexagons = self.options.extra_hexagon_percentage | ||
items_to_create[gold_hexagon] += int((Decimal(100 + extra_hexagons) / 100 * hexagon_goal).to_integral_value(rounding=ROUND_HALF_UP)) | ||
|
||
# Replace pages and normal hexagons with filler | ||
for replaced_item in list(filter(lambda item: "Pages" in item or item in hexagon_locations, items_to_create)): | ||
items_to_create[self.get_filler_item_name()] += items_to_create[replaced_item] | ||
items_to_create[replaced_item] = 0 | ||
|
||
# Filler items that are still in the item pool to swap out | ||
available_filler: List[str] = [filler for filler in items_to_create if items_to_create[filler] > 0 and | ||
item_table[filler].classification == ItemClassification.filler] | ||
|
||
# Remove filler to make room for extra hexagons | ||
for i in range(0, items_to_create[gold_hexagon]): | ||
fill = self.random.choice(available_filler) | ||
items_to_create[fill] -= 1 | ||
if items_to_create[fill] == 0: | ||
available_filler.remove(fill) | ||
|
||
if self.options.maskless: | ||
mask_item = TunicItem("Scavenger Mask", ItemClassification.useful, self.item_name_to_id["Scavenger Mask"], self.player) | ||
tunic_items.append(mask_item) | ||
items_to_create["Scavenger Mask"] = 0 | ||
|
||
if self.options.lanternless: | ||
mask_item = TunicItem("Lantern", ItemClassification.useful, self.item_name_to_id["Lantern"], self.player) | ||
tunic_items.append(mask_item) | ||
items_to_create["Lantern"] = 0 | ||
|
||
for item, quantity in items_to_create.items(): | ||
for i in range(0, quantity): | ||
tunic_item: TunicItem = self.create_item(item) | ||
if item in slot_data_item_names: | ||
self.slot_data_items.append(tunic_item) | ||
tunic_items.append(tunic_item) | ||
|
||
self.multiworld.itempool += tunic_items | ||
|
||
def create_regions(self) -> None: | ||
self.tunic_portal_pairs = {} | ||
self.er_portal_hints = {} | ||
self.ability_unlocks = randomize_ability_unlocks(self.random, self.options) | ||
if self.options.entrance_rando: | ||
portal_pairs, portal_hints = create_er_regions(self) | ||
for portal1, portal2 in portal_pairs.items(): | ||
self.tunic_portal_pairs[portal1.scene_destination()] = portal2.scene_destination() | ||
self.er_portal_hints = portal_hints | ||
|
||
else: | ||
for region_name in tunic_regions: | ||
region = Region(region_name, self.player, self.multiworld) | ||
self.multiworld.regions.append(region) | ||
|
||
for region_name, exits in tunic_regions.items(): | ||
region = self.multiworld.get_region(region_name, self.player) | ||
region.add_exits(exits) | ||
|
||
for location_name, location_id in self.location_name_to_id.items(): | ||
region = self.multiworld.get_region(location_table[location_name].region, self.player) | ||
location = TunicLocation(self.player, location_name, location_id, region) | ||
region.locations.append(location) | ||
|
||
victory_region = self.multiworld.get_region("Spirit Arena", self.player) | ||
victory_location = TunicLocation(self.player, "The Heir", None, victory_region) | ||
victory_location.place_locked_item(TunicItem("Victory", ItemClassification.progression, None, self.player)) | ||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) | ||
victory_region.locations.append(victory_location) | ||
|
||
def set_rules(self) -> None: | ||
if self.options.entrance_rando: | ||
set_er_location_rules(self, self.ability_unlocks) | ||
else: | ||
set_region_rules(self, self.ability_unlocks) | ||
set_location_rules(self, self.ability_unlocks) | ||
|
||
def get_filler_item_name(self) -> str: | ||
return self.random.choice(filler_items) | ||
|
||
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): | ||
if self.options.entrance_rando: | ||
hint_data[self.player] = self.er_portal_hints | ||
|
||
def fill_slot_data(self) -> Dict[str, Any]: | ||
slot_data: Dict[str, Any] = { | ||
"seed": self.random.randint(0, 2147483647), | ||
"start_with_sword": self.options.start_with_sword.value, | ||
"keys_behind_bosses": self.options.keys_behind_bosses.value, | ||
"sword_progression": self.options.sword_progression.value, | ||
"ability_shuffling": self.options.ability_shuffling.value, | ||
"hexagon_quest": self.options.hexagon_quest.value, | ||
"fool_traps": self.options.fool_traps.value, | ||
"entrance_rando": self.options.entrance_rando.value, | ||
"Hexagon Quest Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"], | ||
"Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"], | ||
"Hexagon Quest Ice Rod": self.ability_unlocks["Pages 52-53 (Ice Rod)"], | ||
"Hexagon Quest Goal": self.options.hexagon_goal.value, | ||
"Entrance Rando": self.tunic_portal_pairs | ||
} | ||
|
||
for tunic_item in filter(lambda item: item.location is not None and item.code is not None, self.slot_data_items): | ||
if tunic_item.name not in slot_data: | ||
slot_data[tunic_item.name] = [] | ||
if tunic_item.name == gold_hexagon and len(slot_data[gold_hexagon]) >= 6: | ||
continue | ||
slot_data[tunic_item.name].extend([tunic_item.location.name, tunic_item.location.player]) | ||
|
||
for start_item in self.options.start_inventory_from_pool: | ||
if start_item in slot_data_item_names: | ||
if start_item not in slot_data: | ||
slot_data[start_item] = [] | ||
for i in range(0, self.options.start_inventory_from_pool[start_item]): | ||
slot_data[start_item].extend(["Your Pocket", self.player]) | ||
|
||
for plando_item in self.multiworld.plando_items[self.player]: | ||
if plando_item["from_pool"]: | ||
items_to_find = set() | ||
for item_type in [key for key in ["item", "items"] if key in plando_item]: | ||
for item in plando_item[item_type]: | ||
items_to_find.add(item) | ||
for item in items_to_find: | ||
if item in slot_data_item_names: | ||
slot_data[item] = [] | ||
for item_location in self.multiworld.find_item_locations(item, self.player): | ||
slot_data[item].extend([item_location.name, item_location.player]) | ||
|
||
return slot_data | ||
|
||
# for the universal tracker, doesn't get called in standard gen | ||
def interpret_slot_data(self, slot_data: Dict[str, Any]) -> None: | ||
# bypassing random yaml settings | ||
self.options.start_with_sword.value = slot_data["start_with_sword"] | ||
self.options.keys_behind_bosses.value = slot_data["keys_behind_bosses"] | ||
self.options.sword_progression.value = slot_data["sword_progression"] | ||
self.options.ability_shuffling.value = slot_data["ability_shuffling"] | ||
self.options.hexagon_quest.value = slot_data["hexagon_quest"] | ||
self.ability_unlocks["Pages 24-25 (Prayer)"] = slot_data["Hexagon Quest Prayer"] | ||
self.ability_unlocks["Pages 42-43 (Holy Cross)"] = slot_data["Hexagon Quest Holy Cross"] | ||
self.ability_unlocks["Pages 52-53 (Ice Rod)"] = slot_data["Hexagon Quest Ice Rod"] | ||
|
||
# swapping entrances around so the mapping matches what was generated | ||
if slot_data["entrance_rando"]: | ||
from BaseClasses import Entrance | ||
from .er_data import portal_mapping | ||
entrance_dict: Dict[str, Entrance] = {entrance.name: entrance | ||
for region in self.multiworld.get_regions(self.player) | ||
for entrance in region.entrances} | ||
slot_portals: Dict[str, str] = slot_data["Entrance Rando"] | ||
for portal1, portal2 in slot_portals.items(): | ||
portal_name1: str = "" | ||
portal_name2: str = "" | ||
entrance1 = None | ||
entrance2 = None | ||
for portal in portal_mapping: | ||
if portal.scene_destination() == portal1: | ||
portal_name1 = portal.name | ||
if portal.scene_destination() == portal2: | ||
portal_name2 = portal.name | ||
|
||
for entrance_name, entrance in entrance_dict.items(): | ||
if entrance_name.startswith(portal_name1): | ||
entrance1 = entrance | ||
if entrance_name.startswith(portal_name2): | ||
entrance2 = entrance | ||
if entrance1 is None: | ||
raise Exception("entrance1 not found, portal1 is " + portal1) | ||
if entrance2 is None: | ||
raise Exception("entrance2 not found, portal2 is " + portal2) | ||
entrance1.connected_region = entrance2.parent_region | ||
entrance2.connected_region = entrance1.parent_region |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
# TUNIC | ||
|
||
## Where is the settings page? | ||
|
||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file. | ||
|
||
## What does randomization do to this game? | ||
|
||
In the TUNIC Randomizer, every item in the game is randomized. All chests, key item pickups, instruction manual pages, hero relics, | ||
and other unique items are shuffled.<br> | ||
|
||
Ability shuffling is an option available from the settings page to shuffle certain abilities (prayer, holy cross, and the ice rod combo), | ||
preventing them from being used until they are unlocked.<br> | ||
|
||
Enemy randomization and other options are also available and can be turned on in the client mod. | ||
|
||
## What is the goal of TUNIC when randomized? | ||
The standard goal is the same as the vanilla game, which is to find the three hexagon keys, at which point you may either Take Your | ||
Rightful Place or seek another path and Share Your Wisdom. | ||
|
||
Alternatively, Hexagon Quest is a mode that shuffles a certain number of Gold Questagons into the item pool, with the goal | ||
being to find the required amount of them and then Share Your Wisdom. | ||
|
||
## What items from TUNIC can appear in another player's world? | ||
Every item has a chance to appear in another player's world. | ||
|
||
## How many checks are in TUNIC? | ||
There are 302 checks located across the world of TUNIC. | ||
|
||
## What do items from other worlds look like in TUNIC? | ||
Items belonging to other TUNIC players will either appear as that item directly (if in a freestanding location) or in a | ||
chest with the original chest texture for that item. | ||
|
||
Items belonging to non-TUNIC players will either appear as a question-mark block (if in a freestanding location) or in a chest with | ||
a question mark symbol on it. Additionally, non-TUNIC items are color-coded by classification, with green for filler, blue for useful, and gold for progression. | ||
|
||
## Is there a tracker pack? | ||
There is a [tracker pack](https://github.com/SapphireSapphic/TunicTracker/releases/latest). It is compatible with both Poptracker and Emotracker. Using Poptracker, it will automatically track checked locations and important items received. It can also automatically tab between maps as you traverse the world. This tracker was originally created by SapphireSapphic and ScoutJD, and has been extensively updated by Br00ty. | ||
|
||
There is also a [standalone item tracker](https://github.com/radicoon/tunic-rando-tracker/releases/latest), which tracks what items you have received. It is great for adding an item overlay to streaming setups. This item tracker was created by Radicoon. | ||
|
||
## What should I know regarding logic? | ||
- Nighttime is not considered in logic. Every check in the game is obtainable during the day. | ||
- The Cathedral is accessible during the day by using the Hero's Laurels to reach the Overworld fuse near the Swamp entrance. | ||
- The Secret Legend chest at the Cathedral can be obtained during the day by opening the Holy Cross door from the outside. | ||
|
||
For Entrance Rando specifically: | ||
- Activating a fuse to turn on a yellow teleporter pad also activates its counterpart in the Far Shore. | ||
- The West Garden fuse can be activated from below. | ||
- You can pray at the tree at the exterior of the Library. | ||
- The elevators in the Rooted Ziggurat only go down. | ||
- The portal in the trophy room of the Old House is active from the start. | ||
- The elevator in Cathedral is immediately usable without activating the fuse. Activating the fuse does nothing. | ||
|
||
## What item groups are there? | ||
Bombs, consumables (non-bomb ones), weapons, melee weapons (stick and sword), keys, hexagons, offerings, hero relics, cards, golden treasures, money, pages, and abilities (the three ability pages). There are also a few groups being used for singular items: laurels, orb, dagger, magic rod, holy cross, prayer, ice rod, and progressive sword. | ||
|
||
## What location groups are there? | ||
Holy cross (for all holy cross checks), fairies (for the two fairy checks), well (for the coin well checks), and shop. Additionally, for checks that do not fall into the above categories, the name of the region is the name of the location group. |
Oops, something went wrong.