Created
July 29, 2019 22:02
-
-
Save Aerodos12/5d0271b2464506d82425de0c1737636e to your computer and use it in GitHub Desktop.
Battleship Stuff
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Account | |
The Account represents the game "account" and each login has only one | |
Account object. An Account is what chats on default channels but has no | |
other in-game-world existence. Rather the Account puppets Objects (such | |
as Characters) in order to actually participate in the game world. | |
Guest | |
Guest accounts are simple low-level accounts that are created/deleted | |
on the fly and allows users to test the game without the commitment | |
of a full registration. Guest accounts are deactivated by default; to | |
activate them, add the following line to your settings file: | |
GUEST_ENABLED = True | |
You will also need to modify the connection screen to reflect the | |
possibility to connect with a guest account. The setting file accepts | |
several more options for customizing the Guest account system. | |
""" | |
from evennia import DefaultAccount, DefaultGuest | |
class Admiralty(DefaultAccount): | |
""" | |
This class describes the actual OOC account (i.e. the user connecting | |
to the MUD). It does NOT have visual appearance in the game world (that | |
is handled by the character which is connected to this). Comm channels | |
are attended/joined using this object. | |
It can be useful e.g. for storing configuration options for your game, but | |
should generally not hold any character-related info (that's best handled | |
on the character level). | |
Can be set using BASE_ACCOUNT_TYPECLASS. | |
* available properties | |
key (string) - name of account | |
name (string)- wrapper for user.username | |
aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings. | |
dbref (int, read-only) - unique #id-number. Also "id" can be used. | |
date_created (string) - time stamp of object creation | |
permissions (list of strings) - list of permission strings | |
user (User, read-only) - django User authorization object | |
obj (Object) - game object controlled by account. 'character' can also be used. | |
sessions (list of Sessions) - sessions connected to this account | |
is_superuser (bool, read-only) - if the connected user is a superuser | |
* Handlers | |
locks - lock-handler: use locks.add() to add new lock strings | |
db - attribute-handler: store/retrieve database attributes on this self.db.myattr=val, val=self.db.myattr | |
ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data | |
scripts - script-handler. Add new scripts to object with scripts.add() | |
cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object | |
nicks - nick-handler. New nicks with nicks.add(). | |
* Helper methods | |
msg(text=None, **kwargs) | |
execute_cmd(raw_string, session=None) | |
search(ostring, global_search=False, attribute_name=None, use_nicks=False, location=None, ignore_errors=False, account=False) | |
is_typeclass(typeclass, exact=False) | |
swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) | |
access(accessing_obj, access_type='read', default=False) | |
check_permstring(permstring) | |
* Hook methods (when re-implementation, remember methods need to have self as first arg) | |
basetype_setup() | |
at_account_creation() | |
- note that the following hooks are also found on Objects and are | |
usually handled on the character level: | |
at_init() | |
at_cmdset_get(**kwargs) | |
at_first_login() | |
at_post_login(session=None) | |
at_disconnect() | |
at_message_receive() | |
at_message_send() | |
at_server_reload() | |
at_server_shutdown() | |
""" | |
pass | |
class Guest(DefaultGuest): | |
""" | |
This class is used for guest logins. Unlike Accounts, Guests and their | |
characters are deleted after disconnection. | |
""" | |
pass |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Characters | |
Characters are (by default) Objects setup to be puppeted by Accounts. | |
They are what you "see" in game. The Character class in this module | |
is setup to be the "default" character type created by the default | |
creation commands. | |
""" | |
from evennia import DefaultCharacter | |
from evennia.utils import lazy_property | |
from world.traits import TraitHandler | |
class Fleet(DefaultCharacter): | |
""" | |
Base class for a group of ships in-game (fleets) and for characters. | |
The Character defaults to reimplementing some of base Object's hook methods with the | |
following functionality: | |
at_basetype_setup - always assigns the DefaultCmdSet to this object type | |
(important!)sets locks so character cannot be picked up | |
and its commands only be called by itself, not anyone else. | |
(to change things, use at_object_creation() instead). | |
at_after_move(source_location) - Launches the "look" command after every move. | |
at_post_unpuppet(account) - when Account disconnects from the Character, we | |
store the current location in the pre_logout_location Attribute and | |
move it to a None-location so the "unpuppeted" character | |
object does not need to stay on grid. Echoes "Account has disconnected" | |
to the room. | |
at_pre_puppet - Just before Account re-connects, retrieves the character's | |
pre_logout_location Attribute and move it back on the grid. | |
at_post_puppet - Echoes "AccountName has entered the game" to the room. | |
""" | |
def at_object_creation(self): | |
self.cmdset.add("commands.fleet_creation.FleetCreationCmdSet",permanent=True) | |
self.db.ship_classes = { | |
"battleship": "war", | |
"patrol_boat":"patrol", | |
} | |
def is_shipclass_valid(self,ship_class): | |
""" | |
Checks if a ship's class is recognized by a fleet (the current fleet). | |
""" | |
return ship_class in self.ship_classes | |
@property | |
def ship_classes(self): | |
return self.db.ship_classes | |
@lazy_property | |
def traits(self): | |
return TraitHandler(self,db_attribute="stats") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Exits | |
Exits are connectors between Rooms. An exit always has a destination property | |
set and has a single command defined on itself with the same name as its key, | |
for allowing Characters to traverse the exit to its destination. | |
""" | |
from evennia import DefaultExit | |
class Exit(DefaultExit): | |
""" | |
Exits are connectors between rooms. Exits are normal Objects except | |
they defines the `destination` property. It also does work in the | |
following methods: | |
basetype_setup() - sets default exit locks (to change, use `at_object_creation` instead). | |
at_cmdset_get(**kwargs) - this is called when the cmdset is accessed and should | |
rebuild the Exit cmdset along with a command matching the name | |
of the Exit object. Conventionally, a kwarg `force_init` | |
should force a rebuild of the cmdset, this is triggered | |
by the `@alias` command when aliases are changed. | |
at_failed_traverse() - gives a default error message ("You cannot | |
go there") if exit traversal fails and an | |
attribute `err_traverse` is not defined. | |
Relevant hooks to overload (compared to other types of Objects): | |
at_traverse(traveller, target_loc) - called to do the actual traversal and calling of the other hooks. | |
If overloading this, consider using super() to use the default | |
movement implementation (and hook-calling). | |
at_after_traverse(traveller, source_loc) - called by at_traverse just after traversing. | |
at_failed_traverse(traveller) - called by at_traverse if traversal failed for some reason. Will | |
not be called if the attribute `err_traverse` is | |
defined, in which case that will simply be echoed. | |
""" | |
pass |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
NavalObject | |
The NavalObject is the "naked" base class for things in the Titanium Armada game world. | |
""" | |
from evennia import DefaultObject | |
from evennia.utils import lazy_property | |
from world.traits import TraitHandler | |
class NavalObject(DefaultObject): | |
""" | |
This is the root typeclass object for Titanium Armada. | |
The BaseObject class implements several hooks tying into the game | |
engine. By re-implementing these hooks you can control the | |
system. You should never need to re-implement special Python | |
methods, such as __init__ and especially never __getattribute__ and | |
__setattr__ since these are used heavily by the typeclass system | |
of Evennia and messing with them might well break things for you. | |
* Base properties defined/available on all Objects | |
key (string) - name of object | |
name (string)- same as key | |
aliases (list of strings) - aliases to the object. Will be saved to | |
database as AliasDB entries but returned as strings. | |
dbref (int, read-only) - unique #id-number. Also "id" can be used. | |
back to this class | |
date_created (string) - time stamp of object creation | |
permissions (list of strings) - list of permission strings | |
account (Account) - controlling account (if any, only set together with | |
sessid below) | |
sessid (int, read-only) - session id (if any, only set together with | |
account above). Use `sessions` handler to get the | |
Sessions directly. | |
location (Object) - current location. Is None if this is a room | |
home (Object) - safety start-location | |
sessions (list of Sessions, read-only) - returns all sessions connected | |
to this object | |
has_account (bool, read-only)- will only return *connected* accounts | |
contents (list of Objects, read-only) - returns all objects inside this | |
object (including exits) | |
exits (list of Objects, read-only) - returns all exits from this | |
object, if any | |
destination (Object) - only set if this object is an exit. | |
is_superuser (bool, read-only) - True/False if this user is a superuser | |
* Handlers available | |
locks - lock-handler: use locks.add() to add new lock strings | |
db - attribute-handler: store/retrieve database attributes on this | |
self.db.myattr=val, val=self.db.myattr | |
ndb - non-persistent attribute handler: same as db but does not create | |
a database entry when storing data | |
scripts - script-handler. Add new scripts to object with scripts.add() | |
cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object | |
nicks - nick-handler. New nicks with nicks.add(). | |
sessions - sessions-handler. Get Sessions connected to this | |
object with sessions.get() | |
* Helper methods (see src.objects.objects.py for full headers) | |
search(ostring, global_search=False, attribute_name=None, | |
use_nicks=False, location=None, ignore_errors=False, account=False) | |
execute_cmd(raw_string) | |
msg(text=None, **kwargs) | |
msg_contents(message, exclude=None, from_obj=None, **kwargs) | |
move_to(destination, quiet=False, emit_to_obj=None, use_destination=True) | |
copy(new_key=None) | |
delete() | |
is_typeclass(typeclass, exact=False) | |
swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) | |
access(accessing_obj, access_type='read', default=False) | |
check_permstring(permstring) | |
* Hooks (these are class methods, so args should start with self): | |
basetype_setup() - only called once, used for behind-the-scenes | |
setup. Normally not modified. | |
basetype_posthook_setup() - customization in basetype, after the object | |
has been created; Normally not modified. | |
at_object_creation() - only called once, when object is first created. | |
Object customizations go here. | |
at_object_delete() - called just before deleting an object. If returning | |
False, deletion is aborted. Note that all objects | |
inside a deleted object are automatically moved | |
to their <home>, they don't need to be removed here. | |
at_init() - called whenever typeclass is cached from memory, | |
at least once every server restart/reload | |
at_cmdset_get(**kwargs) - this is called just before the command handler | |
requests a cmdset from this object. The kwargs are | |
not normally used unless the cmdset is created | |
dynamically (see e.g. Exits). | |
at_pre_puppet(account)- (account-controlled objects only) called just | |
before puppeting | |
at_post_puppet() - (account-controlled objects only) called just | |
after completing connection account<->object | |
at_pre_unpuppet() - (account-controlled objects only) called just | |
before un-puppeting | |
at_post_unpuppet(account) - (account-controlled objects only) called just | |
after disconnecting account<->object link | |
at_server_reload() - called before server is reloaded | |
at_server_shutdown() - called just before server is fully shut down | |
at_access(result, accessing_obj, access_type) - called with the result | |
of a lock access check on this object. Return value | |
does not affect check result. | |
at_before_move(destination) - called just before moving object | |
to the destination. If returns False, move is cancelled. | |
announce_move_from(destination) - called in old location, just | |
before move, if obj.move_to() has quiet=False | |
announce_move_to(source_location) - called in new location, just | |
after move, if obj.move_to() has quiet=False | |
at_after_move(source_location) - always called after a move has | |
been successfully performed. | |
at_object_leave(obj, target_location) - called when an object leaves | |
this object in any fashion | |
at_object_receive(obj, source_location) - called when this object receives | |
another object | |
at_traverse(traversing_object, source_loc) - (exit-objects only) | |
handles all moving across the exit, including | |
calling the other exit hooks. Use super() to retain | |
the default functionality. | |
at_after_traverse(traversing_object, source_location) - (exit-objects only) | |
called just after a traversal has happened. | |
at_failed_traverse(traversing_object) - (exit-objects only) called if | |
traversal fails and property err_traverse is not defined. | |
at_msg_receive(self, msg, from_obj=None, **kwargs) - called when a message | |
(via self.msg()) is sent to this obj. | |
If returns false, aborts send. | |
at_msg_send(self, msg, to_obj=None, **kwargs) - called when this objects | |
sends a message to someone via self.msg(). | |
return_appearance(looker) - describes this object. Used by "look" | |
command by default | |
at_desc(looker=None) - called by 'look' whenever the | |
appearance is requested. | |
at_get(getter) - called after object has been picked up. | |
Does not stop pickup. | |
at_drop(dropper) - called when this object has been dropped. | |
at_say(speaker, message) - by default, called if an object inside this | |
object speaks | |
""" | |
@lazy_property | |
def traits(self): | |
return TraitHandler(self,db_attribute="stats") | |
class Ship(NavalObject): | |
""" | |
Ship | |
NavalObject class for defining ships (the building blocks of fleets) | |
""" | |
ship_type = 0 | |
secondary_parts = 1 | |
ship_tag = None | |
def at_object_creation(self): | |
self.db.ship_title = "Untitled Ship" | |
self.db.ship_parts = [] | |
self.create_ship_parts() | |
def create_ship_parts(self): | |
self.traits.add("ship_health","Ship Health",type="gauge",base=100,min=0) | |
def is_part_alive(self, part_index=0): | |
""" | |
Indicates whether or not the given part (indicated by index) is operational. | |
Parameters | |
---------- | |
part_index : int, optional | |
the index in which the ShipSegment is stored in the parts list. | |
Returns | |
--------- | |
bool | |
Whether or not the part is alive (operational). | |
""" | |
if self.ship_parts[part_index] != None: | |
return self.ship_parts[part_index].is_alive() | |
else: | |
return False | |
@property | |
def title(self): | |
return self.attributes.get("ship_title") | |
@title.setter | |
def title(self,title_new): | |
self.attributes.set("ship_title",title_new) | |
@property | |
def num_parts(self): | |
return self.num_parts + 1 | |
class PatrolBoat(Ship): | |
""" | |
Patrol Boat | |
A two part ship, this boat is one of the smallest ships in the | |
game. | |
""" | |
ship_type = 1 | |
secondary_parts = 1 | |
ship_tag = "patrol_boat" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Bodies of water | |
Water Bodies are simple containers that has no location of their own (are vast and can host ships). | |
""" | |
from evennia import DefaultRoom | |
class WaterBody(DefaultRoom): | |
""" | |
Bodies of water (WaterBody) are Rooms in which act as a | |
massive naval map or a series of maps. | |
See examples/object.py for a list of | |
properties and methods available on all Objects. | |
""" | |
combat_size_x = 5 | |
combat_size_y = 5 | |
@property | |
def size(self): | |
return (self.combat_size_x + 1, self.combat_size_y + 1) | |
class Sea(WaterBody): | |
""" | |
A 16 by 16 WaterBody that is bigger than a lake, but smaller than an Ocean. | |
""" | |
combat_size_x = 16 | |
combat_size_y = 16 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Scripts | |
Scripts are powerful jacks-of-all-trades. They have no in-game | |
existence and can be used to represent persistent game systems in some | |
circumstances. Scripts can also have a time component that allows them | |
to "fire" regularly or a limited number of times. | |
There is generally no "tree" of Scripts inheriting from each other. | |
Rather, each script tends to inherit from the base Script class and | |
just overloads its hooks to have it perform its function. | |
""" | |
from evennia import DefaultScript | |
class Script(DefaultScript): | |
""" | |
A script type is customized by redefining some or all of its hook | |
methods and variables. | |
* available properties | |
key (string) - name of object | |
name (string)- same as key | |
aliases (list of strings) - aliases to the object. Will be saved | |
to database as AliasDB entries but returned as strings. | |
dbref (int, read-only) - unique #id-number. Also "id" can be used. | |
date_created (string) - time stamp of object creation | |
permissions (list of strings) - list of permission strings | |
desc (string) - optional description of script, shown in listings | |
obj (Object) - optional object that this script is connected to | |
and acts on (set automatically by obj.scripts.add()) | |
interval (int) - how often script should run, in seconds. <0 turns | |
off ticker | |
start_delay (bool) - if the script should start repeating right away or | |
wait self.interval seconds | |
repeats (int) - how many times the script should repeat before | |
stopping. 0 means infinite repeats | |
persistent (bool) - if script should survive a server shutdown or not | |
is_active (bool) - if script is currently running | |
* Handlers | |
locks - lock-handler: use locks.add() to add new lock strings | |
db - attribute-handler: store/retrieve database attributes on this | |
self.db.myattr=val, val=self.db.myattr | |
ndb - non-persistent attribute handler: same as db but does not | |
create a database entry when storing data | |
* Helper methods | |
start() - start script (this usually happens automatically at creation | |
and obj.script.add() etc) | |
stop() - stop script, and delete it | |
pause() - put the script on hold, until unpause() is called. If script | |
is persistent, the pause state will survive a shutdown. | |
unpause() - restart a previously paused script. The script will continue | |
from the paused timer (but at_start() will be called). | |
time_until_next_repeat() - if a timed script (interval>0), returns time | |
until next tick | |
* Hook methods (should also include self as the first argument): | |
at_script_creation() - called only once, when an object of this | |
class is first created. | |
is_valid() - is called to check if the script is valid to be running | |
at the current time. If is_valid() returns False, the running | |
script is stopped and removed from the game. You can use this | |
to check state changes (i.e. an script tracking some combat | |
stats at regular intervals is only valid to run while there is | |
actual combat going on). | |
at_start() - Called every time the script is started, which for persistent | |
scripts is at least once every server start. Note that this is | |
unaffected by self.delay_start, which only delays the first | |
call to at_repeat(). | |
at_repeat() - Called every self.interval seconds. It will be called | |
immediately upon launch unless self.delay_start is True, which | |
will delay the first call of this method by self.interval | |
seconds. If self.interval==0, this method will never | |
be called. | |
at_stop() - Called as the script object is stopped and is about to be | |
removed from the game, e.g. because is_valid() returned False. | |
at_server_reload() - Called when server reloads. Can be used to | |
save temporary variables you want should survive a reload. | |
at_server_shutdown() - called at a full server shutdown. | |
""" | |
pass |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
""" | |
Traits Module | |
`Trait` classes represent modifiable traits on objects or characters. They | |
are instantiated by a `TraitHandler` object, which is typically set up | |
as a property on the object or character's typeclass. | |
**Setup** | |
To use traits on an object, add a function that passes the object | |
itself into the constructor and returns a `TraitHandler`. This function | |
should be decorated with the `lazy_property` decorator. | |
If desired, multiple `TraitHandler` properties can be defined on one | |
object. The optional `db_attribute` argument should be used to specify | |
a different storage key for each `TraitHandler`. The default is `traits`. | |
Example: | |
```python | |
from evennia.utils import lazy_property | |
from world.traits import TraitHandler | |
... | |
class Object(DefaultObject): | |
... | |
@lazy_property | |
def traits(self): | |
return TraitHandler(self) | |
``` | |
**Trait Configuration** | |
`Trait` objects can be configured as one of three basic types with | |
increasingly complex behavior. | |
* Static - A simple trait model with a base value and optional modifier. | |
* Counter - Trait with a base value and a modifiable current value that | |
can vary along a range defined by optional min and max values. | |
* Gauge - Modified counter type modeling a refillable "gauge". | |
All traits have a read-only `actual` property that will report the trait's | |
actual value. | |
Example: | |
```python | |
>>> hp = obj.traits.hp | |
>>> hp.actual | |
100 | |
``` | |
They also support storing arbitrary data via either dictionary key or | |
attribute syntax. Storage of arbitrary data in this way has the same | |
constraints as any nested collection type stored in a persistent Evennia | |
Attribute, so it is best to avoid attempting to store complex objects. | |
Static Trait Configuration | |
A static `Trait` stores a `base` value and a `mod` modifier value. | |
The trait's actual value is equal to `base`+`mod`. | |
Static traits can be used to model many different stats, such as | |
Strength, Character Level, or Defense Rating in many tabletop gaming | |
systems. | |
Constructor Args: | |
name (str): name of the trait | |
type (str): 'static' for static traits | |
base (int, float): base value of the trait | |
mod Optional(int): modifier value | |
extra Optional(dict): keys of this dict are accessible on the | |
`Trait` object as attributes or dict keys | |
Properties: | |
actual (int, float): returns the value of `mod`+`base` properties | |
extra (list[str]): list of keys stored in the extra data dict | |
Methods: | |
reset_mod(): sets the value of the `mod` property to zero | |
Examples: | |
'''python | |
>>> strength = char.traits.str | |
>>> strength.actual | |
5 | |
>>> strength.mod = 2 # add a bonus to strength | |
>>> str(strength) | |
'Strength 7 (+2)' | |
>>> strength.reset_mod() # clear bonuses | |
>>> str(strength) | |
'Strength 5 (+0)' | |
>>> strength.newkey = 'newvalue' | |
>>> strength.extra | |
['newkey'] | |
>>> strength | |
Trait({'name': 'Strength', 'type': 'trait', 'base': 5, 'mod': 0, | |
'min': None, 'max': None, 'extra': {'newkey': 'newvalue'}}) | |
``` | |
Counter Trait Configuration | |
Counter type `Trait` objects have a `base` value similar to static | |
traits, but adds a `current` value and a range along which it may | |
vary. Modifier values are applied to this `current` value instead | |
of `base` when determining the `actual` value. The `current` can | |
also be reset to its `base` value by calling the `reset_counter()` | |
method. | |
Counter style traits are best used to represent game traits such as | |
carrying weight, alignment points, a money system, or bonus/penalty | |
counters. | |
Constructor Args: | |
(all keys listed above for 'static', plus:) | |
min Optional(int, float, None): default None | |
minimum allowable value for current; unbounded if None | |
max Optional(int, float, None): default None | |
maximum allowable value for current; unbounded if None | |
Properties: | |
actual (int, float): returns the value of `mod`+`current` properties | |
Methods: | |
reset_counter(): resets `current` equal to the value of `base` | |
Examples: | |
```python | |
>>> carry = caller.traits.carry | |
>>> str(carry) | |
'Carry Weight 0 ( +0)' | |
>>> carry.current -= 3 # try to go negative | |
>>> carry # enforces zero minimum | |
'Carry Weight 0 ( +0)' | |
>>> carry.current += 15 | |
>>> carry | |
'Carry Weight 15 ( +0)' | |
>>> carry.mod = -5 # apply a modifier to reduce | |
>>> carry # apparent weight | |
'Carry Weight: 10 ( -5)' | |
>>> carry.current = 10000 # set a semi-large value | |
>>> carry # still have the modifier | |
'Carry Weight 9995 ( -5)' | |
>>> carry.reset() # remove modifier | |
>>> carry | |
'Carry Weight 10000 ( +0)' | |
>>> carry.reset_counter() | |
>>> +carry | |
0 | |
``` | |
Gauge Trait Configuration | |
A gauge type `Trait` is a modified counter trait used to model a | |
gauge that can be emptied and refilled. The `base` property of a | |
gauge trait represents its "full" value. The `mod` property increases | |
or decreases that "full" value, rather than the `current`. | |
By default gauge type traits have a `min` of zero, and a `max` set | |
to the `base`+`mod` properties. A gauge will still work if its `max` | |
property is set to a value above its `base` or to None. | |
Gauge type traits are best used to represent traits such as health | |
points, stamina points, or magic points. | |
Constructor Args: | |
(all keys listed above for 'static', plus:) | |
min Optional(int, float, None): default 0 | |
minimum allowable value for current; unbounded if None | |
max Optional(int, float, None, 'base'): default 'base' | |
maximum allowable value for current; unbounded if None; | |
if 'base', returns the value of `base`+`mod`. | |
Properties: | |
actual (int, float): returns the value of the `current` property | |
Methods: | |
fill_gauge(): adds the value of `base`+`mod` to `current` | |
percent(): returns the ratio of actual value to max value as | |
a percentage. if `max` is unbound, return the ratio of | |
`current` to `base`+`mod` instead. | |
Examples: | |
```python | |
>>> hp = caller.traits.hp | |
>>> repr(hp) | |
GaugeTrait({'name': 'HP', 'type': 'gauge', 'base': 10, 'mod': 0, | |
'min': 0, 'max': 'base', 'current': 10, 'extra': {}}) | |
>>> str(hp) | |
'HP: 10 / 10 ( +0)' | |
>>> hp.current -= 6 # take damage | |
>>> str(hp) | |
'HP: 4 / 10 ( +0)' | |
>>> hp.current -= 6 # take damage to below min | |
>>> str(hp) | |
'HP: 0 / 10 ( +0)' | |
>>> hp.fill() # refill trait | |
>>> str(hp) | |
'HP: 10 / 10 ( +0)' | |
>>> hp.current = 15 # try to set above max | |
>>> str(hp) # disallowed because max=='actual' | |
'HP: 10 / 10 ( +0)' | |
>>> hp.mod += 3 # bonus on full trait | |
>>> str(hp) # buffs flow to current | |
'HP: 13 / 13 ( +3)' | |
>>> hp.current -= 5 | |
>>> str(hp) | |
'HP: 8 / 13 ( +3)' | |
>>> hp.reset() # remove bonus on reduced trait | |
>>> str(hp) # debuffs do not affect current | |
'HP: 8 / 10 ( +0)' | |
``` | |
""" | |
from evennia.utils.dbserialize import _SaverDict | |
from evennia.utils import logger, lazy_property | |
from functools import total_ordering | |
TRAIT_TYPES = ('static', 'counter', 'gauge') | |
RANGE_TRAITS = ('counter', 'gauge') | |
class TraitException(Exception): | |
"""Base exception class raised by `Trait` objects. | |
Args: | |
msg (str): informative error message | |
""" | |
def __init__(self, msg): | |
self.msg = msg | |
class TraitHandler(object): | |
"""Factory class that instantiates Trait objects. | |
Args: | |
obj (Object): parent Object typeclass for this TraitHandler | |
db_attribute (str): name of the DB attribute for trait data storage | |
""" | |
def __init__(self, obj, db_attribute='traits'): | |
if not obj.attributes.has(db_attribute): | |
obj.attributes.add(db_attribute, {}) | |
self.attr_dict = obj.attributes.get(db_attribute) | |
self.cache = {} | |
def __len__(self): | |
"""Return number of Traits in 'attr_dict'.""" | |
return len(self.attr_dict) | |
def __setattr__(self, key, value): | |
"""Returns error message if trait objects are assigned directly.""" | |
if key in ('attr_dict', 'cache'): | |
super(TraitHandler, self).__setattr__(key, value) | |
else: | |
raise TraitException( | |
"Trait object not settable. Assign one of " | |
"`{0}.base`, `{0}.mod`, or `{0}.current` ".format(key) + | |
"properties instead." | |
) | |
def __setitem__(self, key, value): | |
"""Returns error message if trait objects are assigned directly.""" | |
return self.__setattr__(key, value) | |
def __getattr__(self, trait): | |
"""Returns Trait instances accessed as attributes.""" | |
return self.get(trait) | |
def __getitem__(self, trait): | |
"""Returns `Trait` instances accessed as dict keys.""" | |
return self.get(trait) | |
def get(self, trait): | |
""" | |
Args: | |
trait (str): key from the traits dict containing config data | |
for the trait. "all" returns a list of all trait keys. | |
Returns: | |
(`Trait` or `None`): named Trait class or None if trait key | |
is not found in traits collection. | |
""" | |
if trait not in self.cache: | |
if trait not in self.attr_dict: | |
return None | |
data = self.attr_dict[trait] | |
self.cache[trait] = Trait(data) | |
return self.cache[trait] | |
def add(self, key, name, type='static', | |
base=0, mod=0, min=None, max=None, extra={}): | |
"""Create a new Trait and add it to the handler.""" | |
if key in self.attr_dict: | |
raise TraitException("Trait '{}' already exists.".format(key)) | |
if type in TRAIT_TYPES: | |
trait = dict(name=name, | |
type=type, | |
base=base, | |
mod=mod, | |
extra=extra) | |
if min: | |
trait.update(dict(min=min)) | |
if max: | |
trait.update(dict(max=max)) | |
self.attr_dict[key] = trait | |
else: | |
raise TraitException("Invalid trait type specified.") | |
def remove(self, trait): | |
"""Remove a Trait from the handler's parent object.""" | |
if trait not in self.attr_dict: | |
raise TraitException("Trait not found: {}".format(trait)) | |
if trait in self.cache: | |
del self.cache[trait] | |
del self.attr_dict[trait] | |
def clear(self): | |
"""Remove all Traits from the handler's parent object.""" | |
for trait in self.all: | |
self.remove(trait) | |
@property | |
def all(self): | |
"""Return a list of all trait keys in this TraitHandler.""" | |
return self.attr_dict.keys() | |
@total_ordering | |
class Trait(object): | |
"""Represents an object or Character trait. | |
Note: | |
See module docstring for configuration details. | |
""" | |
def __init__(self, data): | |
if not 'name' in data: | |
raise TraitException( | |
"Required key not found in trait data: 'name'") | |
if not 'type' in data: | |
raise TraitException( | |
"Required key not found in trait data: 'type'") | |
self._type = data['type'] | |
if not 'base' in data: | |
data['base'] = 0 | |
if not 'mod' in data: | |
data['mod'] = 0 | |
if not 'extra' in data: | |
data['extra'] = {} | |
if 'min' not in data: | |
data['min'] = 0 if self._type == 'gauge' else None | |
if 'max' not in data: | |
data['max'] = 'base' if self._type == 'gauge' else None | |
self._data = data | |
self._keys = ('name', 'type', 'base', 'mod', | |
'current', 'min', 'max', 'extra') | |
self._locked = True | |
if not isinstance(data, _SaverDict): | |
logger.log_warn( | |
'Non-persistent {} class loaded.'.format( | |
type(self).__name__ | |
)) | |
def __repr__(self): | |
"""Debug-friendly representation of this Trait.""" | |
return "{}({{{}}})".format( | |
type(self).__name__, | |
', '.join(["'{}': {!r}".format(k, self._data[k]) | |
for k in self._keys if k in self._data])) | |
def __str__(self): | |
"""User-friendly string representation of this `Trait`""" | |
if self._type == 'gauge': | |
status = "{actual:4} / {base:4}".format( | |
actual=self.actual, | |
base=self.base) | |
else: | |
status = "{actual:11}".format(actual=self.actual) | |
return "{name:12} {status} ({mod:+3})".format( | |
name=self.name, | |
status=status, | |
mod=self.mod) | |
def __unicode__(self): | |
"""User-friendly unicode representation of this `Trait`""" | |
return unicode(str(self)) | |
# Extra Properties magic | |
def __getitem__(self, key): | |
"""Access extra parameters as dict keys.""" | |
try: | |
return self.__getattr__(key) | |
except AttributeError: | |
raise KeyError(key) | |
def __setitem__(self, key, value): | |
"""Set extra parameters as dict keys.""" | |
self.__setattr__(key, value) | |
def __delitem__(self, key): | |
"""Delete extra prameters as dict keys.""" | |
self.__delattr__(key) | |
def __getattr__(self, key): | |
"""Access extra parameters as attributes.""" | |
if key in self._data['extra']: | |
return self._data['extra'][key] | |
else: | |
raise AttributeError( | |
"{} '{}' has no attribute {!r}".format( | |
type(self).__name__, self.name, key | |
)) | |
def __setattr__(self, key, value): | |
"""Set extra parameters as attributes. | |
Arbitrary attributes set on a Trait object will be | |
stored in the 'extra' key of the `_data` attribute. | |
This behavior is enabled by setting the instance | |
variable `_locked` to True. | |
""" | |
propobj = getattr(self.__class__, key, None) | |
if isinstance(propobj, property): | |
if propobj.fset is None: | |
raise AttributeError("can't set attribute") | |
propobj.fset(self, value) | |
else: | |
if (self.__dict__.get('_locked', False) and | |
key not in ('_keys',)): | |
self._data['extra'][key] = value | |
else: | |
super(Trait, self).__setattr__(key, value) | |
def __delattr__(self, key): | |
"""Delete extra parameters as attributes.""" | |
if key in self._data['extra']: | |
del self._data['extra'][key] | |
# Numeric operations magic | |
def __eq__(self, other): | |
"""Support equality comparison between Traits or Trait and numeric. | |
Note: | |
This class uses the @functools.total_ordering() decorator to | |
complete the rich comparison implementation, therefore only | |
`__eq__` and `__lt__` are implemented. | |
""" | |
if type(other) == Trait: | |
return self.actual == other.actual | |
elif type(other) in (float, int): | |
return self.actual == other | |
else: | |
return NotImplemented | |
def __lt__(self, other): | |
"""Support less than comparison between `Trait`s or `Trait` and numeric.""" | |
if isinstance(other, Trait): | |
return self.actual < other.actual | |
elif type(other) in (float, int): | |
return self.actual < other | |
else: | |
return NotImplemented | |
def __pos__(self): | |
"""Access `actual` property through unary `+` operator.""" | |
return self.actual | |
def __add__(self, other): | |
"""Support addition between `Trait`s or `Trait` and numeric""" | |
if isinstance(other, Trait): | |
return self.actual + other.actual | |
elif type(other) in (float, int): | |
return self.actual + other | |
else: | |
return NotImplemented | |
def __sub__(self, other): | |
"""Support subtraction between `Trait`s or `Trait` and numeric""" | |
if isinstance(other, Trait): | |
return self.actual - other.actual | |
elif type(other) in (float, int): | |
return self.actual - other | |
else: | |
return NotImplemented | |
def __mul__(self, other): | |
"""Support multiplication between `Trait`s or `Trait` and numeric""" | |
if isinstance(other, Trait): | |
return self.actual * other.actual | |
elif type(other) in (float, int): | |
return self.actual * other | |
else: | |
return NotImplemented | |
def __floordiv__(self, other): | |
"""Support floor division between `Trait`s or `Trait` and numeric""" | |
if isinstance(other, Trait): | |
return self.actual // other.actual | |
elif type(other) in (float, int): | |
return self.actual // other | |
else: | |
return NotImplemented | |
# yay, commutative property! | |
__radd__ = __add__ | |
__rmul__ = __mul__ | |
def __rsub__(self, other): | |
"""Support subtraction between `Trait`s or `Trait` and numeric""" | |
if isinstance(other, Trait): | |
return other.actual - self.actual | |
elif type(other) in (float, int): | |
return other - self.actual | |
else: | |
return NotImplemented | |
def __rfloordiv__(self, other): | |
"""Support floor division between `Trait`s or `Trait` and numeric""" | |
if isinstance(other, Trait): | |
return other.actual // self.actual | |
elif type(other) in (float, int): | |
return other // self.actual | |
else: | |
return NotImplemented | |
# Public members | |
@property | |
def name(self): | |
"""Display name for the trait.""" | |
return self._data['name'] | |
@property | |
def actual(self): | |
"""The "actual" value of the trait.""" | |
if self._type == 'gauge': | |
return self.current | |
elif self._type == 'counter': | |
return self._mod_current() | |
else: | |
return self._mod_base() | |
@property | |
def base(self): | |
"""The trait's base value. | |
Note: | |
The setter for this property will enforce any range bounds set | |
on this `Trait`. | |
""" | |
return self._data['base'] | |
@base.setter | |
def base(self, amount): | |
if self._data.get('max', None) == 'base': | |
self._data['base'] = amount | |
if type(amount) in (int, float): | |
self._data['base'] = self._enforce_bounds(amount) | |
@property | |
def mod(self): | |
"""The trait's modifier.""" | |
return self._data['mod'] | |
@mod.setter | |
def mod(self, amount): | |
if type(amount) in (int, float): | |
delta = amount - self._data['mod'] | |
self._data['mod'] = amount | |
if self._type == 'gauge': | |
if delta >= 0: | |
# apply increases to current | |
self.current = self._enforce_bounds(self.current + delta) | |
else: | |
# but not decreases, unless current goes out of range | |
self.current = self._enforce_bounds(self.current) | |
@property | |
def min(self): | |
"""The lower bound of the range.""" | |
if self._type in RANGE_TRAITS: | |
return self._data['min'] | |
else: | |
raise AttributeError( | |
"static 'Trait' object has no attribute 'min'.") | |
@min.setter | |
def min(self, amount): | |
if self._type in RANGE_TRAITS: | |
if amount is None: self._data['min'] = amount | |
elif type(amount) in (int, float): | |
self._data['min'] = amount if amount < self.base else self.base | |
else: | |
raise AttributeError( | |
"static 'Trait' object has no attribute 'min'.") | |
@property | |
def max(self): | |
"""The maximum value of the `Trait`. | |
Note: | |
This property may be set to the string literal 'base'. | |
When set this way, the property returns the value of the | |
`mod`+`base` properties. | |
""" | |
if self._type in RANGE_TRAITS: | |
if self._data['max'] == 'base': | |
return self._mod_base() | |
else: | |
return self._data['max'] | |
else: | |
raise AttributeError( | |
"static 'Trait' object has no attribute 'max'.") | |
@max.setter | |
def max(self, value): | |
if self._type in RANGE_TRAITS: | |
if value == 'base' or value is None: | |
self._data['max'] = value | |
elif type(value) in (int, float): | |
self._data['max'] = value if value > self.base else self.base | |
else: | |
raise AttributeError( | |
"static 'Trait' object has no attribute 'max'.") | |
@property | |
def current(self): | |
"""The `current` value of the `Trait`.""" | |
if self._type == 'gauge': | |
return self._data.get('current', self._mod_base()) | |
else: | |
return self._data.get('current', self.base) | |
@current.setter | |
def current(self, value): | |
if self._type in RANGE_TRAITS: | |
if type(value) in (int, float): | |
self._data['current'] = self._enforce_bounds(value) | |
else: | |
raise AttributeError( | |
"'current' property is read-only on static 'Trait'.") | |
@property | |
def extra(self): | |
"""Returns a list containing available extra data keys.""" | |
return self._data['extra'].keys() | |
def reset_mod(self): | |
"""Clears any mod value on the `Trait`.""" | |
self.mod = 0 | |
def reset_counter(self): | |
"""Resets `current` property equal to `base` value.""" | |
self.current = self.base | |
def fill_gauge(self): | |
"""Adds the `mod`+`base` to the `current` value. | |
Note: | |
Will honor the upper bound if set. | |
""" | |
self.current = \ | |
self._enforce_bounds(self.current + self._mod_base()) | |
def percent(self): | |
"""Returns the value formatted as a percentage.""" | |
if self._type in RANGE_TRAITS: | |
if self.max: | |
return "{:3.1f}%".format(self.current * 100.0 / self.max) | |
elif self._type == 'counter' and self.base != 0: | |
return "{:3.1f}%".format(self.current * 100.0 / self._mod_base()) | |
elif self._type == 'gauge' and self._mod_base() != 0: | |
return "{:3.1f}%".format(self.current * 100.0 / self._mod_base()) | |
# if we get to this point, it's either a static trait or | |
# a divide by zero situation | |
return "100.0%" | |
# Private members | |
def _mod_base(self): | |
return self._enforce_bounds(self.mod + self.base) | |
def _mod_current(self): | |
return self._enforce_bounds(self.mod + self.current) | |
def _enforce_bounds(self, value): | |
"""Ensures that incoming value falls within trait's range.""" | |
if self._type in RANGE_TRAITS: | |
if self.min is not None and value <= self.min: | |
return self.min | |
if self._data['max'] == 'base' and value >= self.mod + self.base: | |
return self.mod + self.base | |
if self.max is not None and value >= self.max: | |
return self.max | |
return value | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment