From ec6c28db95243c09b1d595c04fd360faa05ada98 Mon Sep 17 00:00:00 2001 From: Evennia docbuilder action Date: Sun, 28 Aug 2022 12:07:13 +0000 Subject: [PATCH] Updated HTML docs --- docs/0.9.5/.buildinfo | 2 +- docs/0.9.5/Default-Commands.html | 36 +-- docs/0.9.5/_sources/Default-Commands.md.txt | 36 +-- ...evennia.commands.default.batchprocess.html | 2 +- .../evennia.commands.default.building.html | 6 +- .../api/evennia.commands.default.comms.html | 6 +- .../api/evennia.commands.default.general.html | 6 +- .../api/evennia.commands.default.system.html | 6 +- .../evennia.commands.default.unloggedin.html | 10 +- docs/0.9.5/api/evennia.contrib.barter.html | 2 +- docs/0.9.5/api/evennia.contrib.dice.html | 2 +- .../api/evennia.contrib.email_login.html | 10 +- ...vennia.contrib.ingame_python.commands.html | 2 +- ...b.tutorial_examples.cmdset_red_button.html | 6 +- ...vennia.contrib.tutorial_world.objects.html | 6 +- .../evennia.contrib.tutorial_world.rooms.html | 4 +- docs/0.9.5/api/evennia.utils.eveditor.html | 2 +- docs/0.9.5/api/evennia.utils.evmore.html | 2 +- docs/1.0-dev/.buildinfo | 2 +- .../Components/Components-Overview.html | 1 + docs/1.0-dev/Components/Webclient.html | 68 +++++- docs/1.0-dev/Contribs/Contrib-Buffs.html | 96 +++++++- .../evennia/commands/default/general.html | 2 +- .../evennia/contrib/rpg/buffs/buff.html | 226 +++++++++++++----- .../contrib/rpg/buffs/samplebuffs.html | 6 +- .../evennia/contrib/rpg/buffs/tests.html | 40 +++- .../_modules/evennia/objects/objects.html | 2 +- .../evennia/typeclasses/attributes.html | 15 ++ .../_sources/Components/Webclient.md.txt | 68 +++++- .../_sources/Contribs/Contrib-Buffs.md.txt | 89 ++++++- .../api/evennia.commands.default.account.html | 4 +- ...evennia.commands.default.batchprocess.html | 4 +- .../evennia.commands.default.building.html | 12 +- .../api/evennia.commands.default.general.html | 12 +- .../api/evennia.commands.default.tests.html | 2 +- .../evennia.commands.default.unloggedin.html | 8 +- ....base_systems.email_login.email_login.html | 8 +- ...b.base_systems.ingame_python.commands.html | 4 +- ...rib.full_systems.evscaperoom.commands.html | 16 +- ...trib.game_systems.turnbattle.tb_basic.html | 4 +- ...trib.game_systems.turnbattle.tb_equip.html | 4 +- ...trib.game_systems.turnbattle.tb_items.html | 4 +- ...trib.game_systems.turnbattle.tb_magic.html | 4 +- ...trib.game_systems.turnbattle.tb_range.html | 4 +- .../api/evennia.contrib.rpg.buffs.buff.html | 123 ++++++---- .../api/evennia.contrib.rpg.buffs.tests.html | 8 +- ...evennia.contrib.rpg.rpsystem.rpsystem.html | 4 +- ...ntrib.tutorials.red_button.red_button.html | 16 +- ...trib.tutorials.tutorial_world.objects.html | 12 +- ...ontrib.tutorials.tutorial_world.rooms.html | 8 +- docs/1.0-dev/api/evennia.objects.objects.html | 4 +- docs/1.0-dev/api/evennia.utils.eveditor.html | 4 +- docs/1.0-dev/api/evennia.utils.evmenu.html | 4 +- docs/1.0-dev/api/evennia.utils.evmore.html | 4 +- docs/1.0-dev/genindex.html | 20 +- docs/1.0-dev/index.html | 1 + docs/1.0-dev/objects.inv | Bin 143745 -> 144039 bytes docs/1.0-dev/searchindex.js | 2 +- 58 files changed, 743 insertions(+), 318 deletions(-) diff --git a/docs/0.9.5/.buildinfo b/docs/0.9.5/.buildinfo index 8711dbf77a..9c997fc88e 100644 --- a/docs/0.9.5/.buildinfo +++ b/docs/0.9.5/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 5c30377db0aba959976f60592b4f359e +config: 17b3a0eee77a9e8fa590d1b9c4113963 tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/0.9.5/Default-Commands.html b/docs/0.9.5/Default-Commands.html index 10ad68b385..31acacb72c 100644 --- a/docs/0.9.5/Default-Commands.html +++ b/docs/0.9.5/Default-Commands.html @@ -53,45 +53,45 @@ with Batch-Processor’s interactive mode.

+
+

A side note on html messages vrs text2html messages

+

So…lets say you have a desire to make your webclient output more like standard webpages… +For telnet clients, you could collect a bunch of text lines together, with ASCII formatted borders, etc. +Then send the results to be rendered client-side via the text2html plugin.

+

But for webclients, you could format a message directly with the html plugin to render the whole thing as an +HTML table, like so:

