NovaBryan/EVE_Dump
Log | Files | 📦 下载最新源码 (.zip) | Refs | README

randomizeControls.py (14459B)


      1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\eve\client\script\ui\cosmetics\ship\pages\studio\randomizeControls.py
      2 import eveicon
      3 import random
      4 import uthread
      5 import uthread2
      6 from carbonui.control.contextMenu.contextMenuMixin import ContextMenuMixin
      7 from carbonui.control.contextMenu.menuUtil import ClearMenuLayer
      8 from carbonui.primitives.container import Container
      9 from carbonui.primitives.fill import Fill
     10 from carbonui.text.color import TextColor
     11 from carbonui.text.styles import TextBody
     12 from carbonui.uianimations import animations
     13 from carbonui.uiconst import Align, PickState, TIME_ENTRY
     14 from carbonui.uicore import uicore
     15 from cosmetics.client.ships.skins.live_data import current_skin_design, current_skin_design_signals
     16 from cosmetics.common.ships.skins.static_data.slot import SlotsDataLoader
     17 from cosmetics.common.ships.skins.static_data.slot_name import SlotID, PATTERN_SLOT_IDS, PATTERN_MATERIAL_SLOT_IDS, PATTERN_RELATED_SLOT_IDS
     18 from cosmetics.client.shipSkinComponentSvc import get_ship_skin_component_svc
     19 from eve.client.script.ui import eveColor
     20 from eve.client.script.ui.cosmetics.ship.const import CHILD_PAGES_BY_PARENT_PAGE, SkinrPage
     21 from eve.client.script.ui.cosmetics.ship.pages.studio import studioSettings, studioSignals
     22 from eve.client.script.ui.cosmetics.ship.pages.studio.circularButtonIcon import CircularMenuButtonIcon
     23 from eve.client.script.ui.cosmetics.ship.pages.studio.cosmeticSlot import CosmeticSlotButton
     24 from eve.client.script.ui.cosmetics.ship.pages.studio.radiallyAlignedMixin import RadiallyAlignedMixin
     25 from eve.client.script.ui.cosmetics.ship.pages.studio.studioUtil import RANDOMIZE_BUTTON_ANALYTIC_ID
     26 from localization import GetByLabel
     27 from signals import Signal
     28 ICON_BY_SLOT_ID = {SlotID.PRIMARY_NANOCOATING: eveicon.primary_area,
     29  SlotID.SECONDARY_NANOCOATING: eveicon.secondary_area,
     30  SlotID.TERTIARY_NANOCOATING: eveicon.details,
     31  SlotID.TECH_AREA: eveicon.tech_area,
     32  SlotID.PATTERN: eveicon.patterns,
     33  SlotID.SECONDARY_PATTERN: eveicon.patterns,
     34  SlotID.PATTERN_MATERIAL: eveicon.kerr,
     35  SlotID.SECONDARY_PATTERN_MATERIAL: eveicon.kerr}
     36 PRIMARY_PATTERN_TOGGLE_IDS = [SlotID.PATTERN, SlotID.PATTERN_MATERIAL]
     37 SECONDARY_PATTERN_TOGGLE_IDS = [SlotID.SECONDARY_PATTERN, SlotID.SECONDARY_PATTERN_MATERIAL]
     38 NON_PATTERN_TOGGLE_IDS = [SlotID.PRIMARY_NANOCOATING,
     39  SlotID.SECONDARY_NANOCOATING,
     40  SlotID.TERTIARY_NANOCOATING,
     41  SlotID.TECH_AREA]
     42 PAIRED_PATTERN_SLOT_ID = {SlotID.PATTERN: SlotID.PATTERN_MATERIAL,
     43  SlotID.PATTERN_MATERIAL: SlotID.PATTERN,
     44  SlotID.SECONDARY_PATTERN: SlotID.SECONDARY_PATTERN_MATERIAL,
     45  SlotID.SECONDARY_PATTERN_MATERIAL: SlotID.SECONDARY_PATTERN}
     46 TOGGLE_OFFSET = 40
     47 on_filter_changed = Signal('on_filter_changed')
     48 
     49 class RandomizeControls(Container, RadiallyAlignedMixin):
     50     default_align = Align.CENTER
     51     default_width = 128
     52     default_height = 128
     53 
     54     def __init__(self, *args, **kwargs):
     55         super(RandomizeControls, self).__init__(*args, **kwargs)
     56         self.on_randomize = Signal('on_randomize')
     57         self.construct_layout()
     58         self.connect_signals()
     59 
     60     def Close(self):
     61         try:
     62             self.disconnect_signals()
     63         finally:
     64             super(RandomizeControls, self).Close()
     65 
     66     def connect_signals(self):
     67         current_skin_design_signals.on_ship_type_id_changed.connect(self.on_ship_type_id_changed)
     68         current_skin_design_signals.on_slot_fitting_changed.connect(self.on_slot_fitting_changed)
     69         studioSignals.on_page_opened.connect(self.on_page_opened)
     70 
     71     def disconnect_signals(self):
     72         current_skin_design_signals.on_ship_type_id_changed.disconnect(self.on_ship_type_id_changed)
     73         current_skin_design_signals.on_slot_fitting_changed.disconnect(self.on_slot_fitting_changed)
     74         studioSignals.on_page_opened.disconnect(self.on_page_opened)
     75 
     76     def construct_layout(self):
     77         self.randomize_button = CosmeticSlotButton(name='randomize_button', parent=self, align=Align.CENTER, texture_path=eveicon.randomize, icon_size=16, hint=GetByLabel('UI/CharacterCreation/Randomize'), pos=(0, 0, 48, 48))
     78         self.randomize_button.analyticID = RANDOMIZE_BUTTON_ANALYTIC_ID
     79         self.randomize_button.on_click = self.on_randomize_button_click
     80         self.slot_filters = RandomizeSlotFilters(name='slot_filters', parent=self, align=Align.CENTER, slot_id=SlotID.PATTERN, left=48, top=24)
     81 
     82     @uthread2.debounce(0.15)
     83     def on_randomize_button_click(self, *args):
     84         slot_ids = self.slot_filters.selected_slot_ids
     85         uthread.parallel([ (self.apply_random_component, (slot_id,)) for slot_id in slot_ids ])
     86         current_skin_design.add_to_undo_history()
     87 
     88     def on_ship_type_id_changed(self, *args):
     89         self.slot_filters.rebuild()
     90 
     91     def on_slot_fitting_changed(self, slot_id, component_instance):
     92         if slot_id == SlotID.SECONDARY_PATTERN and component_instance is None:
     93             self.slot_filters.deselect_secondary_pattern()
     94 
     95     def on_page_opened(self, page_id, page_args, last_page_id, animate = True):
     96         if last_page_id not in CHILD_PAGES_BY_PARENT_PAGE[SkinrPage.STUDIO]:
     97             return
     98         if page_id not in CHILD_PAGES_BY_PARENT_PAGE[SkinrPage.STUDIO]:
     99             self.slot_filters.clear()
    100 
    101     def apply_random_component(self, slot_id):
    102         only_owned = studioSettings.only_shown_owned_design_elements_setting.is_enabled()
    103         component_data = get_ship_skin_component_svc().get_random_component_data(slot_id, only_owned=only_owned)
    104         component_intance = current_skin_design.get().fit_slot(slot_id, component_data.component_id)
    105         if slot_id in PATTERN_SLOT_IDS:
    106             self.randomize_pattern_attributes(component_intance)
    107 
    108     def randomize_pattern_attributes(self, component_instance):
    109         component_instance.scaling_ratio = 0.5 + 0.5 * random.random()
    110         component_instance.offset_v_ratio = -0.5 + random.random()
    111         component_instance.offset_u_ratio = -0.5 + random.random()
    112         component_instance.yaw = -1.0 + 2 * random.random()
    113         component_instance.pitch = 0.75 + 0.25 * random.random()
    114         component_instance.mirrored = random.choice([True, False])
    115 
    116 
    117 class RandomizeSlotFilters(CircularMenuButtonIcon):
    118     default_pickState = PickState.ON
    119     default_texturePath = eveicon.ship_areas
    120     default_hint = GetByLabel('UI/Personalization/ShipSkins/SKINR/Studio/RandomizeAreaApplication')
    121 
    122     def __init__(self, *args, **kwargs):
    123         super(RandomizeSlotFilters, self).__init__(*args, **kwargs)
    124         self.menu = None
    125         self._filters = {}
    126         self.clear()
    127         self.connect_signals()
    128 
    129     def Close(self):
    130         try:
    131             self.disconnect_signals()
    132         finally:
    133             super(RandomizeSlotFilters, self).Close()
    134 
    135     def connect_signals(self):
    136         on_filter_changed.connect(self.on_filter_changed)
    137 
    138     def disconnect_signals(self):
    139         on_filter_changed.disconnect(self.on_filter_changed)
    140 
    141     def on_filter_changed(self, slot_id, is_selected):
    142         self._filters[slot_id] = is_selected
    143 
    144     def clear(self):
    145         self._filters.clear()
    146         for slot_id in current_skin_design.get().slot_layout.slots.keys():
    147             self._filters[slot_id] = True
    148 
    149         self.clear_if_no_filters()
    150 
    151     def rebuild(self):
    152         existing_filters = self._filters.copy()
    153         self.clear()
    154         for slot_id in self._filters:
    155             if slot_id in existing_filters:
    156                 self._filters[slot_id] = existing_filters[slot_id]
    157 
    158         self.clear_if_no_filters()
    159 
    160     def clear_if_no_filters(self):
    161         if len(self.selected_slot_ids) == 0:
    162             self.clear()
    163 
    164     def deselect_secondary_pattern(self):
    165         for slot_id in SECONDARY_PATTERN_TOGGLE_IDS:
    166             self._filters[slot_id] = False
    167 
    168         if len(self.selected_slot_ids) == 0:
    169             self.clear()
    170 
    171     def OnMouseUp(self, *args):
    172         if self.has_open_menu:
    173             return
    174         ClearMenuLayer()
    175         left, top, width, height = self.GetAbsolute()
    176         self.menu = SlotToggleMenu(parent=uicore.layer.menu, filters=self._filters, left=left + TOGGLE_OFFSET, top=top)
    177 
    178     def GetHint(self):
    179         if self.has_open_menu:
    180             return None
    181         return super(RandomizeSlotFilters, self).GetHint()
    182 
    183     @property
    184     def has_open_menu(self):
    185         return self.menu and not self.menu.destroyed
    186 
    187     @property
    188     def selected_slot_ids(self):
    189         return [ k for k, v in self._filters.items() if v ]
    190 
    191 
    192 class SlotToggleMenu(Container, ContextMenuMixin):
    193     default_align = Align.ABSOLUTE
    194     default_width = 232
    195     default_height = 72
    196 
    197     def __init__(self, filters, *args, **kwargs):
    198         super(SlotToggleMenu, self).__init__(*args, **kwargs)
    199         self._toggles = {}
    200         self._pattern_links = {}
    201         self.construct_layout(filters)
    202         self.appear()
    203 
    204     def construct_layout(self, filters):
    205         for slot_id, is_selected in filters.items():
    206             toggle = SlotToggle(parent=self, slot_id=slot_id, is_selected=is_selected, texturePath=ICON_BY_SLOT_ID[slot_id])
    207             toggle.on_click.connect(self.on_toggle_click)
    208             self._toggles[slot_id] = toggle
    209             if slot_id in PATTERN_MATERIAL_SLOT_IDS:
    210                 link = Fill(parent=self, align=Align.TOPLEFT, pos=(0, 0, 2, 8), color=eveColor.GUNMETAL_GREY, opacity=0.5)
    211                 self._pattern_links[slot_id] = link
    212 
    213     def on_toggle_click(self, toggle):
    214         if toggle.is_selected and self.is_last_toggle_or_pair(toggle.slot_id):
    215             return
    216         if not toggle.is_selected and toggle.slot_id in SECONDARY_PATTERN_TOGGLE_IDS:
    217             if SlotID.PATTERN not in self.selected_slot_ids and not self.is_component_fitted(SlotID.PATTERN):
    218                 return
    219         if toggle.is_selected and toggle.slot_id in PRIMARY_PATTERN_TOGGLE_IDS:
    220             if self.is_component_fitted(SlotID.PATTERN):
    221                 pass
    222             else:
    223                 if self.only_pattern_slot_ids_selected and not self.is_component_fitted(SlotID.SECONDARY_PATTERN):
    224                     return
    225                 if not self.is_component_fitted(SlotID.SECONDARY_PATTERN):
    226                     self.deselect_secondary_pattern()
    227         toggle.toggle_value()
    228         on_filter_changed(toggle.slot_id, toggle.is_selected)
    229         if toggle.slot_id in PAIRED_PATTERN_SLOT_ID:
    230             self.set_paired_toggle(toggle.slot_id, toggle.is_selected)
    231 
    232     def is_last_toggle_or_pair(self, slot_id):
    233         selected_slot_ids = self.selected_slot_ids
    234         if len(selected_slot_ids) == 1 and slot_id in selected_slot_ids:
    235             return True
    236         if len(selected_slot_ids) == 2:
    237             if slot_id in PATTERN_RELATED_SLOT_IDS and slot_id in selected_slot_ids:
    238                 return True
    239         return False
    240 
    241     def set_paired_toggle(self, slot_id, is_selected):
    242         paired_slot_id = PAIRED_PATTERN_SLOT_ID[slot_id]
    243         paired_toggle = self._toggles[paired_slot_id]
    244         paired_toggle.set_value(is_selected)
    245         on_filter_changed(paired_slot_id, is_selected)
    246 
    247     def deselect_secondary_pattern(self):
    248         for slot_id in SECONDARY_PATTERN_TOGGLE_IDS:
    249             toggle = self._toggles.get(slot_id, None)
    250             if not toggle:
    251                 continue
    252             toggle.set_value(False)
    253             on_filter_changed(slot_id, False)
    254 
    255     def appear(self):
    256         left_index = 0
    257         for i, toggle in enumerate(self._toggles.values()):
    258             if toggle.slot_id in PATTERN_MATERIAL_SLOT_IDS:
    259                 if left_index > 0:
    260                     left_index -= 1
    261                 animations.MorphScalar(toggle, 'top', 0, TOGGLE_OFFSET, duration=TIME_ENTRY)
    262                 link = self._pattern_links[toggle.slot_id]
    263                 animations.MorphScalar(obj=link, attrName='top', startVal=TOGGLE_OFFSET * 0.5 - link.height * 0.5, endVal=TOGGLE_OFFSET - link.height, duration=TIME_ENTRY)
    264                 animations.MorphScalar(obj=link, attrName='left', startVal=-TOGGLE_OFFSET * 0.5 - link.width * 0.5, endVal=left_index * TOGGLE_OFFSET + toggle.width * 0.5 - link.width * 0.5, duration=TIME_ENTRY)
    265             animations.MorphScalar(toggle, 'left', -TOGGLE_OFFSET, left_index * TOGGLE_OFFSET, duration=TIME_ENTRY)
    266             left_index += 1
    267 
    268     def is_component_fitted(self, slot_id):
    269         if current_skin_design.get().get_fitted_component_instance(slot_id):
    270             return True
    271         return False
    272 
    273     @property
    274     def selected_slot_ids(self):
    275         return [ k for k, v in self._toggles.items() if v.is_selected ]
    276 
    277     @property
    278     def only_pattern_slot_ids_selected(self):
    279         for slot_id in self.selected_slot_ids:
    280             if slot_id in NON_PATTERN_TOGGLE_IDS:
    281                 return False
    282 
    283         return True
    284 
    285 
    286 class SlotToggle(CircularMenuButtonIcon):
    287     default_colorSelected = TextColor.NORMAL
    288 
    289     def __init__(self, slot_id, is_selected, *args, **kwargs):
    290         super(SlotToggle, self).__init__(*args, **kwargs)
    291         self.slot_id = slot_id
    292         self.is_selected = is_selected
    293         self.on_click = Signal('on_click')
    294         self.construct_layout()
    295         self.update()
    296 
    297     def construct_layout(self):
    298         slot_data = SlotsDataLoader.get_slot_data(self.slot_id)
    299         self.text = TextBody(parent=self, text=slot_data.name, align=Align.CENTER, opacity=0.0)
    300 
    301     def update(self):
    302         if self.is_selected:
    303             self.SetSelected()
    304         else:
    305             self.SetDeselected()
    306 
    307     def toggle_value(self):
    308         self.is_selected = not self.is_selected
    309         self.update()
    310 
    311     def set_value(self, is_selected):
    312         self.is_selected = is_selected
    313         self.update()
    314 
    315     def OnClick(self, *args):
    316         self.on_click(self)
    317 
    318     def OnMouseEnter(self, *args):
    319         animations.FadeIn(self.text, endVal=TextColor.NORMAL[-1], duration=TIME_ENTRY)
    320         offset = TOGGLE_OFFSET * 0.5
    321         if self.slot_id in PATTERN_MATERIAL_SLOT_IDS:
    322             animations.MorphScalar(self.text, 'top', offset, offset + 8, duration=0.1)
    323         else:
    324             animations.MorphScalar(self.text, 'top', -offset, -offset - 8, duration=0.1)
    325 
    326     def OnMouseExit(self, *args):
    327         animations.FadeOut(self.text, duration=0.1)
    328         offset = TOGGLE_OFFSET * 0.5
    329         if self.slot_id in PATTERN_MATERIAL_SLOT_IDS:
    330             animations.MorphScalar(self.text, 'top', self.text.top, offset, duration=0.1)
    331         else:
    332             animations.MorphScalar(self.text, 'top', self.text.top, -offset, duration=0.1)