Compare commits

...

48 Commits
vanta ... vanta

Author SHA1 Message Date
vanta black 5a4212f862 Merge pull request 'Redesigning the web look' (#38) from oreolek/vantaMOO:redesign into vanta
Reviewed-on: vantablack/vantaMOO#38
2023-02-19 02:10:24 +00:00
vanta black 5ade61d666 Merge pull request 'Do not presume every new person as normal' (#37) from oreolek/vantaMOO:sdesc into vanta
Reviewed-on: vantablack/vantaMOO#37
2023-02-19 02:10:17 +00:00
Alexander Yakovlev c66b8914f1 Redesigning the web look 2023-02-04 19:51:03 +07:00
Alexander Yakovlev 8b34061620 Do not init sdesc for everyone 2023-02-04 18:47:00 +07:00
vanta black 6d40d8f49c Merge pull request 'Easter eggs' (#36) from oreolek/vantaMOO:eastereggs into vanta
Reviewed-on: vantablack/vantaMOO#36
2023-01-30 01:30:17 +00:00
Alexander Yakovlev 991814c43f Generic version, with tags
Applicable everywhere
2023-01-30 06:47:51 +06:00
Alexander Yakovlev 839b4431a2 Movie screen
This bad joke requires Tracery.
2023-01-24 21:21:38 +06:00
Alexander Yakovlev c644f274ff Pager 2023-01-24 20:35:08 +06:00
Alexander Yakovlev 2be244b5b6 Add an entire Moby Dick as a book 2023-01-24 20:23:13 +06:00
vanta black 3267572f56 Merge pull request 'RPG dice module' (#32) from oreolek/vantaMOO:dice into vanta
Reviewed-on: vantablack/vantaMOO#32
2023-01-22 05:46:40 +00:00
vanta black 945be61e78 Merge pull request 'Refactor character code' (#33) from lunacb/vantaMOO:refactor_characters into vanta
Reviewed-on: vantablack/vantaMOO#33
2023-01-22 05:46:26 +00:00
lunacb b56d747a6f rewording of documentation 2023-01-17 17:53:52 -05:00
lunacb 08facf7d00 refactor character code structure 2023-01-17 17:51:04 -05:00
Alexander Yakovlev bab90c1ba7 Merge branch 'vanta' into dice 2023-01-17 18:50:36 +06:00
vanta black 8a458b21b5 Merge pull request 'pronounsub module' (#28) from lunacb/vantaMOO:pronounsub into vanta
Reviewed-on: vantablack/vantaMOO#28
2023-01-17 07:09:27 +00:00
vanta black 384c0bacfc Merge pull request 'Add a test script' (#22) from TheSystem/vantaMOO2:vanta into vanta
Reviewed-on: vantablack/vantaMOO#22
2023-01-17 07:09:12 +00:00
Alexander Yakovlev 5f27514eb1
add dice module 2023-01-15 20:39:32 +07:00
lunacb 88ba451426 make pronounsub more resillient 2023-01-14 11:37:50 -05:00
lunacb 7e34080666 cleanup and bugfixing 2023-01-13 19:30:57 -05:00
lunacb d1f33e858a function renaming 2023-01-13 19:02:40 -05:00
lunacb fbe670651e rewrite documentation 2023-01-13 19:00:40 -05:00
lunacb 9780740ab9 Merge remote-tracking branch 'upstream/vanta' into pronounsub 2023-01-13 18:52:35 -05:00
lunacb 125252078a refactor interface 2023-01-13 18:37:53 -05:00
lunacb c359ae2f1c allow Builder configuration of known pronouns 2023-01-11 21:09:01 -05:00
lunacb d2f6684026 fix character inherited class method calling 2023-01-11 20:27:37 -05:00
vanta black 02e2c8832c Merge pull request 'Hide creator_ip from non-admins' (#31) from oreolek/vantaMOO:hiddenip into vanta
Reviewed-on: vantablack/vantaMOO#31
2023-01-11 15:38:03 +00:00
lunacb 8daecbf9fc implement pronounsub 2023-01-11 05:14:04 -05:00
lunacb 488595e2b8 remove `lib` entry from the .gitignore 2023-01-11 05:13:26 -05:00
Alexander Yakovlev 7881903b7e
Hide creator_ip from non-admins
Hides `creator_ip` attribute in @examine command from non-admins
(i.e. Builders).
2023-01-11 13:52:00 +07:00
vanta black cd464ce40e Merge pull request 'Patches for rpmodule' (#25) from oreolek/vantaMOO:yesrpsystem into vanta
Reviewed-on: vantablack/vantaMOO#25
2023-01-09 16:13:22 +00:00
Alexander Yakovlev a004641121
do not capitalize nicks 2023-01-09 11:05:23 +07:00
Alexander Yakovlev 7c34a93874
Pass NOMATCH errors, do not capitalize nicks
kenneaal's regex didn't work so now we just ignore match errors
2023-01-09 10:49:47 +07:00
Alexander Yakovlev c9d5639d18
Move RPSystem 2023-01-09 10:03:29 +07:00
Erin Nova 0bb41b771c Add test script 2023-01-08 10:39:20 -05:00
Erin Nova b99bc291c3 Add specific vantaMOO text to README 2023-01-08 10:28:19 -05:00
vanta black 91d2c19d1e Merge pull request 'Undo container PR #19' (#21) from oreolek/vantaMOO:undo_container into vanta
Reviewed-on: vantablack/vantaMOO#21
2023-01-08 09:07:09 +00:00
Alexander Yakovlev 0b0b1f3fa5
Revert "Merge pull request 'add container from arxcode' (#19) from Kaypar/vantaMOO:vanta into vanta"
This reverts commit f9caf42356, reversing
changes made to 486326bced.
2023-01-08 16:03:10 +07:00
vanta black 1e3b6bf0ca Merge pull request 'Intro screen' (#20) from oreolek/vantaMOO:intro_screen into vanta
Reviewed-on: vantablack/vantaMOO#20
2023-01-08 08:37:47 +00:00
vanta black f9caf42356 Merge pull request 'add container from arxcode' (#19) from Kaypar/vantaMOO:vanta into vanta
Reviewed-on: vantablack/vantaMOO#19
2023-01-08 08:36:56 +00:00
Alexander Yakovlev b25671f224
ANSI art by garuma
Converted from ANSI by me
2023-01-08 14:49:25 +07:00
Alexander Yakovlev c42e0b45f2
add pre-commit config 2023-01-08 14:49:19 +07:00
Kaypar a69417d3bd Upload files to 'typeclasses' 2023-01-08 07:06:19 +00:00
Kaypar 64c00188b5 Update 'commands/default_cmdsets.py' 2023-01-08 07:05:46 +00:00
vanta black 486326bced Merge pull request 'Clothing module' (#2) from oreolek/vantaMOO:clothing into vanta
Reviewed-on: vantablack/vantaMOO#2
2023-01-06 19:06:20 +00:00
Alexander Yakovlev 62bf0de9d2
Import the clothing module 2023-01-06 14:39:56 +07:00
Alexander Yakovlev 5427718b1a
Ignore evenv in git 2023-01-06 14:32:43 +07:00
vanta black ea4aa2eb6a did a thing 2023-01-06 06:39:21 +00:00
vanta black ddb7d687f5 Merge pull request 'Add contrib code' (#1) from lunacb/vantaMOO:vanta into vanta
Reviewed-on: vantablack/vantaMOO#1
2023-01-06 05:11:05 +00:00
43 changed files with 25495 additions and 466 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.gif filter=lfs diff=lfs merge=lfs -text

11
.gitignore vendored
View File

@ -14,7 +14,6 @@ var
sdist
develop-eggs
.installed.cfg
lib
lib64
__pycache__
@ -52,3 +51,13 @@ nosetests.xml
# PyCharm config
.idea
# Virtualenv
evenv
# Compiled CSS
web/static/website/css/
# JS modules
node_modules/
pnpm-lock.yaml
package.json.lock

7
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,7 @@
fail_fast: true
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: check-ast

View File

@ -1,4 +1,18 @@
# Welcome to Evennia!
# vantaMOO
This is the git repo for all the vantaMOO code.
Feel free to fork and make your own changes.
## Building the frontend
Install Node.js first.
* npm install
* npm run build
Restart Evennia after everytime you change and rebuild SCSS to pick up the changes. Do not edit CSS directly.
## Welcome to Evennia!
This is your game directory, set up to let you start with
your new game right away. An overview of this directory is found here:
@ -29,7 +43,7 @@ to your new game using a MUD client on `localhost`, port `4000`. You can
also log into the web client by pointing a browser to
`http://localhost:4001`.
# Getting started
## Getting started
From here on you might want to look at one of the beginner tutorials:
http://github.com/evennia/evennia/wiki/Tutorials.

View File

@ -0,0 +1,14 @@
from evennia.commands.default.building import CmdExamine
class CmdCustomExamine(CmdExamine):
def format_attributes(self, obj):
caller = self.caller
output = "\n "
for attr in obj.db_attributes.all():
if attr.db_key == "creator_ip" and not caller.permissions.check("Admin"):
continue
output += self.format_single_attribute(attr)
output += "\n "
if output.strip():
# we don't want just an empty line
return output

View File

@ -16,12 +16,16 @@ own cmdsets by inheriting from them or directly from `evennia.CmdSet`.
from evennia import default_cmds
from commands.cmdcustomexamine import CmdCustomExamine
from evennia.contrib.base_systems.building_menu import GenericBuildingCmd
from evennia.contrib.game_systems import mail
from evennia.contrib.game_systems.clothing import ClothedCharacterCmdSet
from evennia.contrib.game_systems.multidescer import CmdMultiDesc
from evennia.contrib.grid.ingame_map_display import MapDisplayCmdSet
from evennia.contrib.grid import simpledoor
from evennia.contrib.rpg.rpsystem import RPSystemCmdSet
from evennia.contrib.grid.ingame_map_display import MapDisplayCmdSet
from lib.pronounsub.commands import *
from lib.rpsystem.rpsystem import RPSystemCmdSet
from evennia.contrib.rpg import dice
class CharacterCmdSet(default_cmds.CharacterCmdSet):
@ -61,6 +65,19 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet):
# rpsystem
self.add(RPSystemCmdSet())
# Clothing: wear
self.add(ClothedCharacterCmdSet())
# gendersub
self.add(PronounAdminCommand())
self.add(PronounsCommand())
# Overrides @examine
self.add(CmdCustomExamine())
# RPG dice
self.add(dice.CmdDice())
class AccountCmdSet(default_cmds.AccountCmdSet):
"""
This is the cmdset available to the Account at all times. It is

View File

@ -1,247 +0,0 @@
<#
.Synopsis
Activate a Python virtual environment for the current PowerShell session.
.Description
Pushes the python executable for a virtual environment to the front of the
$Env:PATH environment variable and sets the prompt to signify that you are
in a Python virtual environment. Makes use of the command line switches as
well as the `pyvenv.cfg` file values present in the virtual environment.
.Parameter VenvDir
Path to the directory that contains the virtual environment to activate. The
default value for this is the parent of the directory that the Activate.ps1
script is located within.
.Parameter Prompt
The prompt prefix to display when this virtual environment is activated. By
default, this prompt is the name of the virtual environment folder (VenvDir)
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
.Example
Activate.ps1
Activates the Python virtual environment that contains the Activate.ps1 script.
.Example
Activate.ps1 -Verbose
Activates the Python virtual environment that contains the Activate.ps1 script,
and shows extra information about the activation as it executes.
.Example
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
Activates the Python virtual environment located in the specified location.
.Example
Activate.ps1 -Prompt "MyPython"
Activates the Python virtual environment that contains the Activate.ps1 script,
and prefixes the current prompt with the specified string (surrounded in
parentheses) while the virtual environment is active.
.Notes
On Windows, it may be required to enable this Activate.ps1 script by setting the
execution policy for the user. You can do this by issuing the following PowerShell
command:
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
For more information on Execution Policies:
https://go.microsoft.com/fwlink/?LinkID=135170
#>
Param(
[Parameter(Mandatory = $false)]
[String]
$VenvDir,
[Parameter(Mandatory = $false)]
[String]
$Prompt
)
<# Function declarations --------------------------------------------------- #>
<#
.Synopsis
Remove all shell session elements added by the Activate script, including the
addition of the virtual environment's Python executable from the beginning of
the PATH variable.
.Parameter NonDestructive
If present, do not remove this function from the global namespace for the
session.
#>
function global:deactivate ([switch]$NonDestructive) {
# Revert to original values
# The prior prompt:
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
}
# The prior PYTHONHOME:
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
}
# The prior PATH:
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
}
# Just remove the VIRTUAL_ENV altogether:
if (Test-Path -Path Env:VIRTUAL_ENV) {
Remove-Item -Path env:VIRTUAL_ENV
}
# Just remove VIRTUAL_ENV_PROMPT altogether.
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
}
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
}
# Leave deactivate function in the global namespace if requested:
if (-not $NonDestructive) {
Remove-Item -Path function:deactivate
}
}
<#
.Description
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
given folder, and returns them in a map.
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
two strings separated by `=` (with any amount of whitespace surrounding the =)
then it is considered a `key = value` line. The left hand string is the key,
the right hand is the value.
If the value starts with a `'` or a `"` then the first and last character is
stripped from the value before being captured.
.Parameter ConfigDir
Path to the directory that contains the `pyvenv.cfg` file.
#>
function Get-PyVenvConfig(
[String]
$ConfigDir
) {
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
# An empty map will be returned if no config file is found.
$pyvenvConfig = @{ }
if ($pyvenvConfigPath) {
Write-Verbose "File exists, parse `key = value` lines"
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
$pyvenvConfigContent | ForEach-Object {
$keyval = $PSItem -split "\s*=\s*", 2
if ($keyval[0] -and $keyval[1]) {
$val = $keyval[1]
# Remove extraneous quotations around a string value.
if ("'""".Contains($val.Substring(0, 1))) {
$val = $val.Substring(1, $val.Length - 2)
}
$pyvenvConfig[$keyval[0]] = $val
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
}
}
}
return $pyvenvConfig
}
<# Begin Activate script --------------------------------------------------- #>
# Determine the containing directory of this script
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$VenvExecDir = Get-Item -Path $VenvExecPath
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
# Set values required in priority: CmdLine, ConfigFile, Default
# First, get the location of the virtual environment, it might not be
# VenvExecDir if specified on the command line.
if ($VenvDir) {
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
}
else {
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
Write-Verbose "VenvDir=$VenvDir"
}
# Next, read the `pyvenv.cfg` file to determine any required value such
# as `prompt`.
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
# Next, set the prompt from the command line, or the config file, or
# just use the name of the virtual environment folder.
if ($Prompt) {
Write-Verbose "Prompt specified as argument, using '$Prompt'"
}
else {
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
$Prompt = $pyvenvCfg['prompt'];
}
else {
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
$Prompt = Split-Path -Path $venvDir -Leaf
}
}
Write-Verbose "Prompt = '$Prompt'"
Write-Verbose "VenvDir='$VenvDir'"
# Deactivate any currently active virtual environment, but leave the
# deactivate function in place.
deactivate -nondestructive
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
# that there is an activated venv.
$env:VIRTUAL_ENV = $VenvDir
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
Write-Verbose "Setting prompt to '$Prompt'"
# Set the prompt to include the env name
# Make sure _OLD_VIRTUAL_PROMPT is global
function global:_OLD_VIRTUAL_PROMPT { "" }
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
function global:prompt {
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
_OLD_VIRTUAL_PROMPT
}
$env:VIRTUAL_ENV_PROMPT = $Prompt
}
# Clear PYTHONHOME
if (Test-Path -Path Env:PYTHONHOME) {
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
Remove-Item -Path Env:PYTHONHOME
}
# Add the venv to the PATH
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"

View File

@ -1,69 +0,0 @@
# This file must be used with "source bin/activate" *from bash*
# you cannot run it directly
deactivate () {
# reset old environment variables
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
PATH="${_OLD_VIRTUAL_PATH:-}"
export PATH
unset _OLD_VIRTUAL_PATH
fi
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
export PYTHONHOME
unset _OLD_VIRTUAL_PYTHONHOME
fi
# This should detect bash and zsh, which have a hash command that must
# be called to get it to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
hash -r 2> /dev/null
fi
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
PS1="${_OLD_VIRTUAL_PS1:-}"
export PS1
unset _OLD_VIRTUAL_PS1
fi
unset VIRTUAL_ENV
unset VIRTUAL_ENV_PROMPT
if [ ! "${1:-}" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}
# unset irrelevant variables
deactivate nondestructive
VIRTUAL_ENV="/home/ubuntu/vantamoo/evenv"
export VIRTUAL_ENV
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/bin:$PATH"
export PATH
# unset PYTHONHOME if set
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
# could use `if (set -u; : $PYTHONHOME) ;` in bash
if [ -n "${PYTHONHOME:-}" ] ; then
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
unset PYTHONHOME
fi
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
_OLD_VIRTUAL_PS1="${PS1:-}"
PS1="(evenv) ${PS1:-}"
export PS1
VIRTUAL_ENV_PROMPT="(evenv) "
export VIRTUAL_ENV_PROMPT
fi
# This should detect bash and zsh, which have a hash command that must
# be called to get it to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
hash -r 2> /dev/null
fi

View File

@ -1,26 +0,0 @@
# This file must be used with "source bin/activate.csh" *from csh*.
# You cannot run it directly.
# Created by Davide Di Blasi <davidedb@gmail.com>.
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
# Unset irrelevant variables.
deactivate nondestructive
setenv VIRTUAL_ENV "/home/ubuntu/vantamoo/evenv"
set _OLD_VIRTUAL_PATH="$PATH"
setenv PATH "$VIRTUAL_ENV/bin:$PATH"
set _OLD_VIRTUAL_PROMPT="$prompt"
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
set prompt = "(evenv) $prompt"
setenv VIRTUAL_ENV_PROMPT "(evenv) "
endif
alias pydoc python -m pydoc
rehash

View File

@ -1,66 +0,0 @@
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
# (https://fishshell.com/); you cannot run it directly.
function deactivate -d "Exit virtual environment and return to normal shell environment"
# reset old environment variables
if test -n "$_OLD_VIRTUAL_PATH"
set -gx PATH $_OLD_VIRTUAL_PATH
set -e _OLD_VIRTUAL_PATH
end
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
set -e _OLD_VIRTUAL_PYTHONHOME
end
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
functions -e fish_prompt
set -e _OLD_FISH_PROMPT_OVERRIDE
functions -c _old_fish_prompt fish_prompt
functions -e _old_fish_prompt
end
set -e VIRTUAL_ENV
set -e VIRTUAL_ENV_PROMPT
if test "$argv[1]" != "nondestructive"
# Self-destruct!
functions -e deactivate
end
end
# Unset irrelevant variables.
deactivate nondestructive
set -gx VIRTUAL_ENV "/home/ubuntu/vantamoo/evenv"
set -gx _OLD_VIRTUAL_PATH $PATH
set -gx PATH "$VIRTUAL_ENV/bin" $PATH
# Unset PYTHONHOME if set.
if set -q PYTHONHOME
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
set -e PYTHONHOME
end
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
# fish uses a function instead of an env var to generate the prompt.
# Save the current fish_prompt function as the function _old_fish_prompt.
functions -c fish_prompt _old_fish_prompt
# With the original prompt function renamed, we can override with our own.
function fish_prompt
# Save the return status of the last command.
set -l old_status $status
# Output the venv prompt; color taken from the blue of the Python logo.
printf "%s%s%s" (set_color 4B8BBE) "(evenv) " (set_color normal)
# Restore the return status of the previous command.
echo "exit $old_status" | .
# Output the original/"old" prompt.
_old_fish_prompt
end
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
set -gx VIRTUAL_ENV_PROMPT "(evenv) "
end

View File

@ -1,8 +0,0 @@
#!/home/ubuntu/vantamoo/evenv/bin/python3.11
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -1,8 +0,0 @@
#!/home/ubuntu/vantamoo/evenv/bin/python3.11
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -1,8 +0,0 @@
#!/home/ubuntu/vantamoo/evenv/bin/python3.11
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -1,8 +0,0 @@
#!/home/ubuntu/vantamoo/evenv/bin/python3.11
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -1 +0,0 @@
python3.11

View File

@ -1 +0,0 @@
python3.11

View File

@ -1 +0,0 @@
/usr/bin/python3.11

View File

@ -1,5 +0,0 @@
home = /usr/bin
include-system-site-packages = false
version = 3.11.0
executable = /usr/bin/python3.11
command = /usr/bin/python3.11 -m venv /home/ubuntu/vantamoo/evenv

0
lib/__init__.py Normal file
View File

View File

View File

@ -0,0 +1,49 @@
from .common import *
# This should be called before every use of a Character to make sure it's
# initialized right.
def object_init(character):
if character.db.pronouns == None:
character.db.pronouns = DEFAULT_CHARACTER_PRONOUNS[:]
if character.db.pronoun_specs == None:
character.db.pronoun_specs = []
def msg(character, text=None, from_obj=None, session=None, **kwargs):
object_init(character)
"""
Emits something to a session attached to the object.
Overloads the default msg() implementation to include
gender-aware markers in output.
Args:
text (str or tuple, optional): The message to send. This
is treated internally like any send-command, so its
value can be a tuple if sending multiple arguments to
the `text` oob command.
from_obj (obj, optional): object that is sending. If
given, at_msg_send will be called
session (Session or list, optional): session or list of
sessions to relay to, if any. If set, will
force send regardless of MULTISESSION_MODE.
Note:
`at_msg_receive` will be called on this Object.
All extra kwargs will be passed on to the protocol.
"""
if text is None:
super().msg(from_obj=from_obj, session=session, **kwargs)
return
try:
repl = lambda a: pronoun_repl(character, a)
if text and isinstance(text, tuple):
text = (RE_GENDER_PRONOUN.sub(repl, text[0]), *text[1:])
else:
text = RE_GENDER_PRONOUN.sub(repl, text)
except TypeError:
pass
except Exception as e:
logger.log_trace(e)
return (text, from_obj, session, kwargs)

225
lib/pronounsub/commands.py Normal file
View File

@ -0,0 +1,225 @@
from .common import *
from .character import object_init
class PronounAdminCommand(Command):
"""
Manages the list of known server-wide pronouns.
Usage:
@pronounadmin help
@pronounadmin list_known
@pronounadmin spec <specific>
@pronounadmin unspec <specific>
On each character and the server are stored character pronouns and specific
pronouns. The character pronouns specify what pronouns should actually be
used for your character, and the specific pronouns define all the
grammatical forms of a pronoun so the character pronouns can be used in
text substitution.
The specific format is: <subjective>,<objective>,<possessive>,<absolute>
subjective: *They* went to the store.
objective: You took *them* to the store.
possessive: You took *their* friend to the store.
absolute: The sandwich in the fridge was *theirs*.
The `list_known` command lists all specific pronouns known by the server.
The `spec` command adds a new specific pronoun.
The `unspec` command removes an existing specific pronoun.
Examples:
@pronounadmin spec they,them,their,theirs
"""
key = "pronounadmin"
locks = "cmd:id(1) or perm(Admin)"
def parse(self):
caller = self.caller
args = self.args.strip().lower().split()
if len(args) == 0:
caller.msg(self.get_help(caller, self.cmdset))
raise InterruptCommand()
command = args[0]
if command == "list_known" and len(args) == 1:
self.args = tuple([command])
return
if len(args) != 2:
caller.msg(self.get_help(caller, self.cmdset))
raise InterruptCommand()
if not command in [ "spec", "unspec" ] or command == "help":
caller.msg(self.get_help(caller, self.cmdset))
raise InterruptCommand()
pronoun = parse_specific_pronoun(args[1])
if pronoun == None:
caller.msg(self.get_help(caller, self.cmdset))
raise InterruptCommand()
self.args = ( command, pronoun )
def func(self):
caller = self.caller
command = self.args[0]
if command == "list_known":
caller.msg("Known server-defined pronouns:")
for spec in get_pronoun_list():
caller.msg(f" {stringify_specific_pronoun(spec)}")
return
_, pronoun = self.args
pronoun_list = get_pronoun_list()
if command == "spec":
if not pronoun in pronoun_list:
pronoun_list.append(pronoun)
self.caller.msg(f"Added the pronoun {stringify_specific_pronoun(pronoun)}.")
elif command == "unspec":
if pronoun in pronoun_list:
pronoun_list.remove(pronoun)
self.caller.msg(f"Removed the pronoun {stringify_specific_pronoun(pronoun)}.")
class PronounsCommand(Command):
"""
Manages your pronouns.
Usage:
@pronouns help
@pronouns set <pronouns>
@pronouns get
@pronouns list_known
@pronouns spec <specific>
@pronouns unspec <specific>
On each character and the server are stored character pronouns and specific
pronouns. The character pronouns specify what pronouns should actually be
used for your character, and the specific pronouns define all the
grammatical forms of a pronoun so the character pronouns can be used in
text substitution.
The server already has some specific pronouns defined, so it's likely you
can just give your character pronouns and be done. If the server doesn't
know about your pronouns already then you'll have to tell the server how to
use them with the `spec` command.
The specific format is: <subjective>,<objective>,<possessive>,<absolute>
subjective: *They* went to the store.
objective: You took *them* to the store.
possessive: You took *their* friend to the store.
absolute: The sandwich in the fridge was *theirs*.
The `set` command:
Sets your current character pronouns. Pronouns look the same as how you'd
write them anywhere online. They're separated by a slash ("/"), like
"she/they/it", or "he/him". Each pronoun should match to the grammatical
form of a specific pronoun. When multiple pronouns are given, like both
"she" and "they", a random one will be used in each text substitution.
The `get` command prints your current configured character pronouns.
The `list_known` command lists all specific pronouns known by the
server and specified for your acount.
The `spec` command adds a new specific pronoun.
The `unspec` command removes an existing specific pronoun.
Examples:
@pronouns set he/him
@pronouns set she/they/it
@pronouns spec fae,faer,faer,faers
"""
key = "pronouns"
def parse(self):
caller = self.caller
object_init(caller)
args = self.args.strip().lower().split()
if len(args) == 1:
command = args[0]
if not command in [ "get", "list_known" ] or command == "help":
caller.msg(self.get_help(caller, self.cmdset))
raise InterruptCommand()
self.args = tuple([command])
return
if len(args) != 2:
caller.msg(self.get_help(caller, self.cmdset))
raise InterruptCommand()
command = args[0]
if command == "spec" or command == "unspec":
new = parse_specific_pronoun(args[1])
if new == None:
caller.msg(self.get_help(caller, self.cmdset))
raise InterruptCommand()
else:
res = new
elif command == "set":
res = args[1].split('/')
self.args = (command, res)
def func(self):
caller = self.caller
object_init(caller)
command = self.args[0]
match self.args[0]:
case "set":
_, pronouns = self.args
has_unmatched = False
for pronoun in pronouns:
if not specific_pronoun_from_character(pronoun, caller):
has_unmatched = True
caller.msg(f"Warning: The character pronoun \"{pronoun}\" is "
"not in any list of specific pronouns. It will "
"be ignored. |/")
if has_unmatched:
caller.msg("You have unkown pronouns set! If you want to "
"actually use them, you'll need to set their "
"specific form. Type `pronouns help` for "
"details. |/")
caller.db.pronouns = pronouns
caller.msg(f"Your pronouns have been set to {stringify_pronouns(caller)}.")
case "get":
caller.msg(f"Your pronouns are {stringify_pronouns(caller)}.")
case "list_known":
caller.msg("Known user-defined pronouns:")
for spec in caller.db.pronoun_specs:
caller.msg(f" {stringify_specific_pronoun(spec)}")
caller.msg("Known server-defined pronouns:")
for spec in get_pronoun_list():
caller.msg(f" {stringify_specific_pronoun(spec)}")
case "spec":
_, pronoun = self.args
if not pronoun in caller.db.pronoun_specs:
caller.db.pronoun_specs.append(pronoun)
self.caller.msg(f"Added the pronoun {stringify_specific_pronoun(pronoun)}.")
case "unspec":
_, pronoun = self.args
if pronoun in caller.db.pronoun_specs:
caller.db.pronoun_specs.remove(pronoun)
self.caller.msg(f"Remobed the pronoun {stringify_specific_pronoun(pronoun)}.")

137
lib/pronounsub/common.py Normal file
View File

@ -0,0 +1,137 @@
"""
Pronounsub
Griatch 2015
Copyright (c) 2023 lunacb <lunacb@disroot.org>
This is a gender-aware Character class that allows people to set pronouns and
use them in text.
Usage
When in use, messages can contain special tags to indicate pronouns gendered
based on the one being addressed. Capitalization will be retained.
- `|s`, `|S`: Subjective form, like They
- `|o`, `|O`: Objective form, like Them
- `|p`, `|P`: Possessive form, like Their
- `|a`, `|A`: Absolute Possessive form, like Theirs
For example,
```
char.msg("%s falls on |p face with a thud." % char.key)
"Tom falls on their face with a thud"
```
To use, have DefaultCharacter inherit from this, or change
setting.DEFAULT_CHARACTER to point to this class.
The `pronouns` command is used to set pronouns. It needs to be added to the
default cmdset before it becomes available.
"""
import copy
import random
import re
from evennia import Command, DefaultCharacter
from evennia import DefaultScript
from evennia import InterruptCommand
from evennia.utils import logger
from evennia.utils.create import create_script
from evennia.utils.search import search_script
class PronounDbScript(DefaultScript):
def at_script_creation(self):
self.db.pronouns = copy.deepcopy(DEFAULT_SPECIFIC_PRONOUNS)
DEFAULT_SPECIFIC_NEUTRAL_PRONOUNS = {"s": "they", "o": "them", "p": "their", "a": "theirs"}
DEFAULT_SPECIFIC_PRONOUNS = [
{"s": "he", "o": "him", "p": "his", "a": "his"},
{"s": "she", "o": "her", "p": "her", "a": "hers"},
{"s": "it", "o": "it", "p": "its", "a": "its"},
{"s": "they", "o": "them", "p": "their", "a": "theirs"}
]
DEFAULT_CHARACTER_PRONOUNS = [ "they", "them" ]
PRONOUN_FORMS = [ "s", "o", "p", "a" ]
RE_GENDER_PRONOUN = re.compile(r"(?<!\|)\|(?!\|)[sSoOpPaA]")
# in-game command for setting the gender
def parse_specific_pronoun(pronoun):
specific = [ p.strip() for p in pronoun.split(",") ]
if len(specific) != len(PRONOUN_FORMS):
return None
new = {}
for i in range(len(specific)):
new[PRONOUN_FORMS[i]] = specific[i]
return new
def get_pronoun_list():
search = search_script("pronoun_db")
if not any(search):
script = create_script("lib.pronounsub.PronounDbScript", key="pronoun_db")
else:
script = search[0]
return script.db.pronouns
def specific_pronoun_from_character(character, caller=None):
pronoun_list = get_pronoun_list()[:]
if caller != None:
pronoun_list = caller.db.pronoun_specs + pronoun_list
for pronoun in pronoun_list:
if character in pronoun.values():
return pronoun
return None
def stringify_specific_pronoun(spec):
return ",".join([ spec[form] for form in PRONOUN_FORMS ])
def stringify_pronouns(character):
pronouns = character.attributes.get("pronouns", default=DEFAULT_CHARACTER_PRONOUNS[:])
return "/".join(pronouns)
def pronoun_repl(character, regex_match):
"""
Get pronoun from the pronoun marker in the text. This is used as
the callable for the re.sub function.
Args:
regex_match (MatchObject): the regular expression match.
Notes:
- `|s`, `|S`: Subjective form, like They
- `|o`, `|O`: Objective form, like Them
- `|p`, `|P`: Possessive form, like Their
- `|a`, `|A`: Absolute Possessive form, like Theirs
"""
typ = regex_match.group()[1] # "s", "O" etc
dup_specifics = []
for pronoun in character.attributes.get("pronouns", default=DEFAULT_CHARACTER_PRONOUNS[:]):
specific = specific_pronoun_from_character(pronoun, character)
if specific != None:
dup_specifics.append(specific)
specifics = []
for s in dup_specifics:
if not s in specifics:
specifics.append(s)
if len(specifics) == 0:
choice = DEFAULT_SPECIFIC_NEUTRAL_PRONOUNS
else:
choice = random.choice(specifics)
pronoun = choice[typ.lower()]
return pronoun.capitalize() if typ.isupper() else pronoun

267
lib/rpsystem/README.md Normal file
View File

@ -0,0 +1,267 @@
# Roleplaying base system for Evennia
Contribution by Griatch, 2015
A full roleplaying emote system. Short-descriptions and recognition (only
know people by their looks until you assign a name to them). Room poses. Masks/disguises
(hide your description). Speak directly in emote, with optional language obscuration
(words get garbled if you don't know the language, you can also have different languages
with different 'sounding' garbling). Whispers can be partly overheard from a distance. A
very powerful in-emote reference system, for referencing and differentiate targets
(including objects).
The system contains of two main modules - the roleplaying emote system and the language
obscuration module.
## Roleplaying emotes
This module contains the ContribRPObject, ContribRPRoom and
ContribRPCharacter typeclasses. If you inherit your
objects/rooms/character from these (or make them the defaults) from
these you will get the following features:
- Objects/Rooms will get the ability to have poses and will report
the poses of items inside them (the latter most useful for Rooms).
- Characters will get poses and also sdescs (short descriptions)
that will be used instead of their keys. They will gain commands
for managing recognition (custom sdesc-replacement), masking
themselves as well as an advanced free-form emote command.
In more detail, This RP base system introduces the following features
to a game, common to many RP-centric games:
- emote system using director stance emoting (names/sdescs).
This uses a customizable replacement noun (/me, @ etc) to
represent you in the emote. You can use /sdesc, /nick, /key or
/alias to reference objects in the room. You can use any
number of sdesc sub-parts to differentiate a local sdesc, or
use /1-sdesc etc to differentiate them. The emote also
identifies nested says and separates case.
- sdesc obscuration of real character names for use in emotes
and in any referencing such as object.search(). This relies
on an SdescHandler `sdesc` being set on the Character and
makes use of a custom Character.get_display_name hook. If
sdesc is not set, the character's `key` is used instead. This
is particularly used in the emoting system.
- recog system to assign your own nicknames to characters, can then
be used for referencing. The user may recog a user and assign
any personal nick to them. This will be shown in descriptions
and used to reference them. This is making use of the nick
functionality of Evennia.
- masks to hide your identity (using a simple lock).
- pose system to set room-persistent poses, visible in room
descriptions and when looking at the person/object. This is a
simple Attribute that modifies how the characters is viewed when
in a room as sdesc + pose.
- in-emote says, including seamless integration with language
obscuration routine (such as contrib/rplanguage.py)
### Installation:
Add `RPSystemCmdSet` from this module to your CharacterCmdSet:
```python
# mygame/commands/default_cmdsets.py
# ...
from evennia.contrib.rpg.rpsystem import RPSystemCmdSet <---
class CharacterCmdSet(default_cmds.CharacterCmdset):
# ...
def at_cmdset_creation(self):
# ...
self.add(RPSystemCmdSet()) # <---
```
You also need to make your Characters/Objects/Rooms inherit from
the typeclasses in this module:
```python
# in mygame/typeclasses/characters.py
from evennia.contrib.rpg import ContribRPCharacter
class Character(ContribRPCharacter):
# ...
```
```python
# in mygame/typeclasses/objects.py
from evennia.contrib.rpg import ContribRPObject
class Object(ContribRPObject):
# ...
```
```python
# in mygame/typeclasses/rooms.py
from evennia.contrib.rpg import ContribRPRoom
class Room(ContribRPRoom):
# ...
```
You will then need to reload the server and potentially force-reload
your objects, if you originally created them without this.
Example for your character:
> type/reset/force me = typeclasses.characters.Character
Examples:
> look
Tavern
The tavern is full of nice people
*A tall man* is standing by the bar.
Above is an example of a player with an sdesc "a tall man". It is also
an example of a static *pose*: The "standing by the bar" has been set
by the player of the tall man, so that people looking at him can tell
at a glance what is going on.
> emote /me looks at /Tall and says "Hello!"
I see:
Griatch looks at Tall man and says "Hello".
Tall man (assuming his name is Tom) sees:
The godlike figure looks at Tom and says "Hello".
Note that by default, the case of the tag matters, so `/tall` will
lead to 'tall man' while `/Tall` will become 'Tall man' and /TALL
becomes /TALL MAN. If you don't want this behavior, you can pass
case_sensitive=False to the `send_emote` function.
## Language and whisper obfuscation system
This module is intented to be used with an emoting system (such as
`contrib/rpg/rpsystem.py`). It offers the ability to obfuscate spoken words
in the game in various ways:
- Language: The language functionality defines a pseudo-language map
to any number of languages. The string will be obfuscated depending
on a scaling that (most likely) will be input as a weighted average of
the language skill of the speaker and listener.
- Whisper: The whisper functionality will gradually "fade out" a
whisper along as scale 0-1, where the fading is based on gradually
removing sections of the whisper that is (supposedly) easier to
overhear (for example "s" sounds tend to be audible even when no other
meaning can be determined).
### Installation
This module adds no new commands; embed it in your say/emote/whisper commands.
### Usage:
```python
from evennia.contrib import rplanguage
# need to be done once, here we create the "default" lang
rplanguage.add_language()
say = "This is me talking."
whisper = "This is me whispering.
print rplanguage.obfuscate_language(say, level=0.0)
<<< "This is me talking."
print rplanguage.obfuscate_language(say, level=0.5)
<<< "This is me byngyry."
print rplanguage.obfuscate_language(say, level=1.0)
<<< "Daly ly sy byngyry."
result = rplanguage.obfuscate_whisper(whisper, level=0.0)
<<< "This is me whispering"
result = rplanguage.obfuscate_whisper(whisper, level=0.2)
<<< "This is m- whisp-ring"
result = rplanguage.obfuscate_whisper(whisper, level=0.5)
<<< "---s -s -- ---s------"
result = rplanguage.obfuscate_whisper(whisper, level=0.7)
<<< "---- -- -- ----------"
result = rplanguage.obfuscate_whisper(whisper, level=1.0)
<<< "..."
```
To set up new languages, import and use the `add_language()`
helper method in this module. This allows you to customize the
"feel" of the semi-random language you are creating. Especially
the `word_length_variance` helps vary the length of translated
words compared to the original and can help change the "feel" for
the language you are creating. You can also add your own
dictionary and "fix" random words for a list of input words.
Below is an example of "elvish", using "rounder" vowels and sounds:
```python
# vowel/consonant grammar possibilities
grammar = ("v vv vvc vcc vvcc cvvc vccv vvccv vcvccv vcvcvcc vvccvvcc "
"vcvvccvvc cvcvvcvvcc vcvcvvccvcvv")
# all not in this group is considered a consonant
vowels = "eaoiuy"
# you need a representative of all of the minimal grammars here, so if a
# grammar v exists, there must be atleast one phoneme available with only
# one vowel in it
phonemes = ("oi oh ee ae aa eh ah ao aw ay er ey ow ia ih iy "
"oy ua uh uw y p b t d f v t dh s z sh zh ch jh k "
"ng g m n l r w")
# how much the translation varies in length compared to the original. 0 is
# smallest, higher values give ever bigger randomness (including removing
# short words entirely)
word_length_variance = 1
# if a proper noun (word starting with capitalized letter) should be
# translated or not. If not (default) it means e.g. names will remain
# unchanged across languages.
noun_translate = False
# all proper nouns (words starting with a capital letter not at the beginning
# of a sentence) can have either a postfix or -prefix added at all times
noun_postfix = "'la"
# words in dict will always be translated this way. The 'auto_translations'
# is instead a list or filename to file with words to use to help build a
# bigger dictionary by creating random translations of each word in the
# list *once* and saving the result for subsequent use.
manual_translations = {"the":"y'e", "we":"uyi", "she":"semi", "he":"emi",
"you": "do", 'me':'mi','i':'me', 'be':"hy'e", 'and':'y'}
rplanguage.add_language(key="elvish", phonemes=phonemes, grammar=grammar,
word_length_variance=word_length_variance,
noun_translate=noun_translate,
noun_postfix=noun_postfix, vowels=vowels,
manual_translations=manual_translations,
auto_translations="my_word_file.txt")
```
This will produce a decicively more "rounded" and "soft" language than the
default one. The few `manual_translations` also make sure to make it at least
look superficially "reasonable".
The `auto_translations` keyword is useful, this accepts either a
list or a path to a text-file (with one word per line). This listing
of words is used to 'fix' translations for those words according to the
grammatical rules. These translations are stored persistently as long as the
language exists.
This allows to quickly build a large corpus of translated words
that never change. This produces a language that seem moderately
consistent, since words like 'the' will always be translated to the same thing.
The disadvantage (or advantage, depending on your game) is that players can
end up learn what words mean even if their characters don't know the
langauge.

36
lib/rpsystem/__init__.py Normal file
View File

@ -0,0 +1,36 @@
"""
Roleplaying emotes and language - Griatch, 2015
"""
from .rplanguage import LanguageExistsError # noqa
from .rplanguage import LanguageHandler # noqa
from .rplanguage import (
LanguageError,
add_language,
available_languages,
obfuscate_language,
obfuscate_whisper,
)
from .rpsystem import CmdSay # noqa
from .rpsystem import ContribRPCharacter # noqa
from .rpsystem import ContribRPObject # noqa
from .rpsystem import ContribRPRoom # noqa
from .rpsystem import RPSystemCmdSet # noqa
from .rpsystem import (
CmdEmote,
CmdMask,
CmdPose,
CmdRecog,
CmdSdesc,
EmoteError,
LanguageError,
RecogError,
RecogHandler,
RPCommand,
SdescError,
SdescHandler,
parse_language,
parse_sdescs_and_recogs,
send_emote,
)

609
lib/rpsystem/rplanguage.py Normal file
View File

@ -0,0 +1,609 @@
"""
Language and whisper obfuscation system
Evennia contrib - Griatch 2015
This module is intented to be used with an emoting system (such as
contrib/rpsystem.py). It offers the ability to obfuscate spoken words
in the game in various ways:
- Language: The language functionality defines a pseudo-language map
to any number of languages. The string will be obfuscated depending
on a scaling that (most likely) will be input as a weighted average of
the language skill of the speaker and listener.
- Whisper: The whisper functionality will gradually "fade out" a
whisper along as scale 0-1, where the fading is based on gradually
removing sections of the whisper that is (supposedly) easier to
overhear (for example "s" sounds tend to be audible even when no other
meaning can be determined).
## Usage
```python
from evennia.contrib import rplanguage
# need to be done once, here we create the "default" lang
rplanguage.add_language()
say = "This is me talking."
whisper = "This is me whispering.
print rplanguage.obfuscate_language(say, level=0.0)
<<< "This is me talking."
print rplanguage.obfuscate_language(say, level=0.5)
<<< "This is me byngyry."
print rplanguage.obfuscate_language(say, level=1.0)
<<< "Daly ly sy byngyry."
result = rplanguage.obfuscate_whisper(whisper, level=0.0)
<<< "This is me whispering"
result = rplanguage.obfuscate_whisper(whisper, level=0.2)
<<< "This is m- whisp-ring"
result = rplanguage.obfuscate_whisper(whisper, level=0.5)
<<< "---s -s -- ---s------"
result = rplanguage.obfuscate_whisper(whisper, level=0.7)
<<< "---- -- -- ----------"
result = rplanguage.obfuscate_whisper(whisper, level=1.0)
<<< "..."
```
## Custom languages
To set up new languages, you need to run `add_language()`
helper function in this module. The arguments of this function (see below)
are used to store the new language in the database (in the LanguageHandler,
which is a type of Script).
If you want to remember the language definitions, you could put them all
in a module along with the `add_language` call as a quick way to
rebuild the language on a db reset:
```python
# a stand-alone module somewhere under mygame. Just import this
# once to automatically add the language!
from evennia.contrib.rpg.rpsystem import rplanguage
grammar = (...)
vowels = "eaouy"
# etc
rplanguage.add_language(grammar=grammar, vowels=vowels, ...)
```
The variables of `add_language` allows you to customize the "feel" of
the semi-random language you are creating. Especially
the `word_length_variance` helps vary the length of translated
words compared to the original. You can also add your own
dictionary and "fix" random words for a list of input words.
## Example
Below is an example module creating "elvish", using "rounder" vowels and sounds:
```python
# vowel/consonant grammar possibilities
grammar = ("v vv vvc vcc vvcc cvvc vccv vvccv vcvccv vcvcvcc vvccvvcc "
"vcvvccvvc cvcvvcvvcc vcvcvvccvcvv")
# all not in this group is considered a consonant
vowels = "eaoiuy"
# you need a representative of all of the minimal grammars here, so if a
# grammar v exists, there must be atleast one phoneme available with only
# one vowel in it
phonemes = ("oi oh ee ae aa eh ah ao aw ay er ey ow ia ih iy "
"oy ua uh uw y p b t d f v t dh s z sh zh ch jh k "
"ng g m n l r w")
# how much the translation varies in length compared to the original. 0 is
# smallest, higher values give ever bigger randomness (including removing
# short words entirely)
word_length_variance = 1
# if a proper noun (word starting with capitalized letter) should be
# translated or not. If not (default) it means e.g. names will remain
# unchanged across languages.
noun_translate = False
# all proper nouns (words starting with a capital letter not at the beginning
# of a sentence) can have either a postfix or -prefix added at all times
noun_postfix = "'la"
# words in dict will always be translated this way. The 'auto_translations'
# is instead a list or filename to file with words to use to help build a
# bigger dictionary by creating random translations of each word in the
# list *once* and saving the result for subsequent use.
manual_translations = {"the":"y'e", "we":"uyi", "she":"semi", "he":"emi",
"you": "do", 'me':'mi','i':'me', 'be':"hy'e", 'and':'y'}
rplanguage.add_language(key="elvish", phonemes=phonemes, grammar=grammar,
word_length_variance=word_length_variance,
noun_translate=noun_translate,
noun_postfix=noun_postfix, vowels=vowels,
manual_translations=manual_translations,
auto_translations="my_word_file.txt")
```
This will produce a decicively more "rounded" and "soft" language
than the default one. The few manual_translations also make sure
to make it at least look superficially "reasonable".
The `auto_translations` keyword is useful, this accepts either a
list or a path to a file of words (one per line) to automatically
create fixed translations for according to the grammatical rules.
This allows to quickly build a large corpus of translated words
that never change (if this is desired).
"""
import re
from collections import defaultdict
from random import choice, randint
from evennia import DefaultScript
from evennia.utils import logger
# ------------------------------------------------------------
#
# Obfuscate language
#
# ------------------------------------------------------------
# default language grammar
_PHONEMES = (
"ea oh ae aa eh ah ao aw ai er ey ow ia ih iy oy ua uh uw a e i u y p b t d f v t dh "
"s z sh zh ch jh k ng g m n l r w"
)
_VOWELS = "eaoiuy"
# these must be able to be constructed from phonemes (so for example,
# if you have v here, there must exist at least one single-character
# vowel phoneme defined above)
_GRAMMAR = "v cv vc cvv vcc vcv cvcc vccv cvccv cvcvcc cvccvcv vccvccvc cvcvccvv cvcvcvcvv"
_RE_FLAGS = re.MULTILINE + re.IGNORECASE + re.DOTALL + re.UNICODE
_RE_GRAMMAR = re.compile(r"vv|cc|v|c", _RE_FLAGS)
_RE_WORD = re.compile(r"\w+", _RE_FLAGS)
# superfluous chars, except ` ... `
_RE_EXTRA_CHARS = re.compile(r"\s+(?!... )(?=\W)|[,.?;](?!.. )(?=[,?;]|\s+[,.?;])", _RE_FLAGS)
class LanguageError(RuntimeError):
pass
class LanguageExistsError(LanguageError):
pass
class LanguageHandler(DefaultScript):
"""
This is a storage class that should usually not be created on its
own. It's automatically created by a call to `obfuscate_language`
or `add_language` below.
Languages are implemented as a "logical" pseudo- consistent language
algorith here. The idea is that a language is built up from
phonemes. These are joined together according to a "grammar" of
possible phoneme- combinations and allowed characters. It may
sound simplistic, but this allows to easily make
"similar-sounding" languages. One can also custom-define a
dictionary of some common words to give further consistency.
Optionally, the system also allows an input list of common words
to be loaded and given random translations. These will be stored
to disk and will thus not change. This gives a decent "stability"
of the language but if the goal is to obfuscate, this may allow
players to eventually learn to understand the gist of a sentence
even if their characters can not. Any number of languages can be
created this way.
This nonsense language will partially replace the actual spoken
language when so desired (usually because the speaker/listener
don't know the language well enough).
"""
def at_script_creation(self):
"Called when script is first started"
self.key = "language_handler"
self.persistent = True
self.db.language_storage = {}
def add(
self,
key="default",
phonemes=_PHONEMES,
grammar=_GRAMMAR,
word_length_variance=0,
noun_translate=False,
noun_prefix="",
noun_postfix="",
vowels=_VOWELS,
manual_translations=None,
auto_translations=None,
force=False,
):
"""
Add a new language. Note that you generally only need to do
this once per language and that adding an existing language
will re-initialize all the random components to new permanent
values.
Args:
key (str, optional): The name of the language. This
will be used as an identifier for the language so it
should be short and unique.
phonemes (str, optional): Space-separated string of all allowed
phonemes in this language. If either of the base phonemes
(c, v, cc, vv) are present in the grammar, the phoneme list must
at least include one example of each.
grammar (str): All allowed consonant (c) and vowel (v) combinations
allowed to build up words. Grammars are broken into the base phonemes
(c, v, cc, vv) prioritizing the longer bases. So cvv would be a
the c + vv (would allow for a word like 'die' whereas
cvcvccc would be c+v+c+v+cc+c (a word like 'galosch').
word_length_variance (real): The variation of length of words.
0 means a minimal variance, higher variance may mean words
have wildly varying length; this strongly affects how the
language "looks".
noun_translate (bool, optional): If a proper noun should be translated or
not. By default they will not, allowing for e.g. the names of characters
to be understandable. A 'noun' is identified as a capitalized word
*not at the start of a sentence*. This simple metric means that names
starting a sentence always will be translated (- but hey, maybe
the fantasy language just never uses a noun at the beginning of
sentences, who knows?)
noun_prefix (str, optional): A prefix to go before every noun
in this language (if any).
noun_postfix (str, optuonal): A postfix to go after every noun
in this language (if any, usually best to avoid combining
with `noun_prefix` or language becomes very wordy).
vowels (str, optional): Every vowel allowed in this language.
manual_translations (dict, optional): This allows for custom-setting
certain words in the language to mean the same thing. It is
on the form `{real_word: fictional_word}`, for example
`{"the", "y'e"}` .
auto_translations (str or list, optional): These are lists
words that should be auto-translated with a random, but
fixed, translation. If a path to a file, this file should
contain a list of words to produce translations for, one
word per line. If a list, the list's elements should be
the words to translate. The `manual_translations` will
always override overlapping translations created
automatically.
force (bool, optional): Unless true, will not allow the addition
of a language that is already created.
Raises:
LanguageExistsError: Raised if trying to adding a language
with a key that already exists, without `force` being set.
Notes:
The `word_file` is for example a word-frequency list for
the N most common words in the host language. The
translations will be random, but will be stored
persistently to always be the same. This allows for
building a quick, decently-sounding fictive language that
tend to produce the same "translation" (mostly) with the
same input sentence.
"""
if key in self.db.language_storage and not force:
raise LanguageExistsError(
"Language is already created. Re-adding it will re-build"
" its dictionary map. Use 'force=True' keyword if you are sure."
)
# create grammar_component->phoneme mapping
# {"vv": ["ea", "oh", ...], ...}
grammar2phonemes = defaultdict(list)
for phoneme in phonemes.split():
if re.search(r"\W", phoneme, re.U):
raise LanguageError("The phoneme '%s' contains an invalid character." % phoneme)
gram = "".join(["v" if char in vowels else "c" for char in phoneme])
grammar2phonemes[gram].append(phoneme)
# allowed grammar are grouped by length
gramdict = defaultdict(list)
for gram in grammar.split():
if re.search(r"\W|(!=[cv])", gram):
raise LanguageError(
"The grammar '%s' is invalid (only 'c' and 'v' are allowed)" % gram
)
gramdict[len(gram)].append(gram)
grammar = dict(gramdict)
# create automatic translation
translation = {}
if auto_translations:
if isinstance(auto_translations, str):
# path to a file rather than a list
with open(auto_translations, "r") as f:
auto_translations = f.readlines()
for word in auto_translations:
word = word.strip()
lword = len(word)
new_word = ""
wlen = max(0, lword + sum(randint(-1, 1) for i in range(word_length_variance)))
if wlen not in grammar:
# always create a translation, use random length
structure = choice(grammar[choice(list(grammar))])
else:
# use the corresponding length
structure = choice(grammar[wlen])
for match in _RE_GRAMMAR.finditer(structure):
try:
new_word += choice(grammar2phonemes[match.group()])
except IndexError:
raise IndexError(
"Could not find a matching phoneme for the grammar "
f"'{match.group()}'. Make there is at least one phoneme matching this "
"combination of consonants and vowels."
)
translation[word.lower()] = new_word.lower()
if manual_translations:
# update with manual translations
translation.update(
dict((key.lower(), value.lower()) for key, value in manual_translations.items())
)
# store data
storage = {
"translation": translation,
"grammar": grammar,
"grammar2phonemes": dict(grammar2phonemes),
"word_length_variance": word_length_variance,
"noun_translate": noun_translate,
"noun_prefix": noun_prefix,
"noun_postfix": noun_postfix,
}
self.db.language_storage[key] = storage
def _translate_sub(self, match):
"""
Replacer method called by re.sub when
traversing the language string.
Args:
match (re.matchobj): Match object from regex.
Returns:
converted word.
Notes:
Assumes self.lastword and self.level is available
on the object.
"""
word = match.group()
lword = len(word)
# find out what preceeded this word
wpos = match.start()
preceeding = match.string[:wpos].strip()
start_sentence = preceeding.endswith((".", "!", "?")) or not preceeding
if len(word) <= self.level:
# below level. Don't translate
new_word = word
else:
# try to translate the word from dictionary
new_word = self.language["translation"].get(word.lower(), "")
if not new_word:
# no dictionary translation. Generate one
# make up translation on the fly. Length can
# vary from un-translated word.
wlen = max(
0,
lword
+ sum(randint(-1, 1) for i in range(self.language["word_length_variance"])),
)
grammar = self.language["grammar"]
if wlen not in grammar:
if randint(0, 1) == 0:
# this word has no direct translation!
wlen = 0
new_word = ""
else:
# use random word length
wlen = choice(list(grammar.keys()))
if wlen:
structure = choice(grammar[wlen])
grammar2phonemes = self.language["grammar2phonemes"]
for match in _RE_GRAMMAR.finditer(structure):
# there are only four combinations: vv,cc,c,v
try:
new_word += choice(grammar2phonemes[match.group()])
except KeyError:
logger.log_trace(
"You need to supply at least one example of each of "
"the four base phonemes (c, v, cc, vv)"
)
# abort translation here
new_word = ""
break
if word.istitle():
if not start_sentence:
# this is a noun. We miss nouns at the start of
# sentences this way, but it's as good as we can get
# with this simple analysis. Maybe the fantasy language
# just don't consider nouns at the beginning of
# sentences, who knows?
if not self.language.get("noun_translate", False):
# don't translate what we identify as proper nouns (names)
new_word = word
# add noun prefix and/or postfix
new_word = "{prefix}{word}{postfix}".format(
prefix=self.language["noun_prefix"],
word=new_word.capitalize(),
postfix=self.language["noun_postfix"],
)
if len(word) > 1 and word.isupper():
# keep LOUD words loud also when translated
new_word = new_word.upper()
if start_sentence:
new_word = new_word.capitalize()
return new_word
def translate(self, text, level=0.0, language="default"):
"""
Translate the text according to the given level.
Args:
text (str): The text to translate
level (real): Value between 0.0 and 1.0, where
0.0 means no obfuscation (text returned unchanged) and
1.0 means full conversion of every word. The closer to
1, the shorter words will be translated.
language (str): The language key identifier.
Returns:
text (str): A translated string.
"""
if level == 0.0:
# no translation
return text
language = self.db.language_storage.get(language, None)
if not language:
return text
self.language = language
# configuring the translation
self.level = int(10 * (1.0 - max(0, min(level, 1.0))))
translation = _RE_WORD.sub(self._translate_sub, text)
# the substitution may create too long empty spaces, remove those
return _RE_EXTRA_CHARS.sub("", translation)
# Language access functions
_LANGUAGE_HANDLER = None
def obfuscate_language(text, level=0.0, language="default"):
"""
Main access method for the language parser.
Args:
text (str): Text to obfuscate.
level (real, optional): A value from 0.0-1.0 determining
the level of obfuscation where 0 means no obfuscation
(string returned unchanged) and 1.0 means the entire
string is obfuscated.
language (str, optional): The identifier of a language
the system understands.
Returns:
translated (str): The translated text.
"""
# initialize the language handler and cache it
global _LANGUAGE_HANDLER
if not _LANGUAGE_HANDLER:
try:
_LANGUAGE_HANDLER = LanguageHandler.objects.get(db_key="language_handler")
except LanguageHandler.DoesNotExist:
if not _LANGUAGE_HANDLER:
from evennia import create_script
_LANGUAGE_HANDLER = create_script(LanguageHandler)
return _LANGUAGE_HANDLER.translate(text, level=level, language=language)
def add_language(**kwargs):
"""
Access function to creating a new language. See the docstring of
`LanguageHandler.add` for list of keyword arguments.
"""
global _LANGUAGE_HANDLER
if not _LANGUAGE_HANDLER:
try:
_LANGUAGE_HANDLER = LanguageHandler.objects.get(db_key="language_handler")
except LanguageHandler.DoesNotExist:
if not _LANGUAGE_HANDLER:
from evennia import create_script
_LANGUAGE_HANDLER = create_script(LanguageHandler)
_LANGUAGE_HANDLER.add(**kwargs)
def available_languages():
"""
Returns all available language keys.
Returns:
languages (list): List of key strings of all available
languages.
"""
global _LANGUAGE_HANDLER
if not _LANGUAGE_HANDLER:
try:
_LANGUAGE_HANDLER = LanguageHandler.objects.get(db_key="language_handler")
except LanguageHandler.DoesNotExist:
if not _LANGUAGE_HANDLER:
from evennia import create_script
_LANGUAGE_HANDLER = create_script(LanguageHandler)
return list(_LANGUAGE_HANDLER.attributes.get("language_storage", {}))
# -----------------------------------------------------------------------------
#
# Whisper obscuration
#
# This obsucration table is designed by obscuring certain vowels first,
# following by consonants that tend to be more audible over long distances,
# like s. Finally it does non-auditory replacements, like exclamation marks and
# capitalized letters (assumed to be spoken louder) that may still give a user
# some idea of the sentence structure. Then the word lengths are also
# obfuscated and finally the whisper length itself.
#
# ------------------------------------------------------------------------------
_RE_WHISPER_OBSCURE = [
re.compile(r"^$", _RE_FLAGS), # This is a Test! #0 full whisper
re.compile(r"[ae]", _RE_FLAGS), # This -s - Test! #1 add uy
re.compile(r"[aeuy]", _RE_FLAGS), # This -s - Test! #2 add oue
re.compile(r"[aeiouy]", _RE_FLAGS), # Th-s -s - T-st! #3 add all consonants
re.compile(r"[aeiouybdhjlmnpqrv]", _RE_FLAGS), # T--s -s - T-st! #4 add hard consonants
re.compile(r"[a-eg-rt-z]", _RE_FLAGS), # T--s -s - T-s-! #5 add all capitals
re.compile(r"[A-EG-RT-Za-eg-rt-z]", _RE_FLAGS), # ---s -s - --s-! #6 add f
re.compile(r"[A-EG-RT-Za-rt-z]", _RE_FLAGS), # ---s -s - --s-! #7 add s
re.compile(r"[A-EG-RT-Za-z]", _RE_FLAGS), # ---- -- - ----! #8 add capital F
re.compile(r"[A-RT-Za-z]", _RE_FLAGS), # ---- -- - ----! #9 add capital S
re.compile(r"[\w]", _RE_FLAGS), # ---- -- - ----! #10 non-alphanumerals
re.compile(r"[\S]", _RE_FLAGS), # ---- -- - ---- #11 words
re.compile(r"[\w\W]", _RE_FLAGS), # -------------- #12 whisper length
re.compile(r".*", _RE_FLAGS),
] # ... #13 (always same length)
def obfuscate_whisper(whisper, level=0.0):
"""
Obfuscate whisper depending on a pre-calculated level
(that may depend on distance, listening skill etc)
Args:
whisper (str): The whisper string to obscure. The
entire string will be considered in the obscuration.
level (real, optional): This is a value 0-1, where 0
means not obscured (whisper returned unchanged) and 1
means fully obscured.
"""
level = min(max(0.0, level), 1.0)
olevel = int(13.0 * level)
if olevel == 13:
return "..."
else:
return _RE_WHISPER_OBSCURE[olevel].sub("-", whisper)

1807
lib/rpsystem/rpsystem.py Normal file

File diff suppressed because it is too large Load Diff

364
lib/rpsystem/tests.py Normal file
View File

@ -0,0 +1,364 @@
"""
Tests for RP system
"""
import time
from anything import Anything
from evennia import create_object
from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.test_resources import BaseEvenniaTest
from . import rplanguage, rpsystem
mtrans = {"testing": "1", "is": "2", "a": "3", "human": "4"}
atrans = ["An", "automated", "advantageous", "repeatable", "faster"]
text = (
"Automated testing is advantageous for a number of reasons: "
"tests may be executed Continuously without the need for human "
"intervention, They are easily repeatable, and often faster."
)
class TestLanguage(BaseEvenniaTest):
def setUp(self):
super().setUp()
rplanguage.add_language(
key="testlang",
word_length_variance=1,
noun_prefix="bara",
noun_postfix="'y",
manual_translations=mtrans,
auto_translations=atrans,
force=True,
)
rplanguage.add_language(
key="binary",
phonemes="oo ii a ck w b d t",
grammar="cvvv cvv cvvcv cvvcvv cvvvc cvvvcvv cvvc",
noun_prefix="beep-",
word_length_variance=4,
)
def tearDown(self):
super().tearDown()
rplanguage._LANGUAGE_HANDLER.delete()
rplanguage._LANGUAGE_HANDLER = None
def test_obfuscate_language(self):
result0 = rplanguage.obfuscate_language(text, level=0.0, language="testlang")
self.assertEqual(result0, text)
result1 = rplanguage.obfuscate_language(text, level=1.0, language="testlang")
result2 = rplanguage.obfuscate_language(text, level=1.0, language="testlang")
result3 = rplanguage.obfuscate_language(text, level=1.0, language="binary")
self.assertNotEqual(result1, text)
self.assertNotEqual(result3, text)
result1, result2 = result1.split(), result2.split()
self.assertEqual(result1[:4], result2[:4])
self.assertEqual(result1[1], "1")
self.assertEqual(result1[2], "2")
self.assertEqual(result2[-1], result2[-1])
def test_faulty_language(self):
self.assertRaises(
rplanguage.LanguageError,
rplanguage.add_language,
key="binary2",
phonemes="w b d t oe ee, oo e o a wh dw bw", # erroneous comma
grammar="cvvv cvv cvvcv cvvcvvo cvvvc cvvvcvv cvvc c v cc vv ccvvc ccvvccvv ",
vowels="oea",
word_length_variance=4,
)
def test_available_languages(self):
self.assertEqual(list(sorted(rplanguage.available_languages())), ["binary", "testlang"])
def test_obfuscate_whisper(self):
self.assertEqual(rplanguage.obfuscate_whisper(text, level=0.0), text)
assert rplanguage.obfuscate_whisper(text, level=0.1).startswith(
"-utom-t-d t-sting is -dv-nt-g-ous for - numb-r of r--sons: t-sts m-y b- -x-cut-d Continuously"
)
assert rplanguage.obfuscate_whisper(text, level=0.5).startswith(
"--------- --s---- -s -----------s f-- - ------ -f ---s--s: --s-s "
)
self.assertEqual(rplanguage.obfuscate_whisper(text, level=1.0), "...")
# Testing of emoting / sdesc / recog system
sdesc0 = "A nice sender of emotes"
sdesc1 = "The first receiver of emotes."
sdesc2 = "Another nice colliding sdesc-guy for tests"
recog01 = "Mr Receiver"
recog02 = "Mr Receiver2"
recog10 = "Mr Sender"
emote = 'With a flair, /me looks at /first and /colliding sdesc-guy. She says "This is a test."'
fallback_emote = "/Me is distracted from /first by /nomatch."
case_emote = "/Me looks at /first. Then, /me looks at /FIRST, /First and /Colliding twice."
poss_emote = "/Me frowns at /first for trying to steal /me's test."
class TestRPSystem(BaseEvenniaTest):
maxDiff = None
def setUp(self):
super().setUp()
self.room = create_object(rpsystem.ContribRPRoom, key="Location")
self.speaker = create_object(rpsystem.ContribRPCharacter, key="Sender", location=self.room)
self.receiver1 = create_object(
rpsystem.ContribRPCharacter, key="Receiver1", location=self.room
)
self.receiver2 = create_object(
rpsystem.ContribRPCharacter, key="Receiver2", location=self.room
)
def test_posed_contents(self):
self.obj1 = create_object(rpsystem.ContribRPObject, key="thing", location=self.room)
self.obj2 = create_object(rpsystem.ContribRPObject, key="thing", location=self.room)
self.obj3 = create_object(rpsystem.ContribRPObject, key="object", location=self.room)
room_display = self.room.return_appearance(self.speaker)
self.assertIn("An object and two things are here.", room_display)
self.obj3.db.pose = "is on the ground."
room_display = self.room.return_appearance(self.speaker)
self.assertIn("Two things are here.", room_display)
self.assertIn("An object is on the ground.", room_display)
def test_sdesc_handler(self):
self.speaker.sdesc.add(sdesc0)
self.assertEqual(self.speaker.sdesc.get(), sdesc0)
self.speaker.sdesc.add("This is {#324} ignored")
self.assertEqual(self.speaker.sdesc.get(), "This is 324 ignored")
def test_recog_handler(self):
self.speaker.sdesc.add(sdesc0)
self.receiver1.sdesc.add(sdesc1)
self.speaker.recog.add(self.receiver1, recog01)
self.speaker.recog.add(self.receiver2, recog02)
self.assertEqual(self.speaker.recog.get(self.receiver1), recog01)
self.assertEqual(self.speaker.recog.get(self.receiver2), recog02)
self.speaker.recog.remove(self.receiver1)
self.assertEqual(self.speaker.recog.get(self.receiver1), None)
self.assertEqual(self.speaker.recog.all(), {"Mr Receiver2": self.receiver2})
def test_parse_language(self):
self.assertEqual(
rpsystem.parse_language(self.speaker, emote),
(
"With a flair, /me looks at /first and /colliding sdesc-guy. She says {##0}",
{"##0": (None, '"This is a test."')},
),
)
def test_parse_sdescs_and_recogs(self):
speaker = self.speaker
speaker.sdesc.add(sdesc0)
self.receiver1.sdesc.add(sdesc1)
self.receiver2.sdesc.add(sdesc2)
id0 = f"#{speaker.id}"
id1 = f"#{self.receiver1.id}"
id2 = f"#{self.receiver2.id}"
candidates = (self.receiver1, self.receiver2)
result = (
"With a flair, {"
+ id0
+ "} looks at {"
+ id1
+ "} and {"
+ id2
+ '}. She says "This is a test."',
{
id2: self.receiver2,
id1: self.receiver1,
id0: speaker,
},
)
self.assertEqual(
rpsystem.parse_sdescs_and_recogs(speaker, candidates, emote, case_sensitive=False),
result,
)
self.speaker.recog.add(self.receiver1, recog01)
self.assertEqual(
rpsystem.parse_sdescs_and_recogs(speaker, candidates, emote, case_sensitive=False),
result,
)
def test_possessive_selfref(self):
speaker = self.speaker
speaker.sdesc.add(sdesc0)
self.receiver1.sdesc.add(sdesc1)
self.receiver2.sdesc.add(sdesc2)
id0 = f"#{speaker.id}"
id1 = f"#{self.receiver1.id}"
id2 = f"#{self.receiver2.id}"
candidates = (self.receiver1, self.receiver2)
result = (
"{" + id0 + "} frowns at {" + id1 + "} for trying to steal {" + id0 + "}'s test.",
{
id1: self.receiver1,
id0: speaker,
},
)
self.assertEqual(
rpsystem.parse_sdescs_and_recogs(speaker, candidates, poss_emote, case_sensitive=False),
result,
)
def test_get_sdesc(self):
looker = self.speaker # Sender
target = self.receiver1 # Receiver1
looker.sdesc.add(sdesc0) # A nice sender of emotes
target.sdesc.add(sdesc1) # The first receiver of emotes.
# sdesc with no processing
self.assertEqual(looker.get_sdesc(target), "The first receiver of emotes.")
# sdesc with processing
self.assertEqual(
looker.get_sdesc(target, process=True), "|bThe first receiver of emotes.|n"
)
looker.recog.add(target, recog01) # Mr Receiver
# recog with no processing
self.assertEqual(looker.get_sdesc(target), "Mr Receiver")
# recog with processing
self.assertEqual(looker.get_sdesc(target, process=True), "|mMr Receiver|n")
def test_send_emote(self):
speaker = self.speaker
receiver1 = self.receiver1
receiver2 = self.receiver2
receivers = [speaker, receiver1, receiver2]
speaker.sdesc.add(sdesc0)
receiver1.sdesc.add(sdesc1)
receiver2.sdesc.add(sdesc2)
speaker.msg = lambda text, **kwargs: setattr(self, "out0", text)
receiver1.msg = lambda text, **kwargs: setattr(self, "out1", text)
receiver2.msg = lambda text, **kwargs: setattr(self, "out2", text)
rpsystem.send_emote(speaker, receivers, emote, case_sensitive=False)
self.assertEqual(
self.out0[0],
"With a flair, |mSender|n looks at |bThe first receiver of emotes.|n "
'and |bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n',
)
self.assertEqual(
self.out1[0],
"With a flair, |bA nice sender of emotes|n looks at |mReceiver1|n and "
'|bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n',
)
self.assertEqual(
self.out2[0],
"With a flair, |bA nice sender of emotes|n looks at |bThe first "
'receiver of emotes.|n and |mReceiver2|n. She says |w"This is a test."|n',
)
def test_send_emote_fallback(self):
speaker = self.speaker
receiver1 = self.receiver1
receiver2 = self.receiver2
receivers = [speaker, receiver1, receiver2]
speaker.sdesc.add(sdesc0)
receiver1.sdesc.add(sdesc1)
receiver2.sdesc.add(sdesc2)
speaker.msg = lambda text, **kwargs: setattr(self, "out0", text)
receiver1.msg = lambda text, **kwargs: setattr(self, "out1", text)
receiver2.msg = lambda text, **kwargs: setattr(self, "out2", text)
rpsystem.send_emote(speaker, receivers, fallback_emote, fallback="something")
self.assertEqual(
self.out0[0],
"|mSender|n is distracted from |bthe first receiver of emotes.|n by something.",
)
self.assertEqual(
self.out1[0],
"|bA nice sender of emotes|n is distracted from |mReceiver1|n by something.",
)
self.assertEqual(
self.out2[0],
"|bA nice sender of emotes|n is distracted from |bthe first receiver of emotes.|n by something.",
)
def test_send_case_sensitive_emote(self):
"""Test new case-sensitive rp-parsing"""
speaker = self.speaker
receiver1 = self.receiver1
receiver2 = self.receiver2
receivers = [speaker, receiver1, receiver2]
speaker.sdesc.add(sdesc0)
receiver1.sdesc.add(sdesc1)
receiver2.sdesc.add(sdesc2)
speaker.msg = lambda text, **kwargs: setattr(self, "out0", text)
receiver1.msg = lambda text, **kwargs: setattr(self, "out1", text)
receiver2.msg = lambda text, **kwargs: setattr(self, "out2", text)
rpsystem.send_emote(speaker, receivers, case_emote)
self.assertEqual(
self.out0[0],
"|mSender|n looks at |bthe first receiver of emotes.|n. Then, |mSender|n "
"looks at |bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of emotes.|n "
"and |bAnother nice colliding sdesc-guy for tests|n twice.",
)
self.assertEqual(
self.out1[0],
"|bA nice sender of emotes|n looks at |mReceiver1|n. Then, "
"|ba nice sender of emotes|n looks at |mReceiver1|n, |mReceiver1|n "
"and |bAnother nice colliding sdesc-guy for tests|n twice.",
)
self.assertEqual(
self.out2[0],
"|bA nice sender of emotes|n looks at |bthe first receiver of emotes.|n. "
"Then, |ba nice sender of emotes|n looks at |bTHE FIRST RECEIVER OF EMOTES.|n, "
"|bThe first receiver of emotes.|n and |mReceiver2|n twice.",
)
def test_rpsearch(self):
self.speaker.sdesc.add(sdesc0)
self.receiver1.sdesc.add(sdesc1)
self.receiver2.sdesc.add(sdesc2)
self.speaker.msg = lambda text, **kwargs: setattr(self, "out0", text)
self.assertEqual(self.speaker.search("receiver of emotes"), self.receiver1)
self.assertEqual(self.speaker.search("colliding"), self.receiver2)
class TestRPSystemCommands(BaseEvenniaCommandTest):
def setUp(self):
super().setUp()
self.char1.swap_typeclass(rpsystem.ContribRPCharacter)
self.char2.swap_typeclass(rpsystem.ContribRPCharacter)
def test_commands(self):
self.call(
rpsystem.CmdSdesc(), "Foobar Character", "Char's sdesc was set to 'Foobar Character'."
)
self.call(
rpsystem.CmdSdesc(),
"BarFoo Character",
"Char2's sdesc was set to 'BarFoo Character'.",
caller=self.char2,
)
self.call(rpsystem.CmdSay(), "Hello!", 'Char says, "Hello!"')
self.call(rpsystem.CmdEmote(), "/me smiles to /BarFoo.", "Char smiles to BarFoo Character")
self.call(
rpsystem.CmdPose(),
"stands by the bar",
"Pose will read 'Foobar Character stands by the bar.'.",
)
self.call(
rpsystem.CmdRecog(),
"barfoo as friend",
"You will now remember BarFoo Character as friend.",
)
self.call(
rpsystem.CmdRecog(),
"",
"Currently recognized (use 'recog <sdesc> as <alias>' to add new "
"and 'forget <alias>' to remove):\n friend (BarFoo Character)",
)
self.call(
rpsystem.CmdRecog(),
"friend",
"You will now know them only as 'BarFoo Character'",
cmdstring="forget",
)

9
package.json Normal file
View File

@ -0,0 +1,9 @@
{
"scripts": {
"build": "./node_modules/sass/sass.js scss/main.scss web/static/website/css/main.css"
},
"dependencies": {
"bootstrap": "^5.2.3",
"sass": "^1.58.0"
}
}

330
scss/_bootswatch.scss Normal file
View File

@ -0,0 +1,330 @@
// Vapor 5.2.2
// Bootswatch
// Variables
$web-font-path: "https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap" !default;
@if $web-font-path {
@import url($web-font-path);
}
$outrun: false !default;
:root {
color-scheme: dark;
}
// Mixins
@mixin text-shadow($color) {
text-shadow: 0 0 1px rgba($color, .6), 0 0 3px rgba($color, .5), 0 0 .5rem rgba($color, .3), 0 0 2rem rgba($color, .2);
}
@mixin text-shadow-sm($color) {
text-shadow: 0 0 1px rgba($color, .3), 0 0 2px rgba($color, .3), 0 0 5px rgba($color, .2);
}
@mixin box-shadow($color) {
box-shadow: 0 0 2px rgba($color, .9), 0 0 4px rgba($color, .4), 0 0 1rem rgba($color, .3), 0 0 4rem rgba($color, .1);
}
@mixin box-shadow-lg($color) {
box-shadow: 0 0 2rem rgba(tint-color($color, 10%), .4), 0 0 8rem rgba(tint-color($color, 10%), .3);
}
@mixin header-shadow($color) {
h1,
h2,
h3,
h4,
h5,
h6,
.h1,
.h2,
.h3,
.h4,
.h5,
.h6 {
@include text-shadow($color);
}
}
// Body
body {
background-image: linear-gradient(shade-color($body-bg, 10%) 0%, $body-bg 7%, $body-bg 80%, shade-color(#173e98, 50%) 100%);
@include text-shadow-sm($body-color);
@if ($outrun) {
position: relative;
overflow-x: hidden;
&::after {
position: absolute;
bottom: 0;
left: -50vw;
z-index: -1;
display: block;
width: 200vw;
height: 100vh;
content: "";
background-image: repeating-linear-gradient(rgba($blue, .6) 0 1px, transparent 1px 100%), repeating-linear-gradient(90deg, rgba($blue, .6) 0 1px, transparent 1px 100%);
background-size: 200px 200px;
transform: perspective(50vh) rotateX(50deg) translateY(-11.3vh) translateZ(10px);
transform-origin: center center;
}
}
}
// Typography
@include header-shadow($cyan);
.text {
@each $color, $value in $theme-colors {
&-#{$color} {
@include text-shadow-sm($value);
}
}
&-white {
@include text-shadow-sm($white);
@include header-shadow($white);
}
&-muted {
@include text-shadow-sm(shade-color($text-muted, 20%));
}
}
a {
@include text-shadow-sm($link-color);
}
.blockquote {
&-footer {
@include text-shadow-sm(shade-color($blockquote-footer-color, 20%));
}
}
hr {
border-top: 1px solid $primary;
@include box-shadow($primary);
opacity: 1;
}
// Tables
table,
.table {
@include text-shadow-sm($white);
}
// Buttons
.btn {
@each $color, $value in $theme-colors {
@if (($color != dark) or ($color != link)) {
&-#{$color},
&-outline-#{$color} {
@include box-shadow($value);
}
}
&-#{$color} {
@include text-shadow-sm($white);
}
&-outline-#{$color} {
color: $white;
border-width: 2px;
}
}
&-dark {
@include box-shadow($primary);
}
&-link {
box-shadow: none;
@include text-shadow($body-color);
}
&-outline-dark {
color: $white;
}
}
// Navbars
.navbar {
@each $color, $value in $theme-colors {
&.bg-#{$color} {
@include box-shadow($value);
}
}
}
.navbar-dark {
&,
a {
@include text-shadow-sm($navbar-dark-color);
}
.navbar-brand {
@include text-shadow($navbar-dark-color);
}
}
.navbar-light {
&,
a {
@include text-shadow-sm($navbar-light-color);
}
.navbar-brand {
@include text-shadow($navbar-light-color);
}
}
// Navs
.nav-link {
&.disabled {
@include text-shadow-sm($nav-link-disabled-color);
}
}
.nav-tabs .nav-link.active,
.nav-tabs .nav-item.show .nav-link {
@include text-shadow-sm($component-active-bg);
}
.nav-pills .nav-link.active,
.nav-pills .show > .nav-link {
@include box-shadow($component-active-bg);
}
.breadcrumb {
&-item.active {
@include text-shadow-sm($breadcrumb-active-color);
}
&-item + .breadcrumb-item::before {
@include text-shadow-sm($breadcrumb-divider-color);
}
}
.page-link {
border-radius: $border-radius;
}
.page-item.active {
.page-link {
@include box-shadow($component-active-bg);
}
}
// Forms
legend {
@include text-shadow($body-color);
}
.valid-feedback {
@include text-shadow-sm($success);
}
.invalid-feedback {
@include text-shadow-sm($danger);
}
// Indicators
.alert {
@each $color, $value in $theme-colors {
&-#{$color} {
color: $white;
background-color: $value;
@include text-shadow-sm($white);
@include box-shadow-lg($value);
}
}
.alert-link,
a {
color: $white;
}
@include header-shadow($white);
}
.progress {
overflow: visible;
&-bar {
@include box-shadow($primary);
@each $color, $value in $theme-colors {
&.bg-#{$color} {
@include box-shadow($value);
}
}
}
}
.tooltip {
&-inner,
.arrow {
@include box-shadow-lg($primary);
}
}
.modal,
.popover,
.toast {
@include text-shadow-sm($white);
@include header-shadow($white);
}
.popover,
.toast {
@include box-shadow-lg($primary);
}
.modal {
&-content {
@include box-shadow-lg($primary);
}
}
// Containers
.list-group {
&-item.active {
@include header-shadow($white);
}
}
.card {
background-color: transparent;
@include text-shadow-sm($white);
@each $color, $value in $theme-colors {
@if ($color != dark) {
&.border-#{$color} {
@include box-shadow($value);
}
}
}
&.border-dark {
@include box-shadow($primary);
}
@include header-shadow($white);
}

7
scss/_customvars.scss Normal file
View File

@ -0,0 +1,7 @@
.navbar {
a.nav-link,
.navbar-brand {
color: yellow;
text-shadow: #FC0 1px 0 10px;
}
}

162
scss/_variables.scss Normal file
View File

@ -0,0 +1,162 @@
// Vapor 5.2.2
// Bootswatch
$theme: "vapor" !default;
//
// Color system
//
$white: #fff !default;
$gray-100: #f8f9fa !default;
$gray-200: #e9ecef !default;
$gray-300: #dee2e6 !default;
$gray-400: #ced4da !default;
$gray-500: #adb5bd !default;
$gray-600: #6c757d !default;
$gray-700: #495057 !default;
$gray-800: #343a40 !default;
$gray-900: #170229 !default;
$black: #000 !default;
$blue: #1ba2f6 !default;
$indigo: #6610f2 !default;
$purple: #6f42c1 !default;
$pink: #ea39b8 !default;
$red: #e44c55 !default;
$orange: #f1b633 !default;
$yellow: #ffc107 !default;
$green: #3cf281 !default;
$teal: #3f81a2 !default;
$cyan: #32fbe2 !default;
$primary: $purple !default;
$secondary: $pink !default;
$success: $green !default;
$info: $blue !default;
$warning: $yellow !default;
$danger: $red !default;
$light: #44d9e8 !default;
$dark: $gray-900 !default;
$min-contrast-ratio: 1.2 !default;
// Options
// $enable-rounded: false !default;
// Body
$body-bg: #1a0933 !default;
$body-color: $cyan !default;
// Links
$link-color: $body-color !default;
// Components
$border-width: 0 !default;
$border-radius: .15rem !default;
$border-radius-sm: .05rem !default;
$component-active-bg: $pink !default;
// Fonts
// stylelint-disable-next-line value-keyword-case
$font-family-sans-serif: Lato, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !default;
$text-muted: rgba($body-color, .3) !default;
$blockquote-footer-color: $text-muted !default;
// Tables
$table-color: $white !default;
$table-bg-scale: 0% !default;
// Forms
$input-bg: lighten($body-bg, 10%) !default;
$input-disabled-bg: lighten($body-bg, 5%) !default;
$input-color: $white !default;
$input-placeholder-color: rgba($white, .4) !default;
$input-group-addon-bg: $input-disabled-bg !default;
$form-range-track-bg: $input-bg !default;
$form-range-thumb-disabled-bg: $purple !default;
// Navs
$nav-link-disabled-color: $text-muted !default;
$nav-tabs-link-active-color: $component-active-bg !default;
// Pagination
$pagination-bg: transparent !default;
$pagination-focus-bg: transparent !default;
$pagination-hover-bg: transparent !default;
$pagination-disabled-color: $text-muted !default;
$pagination-disabled-bg: transparent !default;
// Cards
$card-border-width: 2px !default;
$card-cap-color: $white !default;
$card-color: $white !default;
// Tooltips
$tooltip-bg: $dark !default;
$tooltip-opacity: 1 !default;
// Popovers
$popover-bg: $primary !default;
$popover-header-color: $white !default;
$popover-body-color: $white !default;
// Toasts
$toast-color: $white !default;
$toast-background-color: $primary !default;
$toast-header-color: $toast-color !default;
$toast-header-background-color: $toast-background-color !default;
// Modals
$modal-content-color: $white !default;
$modal-content-bg: $primary !default;
// Progress bars
$progress-bg: $input-disabled-bg !default;
// List group
$list-group-color: $white !default;
$list-group-bg: $input-disabled-bg !default;
$list-group-hover-bg: $pink !default;
$list-group-disabled-color: $text-muted !default;
$list-group-action-color: $body-color !default;
$list-group-action-hover-color: $white !default;
$list-group-action-active-bg: $list-group-hover-bg !default;
// Breadcrumbs
$breadcrumb-divider-color: $text-muted !default;
$breadcrumb-active-color: $component-active-bg !default;

28
scss/main.scss Normal file
View File

@ -0,0 +1,28 @@
// Bootstrap functions and mixins
@import '../node_modules/bootstrap/scss/_mixins.scss';
@import '../node_modules/bootstrap/scss/_functions.scss';
// Theme variables to redefine Bootstrap vars
@import '_variables';
@import '_customvars';
// Bootstrap base variables
@import '../node_modules/bootstrap/scss/_variables.scss';
// Base Bootstrap styles
@import '../node_modules/bootstrap/scss/bootstrap.scss';
// Bootswatch rules to override defaults
@import '_bootswatch';
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
white-space: nowrap;
border: 0;
clip: rect(0,0,0,0);
}
.navbar {
background-image: url('../images/stars.gif');
background-color: transparent;
}