+
    # Server Side Python Code:
+
+    if target.is_webclient():
+        # This can be styled however you like using CSS, just add the CSS file to web/static/webclient/css/...
+        table = [
+                 "<table>",
+                  "<tr><td>1</td><td>2</td><td>3</td></tr>",
+                  "<tr><td>4</td><td>5</td><td>6</td></tr>",
+                 "</table>"
+               ]
+        target.msg( html=( "".join(table), {"type": "mytag"}) )
+    else:
+        # This will use the client to render this as "plain, simple" ASCII text, the same
+        #   as if it was rendered server-side via the Portal's text2html() functions
+        table = [ 
+                "#############",
+                "# 1 # 2 # 3 #",
+                "#############",
+                "# 4 # 5 # 6 #",
+                "#############"
+               ]
+        target.msg( html2html=( "\n".join(table), {"type": "mytag"}) )
+
+
+

Writing your own Plugins

So, you love the functionality of the webclient, but your game has specific @@ -184,7 +227,7 @@ window and the first input window are unique in that they can’t be “closed output and the one starting input window. This is done by modifying your server’s goldenlayout_default_config.js.

Start by creating a new -mygame/web/static_overrides/webclient/js/plugins/goldenlayout_default_config.js file, and adding +mygame/web/static/webclient/js/plugins/goldenlayout_default_config.js file, and adding the following JSON variable:

