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)