mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Merge branch 'develop' into contrib/evadventure
This commit is contained in:
commit
9c45feaf10
9 changed files with 552 additions and 25 deletions
|
|
@ -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 = [
|
||||
"<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
|
||||
|
||||
|
|
@ -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"></script>
|
|||
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.
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@ buffs after application, they are very useful. The handler's `check`/`trigger` m
|
|||
`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.
|
||||
|
||||
|
|
@ -142,6 +144,44 @@ 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.
|
||||
|
||||
```python
|
||||
(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.
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
def take_damage(self, source, damage):
|
||||
_damage = self.buffs.check(damage, 'taken_damage', strongest=True)
|
||||
self.db.health -= _damage
|
||||
```
|
||||
|
||||
### Trigger Buffs
|
||||
|
||||
Call the handler's `trigger(string)` method when you want an event call. This will call the `at_trigger` hook method on all buffs with the relevant trigger `string`.
|
||||
|
|
@ -219,6 +259,15 @@ class ThornsBuff(BaseBuff):
|
|||
```
|
||||
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.
|
||||
|
||||
- `view`: Returns a dictionary of tuples in the format `{buffkey: (buff.name, buff.flavor)}`. Finds all buffs by default, but optionally accepts a dictionary of buffs to filter as well. Useful for basic buff readouts.
|
||||
- `view_modifiers(stat)`: Returns a nested dictionary of information on modifiers that affect the specified stat. The first layer is the modifier type (`add/mult/div`) and the second layer is the value type (`total/strongest`). Does not return the buffs that cause these modifiers, just the modifiers themselves (akin to using `handler.check` but without actually modifying a value). Useful for stat sheets.
|
||||
|
||||
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
|
||||
|
||||
Creating a new buff is very easy: extend `BaseBuff` into a new class, and fill in all the relevant buff details.
|
||||
|
|
@ -230,24 +279,45 @@ Regardless of any other functionality, all buffs have the following class attrib
|
|||
|
||||
- They have customizable `key`, `name`, and `flavor` strings.
|
||||
- They have a `duration` (float), and automatically clean-up at the end. Use -1 for infinite duration, and 0 to clean-up immediately. (default: -1)
|
||||
- They have a `tickrate` (float), and automatically tick if it is greater than 1 (default: 0)
|
||||
- They can stack, if `maxstacks` (int) is not equal to 1. If it's 0, the buff stacks forever. (default: 1)
|
||||
- They can be `unique` (bool), which determines if they have a unique namespace or not. (default: True)
|
||||
- They can `refresh` (bool), which resets the duration when stacked or reapplied. (default: True)
|
||||
- They can be `playtime` (bool) buffs, where duration only counts down during active play. (default: False)
|
||||
|
||||
They also always store some useful mutable information about themselves in the cache:
|
||||
Buffs also have a few useful properties:
|
||||
|
||||
- `owner`: The object this buff is attached to
|
||||
- `ticknum`: How many ticks the buff has gone through
|
||||
- `timeleft`: How much time is remaining on the buff
|
||||
- `ticking`/`stacking`: If this buff ticks/stacks (checks `tickrate` and `maxstacks`)
|
||||
|
||||
#### 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
|
||||
|
||||
|
|
@ -257,9 +327,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:
|
||||
|
||||
- `stat`: The stat you want to modify. When `check` is called, this string is used to find all the mods that are to be collected.
|
||||
- `mod`: The modifier. Defaults are 'add' and 'mult'. Modifiers are calculated additively, and in standard arithmetic order (see `_calculate_mods` for more)
|
||||
- `mod`: The modifier. Defaults are `add` (addition/subtraction), `mult` (multiply), and `div` (divide). Modifiers are calculated additively (see `_calculate_mods` for more)
|
||||
- `value`: How much value the modifier gives regardless of stacks
|
||||
- `perstack`: How much value the modifier grants per stack, INCLUDING the first. (default: 0)
|
||||
- `perstack`: How much value the modifier grants per stack, **INCLUDING** the first. (default: 0)
|
||||
|
||||
The most basic way to add a Mod to a buff is to do so in the buff class definition, like this:
|
||||
|
||||
|
|
@ -281,8 +351,7 @@ An advanced way to do mods is to generate them when the buff is initialized. Thi
|
|||
```python
|
||||
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:
|
||||
|
|
@ -339,7 +408,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
|
||||
```
|
||||
|
|
@ -354,6 +423,7 @@ Buff instances have a number of helper methods.
|
|||
- `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!
|
||||
|
||||
#### Playtime Duration
|
||||
|
||||
|
|
@ -364,7 +434,6 @@ although if you have less than 1 second of tick duration remaining, it will roun
|
|||
> **Note**: If you want more control over this process, you can comment out the signal subscriptions on the handler and move the autopause logic
|
||||
> to your object's `at_pre/post_puppet/unpuppet` hooks.
|
||||
|
||||
|
||||
----
|
||||
|
||||
<small>This document page is generated from `evennia/contrib/rpg/buffs/README.md`. Changes to this
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class CharacterCmdset(default_cmds.Character_CmdSet):
|
|||
|
||||
```
|
||||
|
||||
Then reload to make the bew commands available. Note that they only work
|
||||
Then reload to make the new commands available. Note that they only work
|
||||
on rooms with the typeclass `ExtendedRoom`. Create new rooms with the right
|
||||
typeclass or use the `typeclass` command to swap existing rooms.
|
||||
|
||||
|
|
|
|||
|
|
@ -391,7 +391,7 @@ class CmdInventory(COMMAND_DEFAULT_CLASS):
|
|||
"{}|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"}))
|
||||
|
||||
|
||||
class CmdGet(COMMAND_DEFAULT_CLASS):
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class CharacterCmdset(default_cmds.Character_CmdSet):
|
|||
|
||||
```
|
||||
|
||||
Then reload to make the bew commands available. Note that they only work
|
||||
Then reload to make the new commands available. Note that they only work
|
||||
on rooms with the typeclass `ExtendedRoom`. Create new rooms with the right
|
||||
typeclass or use the `typeclass` command to swap existing rooms.
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ __pycache__
|
|||
# Other
|
||||
*.swp
|
||||
*.log
|
||||
*.log.*
|
||||
*.pid
|
||||
*.restart
|
||||
*.db3
|
||||
|
|
|
|||
63
evennia/web/static/webclient/js/plugins/html.js
Normal file
63
evennia/web/static/webclient/js/plugins/html.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Evennia server-side-generated "raw" HTML message handler plugin
|
||||
*
|
||||
* PLUGIN ORDER PREREQS:
|
||||
* loaded after:
|
||||
* webclient_gui.js
|
||||
* option2.js
|
||||
* loaded before:
|
||||
*
|
||||
*
|
||||
* To use, at minimum, in evennia python code:
|
||||
* target.msg( html="<div><span>...etc...</span></div>" )
|
||||
*
|
||||
* or, if you prefer tagged routing (RECOMMENDED):
|
||||
* target.msg( html=("<div><span>...etc...</span></div>",{'type':'tag'}) )
|
||||
*
|
||||
*/
|
||||
let html_plugin = (function () {
|
||||
//
|
||||
var html = function (args, kwargs) {
|
||||
let options = window.options;
|
||||
if( !("html" in options) || options["html"] === false ) { return; }
|
||||
|
||||
var mwins = window.plugins["goldenlayout"].routeMessage(args, kwargs);
|
||||
mwins.forEach( function (mwin) {
|
||||
mwin.append(args[0]);
|
||||
mwin.scrollTop(mwin[0].scrollHeight);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
var onOptionsUI = function (parentdiv) {
|
||||
let options = window.options;
|
||||
var checked;
|
||||
|
||||
checked = options["html"] ? "checked='checked'" : "";
|
||||
var mmHtml = $( [ "<label>",
|
||||
"<input type='checkbox' data-setting='html' " + checked + "'>",
|
||||
" Prefer server-side generated direct-HTML messages over old-school ASCII text",
|
||||
"</label>"
|
||||
].join("") );
|
||||
mmHtml.on("change", window.plugins["options2"].onOptionCheckboxChanged);
|
||||
|
||||
parentdiv.append(mmHtml);
|
||||
}
|
||||
|
||||
//
|
||||
// Mandatory plugin init function
|
||||
var init = function () {
|
||||
let options = window.options;
|
||||
options["html"] = true;
|
||||
|
||||
let Evennia = window.Evennia;
|
||||
Evennia.emitter.on("html", html); // capture "image" commands
|
||||
console.log('HTML plugin initialized');
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
onOptionsUI: onOptionsUI,
|
||||
}
|
||||
})();
|
||||
plugin_handler.add("html_plugin", html_plugin);
|
||||
348
evennia/web/static/webclient/js/plugins/text2html.js
Normal file
348
evennia/web/static/webclient/js/plugins/text2html.js
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Evennia Webclient text2html component
|
||||
*
|
||||
* This is used in conjunction with the main evennia.js library, which
|
||||
* handles all the communication with the Server.
|
||||
*
|
||||
*/
|
||||
|
||||
//
|
||||
// Global Parser for text2html
|
||||
//
|
||||
var text2html_plugin = (function () {
|
||||
"use strict"
|
||||
|
||||
let asciiESC = String.fromCharCode(27); // string literal for ASCII ESC bytes
|
||||
|
||||
let foreground = "color-102"; // state tracker for foreground css
|
||||
let background = ""; // state tracker for background css
|
||||
let underline = ""; // state tracker for underlines css
|
||||
|
||||
let pipecodes = /^(\|\[?[0-5][0-5][0-5])|(\|\[[0-9][0-9]?m)|(\|\[?=[a-z])|(\|[![]?[unrgybmcwxhRGYBMCWXH_/>*^-])/;
|
||||
// example ^|000 or ^|[22m or ^|=a or ^|_
|
||||
|
||||
let csslookup = {
|
||||
"|n": "normal",
|
||||
"|r": "color-009",
|
||||
"|g": "color-010",
|
||||
"|y": "color-011",
|
||||
"|b": "color-012",
|
||||
"|m": "color-013",
|
||||
"|c": "color-014",
|
||||
"|w": "color-015",
|
||||
"|x": "color-008",
|
||||
"|R": "color-001",
|
||||
"|G": "color-002",
|
||||
"|Y": "color-003",
|
||||
"|B": "color-004",
|
||||
"|M": "color-005",
|
||||
"|C": "color-006",
|
||||
"|W": "color-007",
|
||||
"|X": "color-000",
|
||||
"|[r": "bgcolor-196",
|
||||
"|[g": "bgcolor-046",
|
||||
"|[y": "bgcolor-226",
|
||||
"|[b": "bgcolor-021",
|
||||
"|[m": "bgcolor-201",
|
||||
"|[c": "bgcolor-051",
|
||||
"|[w": "bgcolor-231",
|
||||
"|[x": "bgcolor-102",
|
||||
"|[R": "bgcolor-001",
|
||||
"|[G": "bgcolor-002",
|
||||
"|[Y": "bgcolor-003",
|
||||
"|[B": "bgcolor-004",
|
||||
"|[M": "bgcolor-005",
|
||||
"|[C": "bgcolor-006",
|
||||
"|[W": "bgcolor-007",
|
||||
"|[X": "bgcolor-000",
|
||||
"|=a": "color-016",
|
||||
"|=b": "color-232",
|
||||
"|=c": "color-233",
|
||||
"|=d": "color-234",
|
||||
"|=e": "color-235",
|
||||
"|=f": "color-236",
|
||||
"|=g": "color-237",
|
||||
"|=h": "color-238",
|
||||
"|=i": "color-239",
|
||||
"|=j": "color-240",
|
||||
"|=k": "color-241",
|
||||
"|=l": "color-242",
|
||||
"|=m": "color-243",
|
||||
"|=n": "color-244",
|
||||
"|=o": "color-245",
|
||||
"|=p": "color-246",
|
||||
"|=q": "color-247",
|
||||
"|=r": "color-248",
|
||||
"|=s": "color-249",
|
||||
"|=t": "color-250",
|
||||
"|=u": "color-251",
|
||||
"|=v": "color-252",
|
||||
"|=w": "color-253",
|
||||
"|=x": "color-254",
|
||||
"|=y": "color-255",
|
||||
"|=z": "color-231",
|
||||
"|[=a": "bgcolor-016",
|
||||
"|[=b": "bgcolor-232",
|
||||
"|[=c": "bgcolor-233",
|
||||
"|[=d": "bgcolor-234",
|
||||
"|[=e": "bgcolor-235",
|
||||
"|[=f": "bgcolor-236",
|
||||
"|[=g": "bgcolor-237",
|
||||
"|[=h": "bgcolor-238",
|
||||
"|[=i": "bgcolor-239",
|
||||
"|[=j": "bgcolor-240",
|
||||
"|[=k": "bgcolor-241",
|
||||
"|[=l": "bgcolor-242",
|
||||
"|[=m": "bgcolor-243",
|
||||
"|[=n": "bgcolor-244",
|
||||
"|[=o": "bgcolor-245",
|
||||
"|[=p": "bgcolor-246",
|
||||
"|[=q": "bgcolor-247",
|
||||
"|[=r": "bgcolor-248",
|
||||
"|[=s": "bgcolor-249",
|
||||
"|[=t": "bgcolor-250",
|
||||
"|[=u": "bgcolor-251",
|
||||
"|[=v": "bgcolor-252",
|
||||
"|[=w": "bgcolor-253",
|
||||
"|[=x": "bgcolor-254",
|
||||
"|[=y": "bgcolor-255",
|
||||
"|[=z": "bgcolor-231",
|
||||
// not sure what these nexts ones are actually supposed to map to
|
||||
"|[0m": "normal",
|
||||
"|[1m": "normal",
|
||||
"|[22m": "normal",
|
||||
"|[36m": "color-006",
|
||||
"|[37m": "color-015",
|
||||
}
|
||||
|
||||
function ascii (l) {
|
||||
let a = new String(l); // force string
|
||||
return a.charCodeAt(0);
|
||||
}
|
||||
|
||||
// dumb convert any leading or trailing spaces/tabs to sequences
|
||||
var handleSpaces = function (text) {
|
||||
// TODO should probably get smart about replacing spaces inside "normal" text
|
||||
return text.replace( /\t/g, " ").replace( / /g, " ");
|
||||
}
|
||||
|
||||
|
||||
// javascript doesn't have a native sprintf-like function
|
||||
function zfill(string, size) {
|
||||
while (string.length < size) string = "0" + string;
|
||||
return string;
|
||||
}
|
||||
|
||||
|
||||
// given string starting with a pipecode |xx
|
||||
// return the css (if any) and text remainder
|
||||
var pipe2css = function(pipecode) {
|
||||
let regx = "";
|
||||
let css = "color-102";
|
||||
|
||||
regx = /^(\|\[?[nrgybmcwxhRGYBMCWX])/;
|
||||
css = pipecode.match( regx );
|
||||
if( css != null ) {
|
||||
return csslookup[ css[1] ];
|
||||
}
|
||||
|
||||
regx = /^(\|\[?=[a-z])/;
|
||||
css = pipecode.match( regx );
|
||||
if( css != null ) {
|
||||
return csslookup[ css[1] ];
|
||||
}
|
||||
|
||||
regx = /^(\|\[[0-9][0-9]?m)/;
|
||||
css = pipecode.match( regx );
|
||||
if( css != null ) {
|
||||
return csslookup[ css[1] ];
|
||||
}
|
||||
|
||||
regx = /^(\|n)/;
|
||||
css = pipecode.match( regx );
|
||||
if( css != null ) {
|
||||
return "normal";
|
||||
}
|
||||
|
||||
regx = /^(\|u)/;
|
||||
css = pipecode.match( regx );
|
||||
if( css != null ) {
|
||||
return "underline";
|
||||
}
|
||||
|
||||
regx = /^\|([0-5][0-5][0-5])/;
|
||||
css = pipecode.match( regx );
|
||||
if( css != null ) {
|
||||
return "color-" + zfill( (parseInt(css[1], 6) + 16).toString(), 3);
|
||||
}
|
||||
|
||||
regx = /^\|\[([0-5][0-5][0-5])/;
|
||||
css = pipecode.match( regx );
|
||||
if( css != null ) {
|
||||
return "bgcolor-" + zfill( (parseInt(css[1], 6) + 16).toString(), 3);
|
||||
}
|
||||
|
||||
return css;
|
||||
}
|
||||
|
||||
|
||||
// convert any HTML sensitive characters to &code; format
|
||||
var htmlEscape = function (text) {
|
||||
text = text.replace(/&/g, "&");
|
||||
text = text.replace(/</g, "<");
|
||||
text = text.replace(/>/g, ">");
|
||||
text = text.replace(/"/g, """);
|
||||
text = text.replace(/'/g, "'");
|
||||
text = handleSpaces(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
// track stateful CSS
|
||||
var trackCSS = function (css) {
|
||||
if( (typeof css !== 'string') && ! (css instanceof String) ) {
|
||||
css = "";
|
||||
}
|
||||
|
||||
if( css.startsWith( "color-" ) ) {
|
||||
foreground = css;
|
||||
}
|
||||
|
||||
if( css.startsWith( "bgcolor-" ) ) {
|
||||
background = css;
|
||||
}
|
||||
|
||||
if( css === "underline" ) {
|
||||
underline = css;
|
||||
}
|
||||
|
||||
if( css === "normal" ) {
|
||||
foreground = "color-102";
|
||||
background = "";
|
||||
underline = "";
|
||||
}
|
||||
|
||||
return foreground + " " + background + " " + underline ;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
*
|
||||
*/
|
||||
var parse2HTML = function (text) {
|
||||
let html = "";
|
||||
foreground = "color-102"; // state tracker for foreground css
|
||||
background = ""; // state tracker for background css
|
||||
underline = ""; // state tracker for underlines css
|
||||
|
||||
// HACK: parse TELNET ASCII byte-by-byte, convert ESC's to |'s -- serverside "raw" bug?
|
||||
// Bug is further proven out by the fact that |'s don't come through as doubles.
|
||||
let hack = new RegExp( String.fromCharCode(27) );
|
||||
if( text.match( hack ) ) {
|
||||
let chars = text.split(''); // to characters
|
||||
for( let n=0; n<chars.length; n++ ) {
|
||||
if( chars[n] === '|' ) {
|
||||
console.log( 'Got Pipe' );
|
||||
chars[n] = "|";
|
||||
}
|
||||
if( ascii(chars[n]) === 27 ) {
|
||||
chars[n] = '|';
|
||||
}
|
||||
}
|
||||
text = chars.join(''); // from character strings
|
||||
}
|
||||
|
||||
let strings = text.split( /(\n|\|[\/])/ ); // newline or pipe-slash
|
||||
for( let x=0; x<strings.length; x++ ) {
|
||||
// hack double pipes -- convert them temporarily to HTML -- |
|
||||
var string = strings[x].replace( /\|\|/g, "|" );
|
||||
|
||||
// special case for blank lines, avoid <div>'s with no HTML height
|
||||
if( string === "" ) {
|
||||
html += "<div> </div>";
|
||||
continue;
|
||||
}
|
||||
|
||||
html += "<div>";
|
||||
|
||||
let spans = string.split( "|" );
|
||||
|
||||
// this split means there are 2 possible cases
|
||||
|
||||
// possibly-0-length leading span with "default" styling (may be the ONLY span)
|
||||
let span = spans[0].replaceAll("|", "|"); // avoid htmlEscape mangling literal |'s
|
||||
if( span.length > 0 ) {
|
||||
html += "<span class='color-102'>" + htmlEscape(span) + "</span>";
|
||||
}
|
||||
|
||||
// "standard" span array of [xxxtext1, xxtext2, xxtext3], where x's are pipe-codes
|
||||
for( let n=1; n<spans.length; n++ ) {
|
||||
span = "|" + spans[n].replaceAll("|", "|"); // avoid htmlEscape mangling |'s
|
||||
let pipecode = "";
|
||||
let remainder = span;
|
||||
|
||||
let tags = span.match( pipecodes ); // match against large pipecode regex
|
||||
if( tags != null ) {
|
||||
pipecode = tags[0]; // matched text is the pipecode
|
||||
remainder = span.replace( pipecode, "" ); // everything but the pipecode
|
||||
}
|
||||
|
||||
let css = trackCSS( pipe2css( pipecode ) ); // find css associated with pipe-code
|
||||
html += "<span class='" + css + "'>" + htmlEscape(remainder) + "</span>";
|
||||
}
|
||||
|
||||
html += "</div>";
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// The Main text2html message capture fuction -- calls parse2HTML()
|
||||
var text2html = function (args, kwargs) {
|
||||
let options = window.options;
|
||||
if( !("text2html" in options) || options["text2html"] === false ) { return; }
|
||||
|
||||
var mwins = window.plugins["goldenlayout"].routeMessage(args, kwargs);
|
||||
mwins.forEach( function (mwin) {
|
||||
mwin.append( parse2HTML( args[0]) ); // the heavy workload
|
||||
mwin.scrollTop(mwin[0].scrollHeight);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
var onOptionsUI = function (parentdiv) {
|
||||
let options = window.options;
|
||||
var checked;
|
||||
|
||||
checked = options["text2html"] ? "checked='checked'" : "";
|
||||
var text2html = $( [ "<label>",
|
||||
"<input type='checkbox' data-setting='text2html' " + checked + "'>",
|
||||
" Enable client-side Evennia ASCII message rendering",
|
||||
"</label>"
|
||||
].join("") );
|
||||
text2html.on("change", window.plugins["options2"].onOptionCheckboxChanged);
|
||||
|
||||
parentdiv.append(text2html);
|
||||
}
|
||||
|
||||
//
|
||||
// Mandatory plugin init function
|
||||
var init = function () {
|
||||
let options = window.options;
|
||||
options["text2html"] = true;
|
||||
|
||||
let Evennia = window.Evennia;
|
||||
Evennia.emitter.on("text2html", text2html); // capture "text2html" outfunc events
|
||||
|
||||
console.log('Text2Html plugin initialized');
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
onOptionsUI: onOptionsUI,
|
||||
parse2HTML: parse2HTML,
|
||||
}
|
||||
})();
|
||||
plugin_handler.add("text2html_plugin", text2html_plugin);
|
||||
|
|
@ -100,6 +100,8 @@ JQuery available.
|
|||
<script src={% static "webclient/js/plugins/default_in.js" %} language="javascript" type="text/javascript"></script>
|
||||
<script src={% static "webclient/js/plugins/default_out.js" %} language="javascript" type="text/javascript"></script>
|
||||
<script src={% static "webclient/js/plugins/multimedia.js" %} language="javascript" type="text/javascript"></script>
|
||||
<script src={% static "webclient/js/plugins/html.js" %} language="javascript" type="text/javascript"></script>
|
||||
<script src={% static "webclient/js/plugins/text2html.js" %} language="javascript" type="text/javascript"></script>
|
||||
{% endblock %}
|
||||
|
||||
<script src="https://cdn.rawgit.com/ejci/favico.js/master/favico-0.3.10.min.js" language="javascript" type="text/javascript" charset="utf-8"></script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue