Compare commits

...

6 Commits

6 changed files with 386 additions and 16 deletions

View File

@@ -0,0 +1,220 @@
# noinspection PyPackageRequirements
import wx
import itertools
import gui.mainFrame
from eos.saveddata.character import Skill
from eos.saveddata.fighter import Fighter as es_Fighter
from eos.saveddata.module import Module as es_Module
from eos.saveddata.ship import Ship
from gui.utils.clipboard import toClipboard
from gui.utils.numberFormatter import formatAmount
from service.fit import Fit
_t = wx.GetTranslation
class ItemSkills(wx.Panel):
def __init__(self, parent, stuff, item):
wx.Panel.__init__(self, parent)
self.stuff = stuff
self.item = item
mainSizer = wx.BoxSizer(wx.HORIZONTAL)
leftPanel = wx.Panel(self)
leftSizer = wx.BoxSizer(wx.VERTICAL)
leftPanel.SetSizer(leftSizer)
header = wx.StaticText(leftPanel, wx.ID_ANY, _t("Components"))
font = header.GetFont()
font.SetWeight(wx.FONTWEIGHT_BOLD)
header.SetFont(font)
leftSizer.Add(header, 0, wx.ALL, 5)
self.checkboxes = {}
components = [
("Ship", "ship"),
("Modules", "modules"),
("Drones", "drones"),
("Fighters", "fighters"),
("Cargo", "cargo"),
("Implants", "appliedImplants"),
("Boosters", "boosters"),
("Necessary", "necessary"),
]
for label, key in components:
cb = wx.CheckBox(leftPanel, wx.ID_ANY, label)
cb.SetValue(True)
cb.Bind(wx.EVT_CHECKBOX, self.onCheckboxChange)
self.checkboxes[key] = cb
leftSizer.Add(cb, 0, wx.ALL, 2)
leftSizer.AddStretchSpacer()
mainSizer.Add(leftPanel, 0, wx.EXPAND | wx.ALL, 5)
rightPanel = wx.Panel(self)
rightSizer = wx.BoxSizer(wx.VERTICAL)
rightPanel.SetSizer(rightSizer)
headerRight = wx.StaticText(rightPanel, wx.ID_ANY, _t("Skills"))
fontRight = headerRight.GetFont()
fontRight.SetWeight(wx.FONTWEIGHT_BOLD)
headerRight.SetFont(fontRight)
rightSizer.Add(headerRight, 0, wx.ALL, 5)
self.skillsText = wx.TextCtrl(rightPanel, wx.ID_ANY, "", style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_DONTWRAP)
font = wx.Font(9, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
self.skillsText.SetFont(font)
rightSizer.Add(self.skillsText, 1, wx.EXPAND | wx.ALL, 5)
mainSizer.Add(rightPanel, 1, wx.EXPAND | wx.ALL, 5)
self.SetSizer(mainSizer)
self.nbContainer = parent if isinstance(parent, wx.Notebook) else None
if self.nbContainer:
self.nbContainer.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.onTabChanged)
self.updateSkills()
def onCheckboxChange(self, event):
self.updateSkills()
self._copyToClipboard()
def updateSkills(self):
fitID = gui.mainFrame.MainFrame.getInstance().getActiveFit()
if fitID is None:
self.skillsText.SetValue("")
self._updateCheckboxStates(None)
return
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit is None:
self.skillsText.SetValue("")
self._updateCheckboxStates(None)
return
if not fit.calculated:
fit.calculate()
self._updateCheckboxStates(fit)
char = fit.character
skillsMap = {}
items = []
if self.checkboxes["ship"].GetValue():
items.append(fit.ship)
if self.checkboxes["modules"].GetValue():
items.extend(fit.modules)
if self.checkboxes["drones"].GetValue():
items.extend(fit.drones)
if self.checkboxes["fighters"].GetValue():
items.extend(fit.fighters)
if self.checkboxes["cargo"].GetValue():
items.extend(fit.cargo)
if self.checkboxes["appliedImplants"].GetValue():
items.extend(fit.appliedImplants)
if self.checkboxes["boosters"].GetValue():
items.extend(fit.boosters)
for thing in items:
self._collectAffectingSkills(thing, char, skillsMap)
if self.checkboxes["necessary"].GetValue():
self._collectRequiredSkills(items, char, skillsMap)
skillsList = ""
for skillName in sorted(skillsMap):
charLevel = skillsMap[skillName]
for level in range(1, charLevel + 1):
skillsList += "%s %d\n" % (skillName, level)
self.skillsText.SetValue(skillsList)
self._copyToClipboard()
def _copyToClipboard(self):
skillsText = self.skillsText.GetValue()
if skillsText:
toClipboard(skillsText)
def onTabChanged(self, event):
if self.nbContainer:
pageIndex = self.nbContainer.FindPage(self)
if pageIndex != -1 and event.GetSelection() == pageIndex:
self.updateSkills()
event.Skip()
def _updateCheckboxStates(self, fit):
if fit is None:
for cb in self.checkboxes.values():
cb.Enable(False)
return
self.checkboxes["ship"].Enable(True)
self.checkboxes["modules"].Enable(len(fit.modules) > 0)
self.checkboxes["drones"].Enable(len(fit.drones) > 0)
self.checkboxes["fighters"].Enable(len(fit.fighters) > 0)
self.checkboxes["cargo"].Enable(len(fit.cargo) > 0)
self.checkboxes["appliedImplants"].Enable(len(fit.appliedImplants) > 0)
self.checkboxes["boosters"].Enable(len(fit.boosters) > 0)
self.checkboxes["necessary"].Enable(True)
def _collectAffectingSkills(self, thing, char, skillsMap):
for attr in ("item", "charge"):
if attr == "charge" and isinstance(thing, es_Fighter):
continue
subThing = getattr(thing, attr, None)
if subThing is None:
continue
if isinstance(thing, es_Fighter) and attr == "charge":
continue
if attr == "charge":
cont = getattr(thing, "chargeModifiedAttributes", None)
else:
cont = getattr(thing, "itemModifiedAttributes", None)
if cont is not None:
for attrName in cont.iterAfflictions():
for fit, afflictors in cont.getAfflictions(attrName).items():
for afflictor, operator, stackingGroup, preResAmount, postResAmount, used in afflictors:
if isinstance(afflictor, Skill) and afflictor.character == char:
skillName = afflictor.item.name
if skillName not in skillsMap:
skillsMap[skillName] = afflictor.level
elif skillsMap[skillName] < afflictor.level:
skillsMap[skillName] = afflictor.level
def _collectRequiredSkills(self, items, char, skillsMap):
"""Collect required skills from items (necessary to use them)"""
for thing in items:
for attr in ("item", "charge"):
if attr == "charge" and isinstance(thing, es_Fighter):
continue
subThing = getattr(thing, attr, None)
if subThing is None:
continue
if isinstance(thing, es_Fighter) and attr == "charge":
continue
if hasattr(subThing, "requiredSkills"):
for reqSkill, level in subThing.requiredSkills.items():
skillName = reqSkill.name
charSkill = char.getSkill(reqSkill) if char else None
charLevel = charSkill.level if charSkill else 0
if charLevel > 0:
if skillName not in skillsMap:
skillsMap[skillName] = charLevel
elif skillsMap[skillName] < charLevel:
skillsMap[skillName] = charLevel
else:
if skillName not in skillsMap:
skillsMap[skillName] = level
elif skillsMap[skillName] < level:
skillsMap[skillName] = level

View File

@@ -23,6 +23,7 @@ class ItemView(Display):
DEFAULT_COLS = ["Base Icon", DEFAULT_COLS = ["Base Icon",
"Base Name", "Base Name",
"Price",
"attr:power,,,True", "attr:power,,,True",
"attr:cpu,,,True"] "attr:cpu,,,True"]

View File

@@ -20,6 +20,7 @@
# noinspection PyPackageRequirements # noinspection PyPackageRequirements
import wx import wx
from eos.gamedata import Item
from eos.saveddata.cargo import Cargo from eos.saveddata.cargo import Cargo
from eos.saveddata.drone import Drone from eos.saveddata.drone import Drone
from eos.saveddata.fighter import Fighter from eos.saveddata.fighter import Fighter
@@ -53,7 +54,14 @@ class Price(ViewColumn):
self.imageId = fittingView.imageList.GetImageIndex("totalPrice_small", "gui") self.imageId = fittingView.imageList.GetImageIndex("totalPrice_small", "gui")
def getText(self, stuff): def getText(self, stuff):
if stuff.item is None or stuff.item.group.name == "Ship Modifiers": if isinstance(stuff, Item):
item = stuff
else:
if not hasattr(stuff, "item") or stuff.item is None:
return ""
item = stuff.item
if item.group.name == "Ship Modifiers":
return "" return ""
if hasattr(stuff, "isEmpty"): if hasattr(stuff, "isEmpty"):
@@ -63,7 +71,7 @@ class Price(ViewColumn):
if isinstance(stuff, Module) and stuff.isMutated: if isinstance(stuff, Module) and stuff.isMutated:
return "" return ""
priceObj = stuff.item.price priceObj = item.price
if not priceObj.isValid(): if not priceObj.isValid():
return False return False
@@ -79,7 +87,11 @@ class Price(ViewColumn):
display.SetItem(colItem) display.SetItem(colItem)
sPrice.getPrices([mod.item], callback, waitforthread=True) if isinstance(mod, Item):
item = mod
else:
item = mod.item
sPrice.getPrices([item], callback, waitforthread=True)
def getImageId(self, mod): def getImageId(self, mod):
return -1 return -1