View File

@ -25,16 +25,32 @@ from django.conf import settings
from evennia import utils
CONNECTION_SCREEN = """
|b==============================================================|n
Welcome to |g{}|n, version {}!
If you have an existing account, connect to it by typing:
|wconnect <username> <password>|n
If you need to create an account, type (without the <>'s):
|wcreate <username> <password>|n
|n |wansi (c) garu|n |n
|n |wWelcome to..|n |n
|n |n
|n |c |n |c|n |n
|n |m |C|m|C|m|C|m|n |n
|n |m |n|w|M|n|w|M|n|w|n |n
|n |w|[m|n |w|[m|n |w|[m|n |w|[m|n |w|[m|n |w|[m|n |w|[m|n |w|[m|n |w|[m|n |w|[m|n |w|m|n|w|m|n|w|M|n |n
|n |M|m|[w|n |m|[w|n |M|[w|n |m|[w|n |M|[w|n |m|[w|n |m|[w|n |m|[w|n |m|C|M|C|M|C|n |n
|n |M|w |M |w|M|w |M|w |n |w|n |w|M|w |C |n |n
|n |x|[W|n|w|x|[W|n |w|n |w |n |w|n |w|n |w|n |n
|n |x|[W|n |x|[W|n |x|[W|n |x|[W|n |x|[W|n |x|[W|n |x|[W|n |x|[W|n |n
|n |m |n |n
|n |wBROUGHT TO YOU BY|n |m |n|m |n |n
|n |mVANTABLACK|n |m |n |m |n |m|n |n
|n |m|[w|n |M|w|[m|n|M|n |m|[w|n |m|[w|n |m|[w|n |m|[w|n |m|[w|n|m|n |n
|n |w |M |w |n |w |n |w|n |n
|n |w|n |w |x|[W|w|x|[W|n |x|[W|w|x|[W|n |n
|n |x|[W|n |x|[W|n |x|[W|n |x|[W|n |n
|n|x |rNew users: |ncreate <user> <password>|n |n
|n |n
|n|w |GExisting users: |nconnect <user> <password>|n |n
|n |n
|n |n
If you have spaces in your username, enclose it in quotes.
Enter |whelp|n for more info. |wlook|n will re-show this screen.
|b==============================================================|n""".format(
""".format(
settings.SERVERNAME, utils.get_evennia_version("short")
)

View File

@ -7,8 +7,18 @@ is setup to be the "default" character type created by the default
creation commands.
"""
from evennia.contrib.rpg.rpsystem import ContribRPCharacter
from lib.rpsystem import ContribRPCharacter
from lib.pronounsub import character as pronounsub
# rpsystem
class Character(ContribRPCharacter):
pass
def at_object_creation(self):
pronounsub.object_init(self)
super().at_object_creation()
def msg(self, text=None, from_obj=None, session=None, **kwargs):
text, from_obj, session, kwargs = pronounsub.msg(self, text, from_obj, session, **kwargs)
super().msg(text, from_obj, session, **kwargs)

50
typeclasses/oatmeal.py Normal file
View File

@ -0,0 +1,50 @@
"""
Tracery Object
This is a joke object that changes its description every time you look at it.
Requires installing pytracery as a pip dependency.
As Kate Compton puts it: "I can easily generate 10,000 bowls of plain oatmeal, with each oat being in a different position and different orientation, and mathematically speaking they will all be completely unique. But the user will likely just see a lot of oatmeal."
This relies on default Tracery and does not have a model memory (i.e. you cannot procgen object attributs based on its description and vice versa).
"""
from typeclasses.objects import Object
import tracery
from tracery.modifiers import base_english
"""
An oatmeal object has an "origin" attribute.
You make a table of rules with attributes, like:
set movie/origin:rules = ['This is #genre.a# #movie# about #subject.s#.', '#subject.s.capitalize# #sequel#: The #family.capitalize# Of All #movie.s.capitalize#!']
The table has to have category "rules" and the main entry point is called "origin". The rest is up to you:
set movie/sequel:rules = ['from hell #number#','#number#','the Resurrection','Origins','Rebooted','Reimagined','Again']
set movie/family:rules = ['mother','father','aunt','xyther','grunkle','grandpa','grandma']
set movie/number:rules = ['2','3','4','5','6','II','III','IV','V','VI','XXIII','MCMXCIII']
set movie/movie:rules = ['movie', 'flick', 'animation', 'anime', 'moving picture', 'picture']
set movie/genre:rules = ['action', 'romance', 'thriller', 'horror', 'abstract', 'slice-of-life', 'comedy', 'documentary']
set movie/subject:rules = ['wombat', '#pair# love #polycule#', 'smartphone', 'astronaut', 'explosion']
set movie/pair:rules = ['gay','aromantic','nonbinary','clueless','crazy']
set movie/polycule:rules = ['triange', 'pair', '3D icosahedron', 'polycule']
And you'll get stuff like "Crazy love polycules Reimagined: The Father Of All Animations!"
"""
class OatmealObject(Object):
"""
Can't be a self.db.desc because we *need* a function here.
"""
def get_display_desc(self, looker, **kwargs):
rules = {}
attrs = self.attributes.get(category='rules', return_obj=True)
for attr in attrs:
v = attr.value.deserialize()
if isinstance(v, list) or isinstance(v, str):
rules[attr.key] = v
if rules == {}:
return "No set of rules detected. Set the intro point with: |wset object/origin:rules = 'text'|n"
grammar = tracery.Grammar(rules)
grammar.add_modifiers(base_english)
return grammar.flatten("#origin#")
pass

View File

@ -10,8 +10,8 @@ the other types, you can do so by adding this as a multiple
inheritance.
"""
from evennia.contrib.rpg.rpsystem import ContribRPObject
from lib.rpsystem import ContribRPObject
from evennia.contrib.game_systems.clothing import ContribClothing as Clothes
class ObjectParent:
"""
@ -27,3 +27,6 @@ class ObjectParent:
# rpsystem
class Object(ContribRPObject):
pass
class Clothing(Clothes):
pass

View File

@ -4,7 +4,7 @@ Room
Rooms are simple containers that has no location of their own.
"""
from evennia.contrib.rpg.rpsystem import ContribRPRoom
from lib.rpsystem import ContribRPRoom
# rpsystem
class Room(ContribRPRoom):

View File

@ -14,6 +14,13 @@ just overloads its hooks to have it perform its function.
from evennia.scripts.scripts import DefaultScript
class TestScript(DefaultScript):
def at_script_creation(self):
self.key = "testscript"
self.interval = 60
def at_repeat(self):
self.obj.msg_contents("hewwo")
class Script(DefaultScript):
"""

21139
typeclasses/single/mobydick.py Normal file

File diff suppressed because it is too large Load Diff

BIN
web/static/website/images/stars.gif (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,90 @@
{% comment %}
Allow to customize the menu that appears at the top of every Evennia
webpage. Copy this file to your game dir's web/template_overrides/website
folder and edit it to add/remove links to the menu.
{% endcomment %}
{% load static %}
<nav class="navbar navbar-expand-md bg-body-tertiary">
<div class="container-fluid">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand" href="/">
<div class="media">
<img class="d-flex navbar-brand-logo mx-3" src="{% static "website/images/evennia_logo.png" %}" alt="{{game_name}} logo" />
<div class="media-body">
{{ game_name }}<br />
<small>{{game_slogan}}</small>
</div>
</div>
</a>
<li class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mg-lg-0">
{% block navbar_left %}
<li class="nav-item">
<a class="nav-link" href="{% url 'index' %}">Home</a>
</li>
<!-- game views -->
<li class="nav-item"><a class="nav-link" href="{% url 'characters' %}">Characters</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'channels' %}">Channels</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'help' %}">Help</a></li>
<!-- end game views -->
{% if webclient_enabled %}
<li class="nav-item"><a class="nav-link" href="{% url 'webclient:index' %}">Play Online</a></li>
{% endif %}
{% if user.is_staff %}
<li class="nav-item"><a class="nav-link" href="{% url 'admin:index' %}">Admin</a></li>
{% if rest_api_enabled %}
<li class="nav-item"><a class="nav-link" href="/api">API</a></li>
{% endif %}
{% endif %}
{% endblock %}
</ul>
<ul class="navbar-nav me-auto mb-2 mb-lg-0 justify-content-end">
{% block navbar_right %}
{% endblock %}
{% block navbar_user %}
{% if account %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" id="user_options" aria-expanded="false">
{% if puppet %}
Welcome, {{ puppet }}! <span class="text-muted">({{ account.username }})</span> <span class="caret"></span>
{% else %}
Logged in as {{ account.username }} <span class="caret"></span>
{% endif %}
</a>
<ul class="dropdown-menu" aria-labelledby="user_options">
<li><a class="dropdown-item" href="{% url 'character-create' %}">Create Character</a></li>
<li><a class="dropdown-item" href="{% url 'character-manage' %}">Manage Characters</a></li>
<li><hr class="dropdown-divider"></li>
{% for character in account.characters|slice:"10" %}
<li><a class="dropdown-item" href="{{ character.web_get_puppet_url }}?next={{ request.path }}">{{ character }}</a></li>
{% empty %}
<li><a class="dropdown-item" href="#">No characters found!</a></li>
{% endfor %}
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{% url 'password_change' %}">Change Password</a></li>
<li><a class="dropdown-item" href="{% url 'logout' %}">Log Out</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'logout' %}">Log Out</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{% url 'login' %}?next={{ request.path }}">Log In</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'register' %}">Register</a>
</li>
{% endif %}
{% endblock %}
</ul>
</div>
</div>
</nav>

View File

@ -0,0 +1,67 @@
{% load static sekizai_tags %}
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="author" content="Evennia Contributors" />
<meta name="generator" content="Evennia" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" type="image/x-icon" href="{% static "website/images/evennia_logo.png" %}" />
{% comment %}
Base Evennia, then our CSS, then whatever's in custom CSS.
You can edit both SCSS and CSS.
{% endcomment %}
<link rel="stylesheet" type="text/css" href="{% static "website/css/website.css" %}">
<link rel="stylesheet" type="text/css" href="{% static "website/css/main.css" %}">
<link rel="stylesheet" type="text/css" href="{% static "website/css/custom.css" %}">
{% block header_ext %}
{% endblock %}
<title>{{game_name}} - {% if flatpage %}{{flatpage.title}}{% else %}{% block titleblock %}{{page_title}}{% endblock %}{% endif %}</title>
</head>
<body>
{% block body %}
<div id="top"><a href="#main-content" class="sr-only sr-only-focusable">Skip to main content.</a></div>
{% include "website/_menu.html" %}
<div class="container main-content mt-4" id="main-copy">
<div class="row">
{% if sidebar %}
<div class="col-4">
{% block sidebar %}
{% endblock %}
</div>
{% endif %}
<div class="{% if sidebar %}col-8{% else %}col{% endif %}">
{% include 'website/messages.html' %}
{% block content %}
{% endblock %}
{% include 'website/pagination.html' %}
</div>
</div>
</div>
<footer class="footer">
{% block footer %}
<div class="container">
<div class="row justify-content-center">
<span class="text-white">Powered by <a class="text-white font-weight-bold" href="https://evennia.com">Evennia.</a></span>
</div>
</div>
{% endblock %}
</footer>
{% endblock %}
<!-- jQuery first, then Tether, then Bootstrap JS. -->
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js" integrity="sha384-oBqDVmMz9ATKxIep9tiCxS/Z9fNfEXiDAYTujMAeBAsjFuCZSmKbSSUnQlmh/jp3" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.min.js" integrity="sha384-mQ93GR66B00ZXjt0YO5KlohRA5SY2XofN4zfuZxLkoj1gXtW8ANNCe9d5Y3eG5eD" crossorigin="anonymous"></script>
</body>
</html>