Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loonyland Halloween Hill: implement new game #4427

Open
wants to merge 61 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
38f81c9
Base init and folder
AutomaticFrenzy Dec 6, 2024
3e37062
Add base docs
AutomaticFrenzy Dec 6, 2024
f63ec52
add base files
AutomaticFrenzy Dec 6, 2024
6ad74ba
base items commit
AutomaticFrenzy Dec 6, 2024
082dda1
forgot to save
AutomaticFrenzy Dec 6, 2024
ba96272
Ids for Items
AutomaticFrenzy Dec 6, 2024
f84b048
location ids and categories
AutomaticFrenzy Dec 6, 2024
296051d
python syntax
AutomaticFrenzy Dec 6, 2024
2746992
add logical regions
AutomaticFrenzy Dec 6, 2024
794f767
base options
AutomaticFrenzy Dec 6, 2024
9b14761
naming, typos
AutomaticFrenzy Dec 6, 2024
015d2fa
base init.py
AutomaticFrenzy Dec 6, 2024
0481d0a
renaming, lots of work in init.py
AutomaticFrenzy Dec 7, 2024
4d14feb
naming locations to avoid duplicates
AutomaticFrenzy Dec 7, 2024
2f04521
Entrance Information
AutomaticFrenzy Dec 7, 2024
4bbf9a7
remove entrance rules, put it in entrance table
AutomaticFrenzy Dec 7, 2024
9bbcc88
start adding exits to regions
AutomaticFrenzy Dec 7, 2024
9b916f6
entrance adding to region code
AutomaticFrenzy Dec 7, 2024
d738ebf
Name fixing
AutomaticFrenzy Dec 7, 2024
0883679
hey its generating
AutomaticFrenzy Dec 7, 2024
ea1e364
Should be working
AutomaticFrenzy Dec 7, 2024
ac66dde
fix tests
AutomaticFrenzy Dec 7, 2024
f5a333f
move logic to regions and clean up rules file
AutomaticFrenzy Dec 7, 2024
bd926f3
Renaming zones, starting on extra subregions for ER
AutomaticFrenzy Dec 11, 2024
83f4f01
auto formatting pass
AutomaticFrenzy Dec 11, 2024
0ad2fb4
More entrance/region work
AutomaticFrenzy Dec 11, 2024
1f975e1
1.0 working
AutomaticFrenzy Dec 12, 2024
2bef9e7
switch to auto generated data objects
AutomaticFrenzy Dec 14, 2024
4bad19b
add difficulty as an option
AutomaticFrenzy Dec 15, 2024
e433371
autogenerated data
AutomaticFrenzy Dec 17, 2024
adbcbb8
Send deathlink info
AutomaticFrenzy Dec 19, 2024
39d265e
Simplify entrance data
AutomaticFrenzy Dec 20, 2024
615c4b4
Cleanup
AutomaticFrenzy Dec 20, 2024
3de6e9d
Merge branch 'ArchipelagoMW:main' into ArchipelagoLoonyland
AutomaticFrenzy Dec 20, 2024
d97ef28
add base id to location
AutomaticFrenzy Dec 20, 2024
03a470f
Renaming, prettifying, etc
AutomaticFrenzy Dec 21, 2024
4f42ef3
ruff format
AutomaticFrenzy Dec 21, 2024
a1799b8
junk filler
AutomaticFrenzy Dec 22, 2024
ae74b5d
Doll support
AutomaticFrenzy Dec 23, 2024
15f6a9c
Handling more options
AutomaticFrenzy Dec 24, 2024
33676db
power level, monster doll stuff, options handling
AutomaticFrenzy Dec 25, 2024
d62b4eb
more logic to items and locations
AutomaticFrenzy Dec 27, 2024
dd0e10a
add some locs as postgame
AutomaticFrenzy Dec 28, 2024
70d6881
add witch reward to item pool
AutomaticFrenzy Dec 28, 2024
3e05563
data updates
AutomaticFrenzy Dec 28, 2024
2a08f5f
logic updates for long tag on two badges and ghost potion in haunted …
AutomaticFrenzy Dec 31, 2024
c0e05d7
logic updates
AutomaticFrenzy Jan 1, 2025
c2a192e
remove rerunning default test, run_default_tests takes care of it
AutomaticFrenzy Jan 1, 2025
ead3f5b
data updates
AutomaticFrenzy Jan 2, 2025
b1c2cd9
guide updates
AutomaticFrenzy Jan 2, 2025
69cd24d
Merge branch 'main' of https://github.com/ArchipelagoMW/Archipelago i…
AutomaticFrenzy Jan 2, 2025
257dc98
relative imports
AutomaticFrenzy Jan 2, 2025
f3c9edb
add VANILLA_POSTGAME for badges that are postgame locs in BADGES_VANILLA
AutomaticFrenzy Jan 3, 2025
820a32d
better game description, removing comment, simplifying region line
AutomaticFrenzy Jan 3, 2025
2cc01eb
rename a cool filler item to its effect, "Max life and gems"
AutomaticFrenzy Jan 3, 2025
9c23ffd
Update worlds/loonyland/__init__.py
AutomaticFrenzy Jan 3, 2025
e5c27e7
Options page is now using rich text
AutomaticFrenzy Jan 12, 2025
a1d9294
Change flags from being a list of strings into a enum.flag
AutomaticFrenzy Jan 12, 2025
778f54b
Merge branch 'main' of https://github.com/ArchipelagoMW/Archipelago i…
AutomaticFrenzy Jan 12, 2025
9381051
remove "world" references from the rules, just pass in player
AutomaticFrenzy Jan 12, 2025
8214b9c
formatting updates for options descriptions
AutomaticFrenzy Jan 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,309 changes: 1,309 additions & 0 deletions worlds/loonyland/Data/game_data.py

Large diffs are not rendered by default.

166 changes: 166 additions & 0 deletions worlds/loonyland/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
from BaseClasses import ItemClassification, Region, Tutorial

from worlds.AutoWorld import WebWorld, World

from .Data.game_data import (
VAR_WBOMBS,
VAR_WHOTPANTS,
loony_item_table,
loonyland_location_table,
loonyland_region_table,
set_entrance_rules,
set_rules,
)
from .Data.game_data import (
ll_base_id as loonyland_base_id,
)
from .flags import LLFlags
from .items import LLItemCat, LoonylandItem
from .locations import LLLocCat, LoonylandLocation
from .options import LoonylandOptions, WinCondition
from .rules import have_x_badges


class LoonylandWebWorld(WebWorld):
rich_text_options_doc = True
theme = "dirt"

setup_en = Tutorial(
tutorial_name="Start Guide",
description="A guide to playing Loonyland.",
language="English",
file_name="setup_en.md",
link="setup/en",
authors=["AutomaticFrenzy"],
)

tutorials = [setup_en]


class LoonylandWorld(World):
"""
Loonyland: Halloween Hill is an action-adventure game,
where you must explore to improve Loony's abilities and gain
access to ever more dangerous areas, all in an effort to find out
what lies behind the madness going on in Halloween Hill.
"""

game = "Loonyland"
web = LoonylandWebWorld()
options: LoonylandOptions
options_dataclass = LoonylandOptions
location_name_to_id = {name: data.id + loonyland_base_id for name, data in loonyland_location_table.items()}
item_name_to_id = {name: data.id for name, data in loony_item_table.items()}
item_name_to_id["Max Life and Gems"] = loonyland_base_id + 3000

item_name_groups = {
"physical_items": {name for name, data in loony_item_table.items() if data.category == LLItemCat.ITEM},
"monster_dolls": {name for name, data in loony_item_table.items() if data.category == LLItemCat.DOLL},
"cheats": {name for name, data in loony_item_table.items() if data.category == LLItemCat.CHEAT},
"special_weapons": {
name
for name, data in loony_item_table.items()
if VAR_WBOMBS <= data.id - loonyland_base_id <= VAR_WHOTPANTS
},
"power": {name for name, data in loony_item_table.items() if LLFlags.PWR in data.flags},
"power_big": {name for name, data in loony_item_table.items() if LLFlags.PWR_BIG in data.flags},
"power_max": {name for name, data in loony_item_table.items() if LLFlags.PWR_MAX in data.flags},
}

location_name_groups = {
"quests": {name for name, data in loonyland_location_table.items() if data.category == LLLocCat.QUEST},
"badges": {name for name, data in loonyland_location_table.items() if data.category == LLLocCat.BADGE},
}

def create_junk(self) -> LoonylandItem:
return LoonylandItem("Max Life and Gems", ItemClassification.filler, loonyland_base_id + 3000, self.player)

def create_item(self, name: str) -> LoonylandItem:
if name == "Max Life and Gems":
return self.create_junk()
return LoonylandItem(
name, loony_item_table[name].modified_classification(self.options), loony_item_table[name].id, self.player
)

def create_items(self) -> None:
item_pool: list[LoonylandItem] = []
for name, item in loony_item_table.items():
if item.id and item.can_create(self.options) and item.in_logic(self.options):
for i in range(item.frequency):
new_item = self.create_item(name)
item_pool.append(new_item)

junk_len = len(self.multiworld.get_unfilled_locations(self.player)) - len(item_pool) - 1
if self.options.win_condition == WinCondition.option_evilizer:
junk_len = junk_len - 1
item_pool += [self.create_junk() for _ in range(junk_len)]

self.multiworld.itempool += item_pool

def create_event(self, event: str) -> LoonylandItem:
return LoonylandItem(event, ItemClassification.progression, None, self.player)

def create_regions(self) -> None:
# print(self.player_name)
# for key in LoonylandOptions.__annotations__:
# print(key, getattr(self.options, key))

for region_name, region_data in loonyland_region_table.items():
if region_data.can_create(self.options):
region = Region(region_name, self.player, self.multiworld)
self.multiworld.regions.append(region)

for loc_name, loc_data in loonyland_location_table.items():
if not loc_data.can_create(self.options):
continue
region = self.get_region(loc_data.region)
new_loc = LoonylandLocation(self.player, loc_name, loc_data.id + loonyland_base_id, region)
if not loc_data.in_logic(self.options):
new_loc.place_locked_item(self.create_event(loc_data.base_item))
new_loc.address = None
region.locations.append(new_loc)
if loc_data.category == LLLocCat.BADGE:
new_loc_as_event = LoonylandLocation(self.player, loc_name + " Earned", None, region)
new_loc_as_event.place_locked_item(self.create_event("BadgeEarned"))
region.locations.append(new_loc_as_event)

def set_rules(self):
# Completion condition.
final_loc = None
if self.options.win_condition == WinCondition.option_evilizer:
final_loc = self.get_location("Q: Save Halloween Hill")
final_loc.address = None
elif self.options.win_condition == WinCondition.option_badges:
final_loc = LoonylandLocation(
self.player, str(self.options.badges_required.value) + " Badges Earned", None, self.get_region("Menu")
)
final_loc.access_rule = lambda state: have_x_badges(state, self.player, self.options.badges_required.value)
self.get_region("Menu").locations.append(final_loc)
else: # no win con
final_loc = self.get_location("Swamp: Outside Luniton")

final_loc.place_locked_item(self.create_event("Victory"))
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)

# force torch at curse the darkness
torch_loc = self.get_location("Q: Curse The Darkness")
torch_loc.place_locked_item(self.create_item("Torch"))

# location rules
set_rules(self.multiworld, self)
# entrance rules
set_entrance_rules(self.multiworld, self)

def fill_slot_data(self):
return {
"WinCondition": self.options.win_condition.value,
"BadgesRequired": self.options.badges_required.value,
"Difficulty": self.options.difficulty.value,
"LongChecks": self.options.long_checks.value,
"MultipleSaves": self.options.multisave.value,
"Remix": self.options.remix.value,
"OverpoweredCheats": self.options.overpowered_cheats.value,
"Badges": self.options.badges.value,
"Dolls": self.options.dolls.value,
"DeathLink": self.options.death_link.value,
}
43 changes: 43 additions & 0 deletions worlds/loonyland/docs/en_Loonyland.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Loonyland: Halloween Hill

## Where is the options page?

[Here](../player-options)

## What does randomization do to this game?

Currently it randomizes all of the physical items placed in the world and some of the quest rewards.

Adding in badges, monster dolls, other quests, other game modes, etc is planned


## What is the goal of LOONYLAND when randomized?

Beating the evilizer!!
That means getting to the top of castle vampy with the 8 vampire statues, so you can open the portal to the roof.

## Which items can be in another player's world?

All inventory items, plus the 100 gem reward for saving zombiton

## What is considered a location check in Loonyland?

Picking up an item found in a game map, as well as completing the following quests:
Tree Trimming, Scaredy Cat, Sticky Shoes, Mushroom Hunt, Zombie Stomp, Smashing Pumpkins, Silver Bullet, Hairy Larry, Ghostbusting, The Rescue, The Collection

## Why isn't my item where the spoiler says it should be?

Uh oh

## What does another world's item look like in Loonyland?

It will look like the archipelago logo, rendered as a cluster of colored gems

## When the player receives an item, what happens?

A message pops up in the bottom left informing them of the item and who sent it.

## What are recommended options to tweak for beginners to the rando?

