mirror of
https://github.com/panda3d/panda3d.git
synced 2025-10-04 10:54:24 -04:00
added EntityTypes
This commit is contained in:
parent
f8d1734379
commit
d2747bd99a
15
direct/src/level/AttribDesc.py
Executable file
15
direct/src/level/AttribDesc.py
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
"""AttribDesc.py module: contains the AttribDesc class"""
|
||||||
|
|
||||||
|
class AttribDesc:
|
||||||
|
"""
|
||||||
|
Entity attribute descriptor
|
||||||
|
name == name of attribute
|
||||||
|
"""
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
def getName(self):
|
||||||
|
return self.name
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
def __repr__(self):
|
||||||
|
return "AttribDesc('%s')" % self.name
|
@ -9,9 +9,6 @@ class NodePathAttribs:
|
|||||||
"""Derive from this class to give an entity the behavior of a
|
"""Derive from this class to give an entity the behavior of a
|
||||||
NodePath, without necessarily deriving from NodePath. Derived class
|
NodePath, without necessarily deriving from NodePath. Derived class
|
||||||
must implement getNodePath()."""
|
must implement getNodePath()."""
|
||||||
__attribs__ = (
|
|
||||||
'parent', 'pos', 'hpr',
|
|
||||||
)
|
|
||||||
def initNodePathAttribs(self, doReparent=1):
|
def initNodePathAttribs(self, doReparent=1):
|
||||||
"""Call this after the entity has been initialized"""
|
"""Call this after the entity has been initialized"""
|
||||||
self.callSetters('pos','x','y','z',
|
self.callSetters('pos','x','y','z',
|
||||||
@ -38,9 +35,6 @@ class NodePathAttribs:
|
|||||||
def setSz(self, *args): self.getNodePath().setSz(*args)
|
def setSz(self, *args): self.getNodePath().setSz(*args)
|
||||||
|
|
||||||
class privNodePathImpl(NodePath.NodePath):
|
class privNodePathImpl(NodePath.NodePath):
|
||||||
__attribs__ = (
|
|
||||||
'parent', 'pos', 'hpr',
|
|
||||||
)
|
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
node = hidden.attachNewNode(name)
|
node = hidden.attachNewNode(name)
|
||||||
NodePath.NodePath.__init__(self, node)
|
NodePath.NodePath.__init__(self, node)
|
||||||
|
@ -10,12 +10,6 @@ class Entity(DirectObject):
|
|||||||
and can be edited with the LevelEditor."""
|
and can be edited with the LevelEditor."""
|
||||||
notify = DirectNotifyGlobal.directNotify.newCategory('Entity')
|
notify = DirectNotifyGlobal.directNotify.newCategory('Entity')
|
||||||
|
|
||||||
__attribs__ = (
|
|
||||||
'type',
|
|
||||||
'name',
|
|
||||||
'comment',
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, level=None, entId=None):
|
def __init__(self, level=None, entId=None):
|
||||||
self.initializeEntity(level, entId)
|
self.initializeEntity(level, entId)
|
||||||
|
|
||||||
@ -71,117 +65,6 @@ class Entity(DirectObject):
|
|||||||
self.__dict__[attrib] = value
|
self.__dict__[attrib] = value
|
||||||
|
|
||||||
if __debug__:
|
if __debug__:
|
||||||
def getAttribDescriptors(entClass):
|
|
||||||
"""pass in an Entity class"""
|
|
||||||
# lazy compilation
|
|
||||||
if not entClass.__dict__.has_key('_attribDescs'):
|
|
||||||
entClass.compileAttribDescs(entClass)
|
|
||||||
return entClass.__dict__['_attribDescs']
|
|
||||||
getAttribDescriptors = staticmethod(getAttribDescriptors)
|
|
||||||
|
|
||||||
def compileAttribDescs(entClass):
|
|
||||||
Entity.notify.debug('compiling attrib descriptors for %s' %
|
|
||||||
entClass.__name__)
|
|
||||||
# create a complete list of attribute descriptors, pulling in
|
|
||||||
# the attribs from the entire class heirarchy
|
|
||||||
def getClassList(obj):
|
|
||||||
"""returns list, ordered from most-derived to base classes,
|
|
||||||
depth-first. Multiple inheritance base classes that do not
|
|
||||||
derive from Entity are listed before those that do.
|
|
||||||
"""
|
|
||||||
assert (type(obj) == types.ClassType)
|
|
||||||
classList = [obj]
|
|
||||||
|
|
||||||
# no need to go below Entity
|
|
||||||
if obj == Entity:
|
|
||||||
return classList
|
|
||||||
|
|
||||||
# explore the base classes
|
|
||||||
entityBases = []
|
|
||||||
nonEntityBases = []
|
|
||||||
for base in obj.__bases__:
|
|
||||||
l = getClassList(base)
|
|
||||||
if Entity in l:
|
|
||||||
entityBases.extend(l)
|
|
||||||
else:
|
|
||||||
nonEntityBases.extend(l)
|
|
||||||
# put bases that derive from Entity last
|
|
||||||
classList = classList + nonEntityBases + entityBases
|
|
||||||
return classList
|
|
||||||
|
|
||||||
def getUniqueClassList(obj):
|
|
||||||
classList = getClassList(obj)
|
|
||||||
# remove duplicates, leaving the last instance
|
|
||||||
uniqueList = []
|
|
||||||
for i in range(len(classList)):
|
|
||||||
if classList[i] not in classList[(i+1):]:
|
|
||||||
uniqueList.append(classList[i])
|
|
||||||
return uniqueList
|
|
||||||
|
|
||||||
classList = getUniqueClassList(entClass)
|
|
||||||
|
|
||||||
# work backwards, through the class list, from Entity to the
|
|
||||||
# most-derived class, aggregating attribute descriptors.
|
|
||||||
allAttribs = []
|
|
||||||
|
|
||||||
def isDistObjAI(obj):
|
|
||||||
# util func: is this class a DistributedObjectAI?
|
|
||||||
lineage = getClassLineage(obj)
|
|
||||||
for item in lineage:
|
|
||||||
if type(item) == types.ClassType:
|
|
||||||
if item.__name__ == 'DistributedObjectAI':
|
|
||||||
return 1
|
|
||||||
return 0
|
|
||||||
|
|
||||||
while len(classList):
|
|
||||||
cl = classList.pop()
|
|
||||||
Entity.notify.debug('looking for attribs on %s' % cl.__name__)
|
|
||||||
|
|
||||||
def getClassAttr(cl, name):
|
|
||||||
"""grab an attribute, such as __attribs__, off of a class"""
|
|
||||||
if cl.__dict__.has_key(name):
|
|
||||||
return cl.__dict__[name]
|
|
||||||
elif isDistObjAI(cl):
|
|
||||||
# It's a distributed AI class.
|
|
||||||
# Check the client-side class
|
|
||||||
globals = {}
|
|
||||||
locals = {}
|
|
||||||
ccn = cl.__name__[:-2] # clientClassName
|
|
||||||
Entity.notify.debug('importing client class %s' % ccn)
|
|
||||||
try:
|
|
||||||
exec 'import %s' % ccn in globals, locals
|
|
||||||
except:
|
|
||||||
print 'could not import %s' % ccn
|
|
||||||
return None
|
|
||||||
exec 'attr = %s.%s.__dict__.get("%s")' % (
|
|
||||||
ccn, ccn, name) in globals, locals
|
|
||||||
return locals['attr']
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# delete some attribs?
|
|
||||||
delAttribs = getClassAttr(cl, '__delAttribs__')
|
|
||||||
if delAttribs is not None:
|
|
||||||
assert type(delAttribs) in (types.TupleType, types.ListType)
|
|
||||||
Entity.notify.debug('delAttribs: %s' % list(delAttribs))
|
|
||||||
for attrib in delAttribs:
|
|
||||||
if attrib in allAttribs:
|
|
||||||
allAttribs.remove(attrib)
|
|
||||||
|
|
||||||
attribs = getClassAttr(cl, '__attribs__')
|
|
||||||
if attribs is not None:
|
|
||||||
assert type(attribs) in (types.TupleType, types.ListType)
|
|
||||||
Entity.notify.debug('attribs: %s' % list(attribs))
|
|
||||||
for attrib in attribs:
|
|
||||||
if attrib not in allAttribs:
|
|
||||||
allAttribs.append(attrib)
|
|
||||||
|
|
||||||
# we now have an ordered list of all of the attribute descriptors
|
|
||||||
# for this class. Cache it on the class object
|
|
||||||
Entity.notify.debug('all attribs: %s' % allAttribs)
|
|
||||||
entClass.__dict__['_attribDescs'] = allAttribs
|
|
||||||
compileAttribDescs = staticmethod(compileAttribDescs)
|
|
||||||
|
|
||||||
# support for level editing
|
# support for level editing
|
||||||
def handleAttribChange(self, attrib, value):
|
def handleAttribChange(self, attrib, value):
|
||||||
# call callback function if it exists
|
# call callback function if it exists
|
||||||
|
99
direct/src/level/EntityTypeRegistry.py
Executable file
99
direct/src/level/EntityTypeRegistry.py
Executable file
@ -0,0 +1,99 @@
|
|||||||
|
"""EntityTypeRegistry module: contains the EntityTypeRegistry class"""
|
||||||
|
|
||||||
|
import DirectNotifyGlobal
|
||||||
|
import types
|
||||||
|
import AttribDesc
|
||||||
|
import EntityTypes
|
||||||
|
from PythonUtil import mostDerivedLast
|
||||||
|
|
||||||
|
class EntityTypeRegistry:
|
||||||
|
notify = DirectNotifyGlobal.directNotify.newCategory('EntityTypeRegistry')
|
||||||
|
|
||||||
|
def __init__(self, entityTypeModule=EntityTypes):
|
||||||
|
"""pass in a module that contains EntityType classes"""
|
||||||
|
# maps entity typename to type class
|
||||||
|
self.name2typeClass = {}
|
||||||
|
|
||||||
|
# get a list of the entity type classes in the type module
|
||||||
|
classes = []
|
||||||
|
for key, value in entityTypeModule.__dict__.items():
|
||||||
|
if type(value) is types.ClassType:
|
||||||
|
if issubclass(value, EntityTypes.Entity):
|
||||||
|
classes.append(value)
|
||||||
|
|
||||||
|
# put derived classes after their bases
|
||||||
|
mostDerivedLast(classes)
|
||||||
|
|
||||||
|
# scrub through the class heirarchy and compile complete
|
||||||
|
# attribute descriptor lists for each concrete Entity type class
|
||||||
|
for c in classes:
|
||||||
|
# if this is a concrete Entity type, add it to the dict
|
||||||
|
if c.__dict__.has_key('name'):
|
||||||
|
if self.name2typeClass.has_key(c.name):
|
||||||
|
EntityTypeRegistry.notify.debug(
|
||||||
|
"replacing %s with %s for type '%s'" %
|
||||||
|
(self.name2typeClass[c.name], c, c.name))
|
||||||
|
self.name2typeClass[c.name] = c
|
||||||
|
|
||||||
|
self.privCompileAttribDescs(c)
|
||||||
|
|
||||||
|
def getAttributeDescriptors(self, entityTypeName):
|
||||||
|
return self.name2typeClass[entityTypeName]._attribDescs
|
||||||
|
|
||||||
|
def privCompileAttribDescs(self, entTypeClass):
|
||||||
|
# has someone already compiled the info?
|
||||||
|
if entTypeClass.__dict__.has_key('_attribDescs'):
|
||||||
|
return
|
||||||
|
|
||||||
|
c = entTypeClass
|
||||||
|
EntityTypeRegistry.notify.debug('compiling attrib descriptors for %s' %
|
||||||
|
c.__name__)
|
||||||
|
|
||||||
|
# make sure all of our base classes have their complete list of
|
||||||
|
# attribDescs
|
||||||
|
for base in c.__bases__:
|
||||||
|
self.privCompileAttribDescs(base)
|
||||||
|
|
||||||
|
# aggregate the attribute descriptors from our direct base classes
|
||||||
|
delAttribs = c.__dict__.get('delAttribs', [])
|
||||||
|
baseADs = []
|
||||||
|
|
||||||
|
bases = list(c.__bases__)
|
||||||
|
# make sure that Entity comes first
|
||||||
|
if EntityTypes.Entity in bases:
|
||||||
|
bases.remove(EntityTypes.Entity)
|
||||||
|
bases = [EntityTypes.Entity] + bases
|
||||||
|
|
||||||
|
for base in bases:
|
||||||
|
for desc in base._attribDescs:
|
||||||
|
# are we blocking this attribute?
|
||||||
|
if desc.getName() in delAttribs:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# make sure we haven't already picked up this attribute
|
||||||
|
# from an earlier base class
|
||||||
|
for d in baseADs:
|
||||||
|
if desc.getName() == d.getName():
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
baseADs.append(desc)
|
||||||
|
|
||||||
|
# now that we have all of the descriptors from our base classes,
|
||||||
|
# add the descriptors from this class
|
||||||
|
attribDescs = []
|
||||||
|
if c.__dict__.has_key('attribs'):
|
||||||
|
for attrib in c.attribs:
|
||||||
|
desc = AttribDesc.AttribDesc(attrib)
|
||||||
|
|
||||||
|
# if we picked up an attribute with the same name from a base
|
||||||
|
# class, this overrides it
|
||||||
|
for ad in baseADs:
|
||||||
|
if ad.getName() == desc.getName():
|
||||||
|
baseADs.remove(ad)
|
||||||
|
# there ought to be no more than one desc with
|
||||||
|
# this name from the base classes
|
||||||
|
break
|
||||||
|
|
||||||
|
attribDescs.append(desc)
|
||||||
|
|
||||||
|
c._attribDescs = baseADs + attribDescs
|
208
direct/src/level/EntityTypes.py
Executable file
208
direct/src/level/EntityTypes.py
Executable file
@ -0,0 +1,208 @@
|
|||||||
|
"""EntityTypes module: contains classes that describe Entity types"""
|
||||||
|
|
||||||
|
class Entity:
|
||||||
|
attribs = (
|
||||||
|
'type',
|
||||||
|
'name',
|
||||||
|
'comment',
|
||||||
|
)
|
||||||
|
|
||||||
|
class LevelMgr(Entity):
|
||||||
|
name = 'levelMgr'
|
||||||
|
attribs = (
|
||||||
|
'cogLevel',
|
||||||
|
'cogTrack',
|
||||||
|
'modelFilename',
|
||||||
|
)
|
||||||
|
|
||||||
|
class LogicGate(Entity):
|
||||||
|
name = 'logicGate'
|
||||||
|
attribs = (
|
||||||
|
'input_input1_bool',
|
||||||
|
'input_input2_bool',
|
||||||
|
'isInput1',
|
||||||
|
'isInput2',
|
||||||
|
'logicType',
|
||||||
|
'output',
|
||||||
|
)
|
||||||
|
|
||||||
|
class NodepathImpl:
|
||||||
|
attribs = (
|
||||||
|
'parent',
|
||||||
|
'pos',
|
||||||
|
'hpr',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Note: this covers Nodepath and DistributedNodepath
|
||||||
|
class Nodepath(Entity, NodepathImpl):
|
||||||
|
name = 'nodepath'
|
||||||
|
|
||||||
|
class NodepathAttribs:
|
||||||
|
attribs = (
|
||||||
|
'parent',
|
||||||
|
'pos',
|
||||||
|
'hpr',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Zone(Entity, NodepathAttribs):
|
||||||
|
name = 'zone'
|
||||||
|
delAttribs = (
|
||||||
|
'parent',
|
||||||
|
'pos',
|
||||||
|
'hpr',
|
||||||
|
)
|
||||||
|
attribs = (
|
||||||
|
'description',
|
||||||
|
'modelZoneNum',
|
||||||
|
)
|
||||||
|
|
||||||
|
class BarrelBase(Nodepath):
|
||||||
|
delAttribs = (
|
||||||
|
'hpr',
|
||||||
|
)
|
||||||
|
attribs = (
|
||||||
|
'h',
|
||||||
|
)
|
||||||
|
|
||||||
|
class BeanBarrel(BarrelBase):
|
||||||
|
name = 'beanBarrel'
|
||||||
|
|
||||||
|
class GagBarrel(BarrelBase):
|
||||||
|
name = 'gagBarrel'
|
||||||
|
attribs = (
|
||||||
|
'gagLevel',
|
||||||
|
'gagTrack',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Switch(Entity, NodepathImpl):
|
||||||
|
attribs = (
|
||||||
|
'scale',
|
||||||
|
'color',
|
||||||
|
'model',
|
||||||
|
'input_isOn_bool',
|
||||||
|
'isOn',
|
||||||
|
'output',
|
||||||
|
'secondsOn',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Button(Switch):
|
||||||
|
name = 'button'
|
||||||
|
|
||||||
|
class Trigger(Switch):
|
||||||
|
name = 'trigger'
|
||||||
|
|
||||||
|
class Crate(Nodepath):
|
||||||
|
name = 'crate'
|
||||||
|
delAttribs = (
|
||||||
|
'hpr',
|
||||||
|
)
|
||||||
|
attribs = (
|
||||||
|
'scale',
|
||||||
|
'gridId',
|
||||||
|
'pushable',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Door(Entity):
|
||||||
|
name = 'door'
|
||||||
|
attribs = (
|
||||||
|
'parent',
|
||||||
|
'pos',
|
||||||
|
'hpr',
|
||||||
|
'scale',
|
||||||
|
'color',
|
||||||
|
'model',
|
||||||
|
'doorwayNum',
|
||||||
|
'input_Lock0_bool',
|
||||||
|
'input_Lock1_bool',
|
||||||
|
'input_Lock2_bool',
|
||||||
|
'input_Lock3_bool',
|
||||||
|
'input_isOpen_bool',
|
||||||
|
'isLock0Unlocked',
|
||||||
|
'isLock1Unlocked',
|
||||||
|
'isLock2Unlocked',
|
||||||
|
'isLock3Unlocked',
|
||||||
|
'isOpen',
|
||||||
|
'output',
|
||||||
|
'secondsOpen',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Grid(Nodepath):
|
||||||
|
name = 'grid'
|
||||||
|
delAttribs = (
|
||||||
|
'hpr',
|
||||||
|
)
|
||||||
|
attribs = (
|
||||||
|
'cellSize',
|
||||||
|
'numCol',
|
||||||
|
'numRow',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Lift(Nodepath):
|
||||||
|
name = 'lift'
|
||||||
|
attribs = (
|
||||||
|
'duration',
|
||||||
|
'startPos',
|
||||||
|
'endPos',
|
||||||
|
'modelPath',
|
||||||
|
'floorName',
|
||||||
|
'modelScale',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Platform(Nodepath):
|
||||||
|
name = 'platform'
|
||||||
|
delAttribs = (
|
||||||
|
'pos',
|
||||||
|
)
|
||||||
|
attribs = (
|
||||||
|
'startPos',
|
||||||
|
'endPos',
|
||||||
|
'speed',
|
||||||
|
'waitDur',
|
||||||
|
'phaseShift',
|
||||||
|
)
|
||||||
|
|
||||||
|
class SinkingPlatform(Nodepath):
|
||||||
|
name = 'sinkingPlatform'
|
||||||
|
delAttribs = (
|
||||||
|
'pos',
|
||||||
|
)
|
||||||
|
attribs = (
|
||||||
|
'endPos',
|
||||||
|
'phaseShift',
|
||||||
|
'startPos',
|
||||||
|
'verticalRange',
|
||||||
|
'sinkRate',
|
||||||
|
'riseRate',
|
||||||
|
'speed',
|
||||||
|
'waitDur',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Stomper(Nodepath):
|
||||||
|
name = 'stomper'
|
||||||
|
attribs = (
|
||||||
|
'headScale',
|
||||||
|
'motion',
|
||||||
|
'period',
|
||||||
|
'phaseShift',
|
||||||
|
'range',
|
||||||
|
'shaftScale',
|
||||||
|
'soundLen',
|
||||||
|
'soundOn',
|
||||||
|
'style',
|
||||||
|
'zOffset',
|
||||||
|
)
|
||||||
|
|
||||||
|
class StomperPair(Nodepath):
|
||||||
|
name = 'stomperPair'
|
||||||
|
attribs = (
|
||||||
|
'headScale',
|
||||||
|
'motion',
|
||||||
|
'period',
|
||||||
|
'phaseShift',
|
||||||
|
'range',
|
||||||
|
'shaftScale',
|
||||||
|
'soundLen',
|
||||||
|
'soundOn',
|
||||||
|
'stomperIds',
|
||||||
|
'style',
|
||||||
|
)
|
@ -6,12 +6,6 @@ import LevelMgrBase
|
|||||||
class LevelMgr(LevelMgrBase.LevelMgrBase):
|
class LevelMgr(LevelMgrBase.LevelMgrBase):
|
||||||
"""This class manages editable client-side level attributes"""
|
"""This class manages editable client-side level attributes"""
|
||||||
|
|
||||||
__attribs__ = (
|
|
||||||
'cogLevel',
|
|
||||||
'cogTrack',
|
|
||||||
'modelFilename',
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, level, entId):
|
def __init__(self, level, entId):
|
||||||
LevelMgrBase.LevelMgrBase.__init__(self, level, entId)
|
LevelMgrBase.LevelMgrBase.__init__(self, level, entId)
|
||||||
|
|
||||||
|
@ -69,10 +69,6 @@ class LogicGateAI(Entity.Entity, PandaObject.PandaObject):
|
|||||||
if __debug__:
|
if __debug__:
|
||||||
notify = DirectNotifyGlobal.directNotify.newCategory(
|
notify = DirectNotifyGlobal.directNotify.newCategory(
|
||||||
'LogicGateAI')
|
'LogicGateAI')
|
||||||
__attribs__ = (
|
|
||||||
'input_input1_bool', 'input_input2_bool',
|
|
||||||
'isInput1', 'isInput2', 'logicType', 'output',
|
|
||||||
)
|
|
||||||
logicTests={
|
logicTests={
|
||||||
"and": andTest,
|
"and": andTest,
|
||||||
"or": orTest,
|
"or": orTest,
|
||||||
|
@ -4,13 +4,6 @@ import ZoneEntityBase
|
|||||||
import BasicEntities
|
import BasicEntities
|
||||||
|
|
||||||
class ZoneEntity(ZoneEntityBase.ZoneEntityBase, BasicEntities.NodePathAttribs):
|
class ZoneEntity(ZoneEntityBase.ZoneEntityBase, BasicEntities.NodePathAttribs):
|
||||||
__delAttribs__ = (
|
|
||||||
'parent', 'pos', 'hpr',
|
|
||||||
)
|
|
||||||
__attribs__ = (
|
|
||||||
'description', 'modelZoneNum',
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, level, entId):
|
def __init__(self, level, entId):
|
||||||
ZoneEntityBase.ZoneEntityBase.__init__(self, level, entId)
|
ZoneEntityBase.ZoneEntityBase.__init__(self, level, entId)
|
||||||
|
|
||||||
|
@ -757,3 +757,13 @@ class PureVirtual:
|
|||||||
pure-virtual methods. """
|
pure-virtual methods. """
|
||||||
raise 'error: derived class must implement %s' % callerInfo()[2]
|
raise 'error: derived class must implement %s' % callerInfo()[2]
|
||||||
|
|
||||||
|
def mostDerivedLast(classList):
|
||||||
|
"""pass in list of classes. sorts list in-place, with derived classes
|
||||||
|
appearing after their bases"""
|
||||||
|
def compare(a,b):
|
||||||
|
if a is b:
|
||||||
|
return 0
|
||||||
|
if issubclass(a,b):
|
||||||
|
return 1
|
||||||
|
return -1
|
||||||
|
classList.sort(compare)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user