Compare commits

...

45 Commits
master ... ecs

Author SHA1 Message Date
troido 9473502e6e started on better spawning 2019-10-12 01:23:46 +02:00
troido ab720679aa finally got rid of all old components 2019-10-11 20:08:15 +02:00
troido 156e0ebff7 actually include control objects in commit 2019-10-11 18:12:52 +02:00
troido abf7200d58 controls are now separate objects 2019-10-11 18:12:30 +02:00
troido 062cedb67b building is now done through ECS 2019-10-09 17:56:25 +02:00
troido bffba2abb7 equipment and food are now ECS 2019-10-09 00:04:08 +02:00
troido daa712fedb started on upgrading items 2019-10-06 22:00:37 +02:00
troido 8d0f2f06b2 spawners are now also in ECS 2019-10-05 21:58:57 +02:00
troido e98118d958 equipment uses ECS now, though all item uses are still broken 2019-10-04 10:05:40 +02:00
troido a22baed6e1 fix import errors 2019-10-03 22:53:51 +02:00
troido c1eec37c4a grow now uses same stuff as volatile 2019-10-03 22:46:55 +02:00
troido 265ad53c18 changed working of volatile 2019-10-03 22:14:50 +02:00
troido deb5385996 volatile is now also done in ecs 2019-10-03 21:41:37 +02:00
troido 480688d36e add conversion from old style objects to new 2019-10-03 19:54:58 +02:00
troido 5369da7c53 fixed some inventory stuff, but using items is still broken 2019-10-03 19:20:47 +02:00
troido cc741be4e8 removed old events. Listen and Inventory are now ECS 2019-10-03 18:41:19 +02:00
troido b987871f27 change way loot works 2019-10-02 20:04:40 +02:00
troido 0ebdd174dd completely replaced old serializers with new serialisers 2019-10-02 19:36:44 +02:00
troido da00624e44 fix more template problems 2019-10-02 11:39:53 +02:00
troido 04b37ca667 use templates more; deprecate makeEntity 2019-10-02 10:51:05 +02:00
troido 7672b945c0 use Template objects for serialisation 2019-10-02 10:08:27 +02:00
troido 6c854a3db9 more through roomData 2019-10-01 23:07:16 +02:00
troido 7af47c6c67 try getting all components through roomData 2019-10-01 22:40:40 +02:00
troido f2be6242cc fix importerrors and cleanup 2019-10-01 22:36:03 +02:00
troido 41232938a0 built messaging architecture using components; tried to get getComponent through roomData 2019-10-01 22:33:58 +02:00
troido d12cd74e64 simplified systems 2019-10-01 19:09:18 +02:00
troido 7341216f52 Portals use ECS now too 2019-10-01 00:54:49 +02:00
troido 820428ba74 removed Target 2019-10-01 00:27:55 +02:00
troido 3812585864 fix missing trap import and forgotten debug message 2019-10-01 00:01:39 +02:00
troido bfcb533880 Trap now uses ECS 2019-09-30 23:58:58 +02:00
troido 1c450ca845 cleanup Change and Harvest components 2019-09-30 17:30:50 +02:00
troido f6c6c8f000 harvest now goes through ECS 2019-09-30 17:23:22 +02:00
troido dc9ae999d6 loot now also goes through ECS 2019-09-30 15:05:02 +02:00
troido 64ce165878 messages now kind of go through ECS 2019-09-30 13:42:46 +02:00
troido 2976b5c038 components are now identified by type instead of name 2019-09-30 11:49:23 +02:00
troido 7a68881489 moved calling of systems back to Room instead of RoomData 2019-09-30 11:04:40 +02:00
troido cd8d7321e2 dataComponents are accessed with a getter in entity 2019-09-30 10:59:35 +02:00
troido c28a63641d datacomponents now know their own name so it doesn't need to be passed when creating the entity 2019-09-30 10:08:02 +02:00
troido 81b5f6b226 System gives required components as arguments 2019-09-30 01:08:49 +02:00
troido 00b3e13d0d systems now check their own dependecies 2019-09-29 23:32:59 +02:00
troido 58e77aac97 cleanup! also, alignment component isn't needed 2019-09-29 22:54:40 +02:00
troido b7db0ab600 controllers use ECS now too 2019-09-29 21:56:14 +02:00
troido 2267582f11 move and heal now use ECS too 2019-09-29 18:20:49 +02:00
troido 060adac899 fighter and attackable now use systems 2019-09-29 16:42:28 +02:00
troido 187306c081 tried to steer away from current components 2019-09-29 00:46:55 +02:00
106 changed files with 1533 additions and 1694 deletions

View File

@ -29,7 +29,7 @@ class CommandHandler:
"selectitem": self.selectItem,
"inputwithselected": self.actWithSelected,
"use": self.useSelected,
"unuse": self.unUseSelected,
"drop": self.dropSelected,
"take": self.takeSelected,
"eval": self.eval,
"exec": self.exec,
@ -74,7 +74,7 @@ class CommandHandler:
self.input(["say", text])
def pick(self, option):
self.input(["pick", option])
self.input(["interact", [None, "north", "south", "east", "west"], option])
def chat(self, text):
self.client.sendChat( text)
@ -103,20 +103,15 @@ class CommandHandler:
menu = self.client.display.getSelectedMenu()
selected = self.client.display.getSelectedItem(menu)
if menu in ("inventory", "equipment"):
action = "use"
self.input(["use", menu, selected])
elif menu == "ground":
action = "interact",
else:
return
self.input([action, selected])
self.input(["interact", selected])
def unUseSelected(self):
def dropSelected(self):
menu = self.client.display.getSelectedMenu()
selected = self.client.display.getSelectedItem(menu)
if menu == "inventory":
action = "drop"
elif menu == "equipment":
action = "unequip"
else:
return
self.input([action, selected])

View File

@ -88,7 +88,7 @@ class Client:
if error == "invalidname":
self.close("Invalid name error: "+ str(message.description))
return
self.log(message.errtype + ": " + message.description)
self.log(message.errType + ": " + message.description)
elif isinstance(message, messages.MessageMessage):
self.log(message.text, message.type)
elif isinstance(message, messages.WorldMessage):

View File

@ -87,7 +87,7 @@ class InputHandler:
elif key == "^I": # tab
# return to game but keep entered string
self.typing = False
elif key.isprintable():
elif key.isprintable() and len(key) == 1:
self.string = self.string[:self.cursor] + key + self.string[self.cursor:]
self.cursor += len(key)

View File

@ -5,10 +5,10 @@
"w": null,
"W": null,
"q": ["move", "west"],
"a": ["unuse"],
"a": ["drop"],
"A": ["take"],
"Z": ["input", ["attack", "north"]],
"Q": ["input", ["attack", "west"]]
"Z": ["input", ["attack", ["north"]]],
"Q": ["input", ["attack", ["west"]]]
},
"help": "Controls:\n aqsd or arrows:\n Move around\n e: Grab\n q: Drop/unequip\n selected\n r: Interact\n f: Attack\n t: Chat\n E: Use selected\n A: Take selected\n xc: select item\n vb: select menu\n ctrl-c: close client"
"help": "Controls:\n aqsd or arrows: Move around\n e: Grab\n q: Drop selected\n r: Interact\n f: Attack\n t: Chat\n E: Use selected\n A: Take selected\n xc: select item\n vb: select menu\n ctrl-c: close client"
}

View File

@ -12,11 +12,11 @@
"j": ["move", "south"],
"l": ["move", "east"],
"h": ["move", "west"],
"e": ["input", ["take"]],
"q": ["unuse"],
"e": ["input", ["take", null]],
"q": ["drop"],
"Q": ["take"],
"E": ["use"],
"R": ["input", ["interact"]],
"R": ["input", ["interact", [null]]],
"r": ["input", ["interact", [null, "north", "south", "east", "west"]]],
"x": ["selectitem", -1, true, true],
"c": ["selectitem", 1, true, true],
@ -27,16 +27,16 @@
"/": ["selectwidget", -1, true, true],
"*": ["selectwidget", 1, true, true],
"f": ["input", ["attack", [null, "north", "south", "east", "west"]]],
"F": ["input", ["attack"]],
"W": ["input", ["attack", "north"]],
"S": ["input", ["attack", "south"]],
"D": ["input", ["attack", "east"]],
"A": ["input", ["attack", "west"]],
"F": ["input", ["attack", [null]]],
"W": ["input", ["attack", ["north"]]],
"S": ["input", ["attack", ["south"]]],
"D": ["input", ["attack", ["east"]]],
"A": ["input", ["attack", ["west"]]],
"t": ["runinput"],
"enter": ["runinput"],
"pageup": ["scrollchat", 1],
"pagedown": ["scrollchat", -1],
"/": ["runinput", "/"]
},
"help": " Controls:\nwasd or arrows: Move around\ne: Grab\nq: Drop/unequip selected\nr: Interact\nf: Attack\nt: Chat\nE: Use selected\nxc: select item\nvb: select menu\nctrl-c: close client\nPgUp/PgDn: scroll chat"
"help": " Controls:\nwasd or arrows: Move around\ne: Grab\nq: Drop selected\nr: Interact\nf: Attack\nt: Chat\nE: Use selected\nxc: select item\nvb: select menu\nctrl-c: close client\nPgUp/PgDn: scroll chat"
}

View File

@ -3,7 +3,7 @@
"height": 11,
"spawn": [17, 8],
"places": {
"stairup": [18, 8]
"stairup": [17, 8]
},
"grid": [

View File

@ -1,10 +1,10 @@
{
"width": 64,
"height": 64,
"spawn": [10, 15],
"spawn": [15, 30],
"places": {
"stairdown": [38, 33],
"caveentrance": [31, 51],
"stairdown": [37, 33],
"caveentrance": [31, 50],
"right": [62, 10],
"cavebridge": [1, 60]
},
@ -43,17 +43,17 @@
",~~~~~,,,,,'..'',,,,,,,,,,,,,,,,,,,,,,,,,,'''''''',,,,,,,,,,,,'1",
",~~~~~,,,,,''.'',,,,,,,,,,,,,,,,,,,,,,,,,,'''T'''',,,,,,,,,,,,'1",
",~~~~~,,,,,'..''''''#####################''''''''',,,,,,,,,,,,'1",
",~~~~~,,,,,''..'''''#+++++++++++++#+++++#''''''''',,,,,,,,,,,,'1",
",~~~~~,,,,,''..'''BB#+++++++++++++#+++++#''''''''',,,,,,,,,,,,'1",
",~~~~~'''''''..'''.'#+++++++++++++#+++++#''''''''',,,,,,,,,,,,'1",
"~~~~~''''''''''..'''#+++++++++++++++++++#''''''''',,,,,,,,,,,,'1",
"~~~~~''''''''.'.''..D+++++++++++++#+++++#''''''''',,,,,,,,,,,,'1",
"~~~~~'''''..''.'....D+++++++++++++#######''''''''',,,,,,,,,,,,'1",
"~~~~~''''''.'..'....#+++++++++++++#+++++#''''''''',,,,,,,,,,,,'1",
"~~~~~'''5''''''../!]#+++++++++++++++++++#''''''''',,,,,,,,,,,,'1",
"~~~~~'''6''''.'.''..D+++++++++++++#+++++#''''''''',,,,,,,,,,,,'1",
"~~~~~'''7'..''.'....D+++++++++++++#######''''''''',,,,,,,,,,,,'1",
"~~~~~'888''.'..'....#+++++++++++++#+++++#''''''''',,,,,,,,,,,,'1",
"~~~~,,,,,,'.'#d.....#+++++++++++++#+++++#''''''''',,,,,,,,,,,,'1",
"~~~~,,,,,,'.'#..d..^#+++++++++++++++++>+#''''''''',,,,,,,,,,,,'1",
"~~~~,,,,,,8.'#..d..^#+++++++++++++++++>+#''''''''',,,,,,,,,,,,'1",
"~~~~,,,,,,'.'########+++++++++++++#+++++#''''''''',,,,,,,,,,,,'1",
"~~~~,,,,,,'.''''''''#####################''''''''',,,,,,,,,,,,'1",
"~~~~,,,,,,'.'''''''''''''''''''''''''''''''''''''',,,,,,,,,,,,'1",
"~~~~,,,,,,8.'''''''''''''''''''''''''''''''''''''',,,,,,,,,,,,'1",
"~~~~~,,,,,'.'''''''''''''''''''''''''''''''''''''',,,,,,,,,,,,'1",
"~~~~~,,,,,'.'''''''''''''''''''''''''''''''''''''''''''''''''''1",
"~~~~~~,,,,'....................................................1",
@ -142,6 +142,11 @@
"args": ["tutorial","tunnelout"]
}, "floor"],
"s": ["grass", "trader"],
"B": ["grass", "freeland", "builtwall"],
"5": ["grass", "plantedradishseed"],
"6": ["grass", "plantedradishseedling"],
"7": ["grass", "youngradishplant"],
"8": ["grass", "radishplant"],
" ": []
}
}

View File

@ -3,7 +3,7 @@
"height": 44,
"spawn": [32, 4],
"places": {
"stairup": [32, 3]
"stairup": [32, 4]
},
"grid": [
" ",

View File

@ -1,24 +0,0 @@
from .alignment import Alignment
from .build import Build
from .change import Change
from .customserializer import CustomSerializer
from .equippable import Equippable
from .fighter import Fighter
from .food import Food
from .grow import Growing
from .harvest import Harvest
from .item import Item
from .loot import Loot
from .move import Move
from .monsterai import MonsterAi
from .optionmenu import OptionMenu
from .portal import Portal
from .randomwalkcontroller import RandomWalkController
from .select import Select
from .selectable import Selectable
from .spawner import Spawner
from .staticserializer import StaticSerializer
from .trap import Trap
from .volatile import Volatile
from .weather import Weather

View File

@ -1,28 +0,0 @@
from .. import faction
from .component import Component
class Alignment(Component):
def __init__(self, faction):
self.faction = faction
def getFaction(self):
return self.faction
def isEnemy(self, obj):
alignment = obj.getComponent("alignment")
if not alignment:
return False
return self.faction.isEnemy(alignment.getFaction())
def __setstate__(self, state):
self.__dict__.update(state)
self.faction = faction.factions[self.faction.getName()]
def toJSON(self):
return self.faction.getName()
@classmethod
def fromJSON(cls, facName):
return cls(faction.factions[facName])

View File

@ -1,43 +0,0 @@
from .. import gameobjects
from .component import Component
class Build(Component):
""" item type for item that can be placed on the map to become something more static (like buildable walls or crops)"""
def __init__(self, objType, objArgs=(), objKwargs=None, flagsNeeded=frozenset(), blockingFlags=frozenset()):
if objKwargs is None:
objKwargs = {}
self.buildType = objType
self.buildArgs = objArgs
self.buildKwargs = objKwargs
self.flagsNeeded = set(flagsNeeded)
self.blockingFlags = set(blockingFlags)
def attach(self, obj):
self.owner = obj
obj.addListener("roomjoin", self.roomJoin)
def roomJoin(self, obj, roomData, stamp):
self.roomData = roomData
def use(self, user):
groundFlags = user.getGround().getFlags()
if not self.flagsNeeded <= groundFlags or groundFlags & self.blockingFlags: # <= means subset when applied on sets
# groundFlags must contain all of self.flagsNeeded, and none of self.blockingFlags
return
roomData = user.getRoomData()
obj = gameobjects.makeEntity(self.buildType, roomData, *self.buildArgs, preserve=True, **self.buildKwargs)
obj.place(user.getGround())
self.owner.trigger("drop")
def toJSON(self):
return {
"objType": self.buildType,
"objArgs": self.buildArgs,
"objKwargs": self.buildKwargs,
"flagsNeeded": list(self.flagsNeeded),
"blockingFlags": list(self.blockingFlags)
}

View File

@ -1,40 +0,0 @@
from .component import Component
from .. import gameobjects
class Change(Component):
""" Objects that change type on interaction.
This could probably also be implemented with a combination of harvest and loot,
but this seemed better."""
def __init__(self, into, permitted=None, nextArgs=None, nextKwargs=None):
self.into = into
self.nextArgs = nextArgs or []
self.nextKwargs = nextKwargs or {}
self.permitted = permitted
def attach(self, obj):
self.owner = obj
obj.addListener("roomjoin", self.roomJoin)
def roomJoin(self, o, roomData, stamp):
self.roomData = roomData
def interact(self, other):
if self.permitted is not None and other.getName() not in self.permitted:
return
obj = gameobjects.makeEntity(self.into, self.roomData, *self.nextArgs, preserve=self.owner.isPreserved(), **self.nextKwargs)
obj.place(self.owner.getGround())
self.owner.remove()
def toJSON(self):
return {
"into": self.into,
"permitted": self.permitted,
"nextArgs": self.nextArgs,
"nextKwargs": self.nextKwargs
}

View File

@ -1,21 +0,0 @@
class Component:
def attach(self, obj):
pass
def remove(self):
pass
def toJSON(self):
return None
@classmethod
def fromJSON(cls, data=None):
if data is None:
return cls()
return cls(**data)

View File

@ -1,14 +0,0 @@
from .component import Component
class CustomSerializer(Component):
def __init__(self, fn):
self.fn = fn
def attach(self, obj):
self.owner = obj
def serialize(self):
return self.fn(self.owner)

View File

@ -1,61 +0,0 @@
from .component import Component
from .. import gameobjects
class Equipment(Component):
def __init__(self, slots=None):
if slots is None:
slots = {}
self.slots = {key: None for key in slots}
self.owner = None
for slot, item in slots.items():
self.equip(slot, item)
def attach(self, obj):
self.owner = obj
def getBonus(self, skill):
return sum(item.getComponent("item").getStat(skill) for item in self.slots.values() if item)
def equip(self, slot, item):
if slot in self.slots:
oldItem = self.slots[slot]
if oldItem:
self.owner.trigger("take", oldItem)
self.slots[slot] = item
if self.owner:
self.owner.trigger("equipmentchange")
else:
# todo: better exception
raise Exception("No such slot")
def unEquip(self, slot):
if slot in self.slots:
self.slots[slot] = None
if self.owner:
self.owner.trigger("equipmentchange")
def getSlots(self):
return self.slots.copy()
def canEquip(self, item):
return item.getSlot() in self.slots
def toJSON(self):
return {
slotName: item.serialize() if item else None
for slotName, item in self.slots.items()
}
@classmethod
def fromJSON(cls, slots):
return cls({
slotname: gameobjects.createEntity(item) if item else None
for slotname, item in slots.items()
})

View File

@ -1,35 +0,0 @@
from .component import Component
class Equippable(Component):
""" item type for item that can be placed on the map to become something more static (like buildable walls or crops)"""
def __init__(self, slot, stats=None):
if stats is None:
stats = {}
self.slot = slot
self.stats = stats
def attach(self, obj):
self.owner = obj
def getSlot(self):
return self.slot
def use(self, user):
equipment = user.getComponent("equipment")
if equipment.canEquip(self):
equipment.equip(self.slot, self.owner)
self.owner.trigger("drop")
def getStat(self, stat):
return self.stats.get(stat, 0)
def toJSON(self):
return {
"slot": self.slot,
"stats": self.stats
}

View File

@ -1,127 +0,0 @@
from asciifarm.common import utils
import random
from .component import Component
from .. import gameobjects
from ..pathfinding import distanceBetween
class Fighter(Component):
def __init__(self, maxHealth, strength=0, slowness=1, health=None, defense=0, range=1, attackable=True):
self.maxHealth = maxHealth
self.health = health or maxHealth
self.strength = strength
self.target = None
self.slowness = slowness
self.attackReady = True
self.defense = defense
self.range = range
self._attackable = attackable
def attach(self, obj):
self.owner = obj
obj.addListener("roomjoin", self.roomJoin)
def roomJoin(self, o, roomData, stamp):
self.roomData = roomData
self.fightEvent = roomData.getEvent("fight")
def damage(self, damage, attacker):
self.health -= damage
self.health = utils.clamp(self.health, 0, self.maxHealth)
# should this be it's own component? ('bleeding' for example)
if damage > 0 and self.owner.getGround() is not None:
obj = gameobjects.makeEntity("wound", self.roomData, 4, self.owner.getHeight() - 0.01)
obj.place(self.owner.getGround())
self.owner.trigger("damage" if damage >= 0 else "heal", attacker, abs(damage))
if self.isDead():
self.die(attacker)
def attack(self, other):
if self.canAttack(other):
self.target = other
self.fightEvent.addListener(self.doAttack)
def doAttack(self):
other = self.target
if other and other.hasComponent("fighter") and self.attackReady:
otherFighter = other.getComponent("fighter")
if otherFighter:
strength = self.getStrength()
defense = otherFighter.getDefense()
damage = random.randint(0, int(100*strength / (defense + 100)))
otherFighter.damage(damage, self.owner)
self.attackReady = False
self.roomData.setAlarm(self.roomData.getStamp() + self.slowness, self.makeReady)
self.owner.trigger("attack", other, damage)
if otherFighter.isDead():
self.owner.trigger("kill", other)
self.target = None
self.fightEvent.removeListener(self.doAttack)
def die(self, killer):
self.owner.trigger("die", killer)
self.owner.remove()
def getStrength(self):
strength = self.strength
if self.owner.hasComponent("equipment"):
strength += self.owner.getComponent("equipment").getBonus("strength")
return strength
def getDefense(self):
defense = self.defense
if self.owner.hasComponent("equipment"):
defense += self.owner.getComponent("equipment").getBonus("defense")
return defense
def getHealth(self):
return (self.health, self.maxHealth)
def healthFull(self):
return self.health >= self.maxHealth
def heal(self, health, source):
self.damage(-health, source)
def isDead(self):
return self.health <= 0
def inRange(self, other):
return distanceBetween(self.owner, other) <= self.range
def remove(self):
self.fightEvent.removeListener(self.doAttack)
self.owner.removeListener("roomjoin", self.roomJoin)
def makeReady(self):
self.attackReady = True
def attackable(self):
return self._attackable
def canAttack(self, other):
return self.inRange(other) and other.getComponent("fighter") and other.getComponent("fighter").attackable()
def toJSON(self):
return {
"maxHealth": self.maxHealth,
"strength": self.strength,
"slowness": self.slowness,
"health": self.health,
"defense": self.defense
}

View File

@ -1,22 +0,0 @@
from .component import Component
class Food(Component):
def __init__(self, health):
self.healing = health
def attach(self, obj):
self.owner = obj
def use(self, user):
fighter = user.getComponent("fighter")
if fighter:
fighter.heal(self.healing, self.owner)
self.owner.trigger("drop")
def toJSON(self):
return {"health": self.healing}

View File

@ -1,57 +0,0 @@
from .. import gameobjects
import random
from .component import Component
class Growing(Component):
def __init__(self, nextStage, duration=None, targetTime=None, nextArgs=None, nextKwargs=None, stepsPassed=None):
# stepsPassed is not used anymore, but included for backwards compatibility
self.nextStage = nextStage
# if both duration and targetTime are passed, duration is ignored
# if both are none, the growth will never happen
self.duration = duration
self.targetTime = targetTime
self.nextArgs = nextArgs or []
self.nextKwargs = nextKwargs or {}
def attach(self, obj):
self.owner = obj
obj.addListener("roomjoin", self.roomJoin)
def roomJoin(self, o, roomData, stamp):
self.roomData = roomData
if self.targetTime is None and self.duration:
duration = int(random.triangular(self.duration/2, self.duration*2, self.duration))
self.targetTime = stamp + duration
self.roomData.setAlarm(self.targetTime, self.grow)
def grow(self):
obj = gameobjects.createEntity({"type": self.nextStage, "args": self.nextArgs, "kwargs": self.nextKwargs})
obj.construct(self.roomData, preserve=self.owner.isPreserved(), stamp=self.targetTime)
obj.place(self.owner.getGround())
self.owner.trigger("grow", obj)
print("{} has grown into {}".format(self.owner.getName(), obj.getName()))
self.owner.remove()
def getTargetTime(self):
return self.targetTime
def toJSON(self):
return {
"nextStage": self.nextStage,
"duration": self.duration,
"targetTime": self.targetTime,
"nextArgs": self.nextArgs,
"nextKwargs": self.nextKwargs
}

View File

@ -1,12 +0,0 @@
from .component import Component
class Harvest(Component):
def attach(self, obj):
self.owner = obj
def interact(self, obj):
self.owner.trigger("die") # loot component will register this
self.owner.remove()

View File

@ -1,53 +0,0 @@
from .component import Component
class Healing(Component):
""" A component to automatically heal its entity over time"""
def __init__(self, interval, amount=1):
""" interval is the number of steps until next healing, amount is the amount of health that gets added in a healing """
self.interval = interval
self.amount = amount
self.delay = 0
self.isHealing = False
def attach(self, obj):
if not obj.getComponent("fighter"):
# todo: better exception
raise Exception("Healing Component needs object with fighter component")
self.fighter = obj.getComponent("fighter")
obj.addListener("damage", self.onDamage)
obj.addListener("roomjoin", self.roomJoin)
def roomJoin(self, o, roomData, stamp):
self.roomData = roomData
self.startHealing(stamp)
def onDamage(self, o, *data):
self.startHealing()
def startHealing(self, start=None):
""" start healing if it is not happening already """
if not self.isHealing and not self.fighter.healthFull():
if start is None:
start = self.roomData.getStamp()
self.roomData.setAlarm(start + self.interval, self.heal)
self.isHealing = True
def heal(self):
if self.fighter.healthFull():
return
self.fighter.heal(self.amount, None)
self.isHealing = False
self.startHealing()
def toJSON(self):
return {
"interval": self.interval,
"amount": self.amount
}

View File

@ -1,25 +0,0 @@
from .component import Component
class HomePortal(Component):
def __init__(self, roomName="*home:{}"):
self.roomName = roomName
def attach(self, obj):
obj.addListener("objectenter", self.onEnter)
self.owner = obj
def onEnter(self, owner, obj=None, *data):
obj.trigger("changeroom", self.destRoom, dest)
def toJSON(self):
return {
"destRoom": self.destRoom,
"destPos": self.origin,
"mask": self.mask
}

View File

@ -1,173 +0,0 @@
from .component import Component
class InputController(Component):
def __init__(self):
self.action = None
self.handlers = {
"move": self.do_move,
"take": self.do_take,
"drop": self.do_drop,
"use": self.do_use,
"unequip": self.do_unequip,
"interact": self.do_interact,
"attack": self.do_attack,
"say": self.do_say,
"pick": self.do_pick
}
self.target = None
def attach(self, obj):
self.owner = obj
for dep in {"inventory", "move", "fighter", "alignment", "equipment", "select"}:
if not obj.getComponent(dep):
# todo: better exception
raise Exception("InputController needs object with " + dep + " component")
setattr(self, dep, obj.getComponent(dep))
obj.addListener("roomjoin", self.roomJoin)
obj.addListener("damage", self.retaliate)
def roomJoin(self, o, roomData, stamp):
self.roomData = roomData
self.controlEvent = roomData.getEvent("control")
self.controlEvent.addListener(self.control)
def setAction(self, action):
self.action = action
def control(self):
action = self.action
if action:
self.target = None
self.action = None
if action is not None:
self.executeAction(action)
if self.target:
self.fighter.attack(self.target)
def executeAction(self, action):
kind = action[0]
if len(action) > 1:
arg = action[1]
else:
arg = None
try:
handler = self.handlers.get(kind)
except TypeError:
handler = None
if handler is None:
print("invalid action", action)
return
handler(arg)
def do_move(self, direction):
if direction not in {"north", "south", "east", "west"}:
return
self.move.move(direction)
def do_take(self, rank):
objects = self.owner.getNearObjects()
if rank is not None:
if rank not in range(len(objects)):
return
objects = [objects[rank]]
for obj in objects:
if obj.getComponent("item") is not None and self.inventory.canAdd(obj):
self.owner.trigger("take", obj)
obj.remove()
break
def do_drop(self, rank):
items = self.inventory.getItems()
if rank is None:
rank = 0
if rank not in range(len(items)):
return False
obj = items[rank]
self.inventory.drop(obj)
obj.construct(self.roomData, preserve=True)
obj.place(self.owner.getGround())
return True
def do_use(self, rank):
items = self.inventory.getItems()
if rank is None:
rank = 0
if rank not in range(len(items)):
return
obj = items[rank]
obj.getComponent("item").use(self.owner)
def do_unequip(self, rank):
slots = sorted(self.equipment.getSlots().items())
if rank is not None:
if rank not in range(len(slots)):
return
slots = [slots[rank]]
for (slot, item) in slots:
if item is not None and self.inventory.canAdd(item):
self.equipment.unEquip(slot)
self.owner.trigger("take", item)
def do_interact(self, directions):
objects = self._getNearbyObjects(directions)
for obj in objects:
if obj.getComponent("interact") is not None:
obj.getComponent("interact").interact(self.owner)
break
def do_attack(self, directions):
objects = self._getNearbyObjects(directions)
if self.target in objects:
objects = {self.target}
for obj in objects:
if self.fighter.canAttack(obj) and self.alignment.isEnemy(obj):
self.fighter.attack(obj)
self.target = obj
break
def do_say(self, text):
if type(text) != str:
return
self.roomData.getEvent("sound").trigger(self.owner, text)
def do_pick(self, option):
selected = self.select.getSelected()
if selected is None:
return
optionmenu = selected.getComponent("options")
if optionmenu is None:
return
optionmenu.choose(option, self.owner)
def _getNearbyObjects(self, directions):
nearPlaces = self.owner.getGround().getNeighbours()
if not isinstance(directions, list):
directions = [directions]
objects = []
for direction in directions:
if direction is None:
objects += self.owner.getNearObjects()
elif isinstance(direction, str) and direction in nearPlaces:
objects += nearPlaces[direction].getObjs()
return objects
def getInteractions(self):
return []
def remove(self):
self.controlEvent.removeListener(self.control)
def retaliate(self, _self, attacker, damage):
self.target = attacker

View File

@ -1,64 +0,0 @@
from .component import Component
from .. import gameobjects
class Inventory(Component):
def __init__(self, capacity, initialItems=None):
if initialItems is None:
initialItems = []
self.capacity = capacity
self.items = []
self.owner = None
for item in initialItems[::-1]:
if item:
self.add(item)
def attach(self, obj):
self.owner = obj
obj.addListener("take", self.onTake)
def canAdd(self, item):
return len(self.items) < self.capacity
def canAddAll(self, items):
return len(self.items) + len(items) <= self.capacity
def add(self, item):
self.items.insert(0, item)
item.addListener("drop", self.onDrop)
if self.owner:
self.owner.trigger("inventorychange")
def drop(self, item):
if self.has(item):
self.items.remove(item)
if self.owner:
self.owner.trigger("inventorychange")
def getItems(self):
return list(self.items)
def has(self, item):
return item in self.items
def onDrop(self, item, *data):
self.drop(item)
def onTake(self, o, item=None, *data):
self.add(item)
def remove(self):
self.owner = None
def toJSON(self):
return {
"capacity": self.capacity,
"items": [item.serialize() for item in self.items]
}
@classmethod
def fromJSON(cls, data):
obj = cls(data["capacity"], [gameobjects.createEntity(item) for item in data["items"]])
return obj

View File

@ -1,7 +0,0 @@
from .component import Component
class Item(Component):
def use(self, user):
pass

View File

@ -1,23 +0,0 @@
from .component import Component
class Listen(Component):
def attach(self, obj):
self.owner = obj
obj.addListener("roomjoin", self.roomJoin)
def roomJoin(self, o, roomData, stamp):
self.soundEvent = roomData.getEvent("sound")
self.soundEvent.addListener(self.notify)
self.roomData = roomData
def notify(self, source, text):
self.owner.trigger("sound", source, text)
def remove(self):
self.soundEvent.removeListener(self.notify)

View File

@ -1,41 +0,0 @@
import random
from .. import gameobjects
from .component import Component
class Loot(Component):
""" entities that have this component will drop loot on death """
def __init__(self, items):
""" Items should be a list of tuples where the first element is the item name, and the second element the chance that that item gets dropped """
self.items = items
def attach(self, obj):
self.owner = obj
obj.addListener("die", self.dropLoot)
obj.addListener("roomjoin", self.roomJoin)
def roomJoin(self, o, roomData, stamp):
self.roomData = roomData
def dropLoot(self, obj, *data):
for itemData in self.items:
item = itemData[0]
chance = 1
args = []
kwargs = {}
if len(itemData) > 1:
chance = itemData[1]
if len(itemData) > 2:
args = itemData[2]
if len(itemData) > 3:
kwargs = itemData[3]
if chance > random.random():
obj = gameobjects.makeEntity(item, self.roomData, *args, preserve=True, **kwargs)
obj.place(self.owner.getGround())
def toJSON(self):
return {"items": self.items}

View File

@ -1,64 +0,0 @@
import random
from .. import pathfinding
from .component import Component
class MonsterAi(Component):
def __init__(self, viewDist, moveChance=1, home=None, homesickness=0.05):
self.moveChance = moveChance
self.viewDist = viewDist
self.home = home # Should home be a place instead of object? that would reduce references
self.homesickness = homesickness
def attach(self, obj):
self.owner = obj
for dep in {"move", "fighter", "alignment"}:
if not obj.getComponent(dep):
# todo: better exception
raise Exception("Controller needs object with " + dep + " component")
setattr(self, dep, obj.getComponent(dep))
obj.addListener("roomjoin", self.roomJoin)
def roomJoin(self, o, roomData, stamp):
self.controlEvent = roomData.getEvent("control")
self.controlEvent.addListener(self.control)
self.roomData = roomData
def control(self):
closestDistance = self.viewDist + 1
closest = None
for obj in self.roomData.getTargets():
distance = pathfinding.distanceBetween(self.owner, obj)
if self.alignment.isEnemy(obj) and distance < closestDistance:
closestDistance = distance
closest = obj
if closest:
if self.fighter.inRange(closest):
self.fighter.attack(closest)
else:
self.move.move(pathfinding.stepTo(self.owner, closest))
else:
if random.random() < self.moveChance:
if self.home and self.home.inRoom() and random.random() < (self.homesickness * pathfinding.distanceBetween(self.owner, self.home)):
direction = pathfinding.stepTo(self.owner, self.home)
else:
direction = random.choice(["north", "south", "east", "west"])
self.move.move(direction)
def remove(self):
self.controlEvent.removeListener(self.control)
def toJSON(self):
return {
"viewDist": self.viewDist,
"moveChance": self.moveChance,
"homesickness": self.homesickness
} # home is not saved now ...

View File

@ -1,51 +0,0 @@
from .component import Component
class Move(Component):
def __init__(self, slowness=1):
self.direction = None
self.slowness = slowness
self.canMove = False
def attach(self, obj):
self.owner = obj
obj.addListener("roomjoin", self.roomJoin)
def roomJoin(self, o, roomData, stamp):
self.roomData = roomData
self.moveEvent = roomData.getEvent("move")
self.canMove = True
def move(self, direction):
self.direction = direction
self.moveEvent.addListener(self.doMove)
def canMove(self, direction):
neighbours = self.owner.getGround().getNeighbours()
return direction in neighbours and neighbours[direction].accessible()
def doMove(self):
neighbours = self.owner.getGround().getNeighbours()
if self.direction in neighbours and self.canMove:
newPlace = neighbours[self.direction]
if newPlace.accessible():
self.owner.place(newPlace)
self.canMove = False
self.roomData.setAlarm(self.roomData.getStamp() + self.slowness, self.makeReady)
self.owner.trigger("move")
self.direction = None
self.moveEvent.removeListener(self.doMove)
def makeReady(self):
self.canMove = True
def remove(self):
self.moveEvent.removeListener(self.doMove)
def toJSON(self):
return {"slowness": self.slowness}

View File

@ -1,23 +0,0 @@
from .component import Component
class OptionMenu(Component):
def __init__(self, description, options):
self.description = description
self.options = {option.name: option for option in options}
def attach(self, obj):
self.owner = obj
def getOptions(self):
return list(self.options.values())
def choose(self, option, obj):
if option in self.options:
self.options[option].act(obj)

View File

@ -1,36 +0,0 @@
from .component import Component
class Portal(Component):
def __init__(self, destRoom, destPos=None, mask=(False, False)):
self.destRoom = destRoom
self.origin = destPos
self.mask = mask
def attach(self, obj):
obj.addListener("objectenter", self.onEnter)
self.owner = obj
def onEnter(self, owner, obj, *data):
offset = self.owner.getGround().getPos()
if self.origin is None:
dest = None
elif isinstance(self.origin, str):
dest = self.origin
else:
dest = tuple(
self.origin[i] + (offset[i] if self.mask[i] else 0)
for i in range(2)
)
obj.trigger("changeroom", self.destRoom, dest)
def toJSON(self):
return {
"destRoom": self.destRoom,
"destPos": self.origin,
"mask": self.mask
}

View File

@ -1,38 +0,0 @@
from .component import Component
import random
class RandomWalkController(Component):
def __init__(self, moveChance=1):
self.moveChance = moveChance
def attach(self, obj):
self.owner = obj
if not obj.getComponent("move"):
# todo: better exception
raise Exception("Controller needs object with move component")
obj.addListener("roomjoin", self.roomJoin)
def roomJoin(self, o, roomData, stamp):
self.controlEvent = roomData.getEvent("control")
self.controlEvent.addListener(self.control)
def control(self):
if random.random() < self.moveChance:
direction = random.choice(["north", "south", "east", "west"])
self.owner.getComponent("move").move(direction)
def remove(self):
self.controlEvent.removeListener(self.control)
def toJSON(self):
return {
"moveChance": self.moveChance
}

View File

@ -1,19 +0,0 @@
from .component import Component
class Select(Component):
def __init__(self):
self.selection = None
def attach(self, obj):
self.owner = obj
def select(self, obj=None):
self.selection = obj
self.owner.trigger("selection", obj)
obj.trigger("selectedby", self.owner)
def getSelected(self):
return self.selection

View File

@ -1,17 +0,0 @@
from .component import Component
class Selectable(Component):
def attach(self, obj):
self.owner = obj
def interact(self, obj):
selector = obj.getComponent("select")
if selector:
selector.select(self.owner)

View File

@ -1,72 +0,0 @@
from .. import gameobjects
import random
from .component import Component
class Spawner(Component):
def __init__(self, objectType, amount=1, respawnDelay=1, setHome=False, initialSpawn=False, objectArgs=None, objectKwargs=None):
if objectArgs is None:
objectArgs = []
if objectKwargs is None:
objectKwargs = {}
self.objectType = objectType
self.amount = amount
self.respawnDelay = respawnDelay
self.spawned = set()
self.objectArgs = objectArgs
self.objectKwargs = objectKwargs
self.setHome = setHome
self.initialSpawn = initialSpawn
def attach(self, obj):
self.owner = obj
obj.addListener("roomjoin", self.roomJoin)
def roomJoin(self, o, roomData, stamp):
self.roomData = roomData
self.updateEvent = roomData.getEvent("update")
for i in range(self.amount):
if self.initialSpawn:
self.goSpawn(stamp)
else:
self.goSpawn()
def goSpawn(self, spawnStamp=None):
if spawnStamp is None:
duration = self.respawnDelay
spawnStamp = random.triangular(duration/2, duration*2, duration) + self.roomData.getStamp()
self.roomData.setAlarm(spawnStamp, self.spawn)
def spawn(self):
objectKwargs = self.objectKwargs.copy()
if self.setHome:
objectKwargs["home"] = self.owner
obj = gameobjects.makeEntity(self.objectType, self.roomData, *self.objectArgs, **objectKwargs)
obj.place(self.owner.getGround())
self.spawned.add(obj)
obj.addListener("remove", self.onSpawnedRemove)
print("{} spawned a {}".format(self.owner.getName(), self.objectType))
def onSpawnedRemove(self, obj, *data):
""" handle spawned object death """
self.spawned.remove(obj)
obj.removeListener("remove", self.onSpawnedRemove)
self.goSpawn()
def toJSON(self):
return {
"objectType": self.objectType,
"amount": self.amount,
"respawnDelay": self.respawnDelay,
"setHome": self.setHome,
"objectArgs": self.objectArgs,
"objectKwargs": self.objectKwargs,
"initialSpawn": self.initialSpawn
} # it won't keep track of spawned entities. It will just spawn new entities again

View File

@ -1,19 +0,0 @@
from .component import Component
class StaticSerializer(Component):
""" This class is for static serialization.
If an entity has this component then the serialization will be the argument passed to the constructor instead of the toJSON of the entity and its components.
This is useful for objects that hold no state and are not unique.
"""
def __init__(self, data, *args, **kwargs):
if isinstance(data, str) and (args or kwargs):
self.data = {"type": data, "args": args, "kwargs": kwargs}
else:
self.data = data
def serialize(self):
return self.data

View File

@ -1,16 +0,0 @@
from .component import Component
class Target(Component):
def attach(self, obj):
self.owner = obj
obj.addListener("roomjoin", self.roomJoin)
def roomJoin(self, obj, roomData, stamp):
self.roomData = roomData
roomData.addTarget(obj)
def remove(self):
self.roomData.removeTarget(self.owner)

View File

@ -1,22 +0,0 @@
from .component import Component
class Trap(Component):
def attach(self, obj):
if not obj.getComponent("fighter"):
# todo: better exception
raise Exception("Trap needs object with fighter component")
self.owner = obj
self.fighter = obj.getComponent("fighter")
obj.addListener("objectenter", self.onEnter)
def onEnter(self, owner, obj=None, *data):
self.fighter.attack(obj)

View File

@ -1,28 +0,0 @@
from .component import Component
class Volatile(Component):
def __init__(self, duration):
self.duration = duration
def attach(self, obj):
self.owner = obj
obj.addListener("roomjoin", self.roomJoin)
def roomJoin(self, o, roomData, stamp):
self.roomData = roomData
roomData.setAlarm(stamp + self.duration, self.end)
def end(self):
self.owner.remove()
def toJSON(self):
# better not to save volatile entities in the first place...
return {
"duration": self.duration
}

View File

@ -1,46 +0,0 @@
import random
from .component import Component
class Weather(Component):
def __init__(self, speed=1, spread=0, direction="south"):
self.speed = speed
self.spread = spread
self.direction = direction
def attach(self, obj):
self.owner = obj
obj.addListener("roomjoin", self.roomJoin)
def roomJoin(self, o, roomData, stamp):
self.moveEvent = roomData.getEvent("move")
self.moveEvent.addListener(self.move)
def move(self):
speed = self.speed
for i in range(int(speed)):
self.moveStep()
if (speed - int(speed)) > random.random():
self.moveStep()
if self.spread > random.random():
self.moveStep(random.choice(["east", "west"]))
def moveStep(self, direction=None):
if direction is None:
direction = self.direction
if self.owner.getGround() is None:
return
neighbours = self.owner.getGround().getNeighbours()
if direction in neighbours:
newPlace = neighbours[direction]
self.owner.place(newPlace)
self.owner.trigger("move")
else:
self.owner.remove()
def remove(self):
self.moveEvent.removeListener(self.move)

View File

@ -0,0 +1,117 @@
class Control:
name = None
def do_json(self):
raise NotImplementedError
@classmethod
def from_json(cls, jsondata):
name, arg = jsondata
assert name == cls.name, "Control names do not match: {}, {}".format(name, cls.name)
return cls(arg)
_DIRECTIONS = {"north", "south", "east", "west"}
_ALL_DIRECTIONS = _DIRECTIONS | {None}
class MoveControl(Control):
name = "move"
def __init__(self, direction):
assert direction in _DIRECTIONS, "Unknown direction: " + str(direction)
self.direction = direction
def to_json(self):
return [self.name, self.direction]
class RankedControl(Control):
def __init__(self, rank=None):
assert rank is None or isinstance(rank, int) and rank >= 0, "rank is not positive numeric: " + str(rank)
self.rank = rank
def to_json(self):
return [self.name, self.rank]
class TakeControl(RankedControl):
name = "take"
class DropControl(RankedControl):
name = "drop"
class UseControl(RankedControl):
name = "use"
def __init__(self, container, rank=None):
assert rank is None or isinstance(rank, int) and rank >= 0, "rank is not positive numeric: " + str(rank)
assert container in ("inventory", "equipment"), "Invalid container: " + str(container)
self.container = container
self.rank = rank
def to_json(self):
return [self.name, self.rank]
@classmethod
def from_json(cls, jsondata):
name, container, rank = jsondata
assert name == cls.name, "Control names do not match: {}, {}".format(name, cls.name)
return cls(container, rank)
class InteractControl(Control):
name = "interact"
def __init__(self, directions=(), parameter=None):
for direction in directions:
assert direction in _ALL_DIRECTIONS, "Unknown direction: " + str(direction)
self.directions = directions
self.parameter = parameter
def to_json(self):
return [self.name, self.directions, parameter]
@classmethod
def from_json(cls, jsondata):
if len(jsondata) == 3:
name, directions, parameter = jsondata
else:
name, directions = jsondata
parameter = None
assert name == cls.name, "Control names do not match: {}, {}".format(name, cls.name)
return cls(directions, parameter)
class AttackControl(Control):
name = "attack"
def __init__(self, directions=()):
for direction in directions:
assert direction in _ALL_DIRECTIONS, "Unknown direction: " + str(direction)
self.directions = directions
def to_json(self):
return [self.name, self.directions]
class SayControl(Control):
name = "say"
def __init__(self, text):
assert isinstance(text, str), "text is not a string: " + str(text)
self.text = text
def to_json(self):
return [self.name, text]
_controls_by_name = {control.name: control for control in [
MoveControl,
TakeControl,
DropControl,
UseControl,
InteractControl,
AttackControl,
SayControl
]}
def controlFromJson(jsondata):
return _controls_by_name[jsondata[0]].from_json(jsondata)

View File

@ -0,0 +1,18 @@
conversions = {
"food": "eldritch_radish",
"sword": "sword",
"pebble": "pebble",
"club": "club",
"seed": "radishseed",
"stone": "stone",
"wall": "wall",
"armour": "armour"
}
def convert(item):
name = item["name"]
if name in conversions:
return conversions[name]
raise ValueError("Unknow old object:", str(item))

View File

@ -0,0 +1,47 @@
from .attackable import Attackable
from .fighter import Fighter
from .heal import Heal
from .move import Move
from .ai import AI
from .input import Input
from .faction import Faction
from .events import Events
from .loot import Loot
from .portal import Portal
from .dc import DC
from .serialise import Serialise, Static
from .inventory import Inventory
from .listen import Listen
from .periodic import Periodic
from .equipment import Equipment
from .spawner import Spawner, Squad
from .item import Item
from .equippable import Equippable
from .buildable import Buildable
from .exchanger import Exchange, Exchanger
from .messages import Message, EnterMessage, LootMessage, StartTimer, Create, SpawnMessage, UseMessage
class Remove:
pass
class Dead:
pass
class Interact:
def __init__(self, *components):
self.components = list(components)
class Trap:
pass
class Home:
def __init__(self, home):
self.home = home
class Food:
def __init__(self, healing):
self.healing = healing

View File

@ -0,0 +1,10 @@
from .dc import DC
class AI(DC):
def __init__(self, viewDist=0, moveChance=1, home=None, homesickness=0.05):
self.moveChance = moveChance
self.viewDist = viewDist
self.home = home # Should home be a place instead of object? that would reduce references. A: Yes
self.homesickness = homesickness

View File

@ -0,0 +1,29 @@
class Attackable:
def __init__(self, maxHealth, health=None, defence=0, defense=None, onDie=None):
self.maxHealth = maxHealth
self.health = health or maxHealth
if defense is not None:
defence = defense
self.defence = defence
self.attacks = []
self.onDie = onDie or []
def attack(self, strength, attacker):
self.attacks.append(("attack", strength, attacker))
def heal(self, health, source):
self.attacks.append(("heal", health, source))
def getHealth(self):
return (self.health, self.maxHealth)
def healthFull(self):
return self.health >= self.maxHealth
def isDead(self):
return self.health <= 0

View File

@ -0,0 +1,7 @@
class Buildable:
def __init__(self, template, flagsneeded=frozenset(), blockingflags=frozenset()):
self.template = template
self.flagsneeded = flagsneeded
self.blockingflags = blockingflags

View File

@ -0,0 +1,3 @@
class DC:
allowMultiple = False

View File

@ -0,0 +1,14 @@
from .equippable import Equippable
class Equipment:
def __init__(self, slots):
self.slots = slots
def getBonus(self, roomData, stat):
bonus = 0
for item in self.slots.values():
if item is not None:
bonus += roomData.getComponent(item, Equippable).stats.get(stat, 0)
return bonus

View File

@ -0,0 +1,6 @@
class Equippable:
def __init__(self, slot, stats):
self.slot = slot
self.stats = stats

View File

@ -0,0 +1,10 @@
import collections
class Events:
def __init__(self):
self.messages = collections.deque()
def add(self, message):
self.messages.append(message)

View File

@ -0,0 +1,19 @@
class Exchange:
def __init__(self, products, costs, description=None, name=None):
self.products = products
self.costs = costs
if description is None:
description = "{} ({})".format(", ".join(str(p) for p in products), ", ".join(str(c) for c in costs))
self.description = description
self.name = name or id(self)
class Exchanger:
def __init__(self, options, description=""):
self.options = {option.name: option for option in options}
self.description = description

View File

@ -1,4 +1,5 @@
class Faction:
def __init__(self, name):
@ -29,6 +30,12 @@ NONE = Faction("none") # things that can always be attacked
GOOD.hates(NONE)
EVIL.hates(NONE)
NONE.hates(NONE)
Faction.NEUTRAL = NEUTRAL
Faction.NONE = NONE
Faction.GOOD = GOOD
Faction.EVIL = EVIL
factions = {}

View File

@ -0,0 +1,16 @@
from ..pathfinding import distanceBetween
from .dc import DC
class Fighter(DC):
def __init__(self, strength=0, slowness=1, range=1):
self.strength = strength
self.target = None
self.slowness = slowness
self.attackReady = 0
self.range = range
def inRange(self, owner, other):
return distanceBetween(owner, other) <= self.range

View File

@ -0,0 +1,13 @@
class Solid:
pass
class Floor:
pass
class Buildable:
pass
class Soil:
pass

View File

@ -0,0 +1,10 @@
from .dc import DC
class Heal(DC):
def __init__(self, interval, amount=1):
""" interval is the number of steps until next healing, amount is the amount of health that gets added in a healing """
self.interval = interval
self.amount = amount
self.nextHeal = None

View File

@ -0,0 +1,8 @@
from .dc import DC
class Input(DC):
def __init__(self):
self.action = None
self.target = None

View File

@ -0,0 +1,10 @@
class Inventory:
def __init__(self, capacity, items=None):
self.items = items or []
self.capacity = capacity
def add(self, item):
self.items.insert(0, item)

View File

@ -0,0 +1,5 @@
class Item:
def __init__(self, onUse=None):
self.onUse = onUse or []

View File

@ -0,0 +1,6 @@
class Listen:
def __init__(self):
self.sounds = []

View File

@ -0,0 +1,20 @@
from random import random as rand
from .dc import DC
from ..template import Template
class Loot(DC):
def __init__(self, items):
self.items = []
for item in items:
if isinstance(item, Template) or isinstance(item, str):
item = (item, 1)
assert len(item) == 2, ValueError(item)
template, chance = item
if isinstance(template, str):
template = Template(template)
self.items.append((template, chance))
def pick(self):
return [template for template, chance in self.items if chance > rand()]

View File

@ -0,0 +1,33 @@
from . import DC
class Message(DC):
allowMultiple = True
class EnterMessage(Message):
def __init__(self, actor):
self.actor = actor
class LootMessage(Message):
pass
class StartTimer(Message):
pass
class Create(Message):
def __init__(self, *templates):
self.templates = templates
class SpawnMessage(Message):
pass
class EatMessage(Message):
pass
class UseMessage(Message):
def __init__(self, actor, parameter=None):
self.actor = actor
self.parameter = parameter

View File

@ -0,0 +1,13 @@
from .dc import DC
class Move(DC):
def __init__(self, slowness=1):
self.direction = None
self.slowness = slowness
self.moveReady = 0
def canMove(self, obj, direction):
neighbours = obj.getGround().getNeighbours()
return direction in neighbours and neighbours[direction].accessible()

View File

@ -0,0 +1,11 @@
# todo: make sure growing continues in unloaded rooms
class Periodic:
def __init__(self, components, interval=None, targetTime=None, randomise=False):
self.interval = interval
self.components = components
self.randomise = randomise
self.targetTime = targetTime

View File

@ -0,0 +1,11 @@
from .dc import DC
class Portal(DC):
def __init__(self, destRoom, destPos=None, mask=(False, False)):
self.destRoom = destRoom
self.origin = destPos
self.mask = mask

View File

@ -0,0 +1,17 @@
from .dc import DC
from ..template import Template
class Serialise(DC):
def __init__(self, func):
self.func = func
def serialise(self, obj, roomData):
return self.func(obj, roomData)
def Static(name, *args, **kwargs):
template = Template(name, *args, **kwargs)
return Serialise(lambda obj, roomData: template)

View File

@ -0,0 +1,20 @@
import random
import string
class Squad:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name is other.name
class Spawner:
def __init__(self, spawned, number=1, setHome=False, squad=None):
self.spawned = spawned
self.number = number
if squad is None:
squad = "_" + "".join(random.choices(string.ascii_lowercase, k=20))
self.squad = squad
self.setHome = setHome

View File

@ -1,7 +1,7 @@
from . import serialize
from .eventtarget import EventTarget
import collections
from .datacomponents import Events, Remove, Serialise
class Entity:
""" Attempt to implement an entity component system
@ -14,21 +14,29 @@ class Entity:
Remove methods are for cleanup, like unsubscribing from events.
"""
def __init__(self, sprite=' ', height=0, name=None, components=None, flags=None):
if components is None:
components = {}
def __init__(self, sprite=' ', height=0, name=None, flags=None, dataComponents=None):
if flags is None:
flags = set()
self.sprite = sprite # the name of the image to display for this entity
self.height = height # if multiple objects are on a square, the tallest one is drawn
self.name = name if name else sprite # human readable name/description
self.components = components
self.observable = EventTarget()
self.flags = set(flags)
self.ground = None
self.roomData = None
for component in self.components.values():
component.attach(self)
if dataComponents is None:
dataComponents = []
self.dataComponents = {}
for component in dataComponents:
if isinstance(component, type):
compt = component
component = component()
else:
compt = type(component)
self.dataComponents[compt] = component
self.listeners = collections.defaultdict(dict)
def construct(self, roomData, preserve=False, stamp=None):
@ -36,16 +44,13 @@ class Entity:
if preserve:
roomData.preserveObject(self)
self._preserve()
self.roomData.addObj(self)
if stamp is None:
stamp = roomData.getStamp()
self.trigger("roomjoin", roomData, stamp)
def hasComponent(self, name):
return name in self.components
def getComponent(self, name):
return self.components.get(name, None)
def getDataComponent(self, component):
return self.roomData.getComponent(self, component)
def place(self, ground):
if self.ground:
@ -53,25 +58,38 @@ class Entity:
self.ground = ground
ground.addObj(self)
def remove(self):
def unPlace(self):
if self.ground:
self.ground.removeObj(self)
self.ground = None
def remove(self):
self.trigger("remove")
self.roomData.addComponent(self, Remove)
def doRemove(self):
self.roomData.removeObj(self)
if self.isPreserved():
self.roomData.removePreserved(self)
for component in self.components.values():
component.remove()
self.trigger("remove")
self.unPlace()
self.roomData = None
def addListener(self, event, callback, key=None):
self.observable.addListener(event, callback, key)
if key is None:
key = callback
self.listeners[event][key] = callback
def removeListener(self, event, key):
self.observable.removeListener(event, key)
self.listeners[event].pop(key)
def trigger(self, event, *args, **kwargs):
self.observable.trigger(event, self, *args, **kwargs)
messages = self.getDataComponent(Events)
if messages is None:
messages = Events()
self.roomData.addComponent(self, messages)
messages.add((event, list(args), dict(kwargs)))
def getSprite(self):
return self.sprite
@ -100,37 +118,11 @@ class Entity:
def isPreserved(self):
return "preserve" in self.flags
def toJSON(self):
return {
"sprite": self.sprite,
"name": self.name,
"height": self.height,
"flags": list(self.flags),
"components": {
name: serialize.serialize(comp)
for name, comp in self.components.items()
}
}
def serialize(self):
if "serialize" not in self.components:
return self.toJSON()
return self.components["serialize"].serialize()
@classmethod
def fromJSON(cls, data):
if data is None:
if Serialise in self.dataComponents:
return self.dataComponents[Serialise].serialise(self, self.roomData)
else:
return None
return cls(
sprite = data["sprite"],
name = data["name"],
height = data["height"],
flags = data["flags"],
components = {
name: serialize.unserialize(comp)
for name, comp in data["components"].items()
}
)
def getRoomData(self):
return self.roomData

View File

@ -1,43 +0,0 @@
from . import gameobjects
class Exchange:
def __init__(self, products, costs, description=None, name=None):
self.products = products
self.costs = costs
if description is None:
description = "{} ({})".format(", ".join(str(p) for p in products), ", ".join(str(c) for c in costs))
self.description = description
self.name = name or id(self)
def perform(self, obj):
inventory = obj.getComponent("inventory")
if inventory is None:
return
costs = list(self.costs)
toRemove = []
for item in inventory.getItems():
# get all the items to remove
if item.name in costs:
toRemove.append(item)
costs.remove(item.name)
if len(costs):
# not all costs can be covered; other party can't afford trade
return
toAdd = [gameobjects.createEntity(product) for product in self.products]
if not inventory.canAddAll(toAdd):
return
for item in toRemove:
inventory.drop(item)
for item in toAdd:
inventory.add(item)
def act(self, obj):
self.perform(obj)

View File

@ -15,24 +15,11 @@ from .structures import entities as structures
entities = {**base, **crops, **items, **misc, **npcs, **structures, **exchangers}
def makeEntity(entType, roomData, *args, preserve=False, **kwargs):
entity = entities[entType](*args, **kwargs)
entity.construct(roomData, preserve)
return entity
def createEntity(template):
return entities[template.name](*template.args, **template.kwargs)
def createEntity(data):
obj = None
if isinstance(data, str):
obj = entities[data]()
elif isinstance(data, dict):
if "type" in data:
obj = entities[data["type"]](*(data.get("args", [])), **(data.get("kwargs", {})))
else:
obj = Entity.fromJSON(data)
return obj
def buildEntity(data, roomData, preserve=False):
obj = createEntity(data)
def buildEntity(template, roomData, preserve=False):
obj = createEntity(template)
if obj is not None:
obj.construct(roomData, preserve)
return obj

View File

@ -1,6 +1,6 @@
from ..entity import Entity
from ..components import StaticSerializer as Static
from ..datacomponents import Static
import random
@ -8,15 +8,15 @@ entities = {}
entities["grass"] = lambda: Entity(
sprite=random.choice(["ground"] + 2*["grass1", "grass2", "grass3"]), name="grass", height=0.1, flags={"floor", "soil"}, components={"serialize": Static("grass")})
sprite=random.choice(["ground"] + 2*["grass1", "grass2", "grass3"]), name="grass", height=0.1, flags={"floor", "soil"}, dataComponents=[Static("grass")])
entities["greengrass"] = lambda: Entity(
sprite=random.choice(["grass1", "grass2", "grass3"]), height=0.1, flags={"floor", "soil"}, components={"serialize": Static("greengrass")})
sprite=random.choice(["grass1", "grass2", "grass3"]), height=0.1, flags={"floor", "soil"}, dataComponents=[Static("greengrass")])
entities["floor"] = lambda: Entity(sprite="floor", height=0.1, flags={"floor"}, components={"serialize": Static("floor")})
entities["floor"] = lambda: Entity(sprite="floor", height=0.1, flags={"floor"}, dataComponents=[Static("floor")])
entities["ground"] = lambda: Entity(sprite="ground", height=0.1, flags={"floor", "soil"}, components={"serialize": Static("ground")})
entities["ground"] = lambda: Entity(sprite="ground", height=0.1, flags={"floor", "soil"}, dataComponents=[Static("ground")])
entities["bridge"] = lambda small=False: Entity(sprite=("smallbridge" if small else "bridge"), height=0.1, flags={"floor"}, components={"serialize": Static("bridge", small)})
entities["bridge"] = lambda small=False: Entity(sprite=("smallbridge" if small else "bridge"), height=0.1, flags={"floor"}, dataComponents=[Static("bridge", small)])
entities["water"] = lambda: Entity(sprite="water", height=0, components={"serialize": Static("water")})
entities["water"] = lambda: Entity(sprite="water", height=0, dataComponents=[Static("water")])