View File

@@ -29,7 +29,7 @@ import config
import gui.globalEvents as GE import gui.globalEvents as GE
import gui.mainFrame import gui.mainFrame
from gui.bitmap_loader import BitmapLoader from gui.bitmap_loader import BitmapLoader
from gui.utils.clipboard import toClipboard from gui.utils.clipboard import toClipboard, fromClipboard
from service.character import Character from service.character import Character
from service.fit import Fit from service.fit import Fit
@@ -50,6 +50,10 @@ class CharacterSelection(wx.Panel):
# cache current selection to fall back in case we choose to open char editor # cache current selection to fall back in case we choose to open char editor
self.charCache = None self.charCache = None
# history for Shift-Tab navigation
self.charHistory = []
self._updatingFromHistory = False
self.charChoice = wx.Choice(self) self.charChoice = wx.Choice(self)
mainSizer.Add(self.charChoice, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT | wx.LEFT, 3) mainSizer.Add(self.charChoice, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT | wx.LEFT, 3)
@@ -92,7 +96,7 @@ class CharacterSelection(wx.Panel):
sFit = Fit.getInstance() sFit = Fit.getInstance()
fit = sFit.getFit(self.mainFrame.getActiveFit()) fit = sFit.getFit(self.mainFrame.getActiveFit())
if not fit or not self.needsSkills: if not fit:
return return
pos = wx.GetMousePosition() pos = wx.GetMousePosition()
@@ -100,17 +104,23 @@ class CharacterSelection(wx.Panel):
menu = wx.Menu() menu = wx.Menu()
grantItem = menu.Append(wx.ID_ANY, _t("Grant Missing Skills")) if self.needsSkills:
self.Bind(wx.EVT_MENU, self.grantMissingSkills, grantItem) grantItem = menu.Append(wx.ID_ANY, _t("Grant Missing Skills"))
self.Bind(wx.EVT_MENU, self.grantMissingSkills, grantItem)
exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills")) exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills"))
self.Bind(wx.EVT_MENU, self.exportSkills, exportItem) self.Bind(wx.EVT_MENU, self.exportSkills, exportItem)
exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills (condensed)")) exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills (condensed)"))
self.Bind(wx.EVT_MENU, self.exportSkillsCondensed, exportItem) self.Bind(wx.EVT_MENU, self.exportSkillsCondensed, exportItem)
exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills (EVEMon)")) exportItem = menu.Append(wx.ID_ANY, _t("Copy Missing Skills (EVEMon)"))
self.Bind(wx.EVT_MENU, self.exportSkillsEveMon, exportItem) self.Bind(wx.EVT_MENU, self.exportSkillsEveMon, exportItem)
menu.AppendSeparator()
importItem = menu.Append(wx.ID_ANY, _t("Import Skills from Clipboard"))
self.Bind(wx.EVT_MENU, self.importSkillsFromClipboard, importItem)
self.PopupMenu(menu, pos) self.PopupMenu(menu, pos)
@@ -189,6 +199,13 @@ class CharacterSelection(wx.Panel):
sFit = Fit.getInstance() sFit = Fit.getInstance()
sFit.changeChar(fitID, charID) sFit.changeChar(fitID, charID)
self.charCache = self.charChoice.GetCurrentSelection() self.charCache = self.charChoice.GetCurrentSelection()
if not self._updatingFromHistory and charID is not None:
currentChar = self.getActiveCharacter()
if currentChar is not None:
if not self.charHistory or self.charHistory[-1] != currentChar:
self.charHistory.append(currentChar)
wx.PostEvent(self.mainFrame, GE.FitChanged(fitIDs=(fitID,))) wx.PostEvent(self.mainFrame, GE.FitChanged(fitIDs=(fitID,)))
def toggleRefreshButton(self): def toggleRefreshButton(self):
@@ -211,6 +228,29 @@ class CharacterSelection(wx.Panel):
return False return False
def selectPreviousChar(self):
currentChar = self.getActiveCharacter()
if currentChar is None:
return
if not self.charHistory:
return
if self.charHistory and self.charHistory[-1] == currentChar:
self.charHistory.pop()
if not self.charHistory:
return
prevChar = self.charHistory.pop()
if currentChar != prevChar:
self.charHistory.append(currentChar)
self._updatingFromHistory = True
if self.selectChar(prevChar):
self.charChanged(None)
self._updatingFromHistory = False
def fitChanged(self, event): def fitChanged(self, event):
""" """
When fit is changed, or new fit is selected When fit is changed, or new fit is selected
@@ -222,7 +262,7 @@ class CharacterSelection(wx.Panel):
self.charChoice.Enable(activeFitID is not None) self.charChoice.Enable(activeFitID is not None)
choice = self.charChoice choice = self.charChoice
sFit = Fit.getInstance() sFit = Fit.getInstance()
currCharID = choice.GetClientData(choice.GetCurrentSelection()) currCharID = choice.GetClientData(choice.GetCurrentSelection()) if choice.GetCurrentSelection() != -1 else None
fit = sFit.getFit(activeFitID) fit = sFit.getFit(activeFitID)
newCharID = fit.character.ID if fit is not None else None newCharID = fit.character.ID if fit is not None else None
@@ -256,6 +296,9 @@ class CharacterSelection(wx.Panel):
self.selectChar(sChar.all5ID()) self.selectChar(sChar.all5ID())
elif currCharID != newCharID: elif currCharID != newCharID:
if currCharID is not None and not self._updatingFromHistory:
if not self.charHistory or self.charHistory[-1] != currCharID:
self.charHistory.append(currCharID)
self.selectChar(newCharID) self.selectChar(newCharID)
if not fit.calculated: if not fit.calculated:
self.charChanged(None) self.charChanged(None)
@@ -289,6 +332,83 @@ class CharacterSelection(wx.Panel):
toClipboard(list) toClipboard(list)
def importSkillsFromClipboard(self, evt):
charID = self.getActiveCharacter()
if charID is None:
return
sChar = Character.getInstance()
char = sChar.getCharacter(charID)
text = fromClipboard()
if not text:
with wx.MessageDialog(self, _t("Clipboard is empty"), _t("Error"), wx.OK | wx.ICON_ERROR) as dlg:
dlg.ShowModal()
return
try:
lines = text.strip().splitlines()
imported = 0
errors = []
for line in lines:
line = line.strip()
if not line:
continue
try:
parts = line.rsplit(None, 1)
if len(parts) != 2:
errors.append(_t("Invalid format: {}").format(line))
continue
skillName = parts[0]
levelStr = parts[1]
try:
level = int(levelStr)
except ValueError:
try:
level = roman.fromRoman(levelStr.upper())
except (roman.InvalidRomanNumeralError, ValueError):
errors.append(_t("Invalid level format: {}").format(line))
continue
if level < 0 or level > 5:
errors.append(_t("Level must be between 0 and 5: {}").format(line))
continue
skill = char.getSkill(skillName)
sChar.changeLevel(charID, skill.item.ID, level)
imported += 1
except KeyError as e:
errors.append(_t("Skill not found: {}").format(skillName))
pyfalog.error("Skill not found: '{}'", skillName)
except Exception as e:
errors.append(_t("Error processing line '{}': {}").format(line, str(e)))
pyfalog.error("Error importing skill from line '{}': {}", line, e)
if imported > 0:
self.refreshCharacterList()
wx.PostEvent(self.mainFrame, GE.CharListUpdated())
fitID = self.mainFrame.getActiveFit()
if fitID is not None:
wx.PostEvent(self.mainFrame, GE.FitChanged(fitIDs=(fitID,)))
if errors:
errorMsg = _t("Imported {} skill(s). Errors:\n{}").format(imported, "\n".join(errors))
with wx.MessageDialog(self, errorMsg, _t("Import Skills"), wx.OK | wx.ICON_WARNING) as dlg:
dlg.ShowModal()
elif imported > 0:
with wx.MessageDialog(self, _t("Successfully imported {} skill(s)").format(imported), _t("Import Skills"), wx.OK) as dlg:
dlg.ShowModal()
except Exception as e:
pyfalog.error("Error importing skills from clipboard: {}", e)
with wx.MessageDialog(self, _t("Error importing skills. Please check the log file."), _t("Error"), wx.OK | wx.ICON_ERROR) as dlg:
dlg.ShowModal()
def _buildSkillsTooltip(self, reqs, currItem="", tabulationLevel=0): def _buildSkillsTooltip(self, reqs, currItem="", tabulationLevel=0):
tip = "" tip = ""
sCharacter = Character.getInstance() sCharacter = Character.getInstance()