Playing with badges set to none and difficulty set to beginner/normal.
Evilizer win con.
23 changes: 23 additions & 0 deletions worlds/loonyland/docs/setup_en.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Setup Guide for Loonyland: Halloween Hill


## Required Software

- [Loonyland AP World](https://github.com/AutomaticFrenzy/Archipelago/releases)
- [Loonyland Client](https://github.com/AutomaticFrenzy/HamSandwich/releases)

## Optional Software

- [Loonyland AP pack for Poptracker](https://github.com/AutomaticFrenzy/loonyland_archipelago_tracker/releases/tag/v1.01)

## Joining a Multiworld Game

1. Launch Loonyland
2. Enter the address, slot name, and password
3. Press "Connect"

## Archipelago Text Client

Having the text client open is recommend to see a log of what has been sent.


27 changes: 27 additions & 0 deletions worlds/loonyland/entrances.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from typing import NamedTuple

from BaseClasses import Entrance

from worlds.generic.Rules import CollectionRule

from .flags import LLFlags
from .options import Badges, LoonylandOptions, Remix


class LoonylandEntrance(Entrance):
game = "Loonyland"


class LLEntrance(NamedTuple):
source_region: str
target_region: str
is_real_loading_zone: bool
rule: CollectionRule = lambda state: True
flags: LLFlags = LLFlags.NONE

def can_create(self, options: LoonylandOptions) -> bool:
if options.badges == Badges.option_none and LLFlags.MODE in self.flags:
return False
if options.remix == Remix.option_excluded and LLFlags.REMIX in self.flags:
return False
return True
21 changes: 21 additions & 0 deletions worlds/loonyland/flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from enum import Flag, auto


class LLFlags(Flag):
NONE = 0
PWR = auto()
PWR_BIG = auto()
PWR_MAX = auto()
OP = auto()

TORCH = auto()

DOLL = auto()
LONG = auto()
LONG_VANILLA_BADGES = auto()
MULTISAVE = auto()
POSTGAME = auto()
VANILLA_POSTGAME = auto()

MODE = auto()
REMIX = auto()
83 changes: 83 additions & 0 deletions worlds/loonyland/items.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from enum import Enum
from typing import NamedTuple

from BaseClasses import Item, ItemClassification

from .flags import LLFlags
from .options import (
Badges,
LongChecks,
LoonylandOptions,
MonsterDolls,
MultipleSaves,
OverpoweredCheats,
Remix,
)


class LoonylandItem(Item):
"""
Item from the game Loonyland
"""

game: str = "Loonyland"


class LLItemCat(Enum):
ITEM = 0
CHEAT = 1
FILLER = 2
TRAP = 3
EVENT = 4
ACCESS = 5
DOLL = 6


class LLItem(NamedTuple):
id: int
category: LLItemCat
classification: ItemClassification
frequency: int = 1
flags: LLFlags = LLFlags.NONE

def can_create(self, options: LoonylandOptions) -> bool:
if (self.category == LLItemCat.CHEAT and options.badges == Badges.option_vanilla) or (
self.category == LLItemCat.DOLL and options.dolls == MonsterDolls.option_vanilla
):
return False
if options.overpowered_cheats == OverpoweredCheats.option_excluded and LLFlags.OP in self.flags:
return False
if options.remix == Remix.option_excluded and (LLFlags.REMIX in self.flags):
return False
if options.multisave == MultipleSaves.option_disabled and (LLFlags.MULTISAVE in self.flags):
return False
if LLFlags.TORCH in self.flags:
return False
return True

def in_logic(self, options: LoonylandOptions) -> bool:
if self.category == LLItemCat.CHEAT and options.badges == Badges.option_none:
return False
if self.category == LLItemCat.DOLL and options.dolls == MonsterDolls.option_none:
return False
return True

def modified_classification(self, options: LoonylandOptions):
if options.long_checks == LongChecks.option_included:
# if self.category == LLItemCat.CHEAT: # 39 badges
# return ItemClassification.progression
if self.category == LLItemCat.ITEM: # 100%
return ItemClassification.progression
if self.category == LLItemCat.DOLL: # 100%
return ItemClassification.progression
if LLFlags.LONG in self.flags: # items that are required for long checks
return ItemClassification.progression
if options.badges == Badges.option_none:
if self.category == LLItemCat.ACCESS:
return ItemClassification.filler
if (
LLFlags.PWR in self.flags or LLFlags.PWR_BIG in self.flags or LLFlags.PWR_MAX in self.flags
): # need to be able to kill bosses, eventually an option for this
return ItemClassification.progression

return self.classification
Loading
Loading