View File

@ -1,18 +1,15 @@
from ..entity import Entity
from ..components import Build, Food, Growing, Harvest, Loot
from ..components import StaticSerializer as Static
from ..components import CustomSerializer as Custom
from ..datacomponents import Interact, Loot, Remove, Serialise, Static, LootMessage, Periodic, StartTimer, Create, Item, Food, Buildable
from ..template import Template
entities = {}
def cropSerializer(name):
return lambda obj: {
"type": name,
"args": [],
"kwargs": {
"targetTime": obj.getComponent("grow").getTargetTime()}}
return (lambda obj, roomData:
Template(name, targetTime=roomData.getComponent(obj, Periodic).targetTime)
)
class Stage:
@ -27,24 +24,29 @@ class Stage:
def create(self, cropname="", nextstage=None, timestep=1):
name = self.name.format(cropname)
def makeEntity(targetTime=None):
components = {}
dataComponents = []
if self.duration is not None:
ongrow = [Remove, Create(Template(nextstage))]
if targetTime is None:
components["grow"] = Growing(nextstage, self.duration*timestep)
timer = Periodic(ongrow, self.duration * timestep)
else:
components["grow"] = Growing(nextstage, targetTime=targetTime)
components["serialize"] = Custom(cropSerializer(name))
timer = Periodic(ongrow, targetTime=targetTime)
dataComponents.append(timer)
dataComponents.append(Serialise(cropSerializer(name)))
dataComponents.append(StartTimer())
else:
components["serialize"] = Static(name)
dataComponents.append(Static(name))
if self.harvest is not None:
components["interact"] = Harvest()
components["loot"] = Loot(self.harvest)
dataComponents.append(Interact(LootMessage, Remove))
dataComponents.append(Loot(self.harvest))
flags = {"occupied"}
return Entity(
sprite=self.sprite.format(cropname),
height=self.height,
name=self.shownname.format(cropname),
flags={"occupied"},
components=components)
flags=flags,
dataComponents=dataComponents
)
entities[name] = makeEntity
@ -69,9 +71,8 @@ def createCrop(name, stages, timestep=1):
sprite="seed",
name=seedname,
height=0.2,
components={
"item": Build(stagenames[0], flagsNeeded={"soil"}, blockingFlags={"occupied", "solid"}),
"serialize": Static(seedname)})
dataComponents=[Static(seedname), Item, Buildable(Template(stagenames[0]), flagsneeded={"soil"}, blockingflags={"occupied", "solid"})]
)
for i, stage in enumerate(stages[:-1]):
nextstage = stagenames[i+1]
@ -83,19 +84,24 @@ createCrop("carrot", [
Stage.Seed(20),
Stage.Seedling(40),
Stage("carrotplant", sprite="smallplant", height=0.5, harvest=[("carrot", 1), ("carrotseed", 1)])
], 600)
], 600)
entities["carrot"] = lambda: Entity(sprite="food", name="carrot", height=0.3, components={"item": Food(4), "serialize": Static("carrot")})
entities["carrot"] = lambda: Entity(sprite="food", name="carrot", height=0.3, dataComponents=[Static("carrot"), Item, Food(5)])
createCrop("radish", [
Stage.Seed(3),
Stage.Seedling(3),
Stage.YoungPlant(6),
Stage("{}plant", sprite="smallplant", height=0.5, harvest=[("radishseed", .92), ("radishseed", .20), ("radishes", .8), ("radishes", .4)])
], 10)
Stage(
"{}plant",
sprite="smallplant",
height=0.5,
harvest=[("radishseed", .92), ("radishseed", .20), ("radishes", .8), ("radishes", .4)]
)
], 10)
entities["radishes"] = lambda: Entity(sprite="food", name="radishes", height=0.3, components={"item": Food(2), "serialize": Static("radishes")})
entities["radishes"] = lambda: Entity(sprite="food", name="radishes", height=0.3, dataComponents=[Static("radishes"), Item, Food(3)])
entities["food"] = entities["radishes"]
entities["sownseed"] = entities["plantedradishseed"]
@ -103,6 +109,7 @@ entities["youngplant"] = entities["youngradishplant"]
entities["plant"] = entities["radishplant"]
entities["seed"] = entities["radishseed"]
entities["eldritch_radish"] = lambda: Entity(sprite="food", name="eldritch radishes", height=0.3, dataComponents=[Static("eldritch_radish"), Item, Food(20)])

