Merge branch 'develop' into contrib/evadventure

This commit is contained in:
Griatch 2022-08-28 14:56:34 +02:00
commit 9c45feaf10
9 changed files with 552 additions and 25 deletions

View file

@ -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.
```

View file

@ -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

View file

@ -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.

View file

@ -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):

View file

@ -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.

View file

@ -21,6 +21,7 @@ __pycache__
# Other
*.swp
*.log
*.log.*
*.pid
*.restart
*.db3

View 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);

View 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 &nbsp; sequences
var handleSpaces = function (text) {
// TODO should probably get smart about replacing spaces inside "normal" text
return text.replace( /\t/g, "&nbsp;&nbsp;&nbsp;&nbsp;").replace( / /g, "&nbsp;");
}
// 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, "&amp;");
text = text.replace(/</g, "&lt;");
text = text.replace(/>/g, "&gt;");
text = text.replace(/"/g, "&quot;");
text = text.replace(/'/g, "&apos;");
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] = "&#124;";
}
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 -- &#124;
var string = strings[x].replace( /\|\|/g, "&#124;" );
// special case for blank lines, avoid <div>'s with no HTML height
if( string === "" ) {
html += "<div>&nbsp;</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("&#124;", "|"); // 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("&#124;", "|"); // 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);

View file

@ -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>