var goldenlayout_config = {
     content: [{
@@ -269,7 +312,7 @@ base.html.

Remember, plugins are load-order dependent, so make sure the new <script> tag comes before the goldenlayout.js

-

Next, create a new plugin file mygame/web/static_overrides/webclient/js/plugins/myplugin.js and +

Next, create a new plugin file mygame/web/static/webclient/js/plugins/myplugin.js and edit it.

let myplugin = (function () {
     //
@@ -331,6 +374,7 @@ window.plugin_handler.add("myplugin", myplugin);
 
  • Plugin Manager API (from webclient_gui.js)
  • Plugin callbacks API
  • Example/Default Plugins (plugins/*.js)
  • +
  • A side note on html messages vrs text2html messages
  • Writing your own Plugins diff --git a/docs/1.0-dev/Contribs/Contrib-Buffs.html b/docs/1.0-dev/Contribs/Contrib-Buffs.html index 57c0f7fc05..a465168198 100644 --- a/docs/1.0-dev/Contribs/Contrib-Buffs.html +++ b/docs/1.0-dev/Contribs/Contrib-Buffs.html @@ -126,6 +126,9 @@ values on the cache.

    buffs after application, they are very useful. The handler’s check/trigger methods utilize some of these getters, while others are just for developer convenience.

    get(key) is the most basic getter. It returns a single buff instance, or None if the buff doesn’t exist on the handler. It is also the only getter that returns a single buff instance, rather than a dictionary.

    +
    +

    Note: The handler method has(buff) allows you to check if a matching key (if a string) or buff class (if a class) is present on the handler cache, without actually instantiating the buff. You should use this method for basic “is this buff present?” checks.

    +

    Group getters, listed below, return a dictionary of values in the format {buffkey: instance}. If you want to iterate over all of these buffs, you should do so via the dict.values() method.

      @@ -183,6 +186,38 @@ buffs that are reactive to being checked; for example, removing themselves, alte

      Note: You can also trigger relevant buffs at the same time as you check them by ensuring the optional argument trigger is True in the check method.

      +

      Modifiers are calculated additively - that is, all modifiers of the same type are added together before being applied. They are then +applied through the following formula.

      +
      (base + total_add) / max(1, 1.0 + total_div) * max(0, 1.0 + total_mult)
      +
      +
      +
      +

      Multiplicative Buffs (Advanced)

      +

      Multiply/divide modifiers in this buff system are additive by default. This means that two +50% modifiers will equal a +100% modifier. But what if you want to apply mods multiplicatively?

      +

      First, you should carefully consider if you truly want multiplicative modifiers. Here’s some things to consider.

      +
        +
      • They are unintuitive to the average user, as two +50% damage buffs equal +125% instead of +100%.

      • +
      • They lead to “power explosion”, where stacking buffs in the right way can turn characters into unstoppable forces

      • +
      +

      Doing purely-additive multipliers allows you to better control the balance of your game. Conversely, doing multiplicative multipliers enables very fun build-crafting where smart usage of buffs and skills can turn you into a one-shot powerhouse. Each has its place.

      +

      The best design practice for multiplicative buffs is to divide your multipliers into “tiers”, where each tier is applied separately. You can easily do this with multiple check calls.

      +
      damage = damage
      +damage = handler.check(damage, 'damage')
      +damage = handler.check(damage, 'empower')
      +damage = handler.check(damage, 'radiant')
      +damage = handler.check(damage, 'overpower')
      +
      +
      +
      +
      +

      Buff Strength Priority (Advanced)

      +

      Sometimes you only want to apply the strongest modifier to a stat. This is supported by the optional strongest bool arg in the handler’s check method

      +
      def take_damage(self, source, damage):
      +    _damage = self.buffs.check(damage, 'taken_damage', strongest=True)
      +    self.db.health -= _damage
      +
      +
      +
  • Trigger Buffs

    @@ -255,6 +290,15 @@ and add a context to the mix.

    Apply the buff, take damage, and watch the thorns buff do its work!

    +
    +

    Viewing

    +

    There are two helper methods on the handler that allow you to get useful buff information back.

    + +

    You can also create your own custom viewing methods through the various handler getters, which will always return the entire buff object.

    +

    Creating New Buffs

    @@ -266,24 +310,46 @@ However, there are a lot of individual moving parts to a buff. Here’s a step-t -

    They also always store some useful mutable information about themselves in the cache:

    +

    Buffs also have a few useful properties:

    + +
    +

    Buff Cache (Advanced)

    +

    Buffs always store some useful mutable information about themselves in the cache (what is stored on the owning object’s database attribute). A buff’s cache corresponds to {buffkey: buffcache}, where buffcache is a dictionary containing at least the information below:

    • ref (class): The buff class path we use to construct the buff.

    • start (float): The timestamp of when the buff was applied.

    • source (Object): If specified; this allows you to track who or what applied the buff.

    • prevtick (float): The timestamp of the previous tick.

    • duration (float): The cached duration. This can vary from the class duration, depending on if the duration has been modified (paused, extended, shortened, etc).

    • +
    • tickrate (float): The buff’s tick rate. Cannot go below 0. Altering the tickrate on an applied buff will not cause it to start ticking if it wasn’t ticking before. (pause and unpause to start/stop ticking on existing buffs)

    • stacks (int): How many stacks they have.

    • paused (bool): Paused buffs do not clean up, modify values, tick, or fire any hook methods.

    -

    You can always access the raw cache dictionary through the cache attribute on an instanced buff. This is grabbed when you get the buff through -a handler method, so it may not always reflect recent changes you’ve made, depending on how you structure your buff calls. All of the above -mutable information can be found in this cache, as well as any arbitrary information you pass through the handler add method (via to_cache).

    +

    Sometimes you will want to dynamically update a buff’s cache at runtime, such as changing a tickrate in a hook method, or altering a buff’s duration. +You can do so by using the interface buff.cachekey. As long as the attribute name matches a key in the cache dictionary, it will update the stored +cache with the new value.

    +

    If there is no matching key, it will do nothing. If you wish to add a new key to the cache, you must use the buff.update_cache(dict) method, +which will properly update the cache (including adding new keys) using the dictionary provided.

    +
    +

    Example: You want to increase a buff’s duration by 30 seconds. You use buff.duration += 30. This new duration is now reflected on both the instance and the cache.

    +
    +

    The buff cache can also store arbitrary information. To do so, pass a dictionary through the handler add method (handler.add(BuffClass, to_cache=dict)), +set the cache dictionary attribute on your buff class, or use the aforementioned buff.update_cache(dict) method.

    +
    +

    Example: You store damage as a value in the buff cache and use it for your poison buff. You want to increase it over time, so you use buff.damage += 1 in the tick method.

    +
    +

    Modifiers

    @@ -292,9 +358,9 @@ mods of a specific stat string and apply their modifications to the value; howev

    Mod objects consist of only four values, assigned by the constructor in this order:

    The most basic way to add a Mod to a buff is to do so in the buff class definition, like this:

    class DamageBuff(BaseBuff):
    @@ -312,8 +378,7 @@ never permanently change a stat modified by a buff. To remove the modification,
     

    An advanced way to do mods is to generate them when the buff is initialized. This lets you create mods on the fly that are reactive to the game state.

    class GeneratedStatBuff(BaseBuff):
         ...
    -    def __init__(self, handler, buffkey, cache={}) -> None:
    -        super().__init__(handler, buffkey, cache)
    +    def at_init(self, *args, **kwargs) -> None:
             # Finds our "modgen" cache value, and generates a mod from it
             modgen = list(self.cache.get("modgen"))
             if modgen:
    @@ -367,7 +432,7 @@ example, if you want a buff that makes the player take more damage when they are
     
    class FireSick(BaseBuff):
         ...
         def conditional(self, *args, **kwargs):
    -        if self.owner.buffs.get_by_type(FireBuff): 
    +        if self.owner.buffs.has(FireBuff): 
                 return True
             return False
     
    @@ -382,6 +447,7 @@ conditionals are checked each tick.

  • remove/dispel: Allows you to remove or dispel the buff. Calls at_remove/at_dispel, depending on optional arguments.

  • pause/unpause: Pauses and unpauses the buff. Calls at_pause/at_unpause.

  • reset: Resets the buff’s start to the current time; same as “refreshing” it.

  • +
  • alter_cache: Updates the buff’s cache with the {key:value} pairs in the provided dictionary. Can overwrite default values, so be careful!

  • @@ -432,14 +498,22 @@ file will be overwritten, so edit that file rather than this one.

  • Apply a Buff
  • Get Buffs
  • Remove Buffs
  • -
  • Check Modifiers
  • +
  • Check Modifiers +
  • Trigger Buffs
  • Ticking
  • Context
  • +
  • Viewing
  • Creating New Buffs
      -
    • Basics
    • +
    • Basics +
    • Modifiers diff --git a/docs/1.0-dev/_modules/evennia/commands/default/general.html b/docs/1.0-dev/_modules/evennia/commands/default/general.html index 366ff8630e..e5795324c7 100644 --- a/docs/1.0-dev/_modules/evennia/commands/default/general.html +++ b/docs/1.0-dev/_modules/evennia/commands/default/general.html @@ -432,7 +432,7 @@ "{}|n".format(utils.crop(raw_ansi(item.db.desc or ""), width=50) or ""), ) string = f"|wYou are carrying:\n{table}" - self.caller.msg(string) + self.caller.msg(text=(string, {"type": "inventory"}))
      [docs]class CmdGet(COMMAND_DEFAULT_CLASS): diff --git a/docs/1.0-dev/_modules/evennia/contrib/rpg/buffs/buff.html b/docs/1.0-dev/_modules/evennia/contrib/rpg/buffs/buff.html index 50cf093dd0..d0bd8502cb 100644 --- a/docs/1.0-dev/_modules/evennia/contrib/rpg/buffs/buff.html +++ b/docs/1.0-dev/_modules/evennia/contrib/rpg/buffs/buff.html @@ -140,7 +140,6 @@ many attributes and hook methods you can overload to create complex, interrelated buffs. """ - from random import random import time from evennia import Command @@ -159,15 +158,14 @@ handler = None start = 0 - # Default buff duration; -1 or lower for permanent, 0 for "instant" (removed immediately) - duration = -1 + duration = -1 # Default buff duration; -1 for permanent, 0 for "instant", >0 normal playtime = False # Does this buff autopause when owning object is unpuppeted? refresh = True # Does the buff refresh its timer on application? unique = True # Does the buff overwrite existing buffs with the same key on the same target? maxstacks = 1 # The maximum number of stacks the buff can have. If >1, this buff will stack. - stacks = 1 # If >1, used as the default when applying this buff + stacks = 1 # Used as the default when applying this buff if no or negative stacks were specified (min: 1) tickrate = 0 # How frequent does this buff tick, in seconds (cannot be lower than 1) mods = [] # List of mod objects. See Mod class below for more detail @@ -176,7 +174,7 @@ @property def ticknum(self): """Returns how many ticks this buff has gone through as an integer.""" - x = (time.time() - self.start) / self.tickrate + x = (time.time() - self.start) / max(1, self.tickrate) return int(x) @property @@ -186,6 +184,16 @@ return None return self.handler.owner + @property + def timeleft(self): + """Returns how much time this buff has left. If -1, it is permanent.""" + _tl = 0 + if not self.start: + _tl = self.duration + else: + _tl = max(-1, self.duration - (time.time() - self.start)) + return _tl + @property def ticking(self) -> bool: """Returns if this buff ticks or not (tickrate => 1)""" @@ -202,17 +210,18 @@ handler: The handler this buff is attached to buffkey: The key this buff uses on the cache cache: The cache dictionary (what you get if you use `handler.buffcache.get(key)`)""" - self.handler: BuffHandler = handler - self.buffkey = buffkey - # Cache assignment - self.cache = cache - # Default system cache values - self.start = self.cache.get("start") - self.duration = self.cache.get("duration") - self.prevtick = self.cache.get("prevtick") - self.paused = self.cache.get("paused") - self.stacks = self.cache.get("stacks") - self.source = self.cache.get("source")
      + required = {"handler": handler, "buffkey": buffkey, "cache": cache} + self.__dict__.update(cache) + self.__dict__.update(required) + # Init hook + self.at_init() + + def __setattr__(self, attr, value): + if attr in self.cache: + if attr == "tickrate": + value = max(0, value) + self.handler.buffcache[self.buffkey][attr] = value + super().__setattr__(attr, value)
      [docs] def conditional(self, *args, **kwargs): """Hook function for conditional evaluation. @@ -264,11 +273,28 @@
      [docs] def reset(self): """Resets the buff start time as though it were just applied; functionally identical to a refresh""" + self.start = time.time() self.handler.buffcache[self.buffkey]["start"] = time.time()
      +
      [docs] def update_cache(self, to_cache: dict): + """Updates this buff's cache using the given values, both internally (this instance) and on the handler. + + Args: + to_cache: The dictionary of values you want to add to the cache""" + if not isinstance(to_cache, dict): + raise TypeError + _cache = dict(self.handler.buffcache[self.buffkey]) + _cache.update(to_cache) + self.cache = _cache + self.handler.buffcache[self.buffkey] = _cache
      + # endregion # region hook methods +
      [docs] def at_init(self, *args, **kwargs): + """Hook function called when this buff object is initialized.""" + pass
      +
      [docs] def at_apply(self, *args, **kwargs): """Hook function to run when this buff is applied to an object.""" pass
      @@ -353,6 +379,7 @@ self.dbkey = dbkey self.autopause = autopause if autopause: + self._validate_state() signals.SIGNAL_OBJECT_POST_UNPUPPET.connect(self._pause_playtime) signals.SIGNAL_OBJECT_POST_PUPPET.connect(self._unpause_playtime)
      @@ -467,10 +494,14 @@ context = {} b = {} _context = dict(context) + + # Initial cache updating, starting with the class cache attribute and/or to_cache if buff.cache: b = dict(buff.cache) if to_cache: b.update(dict(to_cache)) + + # Guarantees we stack either at least 1 stack or whatever the class stacks attribute is if stacks < 1: stacks = min(1, buff.stacks) @@ -480,6 +511,7 @@ "ref": buff, "start": time.time(), "duration": buff.duration, + "tickrate": buff.tickrate, "prevtick": time.time(), "paused": False, "stacks": stacks, @@ -791,7 +823,9 @@ return True return False -
      [docs] def check(self, value: float, stat: str, loud=True, context=None, trigger=False): +
      [docs] def check( + self, value: float, stat: str, loud=True, context=None, trigger=False, strongest=False + ): """Finds all buffs and perks related to a stat and applies their effects. Args: @@ -800,6 +834,7 @@ loud: (optional) Call the buff's at_post_check method after checking (default: True) context: (optional) A dictionary you wish to pass to the at_pre_check/at_post_check and conditional methods as kwargs trigger: (optional) Trigger buffs with the `stat` string as well. (default: False) + strongest: (optional) Applies only the strongest mods of the corresponding stat value (default: False) Returns the value modified by relevant buffs.""" # Buff cleanup to make sure all buffs are valid before processing @@ -811,15 +846,21 @@ applied = self.get_by_stat(stat) if not applied: return value + + # Run pre-check hooks on related buffs for buff in applied.values(): buff.at_pre_check(**context) + # Sift out buffs that won't be applying their mods (paused, conditional) applied = { k: buff for k, buff in applied.items() if buff.conditional(**context) if not buff.paused } - # The final result - final = self._calculate_mods(value, stat, applied) + # The mod totals + calc = self._calculate_mods(stat, applied) + + # The calculated final value + final = self._apply_mods(value, calc, strongest=strongest) # Run the "after check" functions on all relevant buffs for buff in applied.values(): @@ -882,23 +923,25 @@ start = buff["start"] # Start duration = buff["duration"] # Duration prevtick = buff["prevtick"] # Previous tick timestamp - tickrate = buff["ref"].tickrate # Buff's tick rate - - # Original buff ending, and new duration + tickrate = buff["tickrate"] # Buff's tick rate end = start + duration # End - newduration = end - current # New duration - # Apply the new duration - if newduration > 0: - buff["duration"] = newduration - if buff["ref"].ticking: - buff["tickleft"] = max(1, tickrate - (current - prevtick)) - self.buffcache[key] = buff - instance: BaseBuff = buff["ref"](self, key, buff) - instance.at_pause(**context) - else: - self.remove(key) - return
      + # Setting "tickleft" + if buff["ref"].ticking: + buff["tickleft"] = max(1, tickrate - (current - prevtick)) + + # Setting the new duration (if applicable) + if duration > -1: + newduration = end - current # New duration + if newduration > 0: + buff["duration"] = newduration + else: + self.remove(key) + + # Apply new cache info, call pause hook + self.buffcache[key] = buff + instance: BaseBuff = buff["ref"](self, key, buff) + instance.at_pause(**context)
      [docs] def unpause(self, key: str, context=None): """Unpauses a buff. This makes it visible to the various buff systems again. @@ -925,33 +968,60 @@ buff["start"] = current if buff["ref"].ticking: buff["prevtick"] = current - (tickrate - tickleft) + + # Apply new cache info, call hook self.buffcache[key] = buff instance: BaseBuff = buff["ref"](self, key, buff) instance.at_unpause(**context) - utils.delay(buff["duration"], cleanup_buffs, self, persistent=True) + + # Set up typical delays (cleanup/ticking) + if instance.duration > -1: + utils.delay(buff["duration"], cleanup_buffs, self, persistent=True) if instance.ticking: utils.delay( tickrate, tick_buff, handler=self, buffkey=key, initial=False, persistent=True - ) - return
      + ) -
      [docs] def set_duration(self, key, value): - """Sets the duration of the specified buff. +
      [docs] def view(self, to_filter=None) -> dict: + """Returns a buff flavor text as a dictionary of tuples in the format {key: (name, flavor)}. Common use for this is a buff readout of some kind. Args: - key: The key of the buff whose duration you want to set - value: The value you want the new duration to be""" - if key in self.buffcache.keys(): - self.buffcache[key]["duration"] = value - return
      - -
      [docs] def view(self) -> dict: - """Returns a buff flavor text as a dictionary of tuples in the format {key: (name, flavor)}. Common use for this is a buff readout of some kind.""" + to_filter: (optional) The dictionary of buffs to iterate over. If none is provided, returns all buffs (default: None)""" + if not isinstance(to_filter, dict): + raise TypeError self.cleanup() - _cache = self.visible + _cache = self.visible if not to_filter else to_filter _flavor = {k: (buff.name, buff.flavor) for k, buff in _cache.items()} return _flavor
      +
      [docs] def view_modifiers(self, stat: str, context=None): + """Checks all modifiers of the specified stat without actually applying them. Hits the conditional hook for relevant buffs. + + Args: + stat: The mod identifier string to search for + context: (optional) A dictionary you wish to pass to the conditional hooks as kwargs + + Returns a nested dictionary. The first layer's keys represent the type of modifier ('add' and 'mult'), + and the second layer's keys represent the type of value ('total' and 'strongest').""" + # Buff cleanup to make sure all buffs are valid before processing + self.cleanup() + + # Find all buffs and traits related to the specified stat. + if not context: + context = {} + applied = self.get_by_stat(stat) + if not applied: + return None + + # Sift out buffs that won't be applying their mods (paused, conditional) + applied = { + k: buff for k, buff in applied.items() if buff.conditional(**context) if not buff.paused + } + + # Calculate and return our values dictionary + calc = self._calculate_mods(stat, applied) + return calc
      +
      [docs] def cleanup(self): """Removes expired buffs, ensures pause state is respected.""" self._validate_state() @@ -988,29 +1058,58 @@ buff.unpause() pass - def _calculate_mods(self, value, stat: str, buffs: dict): - """Calculates a return value from a base value, a stat string, and a dictionary of instanced buffs with associated mods. + def _calculate_mods(self, stat: str, buffs: dict): + """Calculates the total value of applicable mods. Args: - value: The base value to modify stat: The string identifier to search mods for - buffs: The dictionary of buffs to apply""" + buffs: The dictionary of buffs to calculate mods from + + Returns a nested dictionary. The first layer's keys represent the type of modifier ('add' and 'mult'), + and the second layer's keys represent the type of value ('total' and 'strongest').""" + + # The base return dictionary. If you update how modifiers are calculated, make sure to update this too, or you will get key errors! + calculated = { + "add": {"total": 0, "strongest": 0}, + "mult": {"total": 0, "strongest": 0}, + "div": {"total": 0, "strongest": 0}, + } if not buffs: - return value - add = 0 - mult = 0 + return calculated for buff in buffs.values(): for mod in buff.mods: buff: BaseBuff mod: Mod if mod.stat == stat: - if mod.modifier == "add": - add += mod.value + ((buff.stacks) * mod.perstack) - if mod.modifier == "mult": - mult += mod.value + ((buff.stacks) * mod.perstack) + _modval = mod.value + ((buff.stacks) * mod.perstack) + calculated[mod.modifier]["total"] += _modval + if _modval > calculated[mod.modifier]["strongest"]: + calculated[mod.modifier]["strongest"] = _modval + return calculated - final = (value + add) * max(0, 1.0 + mult) + def _apply_mods(self, value, calc: dict, strongest=False): + """Applies modifiers to a value. + + Args: + value: The value to modify + calc: The dictionary of calculated modifier values (see _calculate_mods) + strongest: (optional) Applies only the strongest mods of the corresponding stat value (default: False) + + Returns value modified by the relevant mods.""" + final = value + if strongest: + final = ( + (value + calc["add"]["strongest"]) + / max(1, 1.0 + calc["div"]["strongest"]) + * max(0, 1.0 + calc["mult"]["strongest"]) + ) + else: + final = ( + (value + calc["add"]["total"]) + / max(1, 1.0 + calc["div"]["total"]) + * max(0, 1.0 + calc["mult"]["total"]) + ) return final def _remove_via_dict(self, buffs: dict, loud=True, dispel=False, expire=False, context=None): @@ -1107,10 +1206,10 @@ # Instantiate the buff and tickrate buff: BaseBuff = handler.get(buffkey) - tr = buff.tickrate + tr = max(1, buff.tickrate) # This stops the old ticking process if you refresh/stack the buff - if tr > time.time() - buff.prevtick and initial != True: + if (tr > time.time() - buff.prevtick and initial != True) or buff.paused: return # Only fire the at_tick methods if the conditional is truthy @@ -1120,7 +1219,7 @@ buff.at_tick(initial, **context) # Tick this buff one last time, then remove - if buff.duration <= time.time() - buff.start: + if buff.duration > -1 and buff.duration <= time.time() - buff.start: if tr < time.time() - buff.prevtick: buff.at_tick(initial, **context) buff.remove(expire=True) @@ -1131,6 +1230,7 @@ buff.at_tick(initial, **context) handler.buffcache[buffkey]["prevtick"] = time.time() + tr = max(1, buff.tickrate) # Recur this function at the tickrate interval, if it didn't stop/fail utils.delay( diff --git a/docs/1.0-dev/_modules/evennia/contrib/rpg/buffs/samplebuffs.html b/docs/1.0-dev/_modules/evennia/contrib/rpg/buffs/samplebuffs.html index ef827f68a9..65940a82ee 100644 --- a/docs/1.0-dev/_modules/evennia/contrib/rpg/buffs/samplebuffs.html +++ b/docs/1.0-dev/_modules/evennia/contrib/rpg/buffs/samplebuffs.html @@ -126,13 +126,13 @@
      [docs] def at_pause(self, *args, **kwargs): self.owner.db.prelogout_location.msg_contents( - "{actor} stops twitching, their flesh a deathly pallor.".format(actor=self.owner.named) + "{actor} stops twitching, their flesh a deathly pallor.".format(actor=self.owner) )
      [docs] def at_unpause(self, *args, **kwargs): self.owner.location.msg_contents( "{actor} begins to twitch again, their cheeks flushing red with blood.".format( - actor=self.owner.named + actor=self.owner ) )
      @@ -141,7 +141,7 @@ if not initial: self.owner.location.msg_contents( "Poison courses through {actor}'s body, dealing {damage} damage.".format( - actor=self.owner.named, damage=_dmg + actor=self.owner, damage=_dmg ) )
      diff --git a/docs/1.0-dev/_modules/evennia/contrib/rpg/buffs/tests.html b/docs/1.0-dev/_modules/evennia/contrib/rpg/buffs/tests.html index c986027a63..dc9b40521e 100644 --- a/docs/1.0-dev/_modules/evennia/contrib/rpg/buffs/tests.html +++ b/docs/1.0-dev/_modules/evennia/contrib/rpg/buffs/tests.html @@ -55,9 +55,17 @@ class _EmptyBuff(BaseBuff): + key = "empty" pass +class _TestDivBuff(BaseBuff): + key = "tdb" + name = "tdb" + flavor = "divverbuff" + mods = [Mod("stat1", "div", 1)] + + class _TestModBuff(BaseBuff): key = "tmb" name = "tmb" @@ -232,12 +240,19 @@
      [docs] @patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock()) def test_details(self): - """tests that buff details like name and flavor are correct""" + """tests that buff details like name and flavor are correct; also test modifier viewing""" handler: BuffHandler = self.testobj.buffs handler.add(_TestModBuff) handler.add(_TestTrigBuff) self.assertEqual(handler.get("tmb").flavor, "modderbuff") - self.assertEqual(handler.get("ttb").name, "ttb")
      + self.assertEqual(handler.get("ttb").name, "ttb") + mods = handler.view_modifiers("stat1") + _testmods = { + "add": {"total": 15, "strongest": 15}, + "mult": {"total": 0, "strongest": 0}, + "div": {"total": 0, "strongest": 0}, + } + self.assertDictEqual(mods, _testmods)
      [docs] @patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock()) def test_modify(self): @@ -270,9 +285,15 @@ handler.add(_TestModBuff2) self.assertEqual(handler.check(_stat1, "stat1"), 100) self.assertEqual(handler.check(_stat2, "stat2"), 30) + # apply only the strongest value + self.assertEqual(handler.check(_stat1, "stat1", strongest=True), 80) + # removing mod properly reduces value, doesn't affect other mods handler.remove_by_type(_TestModBuff) self.assertEqual(handler.check(_stat1, "stat1"), 30) - self.assertEqual(handler.check(_stat2, "stat2"), 20)
      + self.assertEqual(handler.check(_stat2, "stat2"), 20) + # divider mod test + handler.add(_TestDivBuff) + self.assertEqual(handler.check(_stat1, "stat1"), 15)
      [docs] @patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock()) def test_trigger(self): @@ -389,11 +410,22 @@ _instance.at_tick() self.assertTrue(self.testobj.db.ticktest) # test duration modification and cleanup - handler.set_duration("ttib", 0) + _instance.duration = 0 self.assertEqual(handler.get("ttib").duration, 0) handler.cleanup() self.assertFalse(handler.get("ttib"), None)
      + +
      [docs] @patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock()) def test_buffableproperty(self): """tests buffable properties""" diff --git a/docs/1.0-dev/_modules/evennia/objects/objects.html b/docs/1.0-dev/_modules/evennia/objects/objects.html index 5fc17d5ea1..693fe4cb48 100644 --- a/docs/1.0-dev/_modules/evennia/objects/objects.html +++ b/docs/1.0-dev/_modules/evennia/objects/objects.html @@ -1678,7 +1678,7 @@ now in. Args: - source_location (Object): Wwhere we came from. This may be `None`. + source_location (Object): Where we came from. This may be `None`. move_type (str): The type of move. "give", "traverse", etc. This is an arbitrary string provided to obj.move_to(). Useful for altering messages or altering logic depending diff --git a/docs/1.0-dev/_modules/evennia/typeclasses/attributes.html b/docs/1.0-dev/_modules/evennia/typeclasses/attributes.html index 832da0aed4..68dd592439 100644 --- a/docs/1.0-dev/_modules/evennia/typeclasses/attributes.html +++ b/docs/1.0-dev/_modules/evennia/typeclasses/attributes.html @@ -244,6 +244,21 @@ self._lockstring = lockstring self._autocreate = autocreate self._key = ""
      + + @property + def _default(self): + """ + Tries returning a new instance of default if callable. + + """ + if callable(self.__default): + return self.__default() + + return self.__default + + @_default.setter + def _default(self, value): + self.__default = value def __set_name__(self, cls, name): """ diff --git a/docs/1.0-dev/_sources/Components/Webclient.md.txt b/docs/1.0-dev/_sources/Components/Webclient.md.txt index da2e3a5506..ea24a81972 100644 --- a/docs/1.0-dev/_sources/Components/Webclient.md.txt +++ b/docs/1.0-dev/_sources/Components/Webclient.md.txt @@ -2,7 +2,7 @@ Evennia comes with a MUD client accessible from a normal web browser. During development you can try it at `http://localhost:4001/webclient`. The client consists of several parts, all under -`evennia/web/webclient/`: +`evennia/web`: `templates/webclient/webclient.html` and `templates/webclient/base.html` are the very simplistic django html templates describing the webclient layout. @@ -18,7 +18,7 @@ be used also if swapping out the gui front end. various plugins, and uses the Evennia object library for all in/out. `static/webclient/js/plugins` provides a default set of plugins that implement a "telnet-like" -interface. +interface, and a couple of example plugins to show how you could implement new plugin features. `static/webclient/css/webclient.css` is the CSS file for the client; it also defines things like how to display ANSI/Xterm256 colors etc. @@ -30,17 +30,17 @@ these. ## Customizing the web client Like was the case for the website, you override the webclient from your game directory. You need to -add/modify a file in the matching directory location within one of the _overrides directories. -These _override directories are NOT directly used by the web server when the game is running, the -server copies everything web related in the Evennia folder over to `mygame/web/static/` and then -copies in all of your _overrides. This can cause some cases were you edit a file, but it doesn't +add/modify a file in the matching directory locations within your project's `mygame/web/` directories. +These directories are NOT directly used by the web server when the game is running, the +server copies everything web related in the Evennia folder over to `mygame/server/.static/` and then +copies in all of your `mygame/web/` files. This can cause some cases were you edit a file, but it doesn't seem to make any difference in the servers behavior. **Before doing anything else, try shutting down the game and running `evennia collectstatic` from the command line then start it back up, clear your browser cache, and see if your edit shows up.** -Example: To change the utilized plugin list, you need to override base.html by copying -`evennia/web/webclient/templates/webclient/base.html` to -`mygame/web/template_overrides/webclient/base.html` and editing it to add your new plugin. +Example: To change the list of in-use plugins, you need to override base.html by copying +`evennia/web/templates/webclient/base.html` to +`mygame/web/templates/webclient/base.html` and editing it to add your new plugin. # Evennia Web Client API (from evennia.js) * `Evennia.init( opts )` @@ -96,6 +96,8 @@ manager for drag-n-drop windows, text routing and more. keys to peruse. * `hotbuttons.js` Defines onGotOptions. A Disabled-by-default plugin that defines a button bar with user-assignable commands. +* `html.js` A basic plugin to allow the client to handle "raw html" messages from the server, this +allows the server to send native HTML messages like >div style='s'<styled text>/div< * `iframe.js` Defines onOptionsUI. A goldenlayout-only plugin to create a restricted browsing sub- window for a side-by-side web/text interface, mostly an example of how to build new HTML "components" for goldenlayout. @@ -108,8 +110,50 @@ from the server and display them as inline HTML. while the tab is hidden. * `oob.js` Defines onSend. Allows the user to test/send Out Of Band json messages to the server. * `options.js` Defines most callbacks. Provides a popup-based UI to coordinate options settings with the server. -* `options2.js` Defines most callbacks. Provides a goldenlayout-based version of the options/settings tab. Integrates with other plugins via the custom onOptionsUI callback. +* `options2.js` Defines most callbacks. Provides a goldenlayout-based version of the options/settings tab. +Integrates with other plugins via the custom onOptionsUI callback. * `popups.js` Provides default popups/Dialog UI for other plugins to use. +* `text2html.js` Provides a new message handler type: `text2html`, similar to the multimedia and html +plugins. This plugin provides a way to offload rendering the regular pipe-styled ASCII messages +to the client. This allows the server to do less work, while also allowing the client a place to +customize this conversion process. To use this plugin you will need to override the current commands +in Evennia, changing any place where a raw text output message is generated and turn it into a +`text2html` message. For example: `target.msg("my text")` becomes: `target.msg(text2html=("my text"))` +(even better, use a webclient pane routing tag: `target.msg(text2html=("my text", {"type": "sometag"}))`) +`text2html` messages should format and behave identically to the server-side generated text2html() output. + +# A side note on html messages vrs text2html messages + +So...lets say you have a desire to make your webclient output more like standard webpages... +For telnet clients, you could collect a bunch of text lines together, with ASCII formatted borders, etc. +Then send the results to be rendered client-side via the text2html plugin. + +But for webclients, you could format a message directly with the html plugin to render the whole thing as an +HTML table, like so: +``` + # Server Side Python Code: + + if target.is_webclient(): + # This can be styled however you like using CSS, just add the CSS file to web/static/webclient/css/... + table = [ + "", + "", + "", + "
      123
      456
      " + ] + target.msg( html=( "".join(table), {"type": "mytag"}) ) + else: + # This will use the client to render this as "plain, simple" ASCII text, the same + # as if it was rendered server-side via the Portal's text2html() functions + table = [ + "#############", + "# 1 # 2 # 3 #", + "#############", + "# 4 # 5 # 6 #", + "#############" + ] + target.msg( html2html=( "\n".join(table), {"type": "mytag"}) ) +``` # Writing your own Plugins @@ -131,7 +175,7 @@ output and the one starting input window. This is done by modifying your server goldenlayout_default_config.js. Start by creating a new -`mygame/web/static_overrides/webclient/js/plugins/goldenlayout_default_config.js` file, and adding +`mygame/web/static/webclient/js/plugins/goldenlayout_default_config.js` file, and adding the following JSON variable: ``` @@ -222,7 +266,7 @@ type="text/javascript"> Remember, plugins are load-order dependent, so make sure the new `