View File

@ -2,23 +2,24 @@
from ..entity import Entity
from .. import faction
from ..option import Option
from ..exchange import Exchange
from ..components import StaticSerializer as Static
from ..components import CustomSerializer as Custom
from ..components import OptionMenu
from ..components import Selectable
from ..datacomponents import Static, Interact, Exchanger, Exchange
entities = {}
entities["trader"] = lambda: Entity(sprite="sign", height=1, components={
"interact": Selectable(),
"options": OptionMenu("Trade", [
Exchange(["carrotseed"], ["radishes"], None, "buy_carrotseeds"),
Exchange(["pebble"], ["radishes"], None, "buy_pebble")]),
"serialize": Static("trader")})
entities["trader"] = lambda: Entity(
sprite="sign",
name="trader",
height=1,
dataComponents=[
Static("trader"),
Interact,
Exchanger([
Exchange(["carrotseed"], ["radishes"], None, "buy_carrotseeds"),
Exchange(["pebble"], ["radishes"], None, "buy_pebble")
], "Trade")
]
)

View File

@ -1,28 +1,28 @@
from ..entity import Entity
from ..components import StaticSerializer as Static
from ..components import Build, Item, Equippable
from ..datacomponents import Static, Item, Equippable, Buildable
from ..template import Template
entities = {}
entities["stone"] = lambda: Entity(sprite="stone", height=0.4, components={"item": Build("builtwall", flagsNeeded={"freeland"}, blockingFlags={"solid", "occupied"}), "serialize": Static("stone")})
entities["stone"] = lambda: Entity(sprite="stone", height=0.4, dataComponents=[Static("stone"), Item, Build(Template("builtwall"), flagsneeded={"freeland"}, blockingflags={"solid", "occupied"})])
entities["pebble"] = lambda: Entity(sprite="pebble", height=0.2, components={"item": Item(), "serialize": Static("pebble")})
entities["pebble"] = lambda: Entity(sprite="pebble", height=0.2, dataComponents=[Static("pebble"), Item])
entities["sword"] = lambda: Entity(sprite="sword", height=0.5, components={"item": Equippable("hand", {"strength": 5}), "serialize": Static("sword")})
entities["sword"] = lambda: Entity(sprite="sword", height=0.5, dataComponents=[Static("sword"), Item, Equippable("hand", {"strength": 5})])
entities["club"] = lambda: Entity(sprite="club", height=0.5, components={"item": Equippable("hand", {"strength": 3}), "serialize": Static("club")})
entities["club"] = lambda: Entity(sprite="club", height=0.5, dataComponents=[Static("club"), Item, Equippable("hand", {"strength": 3})])
entities["weapon"] = lambda strength=0, name="weapon": Entity(sprite="sword", name=name, height=0.5, components={"item": Equippable("hand", {"strength": strength}), "serialize": Static("weapon", strength=strength)})
entities["weapon"] = lambda strength=0, name="weapon": Entity(sprite="sword", name=name, height=0.5, dataComponents=[Static("weapon", strength=strength), Item. Equippable("hand", {"strength": strength})])
entities["armour"] = lambda: Entity(sprite="armour", height=0.5, components={"item": Equippable("body", {"defense": 100}), "serialize": Static("armour")})
entities["armour"] = lambda: Entity(sprite="armour", height=0.5, dataComponents=[Static("armour"), Item, Equippable("body", {"defence": 100})])
entities["hardwood"] = lambda: Entity(sprite="hardwood", height=0.4, components={"item": Build("builtwall", flagsNeeded={"freeland"}, blockingFlags={"solid", "occupied"}), "serialize": Static("hardwood")})
#entities["hardwood"] = lambda: Entity(sprite="hardwood", height=0.4, components={"item": Build("builtwall", flagsNeeded={"freeland"}, blockingFlags={"solid", "occupied"})}, dataComponents=[Static("hardwood"), Item])

View File

@ -2,31 +2,40 @@
from ..entity import Entity
from ..components import StaticSerializer as Static
from ..components import Portal, Spawner, Volatile, Weather
from ..datacomponents import Portal, Static, Periodic, Remove, StartTimer, Spawner, SpawnMessage
from ..template import Template
entities = {}
entities["freeland"] = lambda: Entity(name="buildable", flags={"freeland"}, components={"serialize": Static("freeland")})
entities["freeland"] = lambda: Entity(name="buildable", flags={"freeland"}, dataComponents=[Static("freeland")])
entities["spawner"] = lambda objType, number, delay, sprite=None, name=None, height=0, setHome=False, initialSpawn=True, objArgs=None, objKwargs=None: Entity(
sprite=sprite, height=height, name=name, components={
"spawn": Spawner(objType, number, delay, setHome, initialSpawn, objArgs, objKwargs),
"serialize": Static("spawner", objType, number, delay, sprite, name, height, setHome, initialSpawn, objArgs, objKwargs)})
entities["spawner"] = lambda template, number, delay, sprite=None, name=None, height=0, setHome=False, initialSpawn=True: Entity(
sprite=sprite, height=height, name=name, dataComponents=[
StartTimer,
SpawnMessage if initialSpawn else None,
Periodic([StartTimer, SpawnMessage], interval=delay, randomise=True),
Spawner(Template.fromJSON(template), number, setHome=setHome),
Static("spawner", template, number, delay, sprite, name, height, setHome, initialSpawn)
]
)
entities["letter"] = lambda c: Entity(sprite="emptyletter-"+c, components={"serialize": Static("letter", c)})
entities["letter"] = lambda c: Entity(sprite="emptyletter-"+c, dataComponents=[Static("letter", c)])
entities["roomexit"] = lambda destRoom, destPos=None, mask=(False, False), sprite=" ", size=0: Entity(
sprite=sprite, height=size, components={"collision": Portal(destRoom, destPos, mask), "serialize": Static(destRoom, destPos, mask, sprite, size)})
sprite=sprite, height=size, dataComponents=[
Portal(destRoom, destPos, mask),
Static(destRoom, destPos, mask, sprite, size)
]
)
entities["wound"] = lambda duration=4, height=0.2: Entity(sprite="wound", name="", height=height, components={"volatile": Volatile(duration), "serialize": Static(None)})
entities["wound"] = lambda duration=4, height=0.2: Entity(sprite="wound", name="", height=height, dataComponents=[StartTimer(), Periodic([Remove], duration), Static(None)])
entities["raindrop"] = lambda: Entity(sprite="raindrop", name="", height=10, components={"weather": Weather(speed=2.5, spread=0.2), "serialize": Static(None)})
#entities["raindrop"] = lambda: Entity(sprite="raindrop", name="", height=10, components={"weather": Weather(speed=2.5, spread=0.2)}, dataComponents=[Static(None)})
entities["snowflake"] = lambda: Entity(sprite="snowflake", name="", height=10, components={"weather": Weather(speed=1, spread=0.5), "serialize": Static(None)})
#entities["snowflake"] = lambda: Entity(sprite="snowflake", name="", height=10, components={"weather": Weather(speed=1, spread=0.5), "serialize": Static(None)})