View File

@@ -24,6 +24,7 @@ import config
import gui.mainFrame import gui.mainFrame
from eos.saveddata.drone import Drone from eos.saveddata.drone import Drone
from eos.saveddata.module import Module from eos.saveddata.module import Module
from eos.saveddata.ship import Ship
from gui.auxWindow import AuxiliaryFrame from gui.auxWindow import AuxiliaryFrame
from gui.bitmap_loader import BitmapLoader from gui.bitmap_loader import BitmapLoader
from gui.builtinItemStatsViews.itemAffectedBy import ItemAffectedBy from gui.builtinItemStatsViews.itemAffectedBy import ItemAffectedBy
@@ -35,6 +36,7 @@ from gui.builtinItemStatsViews.itemEffects import ItemEffects
from gui.builtinItemStatsViews.itemMutator import ItemMutatorPanel from gui.builtinItemStatsViews.itemMutator import ItemMutatorPanel
from gui.builtinItemStatsViews.itemProperties import ItemProperties from gui.builtinItemStatsViews.itemProperties import ItemProperties
from gui.builtinItemStatsViews.itemRequirements import ItemRequirements from gui.builtinItemStatsViews.itemRequirements import ItemRequirements
from gui.builtinItemStatsViews.itemSkills import ItemSkills
from gui.builtinItemStatsViews.itemTraits import ItemTraits from gui.builtinItemStatsViews.itemTraits import ItemTraits
from service.market import Market from service.market import Market
@@ -156,6 +158,8 @@ class ItemStatsContainer(wx.Panel):
def __init__(self, parent, stuff, item, context=None): def __init__(self, parent, stuff, item, context=None):
wx.Panel.__init__(self, parent) wx.Panel.__init__(self, parent)
sMkt = Market.getInstance() sMkt = Market.getInstance()
self.stuff = stuff
self.context = context
mainSizer = wx.BoxSizer(wx.VERTICAL) mainSizer = wx.BoxSizer(wx.VERTICAL)
@@ -196,6 +200,10 @@ class ItemStatsContainer(wx.Panel):
self.affectedby = ItemAffectedBy(self.nbContainer, stuff, item) self.affectedby = ItemAffectedBy(self.nbContainer, stuff, item)
self.nbContainer.AddPage(self.affectedby, _t("Affected by")) self.nbContainer.AddPage(self.affectedby, _t("Affected by"))
if stuff is not None and isinstance(stuff, Ship):
self.skills = ItemSkills(self.nbContainer, stuff, item)
self.nbContainer.AddPage(self.skills, _t("Skills"))
if config.debug: if config.debug:
self.properties = ItemProperties(self.nbContainer, stuff, item, context) self.properties = ItemProperties(self.nbContainer, stuff, item, context)
self.nbContainer.AddPage(self.properties, _t("Properties")) self.nbContainer.AddPage(self.properties, _t("Properties"))

View File

@@ -578,6 +578,7 @@ class MainFrame(wx.Frame):
toggleShipMarketId = wx.NewId() toggleShipMarketId = wx.NewId()
ctabnext = wx.NewId() ctabnext = wx.NewId()
ctabprev = wx.NewId() ctabprev = wx.NewId()
charPrevId = wx.NewId()
# Close Page # Close Page
self.Bind(wx.EVT_MENU, self.CloseCurrentPage, id=self.closePageId) self.Bind(wx.EVT_MENU, self.CloseCurrentPage, id=self.closePageId)
@@ -587,6 +588,7 @@ class MainFrame(wx.Frame):
self.Bind(wx.EVT_MENU, self.toggleShipMarket, id=toggleShipMarketId) self.Bind(wx.EVT_MENU, self.toggleShipMarket, id=toggleShipMarketId)
self.Bind(wx.EVT_MENU, self.CTabNext, id=ctabnext) self.Bind(wx.EVT_MENU, self.CTabNext, id=ctabnext)
self.Bind(wx.EVT_MENU, self.CTabPrev, id=ctabprev) self.Bind(wx.EVT_MENU, self.CTabPrev, id=ctabprev)
self.Bind(wx.EVT_MENU, self.selectPreviousCharacter, id=charPrevId)
actb = [(wx.ACCEL_CTRL, ord('T'), self.addPageId), actb = [(wx.ACCEL_CTRL, ord('T'), self.addPageId),
(wx.ACCEL_CMD, ord('T'), self.addPageId), (wx.ACCEL_CMD, ord('T'), self.addPageId),
@@ -620,7 +622,10 @@ class MainFrame(wx.Frame):
(wx.ACCEL_CMD, wx.WXK_PAGEDOWN, ctabnext), (wx.ACCEL_CMD, wx.WXK_PAGEDOWN, ctabnext),
(wx.ACCEL_CMD, wx.WXK_PAGEUP, ctabprev), (wx.ACCEL_CMD, wx.WXK_PAGEUP, ctabprev),
(wx.ACCEL_CMD | wx.ACCEL_SHIFT, ord("Z"), wx.ID_REDO) (wx.ACCEL_CMD | wx.ACCEL_SHIFT, ord("Z"), wx.ID_REDO),
# Shift+Tab for previous character
(wx.ACCEL_SHIFT, wx.WXK_TAB, charPrevId)
] ]
# Ctrl/Cmd+# for addition pane selection # Ctrl/Cmd+# for addition pane selection
@@ -747,6 +752,9 @@ class MainFrame(wx.Frame):
def CTabPrev(self, event): def CTabPrev(self, event):
self.fitMultiSwitch.PrevPage() self.fitMultiSwitch.PrevPage()
def selectPreviousCharacter(self, event):
self.charSelection.selectPreviousChar()
def HAddPage(self, event): def HAddPage(self, event):
self.fitMultiSwitch.AddPage() self.fitMultiSwitch.AddPage()
@@ -855,7 +863,8 @@ class MainFrame(wx.Frame):
char = fit.character char = fit.character
skillsMap = {} skillsMap = {}
for thing in itertools.chain(fit.modules, fit.drones, fit.fighters, [fit.ship], fit.appliedImplants, fit.boosters, fit.cargo): # for thing in itertools.chain(fit.modules, fit.drones, fit.fighters, [fit.ship], fit.appliedImplants, fit.boosters, fit.cargo):
for thing in itertools.chain(fit.modules, fit.drones, fit.fighters, fit.appliedImplants, fit.boosters, fit.cargo):
self._collectAffectingSkills(thing, char, skillsMap) self._collectAffectingSkills(thing, char, skillsMap)
skillsList = "" skillsList = ""