From 344f4afdbdfa4c7a5f462e9a8e919aea81b7f708 Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Fri, 21 Jan 2022 22:42:11 +0100 Subject: [PATCH] Add VVVVVV to Archipelago (#178) --- .../static/assets/gameInfo/en_VVVVVV.md | 36 ++++++++++++++ .../static/assets/tutorial/tutorials.json | 19 ++++++++ .../static/assets/tutorial/v6/setup_en.md | 37 +++++++++++++++ worlds/v6/Items.py | 27 +++++++++++ worlds/v6/Locations.py | 27 +++++++++++ worlds/v6/Options.py | 20 ++++++++ worlds/v6/Regions.py | 45 ++++++++++++++++++ worlds/v6/Rules.py | 34 ++++++++++++++ worlds/v6/__init__.py | 47 +++++++++++++++++++ 9 files changed, 292 insertions(+) create mode 100644 WebHostLib/static/assets/gameInfo/en_VVVVVV.md create mode 100644 WebHostLib/static/assets/tutorial/v6/setup_en.md create mode 100644 worlds/v6/Items.py create mode 100644 worlds/v6/Locations.py create mode 100644 worlds/v6/Options.py create mode 100644 worlds/v6/Regions.py create mode 100644 worlds/v6/Rules.py create mode 100644 worlds/v6/__init__.py diff --git a/WebHostLib/static/assets/gameInfo/en_VVVVVV.md b/WebHostLib/static/assets/gameInfo/en_VVVVVV.md new file mode 100644 index 00000000..5c2aa8fe --- /dev/null +++ b/WebHostLib/static/assets/gameInfo/en_VVVVVV.md @@ -0,0 +1,36 @@ +# VVVVVV + +## Where is the settings page? + +The player settings page for this game contains all the options you need to configure and export a config file. Player +settings page link: [VVVVVV Player Settings Page](../player-settings). + +## What does randomization do to this game? +All 20 Trinkets are now Location Checks and may not actually contain Trinkets, but Items for different games. + +Optionally, you may enable DoorCost, which will gate away some areas: +- Laboratory +- The Tower +- Space Station 2 and +- Warp Zone +until you've collected some Trinkets. +Examples: +- If you set DoorCost at 2, then to enter Laboratory you will need Trinkets 1-2, for The Tower 3-4, etc. +- If you set DoorCost at 3, then to enter Laboratory you will need Trinkets 1-3, for The Tower 4-6, etc. + +## What is the goal of VVVVVV when randomized? +Save all crew members, and finish the story. + +## Which items can be in another player's world? +Any of the 20 Trinkets. + +## What does another world's item look like in VVVVVV? +The Trinkets are visually unchanged, though after collecting a textbox will pop up to inform you what you collected, +and who will receive it. + +## When the player receives an item, what happens? +When you receive a Trinket, the standard Animation will play. Afterwards a textbox will inform you where +you received the Trinket from, and which one it is. + +NOTE: You can't check your trinkets in the Spaceship. Instead, you can check them in the pause menu under 'Stats'. +This is especially useful if you have DoorCost enabled. \ No newline at end of file diff --git a/WebHostLib/static/assets/tutorial/tutorials.json b/WebHostLib/static/assets/tutorial/tutorials.json index a505bba5..589d2215 100644 --- a/WebHostLib/static/assets/tutorial/tutorials.json +++ b/WebHostLib/static/assets/tutorial/tutorials.json @@ -441,5 +441,24 @@ ] } ] + }, + { + "gameTitle": "VVVVVV", + "tutorials": [ + { + "name": "Multiworld Setup Guide", + "description": "A guide to setting up VVVVVV for MultiWorld.", + "files": [ + { + "language": "English", + "filename": "v6/setup_en.md", + "link": "v6/setup/en", + "authors": [ + "N00byKing" + ] + } + ] + } + ] } ] diff --git a/WebHostLib/static/assets/tutorial/v6/setup_en.md b/WebHostLib/static/assets/tutorial/v6/setup_en.md new file mode 100644 index 00000000..c4b28fde --- /dev/null +++ b/WebHostLib/static/assets/tutorial/v6/setup_en.md @@ -0,0 +1,37 @@ +# VVVVVV MultiWorld Setup Guide + +## Required Software + +- VVVVVV (Bought from the [Steam Store](https://store.steampowered.com/app/70300/VVVVVV/) or [GOG Store](https://www.gog.com/game/vvvvvv) Page, NOT Make and Play Edition!) +- [V6AP](https://github.com/N00byKing/VVVVVV/actions/workflows/ci.yml?query=branch%3Aarchipelago) + +## Installation and Game Start Procedures + +1. Install VVVVVV through either Steam or GOG +2. Go to the page linked for V6AP, and press on the topmost entry +3. Scroll down, and download the zip file corresponding to your platform (NOTE: Linux currently does not build automatically. Linux users will have to compile manually for now. Mac is unsupported, but may work if [APCpp](https://github.com/N00byKing/APCpp) is compiled and supplied) +4. Unpack the zip file where you have VVVVVV installed. + +# Joining a MultiWorld Game + +To join, set the following launch options: `-v6ap_name "YourName" -v6ap_ip "ServerIP"`. +Optionally, add `-v6ap_passwd "YourPassword"` if the room you are using requires a password. All parameters without quotation marks. +The Name in this case is the one specified in your generated .yaml file. +In case you are using the Archipelago Website, the IP should be `archipelago.gg`. + +If everything worked out, you will see a textbox informing you the connection has been established after the story intro. + +## Installation Troubleshooting + +Start the game from the command line to view helpful messages regarding V6AP. These will look something like "V6AP: Message" + +### Game no longer starts after copying the .exe + +Most likely you forgot to set the launch options. `-v6ap_name "YourName"` and `-v6ap_ip "ServerIP"` are required for startup. + +## Game Troubleshooting + +### What happens if I lose connection? + +V6AP tries to reconnect a few times, so be patient. +Should the problem still be there after about a minute or two, just save and restart the game. diff --git a/worlds/v6/Items.py b/worlds/v6/Items.py new file mode 100644 index 00000000..b28b00fe --- /dev/null +++ b/worlds/v6/Items.py @@ -0,0 +1,27 @@ +from BaseClasses import Item + +class V6Item(Item): + game: str = "VVVVVV" + +item_table = { + "Trinket 01": 2515000, + "Trinket 02": 2515001, + "Trinket 03": 2515002, + "Trinket 04": 2515003, + "Trinket 05": 2515004, + "Trinket 06": 2515005, + "Trinket 07": 2515006, + "Trinket 08": 2515007, + "Trinket 09": 2515008, + "Trinket 10": 2515009, + "Trinket 11": 2515010, + "Trinket 12": 2515011, + "Trinket 13": 2515012, + "Trinket 14": 2515013, + "Trinket 15": 2515014, + "Trinket 16": 2515015, + "Trinket 17": 2515016, + "Trinket 18": 2515017, + "Trinket 19": 2515018, + "Trinket 20": 2515019 +} \ No newline at end of file diff --git a/worlds/v6/Locations.py b/worlds/v6/Locations.py new file mode 100644 index 00000000..6af03969 --- /dev/null +++ b/worlds/v6/Locations.py @@ -0,0 +1,27 @@ +from BaseClasses import Location + +class V6Location(Location): + game: str = "VVVVVV" + +location_table = { # Correspond to 2515000 + index in collect array of game code + "It's a Secret to Nobody": 2515000, + "Trench Warfare": 2515001, + "One Way Room": 2515002, + "You Just Keep Coming Back": 2515003, + "Clarion Call": 2515004, + "Doing things the hard way": 2515005, + "Prize for the Reckless": 2515006, + "The Tower 1": 2515007, + "The Tower 2": 2515008, + "Young Man, It's Worth the Challenge": 2515009, + "The Tantalizing Trinket": 2515010, + "Purest Unobtainium": 2515011, + "Edge Games": 2515012, + "Overworld (Pipe-shaped Segment)": 2515013, + "Overworld (Outside Entanglement Generator)": 2515014, + "Overworld (Left of Ship)": 2515015, + "Overworld (Square Room)": 2515016, + "Overworld (Sad Elephant)": 2515017, + "NPC Trinket": 2515018, + "V": 2515019 +} diff --git a/worlds/v6/Options.py b/worlds/v6/Options.py new file mode 100644 index 00000000..fac891ba --- /dev/null +++ b/worlds/v6/Options.py @@ -0,0 +1,20 @@ +import typing +from Options import Option, DeathLink, Range + +class DoorCost(Range): + """Amount of Trinkets required to enter Areas. Set to 0 to disable artificial locks.""" + range_start = 0 + range_end = 3 + default = 3 + +class DeathLinkAmnesty(Range): + """Amount of Deaths to take before sending a DeathLink signal, for balancing difficulty""" + range_start = 0 + range_end = 30 + default = 15 + +v6_options: typing.Dict[str,type(Option)] = { + "DoorCost": DoorCost, + "DeathLink": DeathLink, + "DeathLinkAmnesty": DeathLinkAmnesty +} \ No newline at end of file diff --git a/worlds/v6/Regions.py b/worlds/v6/Regions.py new file mode 100644 index 00000000..b1c422ba --- /dev/null +++ b/worlds/v6/Regions.py @@ -0,0 +1,45 @@ +import typing +from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType +from .Locations import V6Location, location_table + +def create_regions(world: MultiWorld, player: int): + regOvr = Region("Menu", RegionType.Generic, "Dimension VVVVVV", player, world) + locOvr_names = ["Overworld (Pipe-shaped Segment)", "Overworld (Left of Ship)", "Overworld (Square Room)", "Overworld (Sad Elephant)", + "It's a Secret to Nobody", "Trench Warfare", "NPC Trinket"] + regOvr.locations += [V6Location(player, loc_name, location_table[loc_name], regOvr) for loc_name in locOvr_names] + world.regions.append(regOvr) + + regLab = Region("Laboratory", RegionType.Generic, "Laboratory", player, world) + locLab_names = ["Young Man, It's Worth the Challenge", "Overworld (Outside Entanglement Generator)", "The Tantalizing Trinket", "Purest Unobtainium"] + regLab.locations += [V6Location(player, loc_name, location_table[loc_name], regLab) for loc_name in locLab_names] + world.regions.append(regLab) + + regTow = Region("The Tower", RegionType.Generic, "The Tower", player, world) + locTow_names = ["The Tower 1", "The Tower 2"] + regTow.locations += [V6Location(player, loc_name, location_table[loc_name], regTow) for loc_name in locTow_names] + world.regions.append(regTow) + + regSp2 = Region("Space Station 2", RegionType.Generic, "Space Station 2", player, world) + locSp2_names = ["One Way Room", "You Just Keep Coming Back", "Clarion Call", "Prize for the Reckless", "Doing things the hard way"] + regSp2.locations += [V6Location(player, loc_name, location_table[loc_name], regSp2) for loc_name in locSp2_names] + world.regions.append(regSp2) + + regWrp = Region("Warp Zone", RegionType.Generic, "Warp Zone", player, world) + locWrp_names = ["Edge Games"] + regWrp.locations += [V6Location(player, loc_name, location_table[loc_name], regWrp) for loc_name in locWrp_names] + world.regions.append(regWrp) + + regEnd = Region("The Final Level", RegionType.Generic, "The Final Level", player, world) + locEnd_names = ["V"] + regEnd.locations += [V6Location(player, loc_name, location_table[loc_name], regEnd) for loc_name in locEnd_names] + world.regions.append(regEnd) + +def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule): + sourceRegion = world.get_region(source, player) + targetRegion = world.get_region(target, player) + + connection = Entrance(player,'', sourceRegion) + connection.access_rule = rule + + sourceRegion.exits.append(connection) + connection.connect(targetRegion) \ No newline at end of file diff --git a/worlds/v6/Rules.py b/worlds/v6/Rules.py new file mode 100644 index 00000000..1daa8d38 --- /dev/null +++ b/worlds/v6/Rules.py @@ -0,0 +1,34 @@ +import typing +from ..generic.Rules import add_rule +from .Regions import connect_regions + +def _has_trinket_range(state,player,start,end): + for i in range(start+1,end+1): + if (not state.has("Trinket " + str(i).zfill(2), player)): + return False + return True + +def create_npctrinket_rules(world,location,player): + add_rule(location, lambda state: state.can_reach(world.get_region("Laboratory",player),'Region',player) or + state.can_reach(world.get_region("Space Station 2",player),'Region',player)) + +def set_rules(world,player): + if (world.DoorCost[player].value == 0): pass + connect_regions(world, player, "Menu", "Laboratory", lambda state: _has_trinket_range(state,player,0,world.DoorCost[player].value)) + connect_regions(world, player, "Menu", "The Tower", lambda state: _has_trinket_range(state,player,world.DoorCost[player].value,world.DoorCost[player].value*2)) + connect_regions(world, player, "Menu", "Space Station 2", lambda state: _has_trinket_range(state,player,world.DoorCost[player].value*2,world.DoorCost[player].value*3)) + connect_regions(world, player, "Menu", "Warp Zone", lambda state: _has_trinket_range(state,player,world.DoorCost[player].value*3,world.DoorCost[player].value*4)) + + connect_regions(world, player, "Menu", "The Final Level", lambda state : state.can_reach("Laboratory",'Region',player) and + state.can_reach("The Tower",'Region',player) and + state.can_reach("Space Station 2",'Region',player) and + state.can_reach("Warp Zone",'Region',player)) + + connect_regions(world, player, "Laboratory", "Menu", lambda state: True) + connect_regions(world, player, "The Tower", "Menu", lambda state: True) + connect_regions(world, player, "Space Station 2", "Menu", lambda state: True) + connect_regions(world, player, "Warp Zone", "Menu", lambda state: True) + connect_regions(world, player, "The Final Level", "Menu", lambda state: True) + + create_npctrinket_rules(world,world.get_location("NPC Trinket",player),player) + world.completion_condition[player] = lambda state: state.can_reach(world.get_region("The Final Level",player),'Region',player) \ No newline at end of file diff --git a/worlds/v6/__init__.py b/worlds/v6/__init__.py new file mode 100644 index 00000000..878b53c0 --- /dev/null +++ b/worlds/v6/__init__.py @@ -0,0 +1,47 @@ +import string +from .Items import item_table, V6Item +from .Locations import location_table, V6Location +from .Options import v6_options +from .Rules import set_rules +from .Regions import create_regions +from BaseClasses import Region, RegionType, Entrance, Item, MultiWorld +from ..AutoWorld import World + +client_version = 1 + +class V6World(World): + """ + VVVVVV is a platform game all about exploring one simple mechanical idea - what if you reversed gravity instead of jumping? + """ #Lifted from Store Page + + game: str = "VVVVVV" + topology_present = False + + item_name_to_id = item_table + location_name_to_id = location_table + + data_version = 1 + forced_auto_forfeit = False + + options = v6_options + + def create_regions(self): + create_regions(self.world,self.player) + + def set_rules(self): + set_rules(self.world,self.player) + + def create_item(self, name: str) -> Item: + item_id = item_table[name] + item = V6Item(name, True, item_id, self.player) + return item + + def generate_basic(self): + self.world.itempool += [self.create_item(name) for name in self.item_names] + + def fill_slot_data(self): + return { + "DoorCost": self.world.DoorCost[self.player].value, + "DeathLink": self.world.DeathLink[self.player].value, + "DeathLink_Amnesty": self.world.DeathLinkAmnesty[self.player].value + }