View File

@ -2,47 +2,58 @@
from ..entity import Entity
from ..components import StaticSerializer as Static
from ..components import Alignment, Fighter, Loot, MonsterAi, Move, RandomWalkController, Trap
from .. import faction
from ..datacomponents import Fighter, Move, Attackable, AI, Faction, Loot, LootMessage, Trap, Static
entities = {}
entities["rabbit"] = lambda: Entity(
sprite="rabbit", name="bunny", height=1, components={"move": Move(slowness=4), "controller": RandomWalkController(moveChance=0.05), "serialize": Static("rabbit")})
sprite="rabbit", name="bunny", height=1, dataComponents=[AI(moveChance=0.05), Move(slowness=4), Static("rabbit")]
)
entities["goblin"] = lambda home=None: Entity(sprite="goblin", height=1.2, components={
"move": Move(slowness=3),
"fighter": Fighter(maxHealth=15, strength=5, slowness=8),
"alignment": Alignment(faction.EVIL),
"controller": MonsterAi(viewDist=8, moveChance=0.02, home=home),
"loot": Loot([("sword", .05), ("club", .1), ("radishes", .25)])
})
entities["goblin"] = lambda home=None: Entity(sprite="goblin", height=1.2, dataComponents=[
Faction.EVIL,
AI(viewDist=8, moveChance=0.02, home=home),
Move(slowness=3),
Attackable(maxHealth=15, onDie=[LootMessage]),
Fighter(strength=5, slowness=8),
Loot([("sword", .05), ("club", .1), ("radishes", .25)])
]
)
entities["troll"] = lambda home=None: Entity(sprite="troll", height=1.8, components={
"move": Move(slowness=4),
"fighter": Fighter(maxHealth=75, strength=15, slowness=10),
"alignment": Alignment(faction.EVIL),
"controller": MonsterAi(viewDist=8, moveChance=0.01, home=home),
"loot": Loot([("stone", 1), ("stone", .3), ("pebble", .5), ("pebble", .5), ("pebble", .5)])
})
entities["troll"] = lambda home=None: Entity(sprite="troll", height=1.8, dataComponents=[
Faction.EVIL,
AI(viewDist=8, moveChance=0.01, home=home),
Move(slowness=4),
Attackable(maxHealth=75, onDie=[LootMessage]),
Fighter(strength=15, slowness=10),
Loot([("stone", 1), ("stone", .3), ("pebble", .5), ("pebble", .5), ("pebble", .5)])
]
)
entities["rat"] = lambda home=None: Entity(sprite="rat", height=1, components={
"move": Move(slowness=3),
"fighter": Fighter(maxHealth=8, strength=2, slowness=6),
"alignment": Alignment(faction.EVIL),
"controller": MonsterAi(viewDist=3, moveChance=0.08, home=home, homesickness=0.1),
"loot": Loot([("radishseed", 0.9), ("radishseed", 0.3)])
})
entities["rat"] = lambda home=None: Entity(sprite="rat", height=1, dataComponents=[
Faction.EVIL,
AI(viewDist=3, moveChance=0.08, home=home, homesickness=0.1),
Move(slowness=3),
Attackable(maxHealth=8, onDie=[LootMessage]),
Fighter(strength=2, slowness=6),
Loot([("radishseed", 0.9), ("radishseed", 0.3)])
]
)
entities["dummy"] = lambda: Entity(
sprite="dummy", height=1, flags={"occupied"}, components={"fighter": Fighter(maxHealth=20, strength=0), "alignment": Alignment(faction.NONE)})
sprite="dummy", height=1, flags={"occupied"}, dataComponents=[
Attackable(maxHealth=20),
Faction.NONE
])
entities["spiketrap"] = lambda damage=25: Entity(
sprite="spikes", height=1, flags={"occupied"}, components={"fighter": Fighter(maxHealth=25, strength=damage, attackable=False), "collision": Trap(), "serialize": Static("spiketrap")})
entities["spiketrap"] = lambda damage=15: Entity(
sprite="spikes",
height=1,
flags={"occupied"},
dataComponents=[Fighter(strength=damage, slowness=10), Trap(), Static("spiketrap")]
)

View File

@ -2,58 +2,43 @@
from ..entity import Entity
from .. import faction
from ..components import StaticSerializer as Static
from ..components import CustomSerializer as Custom
from ..components import Change
from ..components import Fighter
from ..components import Alignment
from ..components import Loot
from ..datacomponents import Attackable, Faction, Loot, LootMessage, Interact, Remove, Serialise, Static, Create
from ..template import Template
entities = {}
entities["wall"] = lambda: Entity(sprite="wall", height=2, flags={"solid"}, components={"serialize": Static("wall")})
entities["wall"] = lambda: Entity(sprite="wall", height=2, flags={"solid"}, dataComponents=[Static("wall")])
entities["rock"] = lambda: Entity(sprite="rock", height=10, flags={"solid"}, components={"serialize": Static("rock")})
entities["rock"] = lambda: Entity(sprite="rock", height=10, flags={"solid"}, dataComponents=[Static("rock")])
entities["tree"] = lambda: Entity(sprite="tree", height=3, flags={"solid"}, components={"serialize": Static("tree")})
entities["tree"] = lambda: Entity(sprite="tree", height=3, flags={"solid"}, dataComponents=[Static("tree")])
entities["house"] = lambda: Entity(sprite="house", height=3, flags={"solid"}, components={"serialize": Static("house")})
entities["house"] = lambda: Entity(sprite="house", height=3, flags={"solid"}, dataComponents=[Static("house")])
entities["fence"] = lambda: Entity(sprite="fence", height=1, flags={"solid"}, components={"serialize": Static("fence")})
entities["fence"] = lambda: Entity(sprite="fence", height=1, flags={"solid"}, dataComponents=[Static("fence")])
entities["builtwall"] = lambda health=None: Entity(
sprite="builtwall", height=2, flags={"solid"}, components={
"fighter": Fighter(maxHealth=100, strength=0, health=None),
"alignment": Alignment(faction.NONE),
"loot": Loot([("stone", 1)]),
"serialize": Custom(
lambda obj: {
"type": "builtwall",
"kwargs": {"health": obj.getComponent("fighter").health}
}
)
})
sprite="builtwall",
height=2,
flags={"solid"},
dataComponents=[
Faction.NONE,
Attackable(health=health, maxHealth=100, onDie=[LootMessage]),
Serialise(
lambda obj, roomData:
Template("builtwall", health=roomData.getComponent(obj, Attackable).health)
),
Loot(["stone"])
]
)
entities["closeddoor"] = lambda: Entity(sprite="closeddoor", name="door", height=2, flags={"solid"}, components={"interact": Change("opendoor"), "serialize": Static("closeddoor")})
entities["closeddoor"] = lambda: Entity(sprite="closeddoor", name="door", height=2, flags={"solid"}, dataComponents=[Interact(Remove, Create(Template("opendoor"))), Static("closeddoor")])
entities["opendoor"] = lambda: Entity(sprite="opendoor", name="door", height=1, flags={"occupied"}, components={"interact": Change("closeddoor"), "serialize": Static("opendoor")})
#entities["builtcloseddoor"] = lambda health=None: Entity(sprite="closeddoor", name="door", height=2, flags={"solid"}, components={
#"interact": Change(transferComponents={"fighter"}),
#"fighter": Fighter(maxHealth=100, health=health, strength=0),
#"alignment": Alignment(faction.NONE),
#"loot": Loot([("hardwood", 1)])})
#entities["builtopendoor"] = lambda health=None: Entity(sprite="opendoor", name="door", height=1, flags={"occupied"}, components={
#"interact": Change(),
#"fighter": Fighter(maxHealth=100, health=health, strength=0),
#"alignment": Alignment(faction.NONE),
#"loot": Loot([("hardwood", 1)])})
entities["opendoor"] = lambda: Entity(sprite="opendoor", name="door", height=1, flags={"occupied"}, dataComponents=[Interact(Remove, Create(Template("closeddoor"))), Static("opendoor")])
entities["engraved"] = lambda c: Entity(sprite="engravedwall-"+c, height=2, flags={"solid"}, components={"serialize": Static("wall", c)})
entities["engraved"] = lambda c: Entity(sprite="engravedwall-"+c, height=2, flags={"solid"}, dataComponents=[Static("wall", c)])

View File

@ -10,6 +10,7 @@ import threading
from . import view
from . import socketserver as server
from . import player
from . import controls
from asciifarm.common import messages
@ -76,7 +77,7 @@ class GameServer:
except json.JSONDecodeError:
self.error(connection, "invalidmessage", "Invalid JSON")
except Exception as e:
print(e)
print(type(e), e, data)
self.error(connection, "invalidmessage", "An unknown error occured in handling the message")
def handleMessage(self, connection, message):
@ -107,7 +108,8 @@ class GameServer:
def handleInputMessage(self, connection, message):
if connection in self.connections:
self.messages.put(("input", self.connections[connection], message.inp))
control = controls.controlFromJson(message.inp)
self.messages.put(("input", self.connections[connection], control))
def handleChatMessage(self, connection, msg):
name = self.connections[connection]

View File

@ -77,6 +77,6 @@ class GroundPatch:
def __getstate__(self):
state = self.__dict__.copy()
state['neighbours'] = None
state["neighbours"] = None
return state

