2024-03-15 16:33:03 +00:00
import typing
from dataclasses import fields
from typing import List , Set , Iterable , Sequence , Dict , Callable , Union
from math import floor , ceil
from BaseClasses import Item , MultiWorld , Location , Tutorial , ItemClassification
from worlds . AutoWorld import WebWorld , World
from . import ItemNames
from . Items import StarcraftItem , filler_items , get_item_table , get_full_item_list , \
get_basic_units , ItemData , upgrade_included_names , progressive_if_nco , kerrigan_actives , kerrigan_passives , \
kerrigan_only_passives , progressive_if_ext , not_balanced_starting_units , spear_of_adun_calldowns , \
spear_of_adun_castable_passives , nova_equipment
from . ItemGroups import item_name_groups
from . Locations import get_locations , LocationType , get_location_types , get_plando_locations
from . Regions import create_regions
from . Options import get_option_value , LocationInclusion , KerriganLevelItemDistribution , \
KerriganPresence , KerriganPrimalStatus , RequiredTactics , kerrigan_unit_available , StarterUnit , SpearOfAdunPresence , \
get_enabled_campaigns , SpearOfAdunAutonomouslyCastAbilityPresence , Starcraft2Options
from . PoolFilter import filter_items , get_item_upgrades , UPGRADABLE_ITEMS , missions_in_mission_table , get_used_races
from . MissionTables import MissionInfo , SC2Campaign , lookup_name_to_mission , SC2Mission , \
SC2Race
class Starcraft2WebWorld ( WebWorld ) :
Docs, Starcraft 2: Add French documentation for setup and game page (#3031)
* Started to create the french doc
* First version of sc2 setup in french finish, created the file for the introduction of the game in french
* French-fy upgrade in setup, continue translation of game description
* Finish writing FR game page, added a link to it on the english game page. Re-read and corrected both the game page and setup page.
* Corrected a sentence in the SC2 English setup guide.
* Applied 120 carac limits for french part, applied modification for consistency.
* Added reference to website yaml checker, applied several wording correction/suggestions
* Modified link to AP page to be in relative (fr/en), uniformed SC2 and random writing (fr), applied some suggestons in writing quality(fr), added a mention to the datapackage (fr/en), enhanced prog balancing recommendation (fr)
* Correction of some grammar issues
* Removed name correction for english part since done in other PR; added mention to hotkey and language restriction
* Applied suggestions of peer review
* Applied mofications proposed by reviewer about the external website
---------
Co-authored-by: neocerber <neorcerber@gmail.com>
2024-05-29 01:48:52 +00:00
setup_en = Tutorial (
2024-03-15 16:33:03 +00:00
" Multiworld Setup Guide " ,
" A guide to setting up the Starcraft 2 randomizer connected to an Archipelago Multiworld " ,
" English " ,
" setup_en.md " ,
" setup/en " ,
[ " TheCondor " , " Phaneros " ]
)
Docs, Starcraft 2: Add French documentation for setup and game page (#3031)
* Started to create the french doc
* First version of sc2 setup in french finish, created the file for the introduction of the game in french
* French-fy upgrade in setup, continue translation of game description
* Finish writing FR game page, added a link to it on the english game page. Re-read and corrected both the game page and setup page.
* Corrected a sentence in the SC2 English setup guide.
* Applied 120 carac limits for french part, applied modification for consistency.
* Added reference to website yaml checker, applied several wording correction/suggestions
* Modified link to AP page to be in relative (fr/en), uniformed SC2 and random writing (fr), applied some suggestons in writing quality(fr), added a mention to the datapackage (fr/en), enhanced prog balancing recommendation (fr)
* Correction of some grammar issues
* Removed name correction for english part since done in other PR; added mention to hotkey and language restriction
* Applied suggestions of peer review
* Applied mofications proposed by reviewer about the external website
---------
Co-authored-by: neocerber <neorcerber@gmail.com>
2024-05-29 01:48:52 +00:00
setup_fr = Tutorial (
setup_en . tutorial_name ,
setup_en . description ,
" Français " ,
" setup_fr.md " ,
" setup/fr " ,
[ " Neocerber " ]
)
tutorials = [ setup_en , setup_fr ]
2024-03-15 16:33:03 +00:00
class SC2World ( World ) :
"""
StarCraft II is a science fiction real - time strategy video game developed and published by Blizzard Entertainment .
Play as one of three factions across four campaigns in a battle for supremacy of the Koprulu Sector .
"""
game = " Starcraft 2 "
web = Starcraft2WebWorld ( )
item_name_to_id = { name : data . code for name , data in get_full_item_list ( ) . items ( ) }
location_name_to_id = { location . name : location . code for location in get_locations ( None ) }
options_dataclass = Starcraft2Options
options : Starcraft2Options
item_name_groups = item_name_groups
locked_locations : typing . List [ str ]
location_cache : typing . List [ Location ]
mission_req_table : Dict [ SC2Campaign , Dict [ str , MissionInfo ] ] = { }
final_mission_id : int
victory_item : str
required_client_version = 0 , 4 , 5
def __init__ ( self , multiworld : MultiWorld , player : int ) :
super ( SC2World , self ) . __init__ ( multiworld , player )
self . location_cache = [ ]
self . locked_locations = [ ]
def create_item ( self , name : str ) - > Item :
data = get_full_item_list ( ) [ name ]
return StarcraftItem ( name , data . classification , data . code , self . player )
def create_regions ( self ) :
self . mission_req_table , self . final_mission_id , self . victory_item = create_regions (
self , get_locations ( self ) , self . location_cache
)
def create_items ( self ) :
setup_events ( self . player , self . locked_locations , self . location_cache )
excluded_items = get_excluded_items ( self )
starter_items = assign_starter_items ( self , excluded_items , self . locked_locations , self . location_cache )
fill_resource_locations ( self , self . locked_locations , self . location_cache )
pool = get_item_pool ( self , self . mission_req_table , starter_items , excluded_items , self . location_cache )
fill_item_pool_with_dummy_items ( self , self . locked_locations , self . location_cache , pool )
self . multiworld . itempool + = pool
def set_rules ( self ) :
self . multiworld . completion_condition [ self . player ] = lambda state : state . has ( self . victory_item , self . player )
def get_filler_item_name ( self ) - > str :
return self . random . choice ( filler_items )
def fill_slot_data ( self ) :
slot_data = { }
for option_name in [ field . name for field in fields ( Starcraft2Options ) ] :
option = get_option_value ( self , option_name )
if type ( option ) in { str , int } :
slot_data [ option_name ] = int ( option )
slot_req_table = { }
# Serialize data
for campaign in self . mission_req_table :
slot_req_table [ campaign . id ] = { }
for mission in self . mission_req_table [ campaign ] :
slot_req_table [ campaign . id ] [ mission ] = self . mission_req_table [ campaign ] [ mission ] . _asdict ( )
# Replace mission objects with mission IDs
slot_req_table [ campaign . id ] [ mission ] [ " mission " ] = slot_req_table [ campaign . id ] [ mission ] [ " mission " ] . id
for index in range ( len ( slot_req_table [ campaign . id ] [ mission ] [ " required_world " ] ) ) :
# TODO this is a band-aid, sometimes the mission_req_table already contains dicts
# as far as I can tell it's related to having multiple vanilla mission orders
if not isinstance ( slot_req_table [ campaign . id ] [ mission ] [ " required_world " ] [ index ] , dict ) :
slot_req_table [ campaign . id ] [ mission ] [ " required_world " ] [ index ] = slot_req_table [ campaign . id ] [ mission ] [ " required_world " ] [ index ] . _asdict ( )
enabled_campaigns = get_enabled_campaigns ( self )
slot_data [ " plando_locations " ] = get_plando_locations ( self )
slot_data [ " nova_covert_ops_only " ] = ( enabled_campaigns == { SC2Campaign . NCO } )
slot_data [ " mission_req " ] = slot_req_table
slot_data [ " final_mission " ] = self . final_mission_id
slot_data [ " version " ] = 3
if SC2Campaign . HOTS not in enabled_campaigns :
slot_data [ " kerrigan_presence " ] = KerriganPresence . option_not_present
return slot_data
def setup_events ( player : int , locked_locations : typing . List [ str ] , location_cache : typing . List [ Location ] ) :
for location in location_cache :
if location . address is None :
item = Item ( location . name , ItemClassification . progression , None , player )
locked_locations . append ( location . name )
location . place_locked_item ( item )
def get_excluded_items ( world : World ) - > Set [ str ] :
excluded_items : Set [ str ] = set ( get_option_value ( world , ' excluded_items ' ) )
for item in world . multiworld . precollected_items [ world . player ] :
excluded_items . add ( item . name )
locked_items : Set [ str ] = set ( get_option_value ( world , ' locked_items ' ) )
# Starter items are also excluded items
starter_items : Set [ str ] = set ( get_option_value ( world , ' start_inventory ' ) )
item_table = get_full_item_list ( )
soa_presence = get_option_value ( world , " spear_of_adun_presence " )
soa_autocast_presence = get_option_value ( world , " spear_of_adun_autonomously_cast_ability_presence " )
enabled_campaigns = get_enabled_campaigns ( world )
# Ensure no item is both guaranteed and excluded
invalid_items = excluded_items . intersection ( locked_items )
invalid_count = len ( invalid_items )
# Don't count starter items that can appear multiple times
invalid_count - = len ( [ item for item in starter_items . intersection ( locked_items ) if item_table [ item ] . quantity != 1 ] )
if invalid_count > 0 :
raise Exception ( f " { invalid_count } item { ' s are ' if invalid_count > 1 else ' is ' } both locked and excluded from generation. Please adjust your excluded items and locked items. " )
def smart_exclude ( item_choices : Set [ str ] , choices_to_keep : int ) :
expected_choices = len ( item_choices )
if expected_choices == 0 :
return
item_choices = set ( item_choices )
starter_choices = item_choices . intersection ( starter_items )
excluded_choices = item_choices . intersection ( excluded_items )
item_choices . difference_update ( excluded_choices )
item_choices . difference_update ( locked_items )
candidates = sorted ( item_choices )
exclude_amount = min ( expected_choices - choices_to_keep - len ( excluded_choices ) + len ( starter_choices ) , len ( candidates ) )
if exclude_amount > 0 :
excluded_items . update ( world . random . sample ( candidates , exclude_amount ) )
# Nova gear exclusion if NCO not in campaigns
if SC2Campaign . NCO not in enabled_campaigns :
excluded_items = excluded_items . union ( nova_equipment )
kerrigan_presence = get_option_value ( world , " kerrigan_presence " )
# Exclude Primal Form item if option is not set or Kerrigan is unavailable
if get_option_value ( world , " kerrigan_primal_status " ) != KerriganPrimalStatus . option_item or \
( kerrigan_presence in { KerriganPresence . option_not_present , KerriganPresence . option_not_present_and_no_passives } ) :
excluded_items . add ( ItemNames . KERRIGAN_PRIMAL_FORM )
# no Kerrigan & remove all passives => remove all abilities
if kerrigan_presence == KerriganPresence . option_not_present_and_no_passives :
for tier in range ( 7 ) :
smart_exclude ( kerrigan_actives [ tier ] . union ( kerrigan_passives [ tier ] ) , 0 )
else :
# no Kerrigan, but keep non-Kerrigan passives
if kerrigan_presence == KerriganPresence . option_not_present :
smart_exclude ( kerrigan_only_passives , 0 )
for tier in range ( 7 ) :
smart_exclude ( kerrigan_actives [ tier ] , 0 )
# SOA exclusion, other cases are handled by generic race logic
if ( soa_presence == SpearOfAdunPresence . option_lotv_protoss and SC2Campaign . LOTV not in enabled_campaigns ) \
or soa_presence == SpearOfAdunPresence . option_not_present :
excluded_items . update ( spear_of_adun_calldowns )
if ( soa_autocast_presence == SpearOfAdunAutonomouslyCastAbilityPresence . option_lotv_protoss \
and SC2Campaign . LOTV not in enabled_campaigns ) \
or soa_autocast_presence == SpearOfAdunAutonomouslyCastAbilityPresence . option_not_present :
excluded_items . update ( spear_of_adun_castable_passives )
return excluded_items
def assign_starter_items ( world : World , excluded_items : Set [ str ] , locked_locations : List [ str ] , location_cache : typing . List [ Location ] ) - > List [ Item ] :
starter_items : List [ Item ] = [ ]
non_local_items = get_option_value ( world , " non_local_items " )
starter_unit = get_option_value ( world , " starter_unit " )
enabled_campaigns = get_enabled_campaigns ( world )
first_mission = get_first_mission ( world . mission_req_table )
# Ensuring that first mission is completable
if starter_unit == StarterUnit . option_off :
starter_mission_locations = [ location . name for location in location_cache
if location . parent_region . name == first_mission
and location . access_rule == Location . access_rule ]
if not starter_mission_locations :
# Force early unit if first mission is impossible without one
starter_unit = StarterUnit . option_any_starter_unit
if starter_unit != StarterUnit . option_off :
first_race = lookup_name_to_mission [ first_mission ] . race
if first_race == SC2Race . ANY :
# If the first mission is a logic-less no-build
mission_req_table : Dict [ SC2Campaign , Dict [ str , MissionInfo ] ] = world . mission_req_table
races = get_used_races ( mission_req_table , world )
races . remove ( SC2Race . ANY )
if lookup_name_to_mission [ first_mission ] . race in races :
# The campaign's race is in (At least one mission that's not logic-less no-build exists)
first_race = lookup_name_to_mission [ first_mission ] . campaign . race
elif len ( races ) > 0 :
# The campaign only has logic-less no-build missions. Find any other valid race
first_race = world . random . choice ( list ( races ) )
if first_race != SC2Race . ANY :
# The race of the early unit has been chosen
basic_units = get_basic_units ( world , first_race )
if starter_unit == StarterUnit . option_balanced :
basic_units = basic_units . difference ( not_balanced_starting_units )
if first_mission == SC2Mission . DARK_WHISPERS . mission_name :
# Special case - you don't have a logicless location but need an AA
basic_units = basic_units . difference (
{ ItemNames . ZEALOT , ItemNames . CENTURION , ItemNames . SENTINEL , ItemNames . BLOOD_HUNTER ,
ItemNames . AVENGER , ItemNames . IMMORTAL , ItemNames . ANNIHILATOR , ItemNames . VANGUARD } )
if first_mission == SC2Mission . SUDDEN_STRIKE . mission_name :
# Special case - cliffjumpers
basic_units = { ItemNames . REAPER , ItemNames . GOLIATH , ItemNames . SIEGE_TANK , ItemNames . VIKING , ItemNames . BANSHEE }
local_basic_unit = sorted ( item for item in basic_units if item not in non_local_items and item not in excluded_items )
if not local_basic_unit :
# Drop non_local_items constraint
local_basic_unit = sorted ( item for item in basic_units if item not in excluded_items )
if not local_basic_unit :
raise Exception ( " Early Unit: At least one basic unit must be included " )
unit : Item = add_starter_item ( world , excluded_items , local_basic_unit )
starter_items . append ( unit )
# NCO-only specific rules
if first_mission == SC2Mission . SUDDEN_STRIKE . mission_name :
support_item : Union [ str , None ] = None
if unit . name == ItemNames . REAPER :
support_item = ItemNames . REAPER_SPIDER_MINES
elif unit . name == ItemNames . GOLIATH :
support_item = ItemNames . GOLIATH_JUMP_JETS
elif unit . name == ItemNames . SIEGE_TANK :
support_item = ItemNames . SIEGE_TANK_JUMP_JETS
elif unit . name == ItemNames . VIKING :
support_item = ItemNames . VIKING_SMART_SERVOS
if support_item is not None :
starter_items . append ( add_starter_item ( world , excluded_items , [ support_item ] ) )
starter_items . append ( add_starter_item ( world , excluded_items , [ ItemNames . NOVA_JUMP_SUIT_MODULE ] ) )
starter_items . append (
add_starter_item ( world , excluded_items ,
[
ItemNames . NOVA_HELLFIRE_SHOTGUN ,
ItemNames . NOVA_PLASMA_RIFLE ,
ItemNames . NOVA_PULSE_GRENADES
] ) )
if enabled_campaigns == { SC2Campaign . NCO } :
starter_items . append ( add_starter_item ( world , excluded_items , [ ItemNames . LIBERATOR_RAID_ARTILLERY ] ) )
starter_abilities = get_option_value ( world , ' start_primary_abilities ' )
assert isinstance ( starter_abilities , int )
if starter_abilities :
ability_count = starter_abilities
ability_tiers = [ 0 , 1 , 3 ]
world . random . shuffle ( ability_tiers )
if ability_count > 3 :
ability_tiers . append ( 6 )
for tier in ability_tiers :
abilities = kerrigan_actives [ tier ] . union ( kerrigan_passives [ tier ] ) . difference ( excluded_items , non_local_items )
if not abilities :
abilities = kerrigan_actives [ tier ] . union ( kerrigan_passives [ tier ] ) . difference ( excluded_items )
if abilities :
ability_count - = 1
starter_items . append ( add_starter_item ( world , excluded_items , list ( abilities ) ) )
if ability_count == 0 :
break
return starter_items
def get_first_mission ( mission_req_table : Dict [ SC2Campaign , Dict [ str , MissionInfo ] ] ) - > str :
# The first world should also be the starting world
campaigns = mission_req_table . keys ( )
lowest_id = min ( [ campaign . id for campaign in campaigns ] )
first_campaign = [ campaign for campaign in campaigns if campaign . id == lowest_id ] [ 0 ]
first_mission = list ( mission_req_table [ first_campaign ] ) [ 0 ]
return first_mission
def add_starter_item ( world : World , excluded_items : Set [ str ] , item_list : Sequence [ str ] ) - > Item :
item_name = world . random . choice ( sorted ( item_list ) )
excluded_items . add ( item_name )
item = create_item_with_correct_settings ( world . player , item_name )
world . multiworld . push_precollected ( item )
return item
def get_item_pool ( world : World , mission_req_table : Dict [ SC2Campaign , Dict [ str , MissionInfo ] ] ,
starter_items : List [ Item ] , excluded_items : Set [ str ] , location_cache : List [ Location ] ) - > List [ Item ] :
pool : List [ Item ] = [ ]
# For the future: goal items like Artifact Shards go here
locked_items = [ ]
# YAML items
yaml_locked_items = get_option_value ( world , ' locked_items ' )
assert not isinstance ( yaml_locked_items , int )
# Adjust generic upgrade availability based on options
include_upgrades = get_option_value ( world , ' generic_upgrade_missions ' ) == 0
upgrade_items = get_option_value ( world , ' generic_upgrade_items ' )
assert isinstance ( upgrade_items , int )
# Include items from outside main campaigns
item_sets = { ' wol ' , ' hots ' , ' lotv ' }
if get_option_value ( world , ' nco_items ' ) \
or SC2Campaign . NCO in get_enabled_campaigns ( world ) :
item_sets . add ( ' nco ' )
if get_option_value ( world , ' bw_items ' ) :
item_sets . add ( ' bw ' )
if get_option_value ( world , ' ext_items ' ) :
item_sets . add ( ' ext ' )
def allowed_quantity ( name : str , data : ItemData ) - > int :
if name in excluded_items \
or data . type == " Upgrade " and ( not include_upgrades or name not in upgrade_included_names [ upgrade_items ] ) \
or not data . origin . intersection ( item_sets ) :
return 0
elif name in progressive_if_nco and ' nco ' not in item_sets :
return 1
elif name in progressive_if_ext and ' ext ' not in item_sets :
return 1
else :
return data . quantity
for name , data in get_item_table ( ) . items ( ) :
for _ in range ( allowed_quantity ( name , data ) ) :
item = create_item_with_correct_settings ( world . player , name )
if name in yaml_locked_items :
locked_items . append ( item )
else :
pool . append ( item )
existing_items = starter_items + [ item for item in world . multiworld . precollected_items [ world . player ] if item not in starter_items ]
existing_names = [ item . name for item in existing_items ]
# Check the parent item integrity, exclude items
pool [ : ] = [ item for item in pool if pool_contains_parent ( item , pool + locked_items + existing_items ) ]
# Removing upgrades for excluded items
for item_name in excluded_items :
if item_name in existing_names :
continue
invalid_upgrades = get_item_upgrades ( pool , item_name )
for invalid_upgrade in invalid_upgrades :
pool . remove ( invalid_upgrade )
fill_pool_with_kerrigan_levels ( world , pool )
filtered_pool = filter_items ( world , mission_req_table , location_cache , pool , existing_items , locked_items )
return filtered_pool
def fill_item_pool_with_dummy_items ( self : SC2World , locked_locations : List [ str ] ,
location_cache : List [ Location ] , pool : List [ Item ] ) :
for _ in range ( len ( location_cache ) - len ( locked_locations ) - len ( pool ) ) :
item = create_item_with_correct_settings ( self . player , self . get_filler_item_name ( ) )
pool . append ( item )
def create_item_with_correct_settings ( player : int , name : str ) - > Item :
data = get_full_item_list ( ) [ name ]
item = Item ( name , data . classification , data . code , player )
return item
def pool_contains_parent ( item : Item , pool : Iterable [ Item ] ) :
item_data = get_full_item_list ( ) . get ( item . name )
if item_data . parent_item is None :
# The item has not associated parent, the item is valid
return True
parent_item = item_data . parent_item
# Check if the pool contains the parent item
return parent_item in [ pool_item . name for pool_item in pool ]
def fill_resource_locations ( world : World , locked_locations : List [ str ] , location_cache : List [ Location ] ) :
"""
Filters the locations in the world using a trash or Nothing item
: param multiworld :
: param player :
: param locked_locations :
: param location_cache :
: return :
"""
open_locations = [ location for location in location_cache if location . item is None ]
plando_locations = get_plando_locations ( world )
resource_location_types = get_location_types ( world , LocationInclusion . option_resources )
location_data = { sc2_location . name : sc2_location for sc2_location in get_locations ( world ) }
for location in open_locations :
# Go through the locations that aren't locked yet (early unit, etc)
if location . name not in plando_locations :
# The location is not plando'd
sc2_location = location_data [ location . name ]
if sc2_location . type in resource_location_types :
item_name = world . random . choice ( filler_items )
item = create_item_with_correct_settings ( world . player , item_name )
location . place_locked_item ( item )
locked_locations . append ( location . name )
def place_exclusion_item ( item_name , location , locked_locations , player ) :
item = create_item_with_correct_settings ( player , item_name )
location . place_locked_item ( item )
locked_locations . append ( location . name )
def fill_pool_with_kerrigan_levels ( world : World , item_pool : List [ Item ] ) :
total_levels = get_option_value ( world , " kerrigan_level_item_sum " )
if get_option_value ( world , " kerrigan_presence " ) not in kerrigan_unit_available \
or total_levels == 0 \
or SC2Campaign . HOTS not in get_enabled_campaigns ( world ) :
return
def add_kerrigan_level_items ( level_amount : int , item_amount : int ) :
name = f " { level_amount } Kerrigan Level "
if level_amount > 1 :
name + = " s "
for _ in range ( item_amount ) :
item_pool . append ( create_item_with_correct_settings ( world . player , name ) )
sizes = [ 70 , 35 , 14 , 10 , 7 , 5 , 2 , 1 ]
option = get_option_value ( world , " kerrigan_level_item_distribution " )
assert isinstance ( option , int )
assert isinstance ( total_levels , int )
if option in ( KerriganLevelItemDistribution . option_vanilla , KerriganLevelItemDistribution . option_smooth ) :
distribution = [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ]
if option == KerriganLevelItemDistribution . option_vanilla :
distribution = [ 32 , 0 , 0 , 1 , 3 , 0 , 0 , 0 , 1 , 1 ]
else : # Smooth
distribution = [ 0 , 0 , 0 , 1 , 1 , 2 , 2 , 2 , 1 , 1 ]
for tier in range ( len ( distribution ) ) :
add_kerrigan_level_items ( tier + 1 , distribution [ tier ] )
else :
size = sizes [ option - 2 ]
round_func : Callable [ [ float ] , int ] = round
if total_levels > 70 :
round_func = floor
else :
round_func = ceil
add_kerrigan_level_items ( size , round_func ( float ( total_levels ) / size ) )