View File

@ -19,7 +19,7 @@ thisPath = os.path.dirname(__file__)
farmsPath = os.path.join(thisPath, "..")
defaultWorld = os.path.join(farmsPath, "maps", "world.json")
defaultSaveDir = os.path.join(farmsPath, "saves")
defaultSaveDir = os.path.join(farmsPath, "..", "saves")
def main(argv=None):
parser = argparse.ArgumentParser()
@ -37,7 +37,7 @@ def main(argv=None):
sockets = []
for socktype, address in socketargs:
assert socktype in ["abstract", "unix", "inet"]
assert socktype in ["abstract", "unix", "inet"], "Unknown socket type: " + str(socktype)
if address is "":
address = defaultAdresses[socktype]
if socktype == "abstract":

View File

@ -1,21 +1,9 @@
# this package is not used, but it is important that it is imported before any component modules
# importing components before gameobjects is loaded will result in a cyclic dependency problem
from . import gameobjects
from .components.inventory import Inventory
from .components.inputcontroller import InputController
from .components.move import Move
from .components.fighter import Fighter
from .components.healing import Healing
from .components.alignment import Alignment
from .components.target import Target
from .components.equipment import Equipment
from .components.listen import Listen
from .components import Select
from . import faction
from .datacomponents import Inventory, Attackable, Move, Fighter, Heal, Input, Faction, Listen, Equipment
from .template import Template
from . import entity
from .controls import Control
class Player:
@ -25,7 +13,7 @@ class Player:
self.world = world
self.roomname = None
self.entity = None
self.inventory = Inventory(10)
self.inventory = Inventory(10) # Todo: make player inventory more abstract; just copy the contents to actual inventory on construction
self.equipment = Equipment({"hand": None, "body": None})
self.health = None
self.maxHealth = 50
@ -59,19 +47,25 @@ class Player:
sprite = "player",
height = 2,
name = '&' + self.name,
components={
"inventory": self.inventory,
"move": Move(slowness=2),
"controller": InputController(),
"fighter": Fighter(self.maxHealth, 5, slowness=8, health=self.health or self.maxHealth),
"alignment": Alignment(faction.GOOD),
"heal": Healing(interval=50),
"target": Target(),
"equipment": self.equipment,
"listen": Listen(),
"select": Select()
})
self.entity.construct(room.getRoomData())
dataComponents = [
self.inventory,
self.equipment,
Faction.GOOD,
Input(),
Listen(),
Move(slowness=2),
Heal(interval=50),
Fighter(strength=5, slowness=8),
Attackable(maxHealth=self.maxHealth, health=self.health or self.maxHealth)
]
)
roomData = room.getRoomData()
self.entity.construct(roomData)
for item in self.inventory.items:
item.construct(roomData)
for item in self.equipment.slots.values():
if item is not None:
item.construct(roomData)
for attr in dir(self):
if attr.startswith("on_"):
self.entity.addListener(attr[3:], self.__getattribute__(attr))
@ -131,21 +125,14 @@ class Player:
def on_selection(self, o, obj):
self.changes.add("selection")
def on_sound(self, o, source, text):
if source is not None:
text = source.getName() + ": " + text
self.messages.append([text, "world"])
def control(self, action):
if not self.entity or not (isinstance(action, list) or isinstance(action, tuple)) or len(action) < 1:
if not self.entity or not isinstance(action, Control):
return
controller = self.entity.getComponent("controller")
controller.setAction(action)
self.entity.getDataComponent(Input).action = action
def getHealthPair(self):
if self.entity:
return self.entity.getComponent("fighter").getHealth()
return self.entity.getDataComponent(Attackable).getHealth()
else:
return (0, None)
@ -159,21 +146,22 @@ class Player:
def getInventory(self):
if self.entity:
return self.inventory.getItems()
return self.inventory.items
else:
return []
def getEquipment(self):
if self.entity:
return self.equipment.getSlots()
return self.equipment.slots
else:
return {}
def getInteractions(self):
if not self.entity:
return []
controller = self.entity.getComponent("controller")
return controller.getInteractions()
return NotImplemented
#if not self.entity:
#return []
#controller = self.entity.getComponent("controller")
#return controller.getInteractions()
def getGroundObjs(self):
if not self.entity:
@ -204,9 +192,14 @@ class Player:
print(msg)
def readMessages(self):
m = self.messages
self.messages = []
return m
messages = []
listen = self.entity.getDataComponent(Listen)
for source, text in listen.sounds:
if source is not None:
text = source.getName() + ": " + text
messages.append([text, "world"])
listen.sounds = []
return messages
def readChanges(self):
changes = self.changes
@ -220,8 +213,9 @@ class Player:
return {
"name": self.name,
"roomname": self.roomname,
"inventory": self.inventory.toJSON(),
"equipment": self.equipment.toJSON(),
"inventory": {"capacity": self.inventory.capacity, "items": [item.serialize().toJSON() for item in self.inventory.items]},
"equipment": {slot: (item.serialize().toJSON() if item is not None else None) for slot, item in self.equipment.slots.items()},
#self.equipment.toJSON(),
"health": self.getHealth(),
"maxhealth": self.maxHealth
}
@ -231,8 +225,11 @@ class Player:
self = cls(data["name"], world)
self.health = data["health"]
self.maxHealth = data["maxhealth"]
self.inventory = Inventory.fromJSON(data["inventory"])
self.equipment = Equipment.fromJSON(data["equipment"])
self.inventory = Inventory(data["inventory"]["capacity"], [gameobjects.createEntity(Template.fromJSON(item)) for item in data["inventory"]["items"]])
for slot, item in data["equipment"].items():
if item is not None:
self.equipment.slots[slot] = gameobjects.createEntity(Template.fromJSON(item))
#Equipment.fromJSON(data["equipment"])
self.roomname = data["roomname"]
return self

View File

@ -7,8 +7,11 @@ from . import event
from . import entity
from . import roomdata
from . import serialize
from .template import Template
from .systems import fight, attacked, heal, move, controlai, control, handleevents, remove, droploot, clearinbox, trap, teleport, sound, checktimers, create, spawn, eat, equip, build, exchange
class Room:
@ -23,16 +26,7 @@ class Room:
self.lastStepStamp = 0
self.roomData = roomdata.RoomData(events={
"control": event.Event(),
"move": event.Event(),
"fight": event.Event(),
"update": event.Event(),
"sound": event.Event()
})
def logSound(source, text):
print("{}: {}: {}".format(self.name, source.getName(), text))
self.roomData.getEvent("sound").addListener(logSound)
self.roomData = roomdata.RoomData()
self.places = data.get("places", {})
@ -48,7 +42,7 @@ class Room:
if not isinstance(val, list) :
val = [val]
for obj in val:
ent = gameobjects.buildEntity(obj, self.roomData)
ent = gameobjects.buildEntity(Template.fromJSON(obj), self.roomData)
self.addObj((x, y), ent)
if preserved is not None:
@ -56,6 +50,7 @@ class Room:
self.resetChangedCells()
def getName(self):
return self.name
@ -80,10 +75,32 @@ class Room:
self.roomData.setStamp(stepStamp)
self.roomData.triggerAlarms()
self.roomData.getEvent("control").trigger()
self.roomData.getEvent("move").trigger()
self.roomData.getEvent("fight").trigger()
self.roomData.getEvent("update").trigger(timePassed, stepStamp)
systems = [
control,
controlai,
move,
trap,
teleport,
fight,
heal,
eat,
equip,
build,
exchange,
attacked,
droploot,
spawn,
create,
handleevents,
checktimers,
sound,
clearinbox,
remove
]
for system in systems:
system(self.roomData)
self.lastStepStamp = stepStamp
def getSprites(self, pos):
@ -144,17 +161,13 @@ class Room:
def getPreserved(self):
return {
"changes": [
(obj.getGround().getPos(), obj.serialize())
for obj in self.roomData.getPreserved()],
(obj.getGround().getPos(), obj.serialize().toJSON())
for obj in self.roomData.getPreserved() if obj.getGround()],
"step": self.lastStepStamp}
def loadPreserved(self, objects):
if isinstance(objects, list):
# just for rooms that have been saved in the old format
# when there are no such rooms anymore, this can be removed
objects = {"changes": objects}
for (pos, objData) in objects["changes"]:
obj = gameobjects.buildEntity(objData, self.roomData, preserve=True)
obj = gameobjects.buildEntity(Template.fromJSON(objData), self.roomData, preserve=True)
self.addObj(tuple(pos), obj)
self.lastStepStamp = objects.get("step")
self.roomData.setStamp(self.lastStepStamp)

View File

@ -1,8 +1,18 @@
import heapq
import itertools
import collections
counter = itertools.count() # unique sequence count
from .systems.fight import fight
from .systems.attacked import attacked
from .systems.heal import heal
from .systems.move import move
from .systems.controlai import controlai
from .systems.controlinput import control
from .datacomponents import DC
class RoomData:
@ -11,11 +21,7 @@ class RoomData:
The Ground class does this as well, but Ground only gives local data, whereas this give data about the whole room.
"""
def __init__(self, events=None):
if events is None:
events = []
self.events = events
def __init__(self):
self.targets = set()
self.preservedObjects = set()
@ -23,20 +29,83 @@ class RoomData:
self.stepStamp = 0
self.alarms = [] # treat as priority queue
self.postponed = []
self.objects = set()
self.sounds = []
self.dataComponents = collections.defaultdict(set) # type: {str: set(Entity)}
def addObj(self, obj):
self.objects.add(obj)
for compt in obj.dataComponents:
self.dataComponents[compt].add(obj)
def removeObj(self, obj):
self.objects.remove(obj)
for compt in obj.dataComponents:
if obj not in self.dataComponents[compt]:
print(compt, obj.toJSON())
self.dataComponents[compt].remove(obj)
def getEvent(self, name):
return self.events[name]
def addComponent(self, obj, component):
if isinstance(component, type):
compt = component
component = component()
else:
compt = type(component)
self.dataComponents[compt].add(obj)
if isinstance(component, DC) and component.allowMultiple:
if compt not in obj.dataComponents:
obj.dataComponents[compt] = []
obj.dataComponents[compt].append(component)
else:
obj.dataComponents[compt] = component
def removeComponent(self, obj, component, gone_ok=False):
if isinstance(component, type):
compt = component
component = self.getComponent(obj, compt)
else:
compt = type(component)
if component is None:
if gone_ok:
return
else:
raise KeyError("object {} has no component {}".format(str(object), str(component)))
if isinstance(component, DC) and component.allowMultiple:
l = obj.dataComponents[compt]
l.remove(component)
if not l:
self.dataComponents[compt].remove(l)
else:
self.dataComponents[compt].remove(obj)
obj.dataComponents.pop(compt)
def addTarget(self, obj):
self.targets.add(obj)
def clearComponent(self, compt):
for entity in self.dataComponents[compt]:
del entity.dataComponents[compt]
self.dataComponents[compt] = set()
def removeTarget(self, obj):
self.targets.discard(obj)
def getComponent(self, obj, component):
return obj.dataComponents.get(component)
def getTargets(self):
return frozenset(self.targets)
def getEntities(self, compts, combinator="intersect", avoid=None):
entities = set(self.dataComponents[compts[0]])
if combinator == "intersect":
for compt in compts[1:]:
entities &= self.dataComponents[compt]
elif combinator == "union":
for compt in compts[1:]:
entities |= self.dataComponents[compt]
if avoid is not None:
for compt in avoid:
entities -= self.dataComponents[compt]
return entities
def preserveObject(self, obj):
self.preservedObjects.add(obj)
@ -54,6 +123,12 @@ class RoomData:
heapq.heappush(self.alarms, (stamp, count, callback))
def triggerAlarms(self):
while self.postponed and self.postponed[0][0] <= self.stepStamp:
_plannedTime, _count, obj, components = heapq.heappop(self.postponed)
if obj not in self.objects:
return
for component in components:
self.addComponent(obj, component)
while self.alarms and self.alarms[0][0] <= self.stepStamp:
_plannedTime, _count, callback = heapq.heappop(self.alarms)
callback()
@ -64,3 +139,11 @@ class RoomData:
def getStamp(self):
return self.stepStamp
def makeSound(self, source, text):
self.sounds.append((source, text))
def postpone(self, stamp, obj, *components):
count = next(counter) # tiebreaker for when stamps are equal
# see: https://docs.python.org/3/library/heapq.html#priority-queue-implementation-notes
heapq.heappush(self.postponed, (stamp, count, obj, list(components)))

View File

@ -0,0 +1,13 @@
def system(components, avoid=None):
assert isinstance(components, list) or isinstance(components, tuple)
def system_wrapper(func):
def system_impl(roomData):
entities = roomData.getEntities(components, avoid=avoid)
for entity in entities:
comps = [roomData.getComponent(entity, comp) for comp in components]
func(entity, roomData, *comps)
return system_impl
return system_wrapper

View File

@ -0,0 +1,20 @@
from .attacked import attacked
from .controlai import controlai
from .controlinput import control
from .fight import fight
from .handleevents import handleevents
from .heal import heal
from .move import move
from .remove import remove
from .droploot import droploot
from .clearinbox import clearinbox
from .trap import trap
from .teleport import teleport
from .sound import sound
from .checktimers import checktimers
from .create import create
from .spawn import spawn
from .eat import eat
from .equip import equip
from .build import build
from .exchange import exchange

View File

@ -0,0 +1,51 @@
import random
from asciifarm.common import utils
from .. import gameobjects
from ..system import system
from ..datacomponents import Attackable, Input, StartTimer, Periodic, Equipment
from ..template import Template
@system([Attackable])
def attacked(obj, roomData, attackable):
for type, strength, attacker in attackable.attacks:
if type == "attack":
defence = attackable.defence
equipment = roomData.getComponent(obj, Equipment)
if equipment is not None:
defence += equipment.getBonus(roomData, "defence")
damage = random.randint(0, int(100*strength / (defence + 100)))
elif type == "heal":
damage = -strength
else:
raise ValueError("Unknown attack type " + type)
attackable.health -= damage
attackable.health = utils.clamp(attackable.health, 0, attackable.maxHealth)
# should this be it's own component? ('bleeding' for example)
if damage > 0 and obj.getGround() is not None:
wound = gameobjects.buildEntity(Template("wound", 4, obj.getHeight() - 0.01), roomData)
wound.place(obj.getGround())
if type == "attack":
obj.trigger("damage", attacker, damage)
attacker.trigger("attack", obj, damage)
input = roomData.getComponent(obj, Input)
if input is not None:
input.target = attacker # retaliation
elif type == "heal":
obj.trigger("heal", attacker, -damage)
if attackable.isDead():
obj.trigger("die", attacker)
attacker.trigger("kill", obj)
for component in attackable.onDie:
roomData.addComponent(obj, component)
obj.remove()
attackable.attacks = []

View File

@ -0,0 +1,21 @@
from ..system import system
from ..datacomponents import UseMessage, Buildable, Remove, Inventory
from .. import gameobjects
@system([UseMessage, Buildable])
def build(obj, roomData, use, buildable):
actor = use[0].actor
ground = actor.getGround()
print("building")
groundFlags = ground.getFlags()
if not buildable.flagsneeded <= groundFlags or groundFlags & buildable.blockingflags: # <= means subset when applied on sets
# groundFlags must contain all of self.flagsNeeded, and none of self.blockingFlags
return
builtobj = gameobjects.buildEntity(buildable.template, roomData, preserve=True)
builtobj.place(ground)
roomData.addComponent(obj, Remove)
inv = roomData.getComponent(actor, Inventory)
if inv is not None:
inv.items.remove(obj)
actor.trigger("inventorychange")

View File

@ -0,0 +1,16 @@
import random
from ..system import system
from ..datacomponents import StartTimer, Periodic
@system([StartTimer, Periodic])
def checktimers(obj, roomData, _start, periodic):
if periodic.targetTime is None:
interval = periodic.interval
if interval is None:
raise ValueError("Neither interval nor targettime set for", str(obj), str(periodic))
if periodic.randomise:
interval = random.triangular(interval/2, interval*2, interval)
periodic.targetTime = roomData.getStamp() + interval
roomData.postpone(periodic.targetTime, obj, *periodic.components)

View File

@ -0,0 +1,8 @@
from ..datacomponents import Message
def clearinbox(roomData):
for compt, entities in roomData.dataComponents.items():
if issubclass(compt, Message):
roomData.clearComponent(compt)

View File

@ -0,0 +1,35 @@
import random
from .. import pathfinding
from ..datacomponents import AI, Move, Fighter, Faction, Attackable, Home
from ..system import system
@system([AI, Move])
def controlai(obj, roomData, ai, movable):
fighter = roomData.getComponent(obj, Fighter)
if fighter is not None:
closestDistance = ai.viewDist + 1
closest = None
alignment = roomData.getComponent(obj, Faction) or Faction.NONE
for target in roomData.dataComponents[Attackable]:
distance = pathfinding.distanceBetween(obj, target)
if (alignment.isEnemy(roomData.getComponent(target, Faction) or Faction.NONE)) and distance < closestDistance:
closestDistance = distance
closest = target
if closest:
if fighter.inRange(obj, closest):
fighter.target = closest
else:
movable.direction = pathfinding.stepTo(obj, closest)
return
if random.random() < ai.moveChance:
home = roomData.getComponent(obj, Home)
if home is not None and home.home.inRoom() and random.random() < (ai.homesickness * pathfinding.distanceBetween(obj, home.home)):
direction = pathfinding.stepTo(obj, home.home)
else:
direction = random.choice(["north", "south", "east", "west"])
movable.direction = direction

View File

@ -0,0 +1,128 @@
from ..datacomponents import Input, Fighter, Move, Faction, Interact, Inventory, Equipment, Attackable, Item, UseMessage
from ..system import system
#from ..controls import MoveAction, TakeAction, DropAction, UseAction
@system([Input, Fighter, Move])
def control(obj, roomData, input, fighter, *_args):
action = input.action
if action:
input.target = None
input.action = None
if action is not None:
executeAction(obj, roomData, action)
if input.target:
fighter.target = input.target
def executeAction(obj, roomData, action):
kind = action.name
try:
handler = handlers.get(kind)
except TypeError:
handler = None
if handler is None:
print("invalid action", action)
return
handler(obj, roomData, action)
def do_move(obj, roomData, action):
direction = action.direction
roomData.getComponent(obj, Move).direction = direction
def do_take(obj, roomData, action):
rank = action.rank
inventory = roomData.getComponent(obj, Inventory)
if inventory is None or len(inventory.items) >= inventory.capacity:
# can't take anything if there is no inventory or if it's full
return
objects = obj.getNearObjects()
if rank is not None:
if rank not in range(len(objects)):
return
objects = [objects[rank]]
for item in objects:
if roomData.getComponent(item, Item) is not None:
inventory.add(item)
obj.trigger("inventorychange")
item.unPlace()
break
def do_drop(obj, roomData, action):
rank = action.rank
inventory = roomData.getComponent(obj, Inventory)
if inventory is None:
return False
if rank is None:
rank = 0
if rank not in range(len(inventory.items)):
return False
item = inventory.items[rank]
inventory.items.remove(item)
obj.trigger("inventorychange")
item.place(obj.getGround())
return True
def do_use(obj, roomData, action):
rank = action.rank
if action.container == "inventory":
items = roomData.getComponent(obj, Inventory).items
elif action.container == "equipment":
items = [val for key, val in sorted(roomData.getComponent(obj, Equipment).slots.items())]
if rank is None:
rank = 0
if rank not in range(len(items)):
return
item = items[rank]
onUse = roomData.getComponent(item, Item).onUse
for component in onUse:
roomData.addComponent(item, component)
roomData.addComponent(item, UseMessage(obj))
def do_interact(obj, roomData, action):
directions = action.directions
objects = _getNearbyObjects(obj, directions)
for other in objects:
if roomData.getComponent(other, Interact) is not None:
for component in roomData.getComponent(other, Interact).components:
roomData.addComponent(other, component)
roomData.addComponent(other, UseMessage(obj, action.parameter))
break
def do_attack(obj, roomData, action):
directions = action.directions
objects = _getNearbyObjects(obj, directions)
if roomData.getComponent(obj, Input).target in objects:
objects = {roomData.getComponent(obj, Input).target}
fighter = roomData.getComponent(obj, Fighter)
alignment = roomData.getComponent(obj, Faction) or Faction.NONE
for other in objects:
if fighter.inRange(obj, other) and alignment.isEnemy(roomData.getComponent(other, Faction) or Faction.NONE) and roomData.getComponent(other, Attackable):
fighter.target = other
roomData.getComponent(obj, Input).target = other
break
def do_say(obj, roomData, action):
text = action.text
roomData.makeSound(obj, text)
def _getNearbyObjects(obj, directions):
nearPlaces = obj.getGround().getNeighbours()
objects = []
for direction in directions:
if direction is None:
objects += obj.getNearObjects()
elif isinstance(direction, str) and direction in nearPlaces:
objects += nearPlaces[direction].getObjs()
return objects
handlers = {
"move": do_move,
"take": do_take,
"drop": do_drop,
"use": do_use,
"interact": do_interact,
"attack": do_attack,
"say": do_say
}

View File

@ -0,0 +1,12 @@
from ..system import system
from .. import gameobjects
from ..datacomponents import Create
@system([Create])
def create(obj, roomData, createmessages):
for create in createmessages:
for template in create.templates:
created = gameobjects.buildEntity(template, roomData, preserve=True)
created.place(obj.getGround())

View File

@ -0,0 +1,12 @@
from ..system import system
from .. import gameobjects
from ..datacomponents import Loot, LootMessage
from ..template import Template
@system([LootMessage, Loot])
def droploot(obj, roomData, _message, loot):
for template in loot.pick():
dropped = gameobjects.buildEntity(template, roomData, preserve=True)
dropped.place(obj.getGround())

View File

@ -0,0 +1,16 @@
from ..system import system
from ..datacomponents import UseMessage, Food, Attackable, Remove, Inventory
@system([UseMessage, Food])
def eat(obj, roomData, use, food):
actor = use[0].actor
life = roomData.getComponent(actor, Attackable)
if life is not None:
life.heal(food.healing, obj)
roomData.addComponent(obj, Remove)
inv = roomData.getComponent(actor, Inventory)
if inv is not None:
inv.items.remove(obj)
actor.trigger("inventorychange")

View File

@ -0,0 +1,27 @@
from ..system import system
from ..datacomponents import UseMessage, Equippable, Equipment, Inventory
@system([UseMessage, Equippable])
def equip(obj, roomData, use, equippable):
actor = use[0].actor
equipment = roomData.getComponent(actor, Equipment)
inv = roomData.getComponent(actor, Inventory)
if equipment is None or equippable.slot not in equipment.slots or inv is None:
raise Exception("attempting to equip whithout having inventory", equippable.slot, str(equipment.slots))
if obj in inv.items:
inv.items.remove(obj)
olditem = equipment.slots[equippable.slot]
if olditem is not None:
inv.add(olditem)
equipment.slots[equippable.slot] = obj
elif equipment.slots[equippable.slot] == obj:
if len(inv.items) < inv.capacity:
equipment.slots[equippable.slot] = None
inv.add(obj)
else:
raise Exception("attempting to equip item not in inventory")
actor.trigger("inventorychange")
actor.trigger("equipmentchange")

View File

@ -0,0 +1,50 @@
from ..system import system
from ..datacomponents import Exchanger, Inventory, UseMessage, Listen, Remove
from ..template import Template
from .. import gameobjects
@system([UseMessage, Exchanger])
def exchange(obj, roomData, usemessages, exchanger):
for use in usemessages:
exchange = exchanger.options.get(use.parameter)
if exchange is None:
# give actor a list of options
tell_options(obj, exchanger, roomData.getComponent(use.actor, Listen))
else:
# perform the exchange
perform_exchange(exchange, roomData.getComponent(use.actor, Inventory), roomData)
use.actor.trigger("inventorychange")
def tell_options(source, exchanger, ear):
if ear is None:
return
if exchanger.description:
ear.sounds.append((source, exchanger.description))
for option in exchanger.options.values():
ear.sounds.append((source, "{}: {}".format(option.name, option.description)))
def perform_exchange(exchange, inventory, roomData):
if inventory is None:
return
costs = list(exchange.costs)
toRemove = []
for item in inventory.items:
# get all the items to remove
if item.name in costs:
toRemove.append(item)
costs.remove(item.name)
if len(costs):
# not all costs can be covered; other party can't afford trade
return
toAdd = [gameobjects.buildEntity(Template(product), roomData) for product in exchange.products]
if len(inventory.items) - len(toRemove) + len(toAdd) > inventory.capacity:
return
for item in toRemove:
inventory.items.remove(item)
roomData.addComponent(item, Remove)
for item in toAdd:
inventory.add(item)

View File

@ -0,0 +1,27 @@
from ..system import system
from ..datacomponents import Fighter, Attackable, Equipment
@system([Fighter])
def fight(obj, roomData, fighter):
other = fighter.target
if other is None:
return
otherFighter = roomData.getComponent(other, Attackable)
if otherFighter is not None and fighter.inRange(obj, other) and fighter.attackReady < roomData.getStamp():
strength = fighter.strength
equipment = roomData.getComponent(obj, Equipment)
if equipment is not None:
strength += equipment.getBonus(roomData, "strength")
otherFighter.attack(strength, obj)
fighter.attackReady = roomData.getStamp() + fighter.slowness
fighter.target = None

View File

@ -0,0 +1,15 @@
from ..system import system
from ..datacomponents import Events
@system([Events])
def handleevents(obj, roomData, events):
while True:
try:
event, args, kwargs = events.messages.popleft()
except IndexError:
break
for listener in list(obj.listeners[event]):
listener(obj, *args, **kwargs)
roomData.removeComponent(obj, events)

View File

@ -0,0 +1,14 @@
from ..system import system
from ..datacomponents import Heal, Attackable
@system([Heal, Attackable])
def heal(obj, roomData, healing, attackable):
if not attackable.healthFull():
if healing.nextHeal is not None and roomData.stepStamp > healing.nextHeal:
attackable.heal(healing.amount, None)
healing.nextHeal = roomData.getStamp() + healing.interval
if healing.nextHeal is None:
healing.nextHeal = roomData.getStamp() + healing.interval

View File

@ -0,0 +1,21 @@
from ..system import system
from ..datacomponents import Move, EnterMessage
@system([Move])
def move(obj, roomData, movable):
neighbours = obj.getGround().getNeighbours()
if movable.canMove(obj, movable.direction) and roomData.getStamp() > movable.moveReady:
newPlace = neighbours[movable.direction]
if newPlace.accessible():
for resident in newPlace.getObjs():
roomData.addComponent(resident, EnterMessage(obj))
obj.place(newPlace)
movable.moveReady = roomData.getStamp() + movable.slowness
obj.trigger("move")
movable.direction = None

View File

@ -0,0 +1,7 @@
from ..system import system
from ..datacomponents import Remove
@system([Remove])
def remove(obj, roomData, *_args):
obj.doRemove()

Some files were not shown because too many files have changed in this diff Show More