commit 68fbacfb93ab077d7822dd28f8596d102207ed86 Author: Helge NordgÄrd Date: Mon Aug 9 14:53:02 2021 +0000 first commit, character generation diff --git a/README.md b/README.md new file mode 100644 index 0000000..94e35b0 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Welcome to Outlands MUD + +This is just an experiment codebase to familiarize myself with the [Evennia](https://www.evennia.com) framework. + +##Status + +Currently making some core mechanics and character creation system, loosely based on the GURPS tabletop RPG system and how I can effectively translate those rules into a video game. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..6e3dbee --- /dev/null +++ b/__init__.py @@ -0,0 +1,6 @@ +""" +This sub-package holds the template for creating a new game folder. +The new game folder (when running evennia --init) is a copy of this +folder. + +""" diff --git a/commands/README.md b/commands/README.md new file mode 100644 index 0000000..0425ce6 --- /dev/null +++ b/commands/README.md @@ -0,0 +1,14 @@ +# commands/ + +This folder holds modules for implementing one's own commands and +command sets. All the modules' classes are essentially empty and just +imports the default implementations from Evennia; so adding anything +to them will start overloading the defaults. + +You can change the organisation of this directory as you see fit, just +remember that if you change any of the default command set classes' +locations, you need to add the appropriate paths to +`server/conf/settings.py` so that Evennia knows where to find them. +Also remember that if you create new sub directories you must put +(optionally empty) `__init__.py` files in there so that Python can +find your modules. diff --git a/commands/__init__.py b/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/commands/command.py b/commands/command.py new file mode 100644 index 0000000..e36d907 --- /dev/null +++ b/commands/command.py @@ -0,0 +1,219 @@ +""" +Commands + +Commands describe the input the account can do to the game. + +""" + +from evennia.commands.command import Command as BaseCommand + +# from evennia import default_cmds + + +class Command(BaseCommand): + """ + Inherit from this if you want to create your own command styles + from scratch. Note that Evennia's default commands inherits from + MuxCommand instead. + + Note that the class's `__doc__` string (this text) is + used by Evennia to create the automatic help entry for + the command, so make sure to document consistently here. + + Each Command implements the following methods, called + in this order (only func() is actually required): + - at_pre_cmd(): If this returns anything truthy, execution is aborted. + - parse(): Should perform any extra parsing needed on self.args + and store the result on self. + - func(): Performs the actual work. + - at_post_cmd(): Extra actions, often things done after + every command, like prompts. + + """ + pass + + +# ------------------------------------------------------------- +# +# The default commands inherit from +# +# evennia.commands.default.muxcommand.MuxCommand. +# +# If you want to make sweeping changes to default commands you can +# uncomment this copy of the MuxCommand parent and add +# +# COMMAND_DEFAULT_CLASS = "commands.command.MuxCommand" +# +# to your settings file. Be warned that the default commands expect +# the functionality implemented in the parse() method, so be +# careful with what you change. +# +# ------------------------------------------------------------- + +# from evennia.utils import utils +# +# +# class MuxCommand(Command): +# """ +# This sets up the basis for a MUX command. The idea +# is that most other Mux-related commands should just +# inherit from this and don't have to implement much +# parsing of their own unless they do something particularly +# advanced. +# +# Note that the class's __doc__ string (this text) is +# used by Evennia to create the automatic help entry for +# the command, so make sure to document consistently here. +# """ +# def has_perm(self, srcobj): +# """ +# This is called by the cmdhandler to determine +# if srcobj is allowed to execute this command. +# We just show it here for completeness - we +# are satisfied using the default check in Command. +# """ +# return super().has_perm(srcobj) +# +# def at_pre_cmd(self): +# """ +# This hook is called before self.parse() on all commands +# """ +# pass +# +# def at_post_cmd(self): +# """ +# This hook is called after the command has finished executing +# (after self.func()). +# """ +# pass +# +# def parse(self): +# """ +# This method is called by the cmdhandler once the command name +# has been identified. It creates a new set of member variables +# that can be later accessed from self.func() (see below) +# +# The following variables are available for our use when entering this +# method (from the command definition, and assigned on the fly by the +# cmdhandler): +# self.key - the name of this command ('look') +# self.aliases - the aliases of this cmd ('l') +# self.permissions - permission string for this command +# self.help_category - overall category of command +# +# self.caller - the object calling this command +# self.cmdstring - the actual command name used to call this +# (this allows you to know which alias was used, +# for example) +# self.args - the raw input; everything following self.cmdstring. +# self.cmdset - the cmdset from which this command was picked. Not +# often used (useful for commands like 'help' or to +# list all available commands etc) +# self.obj - the object on which this command was defined. It is often +# the same as self.caller. +# +# A MUX command has the following possible syntax: +# +# name[ with several words][/switch[/switch..]] arg1[,arg2,...] [[=|,] arg[,..]] +# +# The 'name[ with several words]' part is already dealt with by the +# cmdhandler at this point, and stored in self.cmdname (we don't use +# it here). The rest of the command is stored in self.args, which can +# start with the switch indicator /. +# +# This parser breaks self.args into its constituents and stores them in the +# following variables: +# self.switches = [list of /switches (without the /)] +# self.raw = This is the raw argument input, including switches +# self.args = This is re-defined to be everything *except* the switches +# self.lhs = Everything to the left of = (lhs:'left-hand side'). If +# no = is found, this is identical to self.args. +# self.rhs: Everything to the right of = (rhs:'right-hand side'). +# If no '=' is found, this is None. +# self.lhslist - [self.lhs split into a list by comma] +# self.rhslist - [list of self.rhs split into a list by comma] +# self.arglist = [list of space-separated args (stripped, including '=' if it exists)] +# +# All args and list members are stripped of excess whitespace around the +# strings, but case is preserved. +# """ +# raw = self.args +# args = raw.strip() +# +# # split out switches +# switches = [] +# if args and len(args) > 1 and args[0] == "/": +# # we have a switch, or a set of switches. These end with a space. +# switches = args[1:].split(None, 1) +# if len(switches) > 1: +# switches, args = switches +# switches = switches.split('/') +# else: +# args = "" +# switches = switches[0].split('/') +# arglist = [arg.strip() for arg in args.split()] +# +# # check for arg1, arg2, ... = argA, argB, ... constructs +# lhs, rhs = args, None +# lhslist, rhslist = [arg.strip() for arg in args.split(',')], [] +# if args and '=' in args: +# lhs, rhs = [arg.strip() for arg in args.split('=', 1)] +# lhslist = [arg.strip() for arg in lhs.split(',')] +# rhslist = [arg.strip() for arg in rhs.split(',')] +# +# # save to object properties: +# self.raw = raw +# self.switches = switches +# self.args = args.strip() +# self.arglist = arglist +# self.lhs = lhs +# self.lhslist = lhslist +# self.rhs = rhs +# self.rhslist = rhslist +# +# # if the class has the account_caller property set on itself, we make +# # sure that self.caller is always the account if possible. We also create +# # a special property "character" for the puppeted object, if any. This +# # is convenient for commands defined on the Account only. +# if hasattr(self, "account_caller") and self.account_caller: +# if utils.inherits_from(self.caller, "evennia.objects.objects.DefaultObject"): +# # caller is an Object/Character +# self.character = self.caller +# self.caller = self.caller.account +# elif utils.inherits_from(self.caller, "evennia.accounts.accounts.DefaultAccount"): +# # caller was already an Account +# self.character = self.caller.get_puppet(self.session) +# else: +# self.character = None + +from evennia import Command +from evennia.utils.evmenu import EvMenu + +class cmdCmenu(Command): + key = "+cmenu" + + def func(self): + EvMenu(self.caller, "world.cgmenu") + +class cmdSetInt(Command): + key = "+setint" + help_category = "mush" + + def func(self): + errmsg = "You must supply a number between 1-10" + if not self.args: + self.caller.msg(errmsg) + return + try: + intelligence = int(self.args) + except ValueError: + self.caller.msg(errmsg) + return + if not (1 <= intelligence <= 10): + self.caller.msg(errmsg) + return + + self.caller.db.intelligence = intelligence + self.caller.msg("Your intelligence stat was set to %i." % intelligence) + + diff --git a/commands/default_cmdsets.py b/commands/default_cmdsets.py new file mode 100644 index 0000000..838c8c8 --- /dev/null +++ b/commands/default_cmdsets.py @@ -0,0 +1,103 @@ +""" +Command sets + +All commands in the game must be grouped in a cmdset. A given command +can be part of any number of cmdsets and cmdsets can be added/removed +and merged onto entities at runtime. + +To create new commands to populate the cmdset, see +`commands/command.py`. + +This module wraps the default command sets of Evennia; overloads them +to add/remove commands from the default lineup. You can create your +own cmdsets by inheriting from them or directly from `evennia.CmdSet`. + +""" + +from evennia import default_cmds +from evennia import CmdSet +from commands import command + +class CharacterCmdSet(default_cmds.CharacterCmdSet): + """ + The `CharacterCmdSet` contains general in-game commands like `look`, + `get`, etc available on in-game Character objects. It is merged with + the `AccountCmdSet` when an Account puppets a Character. + """ + + key = "DefaultCharacter" + + def at_cmdset_creation(self): + """ + Populates the cmdset + """ + super().at_cmdset_creation() + # + # any commands you add below will overload the default ones. + # + + +class AccountCmdSet(default_cmds.AccountCmdSet): + """ + This is the cmdset available to the Account at all times. It is + combined with the `CharacterCmdSet` when the Account puppets a + Character. It holds game-account-specific commands, channel + commands, etc. + """ + + key = "DefaultAccount" + + def at_cmdset_creation(self): + """ + Populates the cmdset + """ + super().at_cmdset_creation() + # + # any commands you add below will overload the default ones. + # + + +class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet): + """ + Command set available to the Session before being logged in. This + holds commands like creating a new account, logging in, etc. + """ + + key = "DefaultUnloggedin" + + def at_cmdset_creation(self): + """ + Populates the cmdset + """ + super().at_cmdset_creation() + # + # any commands you add below will overload the default ones. + # + + +class SessionCmdSet(default_cmds.SessionCmdSet): + """ + This cmdset is made available on Session level once logged in. It + is empty by default. + """ + + key = "DefaultSession" + + def at_cmdset_creation(self): + """ + This is the only method defined in a cmdset, called during + its creation. It should populate the set with command instances. + + As and example we just add the empty base `Command` object. + It prints some info. + """ + super().at_cmdset_creation() + # + # any commands you add below will overload the default ones. + # + +class ChargenCmdSet(CmdSet): + key = "Chargen" + def at_cmdset_creation(self): + self.add(command.cmdSetInt()) + self.add(command.cmdCmenu()) diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..9f530c3 --- /dev/null +++ b/server/README.md @@ -0,0 +1,38 @@ +# server/ + +This directory holds files used by and configuring the Evennia server +itself. + +Out of all the subdirectories in the game directory, Evennia does +expect this directory to exist, so you should normally not delete, +rename or change its folder structure. + +When running you will find four new files appear in this directory: + + - `server.pid` and `portal.pid`: These hold the process IDs of the + Portal and Server, so that they can be managed by the launcher. If + Evennia is shut down uncleanly (e.g. by a crash or via a kill + signal), these files might erroneously remain behind. If so Evennia + will tell you they are "stale" and they can be deleted manually. + - `server.restart` and `portal.restart`: These hold flags to tell the + server processes if it should die or start again. You never need to + modify those files. + - `evennia.db3`: This will only appear if you are using the default + SQLite3 database; it a binary file that holds the entire game + database; deleting this file will effectively reset the game for + you and you can start fresh with `evennia migrate` (useful during + development). + +## server/conf/ + +This subdirectory holds the configuration modules for the server. With +them you can change how Evennia operates and also plug in your own +functionality to replace the default. You usually need to restart the +server to apply changes done here. The most important file is the file +`settings.py` which is the main configuration file of Evennia. + +## server/logs/ + +This subdirectory holds various log files created by the running +Evennia server. It is also the default location for storing any custom +log files you might want to output using Evennia's logging mechanisms. diff --git a/server/__init__.py b/server/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/server/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/server/conf/__init__.py b/server/conf/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/server/conf/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/server/conf/at_initial_setup.py b/server/conf/at_initial_setup.py new file mode 100644 index 0000000..b394a04 --- /dev/null +++ b/server/conf/at_initial_setup.py @@ -0,0 +1,19 @@ +""" +At_initial_setup module template + +Custom at_initial_setup method. This allows you to hook special +modifications to the initial server startup process. Note that this +will only be run once - when the server starts up for the very first +time! It is called last in the startup process and can thus be used to +overload things that happened before it. + +The module must contain a global function at_initial_setup(). This +will be called without arguments. Note that tracebacks in this module +will be QUIETLY ignored, so make sure to check it well to make sure it +does what you expect it to. + +""" + + +def at_initial_setup(): + pass diff --git a/server/conf/at_search.py b/server/conf/at_search.py new file mode 100644 index 0000000..f7f8b6c --- /dev/null +++ b/server/conf/at_search.py @@ -0,0 +1,54 @@ +""" +Search and multimatch handling + +This module allows for overloading two functions used by Evennia's +search functionality: + + at_search_result: + This is called whenever a result is returned from an object + search (a common operation in commands). It should (together + with at_multimatch_input below) define some way to present and + differentiate between multiple matches (by default these are + presented as 1-ball, 2-ball etc) + at_multimatch_input: + This is called with a search term and should be able to + identify if the user wants to separate a multimatch-result + (such as that from a previous search). By default, this + function understands input on the form 1-ball, 2-ball etc as + indicating that the 1st or 2nd match for "ball" should be + used. + +This module is not called by default, to use it, add the following +line to your settings file: + + SEARCH_AT_RESULT = "server.conf.at_search.at_search_result" + +""" + + +def at_search_result(matches, caller, query="", quiet=False, **kwargs): + """ + This is a generic hook for handling all processing of a search + result, including error reporting. + + Args: + matches (list): This is a list of 0, 1 or more typeclass instances, + the matched result of the search. If 0, a nomatch error should + be echoed, and if >1, multimatch errors should be given. Only + if a single match should the result pass through. + caller (Object): The object performing the search and/or which should + receive error messages. + query (str, optional): The search query used to produce `matches`. + quiet (bool, optional): If `True`, no messages will be echoed to caller + on errors. + + Keyword Args: + nofound_string (str): Replacement string to echo on a notfound error. + multimatch_string (str): Replacement string to echo on a multimatch error. + + Returns: + processed_result (Object or None): This is always a single result + or `None`. If `None`, any error reporting/handling should + already have happened. + + """ diff --git a/server/conf/at_server_startstop.py b/server/conf/at_server_startstop.py new file mode 100644 index 0000000..98c29fa --- /dev/null +++ b/server/conf/at_server_startstop.py @@ -0,0 +1,63 @@ +""" +Server startstop hooks + +This module contains functions called by Evennia at various +points during its startup, reload and shutdown sequence. It +allows for customizing the server operation as desired. + +This module must contain at least these global functions: + +at_server_start() +at_server_stop() +at_server_reload_start() +at_server_reload_stop() +at_server_cold_start() +at_server_cold_stop() + +""" + + +def at_server_start(): + """ + This is called every time the server starts up, regardless of + how it was shut down. + """ + pass + + +def at_server_stop(): + """ + This is called just before the server is shut down, regardless + of it is for a reload, reset or shutdown. + """ + pass + + +def at_server_reload_start(): + """ + This is called only when server starts back up after a reload. + """ + pass + + +def at_server_reload_stop(): + """ + This is called only time the server stops before a reload. + """ + pass + + +def at_server_cold_start(): + """ + This is called only when the server starts "cold", i.e. after a + shutdown or a reset. + """ + pass + + +def at_server_cold_stop(): + """ + This is called only when the server goes down due to a shutdown or + reset. + """ + pass diff --git a/server/conf/cmdparser.py b/server/conf/cmdparser.py new file mode 100644 index 0000000..831990a --- /dev/null +++ b/server/conf/cmdparser.py @@ -0,0 +1,55 @@ +""" +Changing the default command parser + +The cmdparser is responsible for parsing the raw text inserted by the +user, identifying which command/commands match and return one or more +matching command objects. It is called by Evennia's cmdhandler and +must accept input and return results on the same form. The default +handler is very generic so you usually don't need to overload this +unless you have very exotic parsing needs; advanced parsing is best +done at the Command.parse level. + +The default cmdparser understands the following command combinations +(where [] marks optional parts.) + +[cmdname[ cmdname2 cmdname3 ...] [the rest] + +A command may consist of any number of space-separated words of any +length, and contain any character. It may also be empty. + +The parser makes use of the cmdset to find command candidates. The +parser return a list of matches. Each match is a tuple with its first +three elements being the parsed cmdname (lower case), the remaining +arguments, and the matched cmdobject from the cmdset. + + +This module is not accessed by default. To tell Evennia to use it +instead of the default command parser, add the following line to +your settings file: + + COMMAND_PARSER = "server.conf.cmdparser.cmdparser" + +""" + + +def cmdparser(raw_string, cmdset, caller, match_index=None): + """ + This function is called by the cmdhandler once it has + gathered and merged all valid cmdsets valid for this particular parsing. + + raw_string - the unparsed text entered by the caller. + cmdset - the merged, currently valid cmdset + caller - the caller triggering this parsing + match_index - an optional integer index to pick a given match in a + list of same-named command matches. + + Returns: + list of tuples: [(cmdname, args, cmdobj, cmdlen, mratio), ...] + where cmdname is the matching command name and args is + everything not included in the cmdname. Cmdobj is the actual + command instance taken from the cmdset, cmdlen is the length + of the command name and the mratio is some quality value to + (possibly) separate multiple matches. + + """ + # Your implementation here diff --git a/server/conf/connection_screens.py b/server/conf/connection_screens.py new file mode 100644 index 0000000..5f46b4c --- /dev/null +++ b/server/conf/connection_screens.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +""" +Connection screen + +This is the text to show the user when they first connect to the game (before +they log in). + +To change the login screen in this module, do one of the following: + +- Define a function `connection_screen()`, taking no arguments. This will be + called first and must return the full string to act as the connection screen. + This can be used to produce more dynamic screens. +- Alternatively, define a string variable in the outermost scope of this module + with the connection string that should be displayed. If more than one such + variable is given, Evennia will pick one of them at random. + +The commands available to the user when the connection screen is shown +are defined in evennia.default_cmds.UnloggedinCmdSet. The parsing and display +of the screen is done by the unlogged-in "look" command. + +""" + +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 |n + If you need to create an account, type (without the <>'s): + |wcreate |n + + Enter |whelp|n for more info. |wlook|n will re-show this screen. +|b==============================================================|n""".format( + settings.SERVERNAME, utils.get_evennia_version("short") +) diff --git a/server/conf/inlinefuncs.py b/server/conf/inlinefuncs.py new file mode 100644 index 0000000..1190597 --- /dev/null +++ b/server/conf/inlinefuncs.py @@ -0,0 +1,51 @@ +""" +Inlinefunc + +Inline functions allow for direct conversion of text users mark in a +special way. Inlinefuncs are deactivated by default. To activate, add + + INLINEFUNC_ENABLED = True + +to your settings file. The default inlinefuncs are found in +evennia.utils.inlinefunc. + +In text, usage is straightforward: + +$funcname([arg1,[arg2,...]]) + +Example 1 (using the "pad" inlinefunc): + say This is $pad("a center-padded text", 50,c,-) of width 50. + -> + John says, "This is -------------- a center-padded text--------------- of width 50." + +Example 2 (using nested "pad" and "time" inlinefuncs): + say The time is $pad($time(), 30)right now. + -> + John says, "The time is Oct 25, 11:09 right now." + +To add more inline functions, add them to this module, using +the following call signature: + + def funcname(text, *args, **kwargs) + +where `text` is always the part between {funcname(args) and +{/funcname and the *args are taken from the appropriate part of the +call. If no {/funcname is given, `text` will be the empty string. + +It is important that the inline function properly clean the +incoming `args`, checking their type and replacing them with sane +defaults if needed. If impossible to resolve, the unmodified text +should be returned. The inlinefunc should never cause a traceback. + +While the inline function should accept **kwargs, the keyword is +never accepted as a valid call - this is only intended to be used +internally by Evennia, notably to send the `session` keyword to +the function; this is the session of the object viewing the string +and can be used to customize it to each session. + +""" + +# def capitalize(text, *args, **kwargs): +# "Silly capitalize example. Used as {capitalize() ... {/capitalize" +# session = kwargs.get("session") +# return text.capitalize() diff --git a/server/conf/inputfuncs.py b/server/conf/inputfuncs.py new file mode 100644 index 0000000..6cb226f --- /dev/null +++ b/server/conf/inputfuncs.py @@ -0,0 +1,52 @@ +""" +Input functions + +Input functions are always called from the client (they handle server +input, hence the name). + +This module is loaded by being included in the +`settings.INPUT_FUNC_MODULES` tuple. + +All *global functions* included in this module are considered +input-handler functions and can be called by the client to handle +input. + +An input function must have the following call signature: + + cmdname(session, *args, **kwargs) + +Where session will be the active session and *args, **kwargs are extra +incoming arguments and keyword properties. + +A special command is the "default" command, which is will be called +when no other cmdname matches. It also receives the non-found cmdname +as argument. + + default(session, cmdname, *args, **kwargs) + +""" + +# def oob_echo(session, *args, **kwargs): +# """ +# Example echo function. Echoes args, kwargs sent to it. +# +# Args: +# session (Session): The Session to receive the echo. +# args (list of str): Echo text. +# kwargs (dict of str, optional): Keyed echo text +# +# """ +# session.msg(oob=("echo", args, kwargs)) +# +# +# def default(session, cmdname, *args, **kwargs): +# """ +# Handles commands without a matching inputhandler func. +# +# Args: +# session (Session): The active Session. +# cmdname (str): The (unmatched) command name +# args, kwargs (any): Arguments to function. +# +# """ +# pass diff --git a/server/conf/lockfuncs.py b/server/conf/lockfuncs.py new file mode 100644 index 0000000..8dac0c7 --- /dev/null +++ b/server/conf/lockfuncs.py @@ -0,0 +1,30 @@ +""" + +Lockfuncs + +Lock functions are functions available when defining lock strings, +which in turn limits access to various game systems. + +All functions defined globally in this module are assumed to be +available for use in lockstrings to determine access. See the +Evennia documentation for more info on locks. + +A lock function is always called with two arguments, accessing_obj and +accessed_obj, followed by any number of arguments. All possible +arguments should be handled with *args, **kwargs. The lock function +should handle all eventual tracebacks by logging the error and +returning False. + +Lock functions in this module extend (and will overload same-named) +lock functions from evennia.locks.lockfuncs. + +""" + +# def myfalse(accessing_obj, accessed_obj, *args, **kwargs): +# """ +# called in lockstring with myfalse(). +# A simple logger that always returns false. Prints to stdout +# for simplicity, should use utils.logger for real operation. +# """ +# print "%s tried to access %s. Access denied." % (accessing_obj, accessed_obj) +# return False diff --git a/server/conf/mssp.py b/server/conf/mssp.py new file mode 100644 index 0000000..270c8f5 --- /dev/null +++ b/server/conf/mssp.py @@ -0,0 +1,105 @@ +""" + +MSSP (Mud Server Status Protocol) meta information + +Modify this file to specify what MUD listing sites will report about your game. +All fields are static. The number of currently active players and your game's +current uptime will be added automatically by Evennia. + +You don't have to fill in everything (and most fields are not shown/used by all +crawlers anyway); leave the default if so needed. You need to reload the server +before the updated information is made available to crawlers (reloading does +not affect uptime). + +After changing the values in this file, you must register your game with the +MUD website list you want to track you. The listing crawler will then regularly +connect to your server to get the latest info. No further configuration is +needed on the Evennia side. + +""" + +MSSPTable = { + # Required fields + "NAME": "Mygame", # usually the same as SERVERNAME + # Generic + "CRAWL DELAY": "-1", # limit how often crawler may update the listing. -1 for no limit + "HOSTNAME": "", # telnet hostname + "PORT": ["4000"], # telnet port - most important port should be *last* in list! + "CODEBASE": "Evennia", + "CONTACT": "", # email for contacting the mud + "CREATED": "", # year MUD was created + "ICON": "", # url to icon 32x32 or larger; <32kb. + "IP": "", # current or new IP address + "LANGUAGE": "", # name of language used, e.g. English + "LOCATION": "", # full English name of server country + "MINIMUM AGE": "0", # set to 0 if not applicable + "WEBSITE": "", # http:// address to your game website + # Categorisation + "FAMILY": "Custom", # evennia goes under 'Custom' + "GENRE": "None", # Adult, Fantasy, Historical, Horror, Modern, None, or Science Fiction + # Gameplay: Adventure, Educational, Hack and Slash, None, + # Player versus Player, Player versus Environment, + # Roleplaying, Simulation, Social or Strategy + "GAMEPLAY": "", + "STATUS": "Open Beta", # Allowed: Alpha, Closed Beta, Open Beta, Live + "GAMESYSTEM": "Custom", # D&D, d20 System, World of Darkness, etc. Use Custom if homebrew + # Subgenre: LASG, Medieval Fantasy, World War II, Frankenstein, + # Cyberpunk, Dragonlance, etc. Or None if not applicable. + "SUBGENRE": "None", + # World + "AREAS": "0", + "HELPFILES": "0", + "MOBILES": "0", + "OBJECTS": "0", + "ROOMS": "0", # use 0 if room-less + "CLASSES": "0", # use 0 if class-less + "LEVELS": "0", # use 0 if level-less + "RACES": "0", # use 0 if race-less + "SKILLS": "0", # use 0 if skill-less + # Protocols set to 1 or 0; should usually not be changed) + "ANSI": "1", + "GMCP": "1", + "MSDP": "1", + "MXP": "1", + "SSL": "1", + "UTF-8": "1", + "MCCP": "1", + "XTERM 256 COLORS": "1", + "XTERM TRUE COLORS": "0", + "ATCP": "0", + "MCP": "0", + "MSP": "0", + "VT100": "0", + "PUEBLO": "0", + "ZMP": "0", + # Commercial set to 1 or 0) + "PAY TO PLAY": "0", + "PAY FOR PERKS": "0", + # Hiring set to 1 or 0) + "HIRING BUILDERS": "0", + "HIRING CODERS": "0", + # Extended variables + # World + "DBSIZE": "0", + "EXITS": "0", + "EXTRA DESCRIPTIONS": "0", + "MUDPROGS": "0", + "MUDTRIGS": "0", + "RESETS": "0", + # Game (set to 1 or 0, or one of the given alternatives) + "ADULT MATERIAL": "0", + "MULTICLASSING": "0", + "NEWBIE FRIENDLY": "0", + "PLAYER CITIES": "0", + "PLAYER CLANS": "0", + "PLAYER CRAFTING": "0", + "PLAYER GUILDS": "0", + "EQUIPMENT SYSTEM": "None", # "None", "Level", "Skill", "Both" + "MULTIPLAYING": "None", # "None", "Restricted", "Full" + "PLAYERKILLING": "None", # "None", "Restricted", "Full" + "QUEST SYSTEM": "None", # "None", "Immortal Run", "Automated", "Integrated" + "ROLEPLAYING": "None", # "None", "Accepted", "Encouraged", "Enforced" + "TRAINING SYSTEM": "None", # "None", "Level", "Skill", "Both" + # World originality: "All Stock", "Mostly Stock", "Mostly Original", "All Original" + "WORLD ORIGINALITY": "All Original", +} diff --git a/server/conf/portal_services_plugins.py b/server/conf/portal_services_plugins.py new file mode 100644 index 0000000..b536c56 --- /dev/null +++ b/server/conf/portal_services_plugins.py @@ -0,0 +1,24 @@ +""" +Start plugin services + +This plugin module can define user-created services for the Portal to +start. + +This module must handle all imports and setups required to start +twisted services (see examples in evennia.server.portal.portal). It +must also contain a function start_plugin_services(application). +Evennia will call this function with the main Portal application (so +your services can be added to it). The function should not return +anything. Plugin services are started last in the Portal startup +process. + +""" + + +def start_plugin_services(portal): + """ + This hook is called by Evennia, last in the Portal startup process. + + portal - a reference to the main portal application. + """ + pass diff --git a/server/conf/server_services_plugins.py b/server/conf/server_services_plugins.py new file mode 100644 index 0000000..e3d41fe --- /dev/null +++ b/server/conf/server_services_plugins.py @@ -0,0 +1,24 @@ +""" + +Server plugin services + +This plugin module can define user-created services for the Server to +start. + +This module must handle all imports and setups required to start a +twisted service (see examples in evennia.server.server). It must also +contain a function start_plugin_services(application). Evennia will +call this function with the main Server application (so your services +can be added to it). The function should not return anything. Plugin +services are started last in the Server startup process. + +""" + + +def start_plugin_services(server): + """ + This hook is called by Evennia, last in the Server startup process. + + server - a reference to the main server application. + """ + pass diff --git a/server/conf/serversession.py b/server/conf/serversession.py new file mode 100644 index 0000000..13fbf1e --- /dev/null +++ b/server/conf/serversession.py @@ -0,0 +1,37 @@ +""" +ServerSession + +The serversession is the Server-side in-memory representation of a +user connecting to the game. Evennia manages one Session per +connection to the game. So a user logged into the game with multiple +clients (if Evennia is configured to allow that) will have multiple +sessions tied to one Account object. All communication between Evennia +and the real-world user goes through the Session(s) associated with that user. + +It should be noted that modifying the Session object is not usually +necessary except for the most custom and exotic designs - and even +then it might be enough to just add custom session-level commands to +the SessionCmdSet instead. + +This module is not normally called. To tell Evennia to use the class +in this module instead of the default one, add the following to your +settings file: + + SERVER_SESSION_CLASS = "server.conf.serversession.ServerSession" + +""" + +from evennia.server.serversession import ServerSession as BaseServerSession + + +class ServerSession(BaseServerSession): + """ + This class represents a player's session and is a template for + individual protocols to communicate with Evennia. + + Each account gets one or more sessions assigned to them whenever they connect + to the game server. All communication between game and account goes + through their session(s). + """ + + pass diff --git a/server/conf/settings.py b/server/conf/settings.py new file mode 100644 index 0000000..5b90088 --- /dev/null +++ b/server/conf/settings.py @@ -0,0 +1,44 @@ +r""" +Evennia settings file. + +The available options are found in the default settings file found +here: + +/home/heno/muddev/evennia/evennia/settings_default.py + +Remember: + +Don't copy more from the default file than you actually intend to +change; this will make sure that you don't overload upstream updates +unnecessarily. + +When changing a setting requiring a file system path (like +path/to/actual/file.py), use GAME_DIR and EVENNIA_DIR to reference +your game folder and the Evennia library folders respectively. Python +paths (path.to.module) should be given relative to the game's root +folder (typeclasses.foo) whereas paths within the Evennia library +needs to be given explicitly (evennia.foo). + +If you want to share your game dir, including its settings, you can +put secret game- or server-specific settings in secret_settings.py. + +""" + +# Use the defaults from Evennia unless explicitly overridden +from evennia.settings_default import * + +###################################################################### +# Evennia base server config +###################################################################### + +# This is the name of your game. Make it catchy! +SERVERNAME = "mygame" + + +###################################################################### +# Settings given in secret_settings.py override those in this file. +###################################################################### +try: + from server.conf.secret_settings import * +except ImportError: + print("secret_settings.py file not found or failed to import.") diff --git a/server/conf/web_plugins.py b/server/conf/web_plugins.py new file mode 100644 index 0000000..ec11ad7 --- /dev/null +++ b/server/conf/web_plugins.py @@ -0,0 +1,41 @@ +""" +Web plugin hooks. +""" + + +def at_webserver_root_creation(web_root): + """ + This is called as the web server has finished building its default + path tree. At this point, the media/ and static/ URIs have already + been added to the web root. + + Args: + web_root (twisted.web.resource.Resource): The root + resource of the URI tree. Use .putChild() to + add new subdomains to the tree. + + Returns: + web_root (twisted.web.resource.Resource): The potentially + modified root structure. + + Example: + from twisted.web import static + my_page = static.File("web/mypage/") + my_page.indexNames = ["index.html"] + web_root.putChild("mypage", my_page) + + """ + return web_root + + +def at_webproxy_root_creation(web_root): + """ + This function can modify the portal proxy service. + Args: + web_root (evennia.server.webserver.Website): The Evennia + Website application. Use .putChild() to add new + subdomains that are Portal-accessible over TCP; + primarily for new protocol development, but suitable + for other shenanigans. + """ + return web_root diff --git a/server/logs/README.md b/server/logs/README.md new file mode 100644 index 0000000..35ad999 --- /dev/null +++ b/server/logs/README.md @@ -0,0 +1,15 @@ +This directory contains Evennia's log files. The existence of this README.md file is also necessary +to correctly include the log directory in git (since log files are ignored by git and you can't +commit an empty directory). + +- `server.log` - log file from the game Server. +- `portal.log` - log file from Portal proxy (internet facing) + +Usually these logs are viewed together with `evennia -l`. They are also rotated every week so as not +to be too big. Older log names will have a name appended by `_month_date`. + +- `lockwarnings.log` - warnings from the lock system. +- `http_requests.log` - this will generally be empty unless turning on debugging inside the server. + +- `channel_.log` - these are channel logs for the in-game channels They are also used + by the `/history` flag in-game to get the latest message history. diff --git a/typeclasses/README.md b/typeclasses/README.md new file mode 100644 index 0000000..e114e59 --- /dev/null +++ b/typeclasses/README.md @@ -0,0 +1,16 @@ +# typeclasses/ + +This directory holds the modules for overloading all the typeclasses +representing the game entities and many systems of the game. Other +server functionality not covered here is usually modified by the +modules in `server/conf/`. + +Each module holds empty classes that just imports Evennia's defaults. +Any modifications done to these classes will overload the defaults. + +You can change the structure of this directory (even rename the +directory itself) as you please, but if you do you must add the +appropriate new paths to your settings.py file so Evennia knows where +to look. Also remember that for Python to find your modules, it +requires you to add an empty `__init__.py` file in any new sub +directories you create. diff --git a/typeclasses/__init__.py b/typeclasses/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/typeclasses/accounts.py b/typeclasses/accounts.py new file mode 100644 index 0000000..ba293c6 --- /dev/null +++ b/typeclasses/accounts.py @@ -0,0 +1,104 @@ +""" +Account + +The Account represents the game "account" and each login has only one +Account object. An Account is what chats on default channels but has no +other in-game-world existence. Rather the Account puppets Objects (such +as Characters) in order to actually participate in the game world. + + +Guest + +Guest accounts are simple low-level accounts that are created/deleted +on the fly and allows users to test the game without the commitment +of a full registration. Guest accounts are deactivated by default; to +activate them, add the following line to your settings file: + + GUEST_ENABLED = True + +You will also need to modify the connection screen to reflect the +possibility to connect with a guest account. The setting file accepts +several more options for customizing the Guest account system. + +""" + +from evennia import DefaultAccount, DefaultGuest + + +class Account(DefaultAccount): + """ + This class describes the actual OOC account (i.e. the user connecting + to the MUD). It does NOT have visual appearance in the game world (that + is handled by the character which is connected to this). Comm channels + are attended/joined using this object. + + It can be useful e.g. for storing configuration options for your game, but + should generally not hold any character-related info (that's best handled + on the character level). + + Can be set using BASE_ACCOUNT_TYPECLASS. + + + * available properties + + key (string) - name of account + name (string)- wrapper for user.username + aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings. + dbref (int, read-only) - unique #id-number. Also "id" can be used. + date_created (string) - time stamp of object creation + permissions (list of strings) - list of permission strings + + user (User, read-only) - django User authorization object + obj (Object) - game object controlled by account. 'character' can also be used. + sessions (list of Sessions) - sessions connected to this account + is_superuser (bool, read-only) - if the connected user is a superuser + + * Handlers + + locks - lock-handler: use locks.add() to add new lock strings + db - attribute-handler: store/retrieve database attributes on this self.db.myattr=val, val=self.db.myattr + ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data + scripts - script-handler. Add new scripts to object with scripts.add() + cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object + nicks - nick-handler. New nicks with nicks.add(). + + * Helper methods + + msg(text=None, **kwargs) + execute_cmd(raw_string, session=None) + search(ostring, global_search=False, attribute_name=None, use_nicks=False, location=None, ignore_errors=False, account=False) + is_typeclass(typeclass, exact=False) + swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) + access(accessing_obj, access_type='read', default=False) + check_permstring(permstring) + + * Hook methods (when re-implementation, remember methods need to have self as first arg) + + basetype_setup() + at_account_creation() + + - note that the following hooks are also found on Objects and are + usually handled on the character level: + + at_init() + at_cmdset_get(**kwargs) + at_first_login() + at_post_login(session=None) + at_disconnect() + at_message_receive() + at_message_send() + at_server_reload() + at_server_shutdown() + + """ + + pass + + +class Guest(DefaultGuest): + """ + This class is used for guest logins. Unlike Accounts, Guests and their + characters are deleted after disconnection. + """ + + pass diff --git a/typeclasses/channels.py b/typeclasses/channels.py new file mode 100644 index 0000000..0b943d0 --- /dev/null +++ b/typeclasses/channels.py @@ -0,0 +1,62 @@ +""" +Channel + +The channel class represents the out-of-character chat-room usable by +Accounts in-game. It is mostly overloaded to change its appearance, but +channels can be used to implement many different forms of message +distribution systems. + +Note that sending data to channels are handled via the CMD_CHANNEL +syscommand (see evennia.syscmds). The sending should normally not need +to be modified. + +""" + +from evennia import DefaultChannel + + +class Channel(DefaultChannel): + """ + Working methods: + at_channel_creation() - called once, when the channel is created + has_connection(account) - check if the given account listens to this channel + connect(account) - connect account to this channel + disconnect(account) - disconnect account from channel + access(access_obj, access_type='listen', default=False) - check the + access on this channel (default access_type is listen) + delete() - delete this channel + message_transform(msg, emit=False, prefix=True, + sender_strings=None, external=False) - called by + the comm system and triggers the hooks below + msg(msgobj, header=None, senders=None, sender_strings=None, + persistent=None, online=False, emit=False, external=False) - main + send method, builds and sends a new message to channel. + tempmsg(msg, header=None, senders=None) - wrapper for sending non-persistent + messages. + distribute_message(msg, online=False) - send a message to all + connected accounts on channel, optionally sending only + to accounts that are currently online (optimized for very large sends) + + Useful hooks: + channel_prefix(msg, emit=False) - how the channel should be + prefixed when returning to user. Returns a string + format_senders(senders) - should return how to display multiple + senders to a channel + pose_transform(msg, sender_string) - should detect if the + sender is posing, and if so, modify the string + format_external(msg, senders, emit=False) - format messages sent + from outside the game, like from IRC + format_message(msg, emit=False) - format the message body before + displaying it to the user. 'emit' generally means that the + message should not be displayed with the sender's name. + + pre_join_channel(joiner) - if returning False, abort join + post_join_channel(joiner) - called right after successful join + pre_leave_channel(leaver) - if returning False, abort leave + post_leave_channel(leaver) - called right after successful leave + pre_send_message(msg) - runs just before a message is sent to channel + post_send_message(msg) - called just after message was sent to channel + + """ + + pass diff --git a/typeclasses/characters.py b/typeclasses/characters.py new file mode 100644 index 0000000..0554595 --- /dev/null +++ b/typeclasses/characters.py @@ -0,0 +1,58 @@ +""" +Characters + +Characters are (by default) Objects setup to be puppeted by Accounts. +They are what you "see" in game. The Character class in this module +is setup to be the "default" character type created by the default +creation commands. + +""" +from evennia import DefaultCharacter + + +class Character(DefaultCharacter): + """ + The Character defaults to reimplementing some of base Object's hook methods with the + following functionality: + + at_basetype_setup - always assigns the DefaultCmdSet to this object type + (important!)sets locks so character cannot be picked up + and its commands only be called by itself, not anyone else. + (to change things, use at_object_creation() instead). + at_after_move(source_location) - Launches the "look" command after every move. + at_post_unpuppet(account) - when Account disconnects from the Character, we + store the current location in the pre_logout_location Attribute and + move it to a None-location so the "unpuppeted" character + object does not need to stay on grid. Echoes "Account has disconnected" + to the room. + at_pre_puppet - Just before Account re-connects, retrieves the character's + pre_logout_location Attribute and move it back on the grid. + at_post_puppet - Echoes "AccountName has entered the game" to the room. + + """ + + def at_object_creation(self): + self.db.money = 50 + self.db.name = "NO_NAME" + self.db.age = 0 + self.db.strength = 10 + self.db.dexterity = 10 + self.db.intelligence = 10 + self.db.health = 10 + self.db.hitpoints = 10 + self.db.currenthp = 10 + self.db.will = 10 + self.db.perception = 10 + self.db.fatiguepoints = 10 + self.db.currentfatigue = 0 + self.db.basicspeed = 10 + self.db.basicsmove = 10 + self.db.alignment = 4 + self.db.gender = 4 + self.db.xp = 0 + self.db.level = 1 + self.db.attribpoints = 100 + self.db.skills = [] + self.db.skillproperties = {} + self.db.skillevel = {} + self.db.created = 0 diff --git a/typeclasses/exits.py b/typeclasses/exits.py new file mode 100644 index 0000000..55e091f --- /dev/null +++ b/typeclasses/exits.py @@ -0,0 +1,38 @@ +""" +Exits + +Exits are connectors between Rooms. An exit always has a destination property +set and has a single command defined on itself with the same name as its key, +for allowing Characters to traverse the exit to its destination. + +""" +from evennia import DefaultExit + + +class Exit(DefaultExit): + """ + Exits are connectors between rooms. Exits are normal Objects except + they defines the `destination` property. It also does work in the + following methods: + + basetype_setup() - sets default exit locks (to change, use `at_object_creation` instead). + at_cmdset_get(**kwargs) - this is called when the cmdset is accessed and should + rebuild the Exit cmdset along with a command matching the name + of the Exit object. Conventionally, a kwarg `force_init` + should force a rebuild of the cmdset, this is triggered + by the `@alias` command when aliases are changed. + at_failed_traverse() - gives a default error message ("You cannot + go there") if exit traversal fails and an + attribute `err_traverse` is not defined. + + Relevant hooks to overload (compared to other types of Objects): + at_traverse(traveller, target_loc) - called to do the actual traversal and calling of the other hooks. + If overloading this, consider using super() to use the default + movement implementation (and hook-calling). + at_after_traverse(traveller, source_loc) - called by at_traverse just after traversing. + at_failed_traverse(traveller) - called by at_traverse if traversal failed for some reason. Will + not be called if the attribute `err_traverse` is + defined, in which case that will simply be echoed. + """ + + pass diff --git a/typeclasses/objects.py b/typeclasses/objects.py new file mode 100644 index 0000000..21443e8 --- /dev/null +++ b/typeclasses/objects.py @@ -0,0 +1,162 @@ +""" +Object + +The Object is the "naked" base class for things in the game world. + +Note that the default Character, Room and Exit does not inherit from +this Object, but from their respective default implementations in the +evennia library. If you want to use this class as a parent to change +the other types, you can do so by adding this as a multiple +inheritance. + +""" +from evennia import DefaultObject + + +class Object(DefaultObject): + """ + This is the root typeclass object, implementing an in-game Evennia + game object, such as having a location, being able to be + manipulated or looked at, etc. If you create a new typeclass, it + must always inherit from this object (or any of the other objects + in this file, since they all actually inherit from BaseObject, as + seen in src.object.objects). + + The BaseObject class implements several hooks tying into the game + engine. By re-implementing these hooks you can control the + system. You should never need to re-implement special Python + methods, such as __init__ and especially never __getattribute__ and + __setattr__ since these are used heavily by the typeclass system + of Evennia and messing with them might well break things for you. + + + * Base properties defined/available on all Objects + + key (string) - name of object + name (string)- same as key + dbref (int, read-only) - unique #id-number. Also "id" can be used. + date_created (string) - time stamp of object creation + + account (Account) - controlling account (if any, only set together with + sessid below) + sessid (int, read-only) - session id (if any, only set together with + account above). Use `sessions` handler to get the + Sessions directly. + location (Object) - current location. Is None if this is a room + home (Object) - safety start-location + has_account (bool, read-only)- will only return *connected* accounts + contents (list of Objects, read-only) - returns all objects inside this + object (including exits) + exits (list of Objects, read-only) - returns all exits from this + object, if any + destination (Object) - only set if this object is an exit. + is_superuser (bool, read-only) - True/False if this user is a superuser + + * Handlers available + + aliases - alias-handler: use aliases.add/remove/get() to use. + permissions - permission-handler: use permissions.add/remove() to + add/remove new perms. + locks - lock-handler: use locks.add() to add new lock strings + scripts - script-handler. Add new scripts to object with scripts.add() + cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object + nicks - nick-handler. New nicks with nicks.add(). + sessions - sessions-handler. Get Sessions connected to this + object with sessions.get() + attributes - attribute-handler. Use attributes.add/remove/get. + db - attribute-handler: Shortcut for attribute-handler. Store/retrieve + database attributes using self.db.myattr=val, val=self.db.myattr + ndb - non-persistent attribute handler: same as db but does not create + a database entry when storing data + + * Helper methods (see src.objects.objects.py for full headers) + + search(ostring, global_search=False, attribute_name=None, + use_nicks=False, location=None, ignore_errors=False, account=False) + execute_cmd(raw_string) + msg(text=None, **kwargs) + msg_contents(message, exclude=None, from_obj=None, **kwargs) + move_to(destination, quiet=False, emit_to_obj=None, use_destination=True) + copy(new_key=None) + delete() + is_typeclass(typeclass, exact=False) + swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) + access(accessing_obj, access_type='read', default=False) + check_permstring(permstring) + + * Hooks (these are class methods, so args should start with self): + + basetype_setup() - only called once, used for behind-the-scenes + setup. Normally not modified. + basetype_posthook_setup() - customization in basetype, after the object + has been created; Normally not modified. + + at_object_creation() - only called once, when object is first created. + Object customizations go here. + at_object_delete() - called just before deleting an object. If returning + False, deletion is aborted. Note that all objects + inside a deleted object are automatically moved + to their , they don't need to be removed here. + + at_init() - called whenever typeclass is cached from memory, + at least once every server restart/reload + at_cmdset_get(**kwargs) - this is called just before the command handler + requests a cmdset from this object. The kwargs are + not normally used unless the cmdset is created + dynamically (see e.g. Exits). + at_pre_puppet(account)- (account-controlled objects only) called just + before puppeting + at_post_puppet() - (account-controlled objects only) called just + after completing connection account<->object + at_pre_unpuppet() - (account-controlled objects only) called just + before un-puppeting + at_post_unpuppet(account) - (account-controlled objects only) called just + after disconnecting account<->object link + at_server_reload() - called before server is reloaded + at_server_shutdown() - called just before server is fully shut down + + at_access(result, accessing_obj, access_type) - called with the result + of a lock access check on this object. Return value + does not affect check result. + + at_before_move(destination) - called just before moving object + to the destination. If returns False, move is cancelled. + announce_move_from(destination) - called in old location, just + before move, if obj.move_to() has quiet=False + announce_move_to(source_location) - called in new location, just + after move, if obj.move_to() has quiet=False + at_after_move(source_location) - always called after a move has + been successfully performed. + at_object_leave(obj, target_location) - called when an object leaves + this object in any fashion + at_object_receive(obj, source_location) - called when this object receives + another object + + at_traverse(traversing_object, source_loc) - (exit-objects only) + handles all moving across the exit, including + calling the other exit hooks. Use super() to retain + the default functionality. + at_after_traverse(traversing_object, source_location) - (exit-objects only) + called just after a traversal has happened. + at_failed_traverse(traversing_object) - (exit-objects only) called if + traversal fails and property err_traverse is not defined. + + at_msg_receive(self, msg, from_obj=None, **kwargs) - called when a message + (via self.msg()) is sent to this obj. + If returns false, aborts send. + at_msg_send(self, msg, to_obj=None, **kwargs) - called when this objects + sends a message to someone via self.msg(). + + return_appearance(looker) - describes this object. Used by "look" + command by default + at_desc(looker=None) - called by 'look' whenever the + appearance is requested. + at_get(getter) - called after object has been picked up. + Does not stop pickup. + at_drop(dropper) - called when this object has been dropped. + at_say(speaker, message) - by default, called if an object inside this + object speaks + + """ + + pass diff --git a/typeclasses/rooms.py b/typeclasses/rooms.py new file mode 100644 index 0000000..e502f88 --- /dev/null +++ b/typeclasses/rooms.py @@ -0,0 +1,27 @@ +""" +Room + +Rooms are simple containers that has no location of their own. + +""" + +from evennia import DefaultRoom +from commands.default_cmdsets import ChargenCmdSet + + +class Room(DefaultRoom): + """ + Rooms are like any Object, except their location is None + (which is default). They also use basetype_setup() to + add locks so they cannot be puppeted or picked up. + (to change that, use at_object_creation instead) + + See examples/object.py for a list of + properties and methods available on all Objects. + """ + + pass + +class chargenRoom(Room): + def at_object_creation(self): + self.cmdset.add(ChargenCmdSet, permanent=True) diff --git a/typeclasses/scripts.py b/typeclasses/scripts.py new file mode 100644 index 0000000..b36db5c --- /dev/null +++ b/typeclasses/scripts.py @@ -0,0 +1,92 @@ +""" +Scripts + +Scripts are powerful jacks-of-all-trades. They have no in-game +existence and can be used to represent persistent game systems in some +circumstances. Scripts can also have a time component that allows them +to "fire" regularly or a limited number of times. + +There is generally no "tree" of Scripts inheriting from each other. +Rather, each script tends to inherit from the base Script class and +just overloads its hooks to have it perform its function. + +""" + +from evennia import DefaultScript + + +class Script(DefaultScript): + """ + A script type is customized by redefining some or all of its hook + methods and variables. + + * available properties + + key (string) - name of object + name (string)- same as key + aliases (list of strings) - aliases to the object. Will be saved + to database as AliasDB entries but returned as strings. + dbref (int, read-only) - unique #id-number. Also "id" can be used. + date_created (string) - time stamp of object creation + permissions (list of strings) - list of permission strings + + desc (string) - optional description of script, shown in listings + obj (Object) - optional object that this script is connected to + and acts on (set automatically by obj.scripts.add()) + interval (int) - how often script should run, in seconds. <0 turns + off ticker + start_delay (bool) - if the script should start repeating right away or + wait self.interval seconds + repeats (int) - how many times the script should repeat before + stopping. 0 means infinite repeats + persistent (bool) - if script should survive a server shutdown or not + is_active (bool) - if script is currently running + + * Handlers + + locks - lock-handler: use locks.add() to add new lock strings + db - attribute-handler: store/retrieve database attributes on this + self.db.myattr=val, val=self.db.myattr + ndb - non-persistent attribute handler: same as db but does not + create a database entry when storing data + + * Helper methods + + start() - start script (this usually happens automatically at creation + and obj.script.add() etc) + stop() - stop script, and delete it + pause() - put the script on hold, until unpause() is called. If script + is persistent, the pause state will survive a shutdown. + unpause() - restart a previously paused script. The script will continue + from the paused timer (but at_start() will be called). + time_until_next_repeat() - if a timed script (interval>0), returns time + until next tick + + * Hook methods (should also include self as the first argument): + + at_script_creation() - called only once, when an object of this + class is first created. + is_valid() - is called to check if the script is valid to be running + at the current time. If is_valid() returns False, the running + script is stopped and removed from the game. You can use this + to check state changes (i.e. an script tracking some combat + stats at regular intervals is only valid to run while there is + actual combat going on). + at_start() - Called every time the script is started, which for persistent + scripts is at least once every server start. Note that this is + unaffected by self.delay_start, which only delays the first + call to at_repeat(). + at_repeat() - Called every self.interval seconds. It will be called + immediately upon launch unless self.delay_start is True, which + will delay the first call of this method by self.interval + seconds. If self.interval==0, this method will never + be called. + at_stop() - Called as the script object is stopped and is about to be + removed from the game, e.g. because is_valid() returned False. + at_server_reload() - Called when server reloads. Can be used to + save temporary variables you want should survive a reload. + at_server_shutdown() - called at a full server shutdown. + + """ + + pass diff --git a/web/__init__.py b/web/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/static_overrides/README.md b/web/static_overrides/README.md new file mode 100644 index 0000000..ab9a09e --- /dev/null +++ b/web/static_overrides/README.md @@ -0,0 +1,13 @@ +If you want to override one of the static files (such as a CSS or JS file) used by Evennia or a Django app installed in your Evennia project, +copy it into this directory's corresponding subdirectory, and it will be placed in the static folder when you run: + + python manage.py collectstatic + +...or when you reload the server via the command line. + +Do note you may have to reproduce any preceeding directory structures for the file to end up in the right place. + +Also note that you may need to clear out existing static files for your new ones to be gathered in some cases. Deleting files in static/ +will force them to be recollected. + +To see what files can be overridden, find where your evennia package is installed, and look in `evennia/web/static/` diff --git a/web/static_overrides/webclient/css/README.md b/web/static_overrides/webclient/css/README.md new file mode 100644 index 0000000..6ab7cbb --- /dev/null +++ b/web/static_overrides/webclient/css/README.md @@ -0,0 +1,3 @@ +You can replace the CSS files for Evennia's webclient here. + +You can find the original files in `evennia/web/static/webclient/css/` diff --git a/web/static_overrides/webclient/js/README.md b/web/static_overrides/webclient/js/README.md new file mode 100644 index 0000000..c785cb1 --- /dev/null +++ b/web/static_overrides/webclient/js/README.md @@ -0,0 +1,3 @@ +You can replace the javascript files for Evennia's webclient page here. + +You can find the original files in `evennia/web/static/webclient/js/` diff --git a/web/static_overrides/website/css/README.md b/web/static_overrides/website/css/README.md new file mode 100644 index 0000000..004fcd8 --- /dev/null +++ b/web/static_overrides/website/css/README.md @@ -0,0 +1,3 @@ +You can replace the CSS files for Evennia's homepage here. + +You can find the original files in `evennia/web/static/website/css/` diff --git a/web/static_overrides/website/images/README.md b/web/static_overrides/website/images/README.md new file mode 100644 index 0000000..2d2060c --- /dev/null +++ b/web/static_overrides/website/images/README.md @@ -0,0 +1,3 @@ +You can replace the image files for Evennia's home page here. + +You can find the original files in `evennia/web/static/website/images/` diff --git a/web/template_overrides/README.md b/web/template_overrides/README.md new file mode 100644 index 0000000..87ba6f1 --- /dev/null +++ b/web/template_overrides/README.md @@ -0,0 +1,4 @@ +Place your own version of templates into this file to override the default ones. +For instance, if there's a template at: `evennia/web/website/templates/website/index.html` +and you want to replace it, create the file `template_overrides/website/index.html` +and it will be loaded instead. diff --git a/web/template_overrides/webclient/README.md b/web/template_overrides/webclient/README.md new file mode 100644 index 0000000..b69d627 --- /dev/null +++ b/web/template_overrides/webclient/README.md @@ -0,0 +1,3 @@ +Replace Evennia's webclient django templates with your own here. + +You can find the original files in `evennia/web/webclient/templates/webclient/` diff --git a/web/template_overrides/website/README.md b/web/template_overrides/website/README.md new file mode 100644 index 0000000..589823a --- /dev/null +++ b/web/template_overrides/website/README.md @@ -0,0 +1,7 @@ +You can replace the django templates (html files) for the website +here. It uses the default "prosimii" theme. If you want to maintain +multiple themes rather than just change the default one in-place, +make new folders under `template_overrides/` and change +`settings.ACTIVE_THEME` to point to the folder name to use. + +You can find the original files under `evennia/web/website/templates/website/` diff --git a/web/template_overrides/website/flatpages/README.md b/web/template_overrides/website/flatpages/README.md new file mode 100644 index 0000000..9cd8142 --- /dev/null +++ b/web/template_overrides/website/flatpages/README.md @@ -0,0 +1,3 @@ +Flatpages require a default.html template, which can be overwritten by placing it in this folder. + +You can find the original files in `evennia/web/website/templates/website/flatpages/` diff --git a/web/template_overrides/website/registration/README.md b/web/template_overrides/website/registration/README.md new file mode 100644 index 0000000..7c0dfbe --- /dev/null +++ b/web/template_overrides/website/registration/README.md @@ -0,0 +1,3 @@ +The templates involving login/logout can be overwritten here. + +You can find the original files in `evennia/web/website/templates/website/registration/` diff --git a/web/urls.py b/web/urls.py new file mode 100644 index 0000000..741706c --- /dev/null +++ b/web/urls.py @@ -0,0 +1,18 @@ +""" +Url definition file to redistribute incoming URL requests to django +views. Search the Django documentation for "URL dispatcher" for more +help. + +""" +from django.conf.urls import url, include + +# default evennia patterns +from evennia.web.urls import urlpatterns + +# eventual custom patterns +custom_patterns = [ + # url(r'/desired/url/', view, name='example'), +] + +# this is required by Django. +urlpatterns = custom_patterns + urlpatterns diff --git a/world/README.md b/world/README.md new file mode 100644 index 0000000..0f3862d --- /dev/null +++ b/world/README.md @@ -0,0 +1,10 @@ +# world/ + +This folder is meant as a miscellanous folder for all that other stuff +related to the game. Code which are not commands or typeclasses go +here, like custom economy systems, combat code, batch-files etc. + +You can restructure and even rename this folder as best fits your +sense of organisation. Just remember that if you add new sub +directories, you must add (optionally empty) `__init__.py` files in +them for Python to be able to find the modules within. diff --git a/world/__init__.py b/world/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/world/basestats.py b/world/basestats.py new file mode 100644 index 0000000..6a0da1c --- /dev/null +++ b/world/basestats.py @@ -0,0 +1,183 @@ +_BASE_STATS_DATA = { + 1: { + 'PC': 80, + 'ST': 'Cannot Walk', + 'DX': 'Infant', + 'IQ': 'Vegetable', + 'HT': 'Barely awake' + }, + + 2: { + 'PC': 70, + 'ST': 'Cannot Walk', + 'DX': 'Cannot Walk', + 'IQ': 'Insect', + 'HT': 'Barely awake' + }, + + 3: { + 'PC': 60, + 'ST': '3 year old', + 'DX': 'Ludicrous', + 'IQ': 'Reptile', + 'HT': 'Very sickly' + }, + + 4: { + 'PC': 50, + 'ST': '4 year old', + 'DX': 'Ludicrous', + 'IQ': 'Horse', + 'HT': 'Very sickly' + }, + + 5: { + 'PC': 40, + 'ST': '6 year old', + 'DX': 'Ludicrous', + 'IQ': 'Dog', + 'HT': 'Sickly' + }, + + 6: { + 'PC': 30, + 'ST': '8 year old', + 'DX': 'Ludicrous', + 'IQ': 'Chimpanzee', + 'HT': 'Sickly' + }, + + 7: { + 'PC': 20, + 'ST': '10 year old', + 'DX': 'Clumsy', + 'IQ': 'Child', + 'HT': 'Weak' + }, + + 8: { + 'PC': 15, + 'ST': '13 year old', + 'DX': 'Clumsy', + 'IQ': 'Dull', + 'HT': 'Weak' + }, + + 9: { + 'PC': 10, + 'ST': 'Average', + 'DX': 'Average', + 'IQ': 'Dull average', + 'HT': 'Average' + }, + + 10: { + 'PC': 0, + 'ST': 'Average', + 'DX': 'Average', + 'IQ': 'Average', + 'HT': 'Average' + }, + + 11: { + 'PC': 10, + 'ST': 'Average', + 'DX': 'Average', + 'IQ': 'Average +', + 'HT': 'Average' + }, + + 12: { + 'PC': 20, + 'ST': 'Weekend Athlete', + 'DX': 'Graceful', + 'IQ': 'Bright average', + 'HT': 'Energetic' + }, + + 13: { + 'PC': 30, + 'ST': 'Athlete', + 'DX': 'Graceful', + 'IQ': 'Bright', + 'HT': 'Energetic' + }, + + 14: { + 'PC': 45, + 'ST': 'Athlete', + 'DX': 'Graceful', + 'IQ': 'Very Bright', + 'HT': 'Energetic' + }, + + 15: { + 'PC': 60, + 'ST': 'Weightlifter', + 'DX': 'Very nimble', + 'IQ': 'Genius minus', + 'HT': 'Very healthy' + }, + + 16: { + 'PC': 80, + 'ST': 'Weightlifter', + 'DX': 'Very nimble', + 'IQ': 'Genius', + 'HT': 'Very healthy' + }, + + 17: { + 'PC': 100, + 'ST': 'Circus strongman', + 'DX': 'Very nimble', + 'IQ': 'Genius-plus', + 'HT': 'Very healthy' + }, + + 18: { + 'PC': 125, + 'ST': 'Circus strongman', + 'DX': 'Very nimble', + 'IQ': 'Genius-plus', + 'HT': 'Very healthy' + }, + + 19: { + 'PC': 150, + 'ST': 'Olympic weightlifter', + 'DX': 'Incredible agile', + 'IQ': 'Nobel prize', + 'HT': 'Perfect health' + }, + + 20: { + 'PC': 175, + 'ST': 'Olympic weightlifter', + 'DX': 'Incredible agile', + 'IQ': 'Nobel prize', + 'HT': 'Perfect health' + }, +} + +def getStatDescription(level, stat): + return _BASE_STATS_DATA[level][stat] + +def getStatCost(currentLevel, newLevel): + pcCurrent = _BASE_STATS_DATA[currentLevel]['PC'] + pcNew = _BASE_STATS_DATA[newLevel]['PC'] + + if newLevel < currentLevel: + if newLevel <= 10 and currentLevel <= 10: + return pcNew - pcCurrent + elif newLevel >= 10 and currentLevel >= 10: + return pcCurrent - pcNew + elif newLevel <= 10 and currentLevel >= 10: + return pcCurrent + pcNew + else: + if currentLevel <= 10 and newLevel <= 10: + return pcCurrent - pcNew + elif currentLevel >= 10 and newLevel >= 10: + return pcNew - pcCurrent + elif currentLevel <= 10 and newLevel >= 10: + return pcCurrent + pcNew \ No newline at end of file diff --git a/world/batch_cmds.ev b/world/batch_cmds.ev new file mode 100644 index 0000000..ff5469e --- /dev/null +++ b/world/batch_cmds.ev @@ -0,0 +1,26 @@ +# +# A batch-command file is a way to build a game world +# in a programmatic way, by placing a sequence of +# build commands after one another. This allows for +# using a real text editor to edit e.g. descriptions +# rather than entering text on the command line. +# +# A batch-command file is loaded with @batchprocess in-game: +# +# @batchprocess[/interactive] tutorial_examples.batch_cmds +# +# A # as the first symbol on a line begins a comment and +# marks the end of a previous command definition. This is important, +# - every command must be separated by at least one line of comment. +# +# All supplied commands are given as normal, on their own line +# and accept arguments in any format up until the first next +# comment line begins. Extra whitespace is removed; an empty +# line in a command definition translates into a newline. +# +# See `evennia/contrib/tutorial_examples/batch_cmds.ev` for +# an example of a batch-command code. See also the batch-code +# system for loading python-code this way. +# + + diff --git a/world/cgmenu.py b/world/cgmenu.py new file mode 100644 index 0000000..3e8ce1c --- /dev/null +++ b/world/cgmenu.py @@ -0,0 +1,808 @@ +# in mygame/world/mychargen.py +import math + +from evennia.utils.evmenu import EvMenu +from evennia import EvForm, EvTable +from world import basestats +from world import skills as skillmodule + + +# Menu functions + +def start(caller): + text = "" + if requiredPropertiesSet(caller): + text = \ + """ + %s + %s + %s + %s + """ % (checkmark(caller,"name"), checkmark(caller,"age"), checkmark(caller,"sex"), checkmark(caller, "alignment")) + else: + text = \ + """ + |b|[yUnfinished required properties for character:|n + %s + %s + %s + %s + """ % (checkmark(caller,"name"), checkmark(caller,"age"), checkmark(caller,"sex"), checkmark(caller, "alignment")) + options = ({"desc": "Set character name", + "goto": "enter_name"}, + {"desc": "Set character age", + "goto": "enter_age"}, + {"desc": "Set attributes", + "goto": "set_attribs"}, + {"desc": "Set alignement", + "goto": "set_align"}, + {"desc": "Set your characters gender", + "goto": "set_sex"}, + {"desc": "Goto skills", + "goto": "skill_menu"}, + {"desc": "Save and exit", + "goto": _save_prefs}) + return text, options + +def set_attribs(caller): + text = \ + """ + Character attributes, abilities and preferences menu. + Choose the attribute you want to modify + + You have |w%i|n attribute points left to spend + """ % caller.db.attribpoints + options = ({"desc": "Modify strength (|w%i|n - |y%s|n)" % (caller.db.strength, basestats.getStatDescription(caller.db.strength, 'ST')), + "goto": ("startMod", {"attr": "ST"})}, + {"desc": "Modify dexterity (|w%i|n - |y%s|n)" % (caller.db.dexterity, basestats.getStatDescription(caller.db.dexterity, 'DX')), + "goto": ("startMod", {"attr": "DX"})}, + {"desc": "Modify intelligence (|w%i|n - |y%s|n)" % (caller.db.intelligence, basestats.getStatDescription(caller.db.intelligence, 'IQ')), + "goto": ("startMod", {"attr": "IQ"})}, + {"desc": "Modify health (|w%i|n - |y%s|n)" % (caller.db.health, basestats.getStatDescription(caller.db.health, 'HT')), + "goto": ("startMod", {"attr": "HT"})}, + {"desc": "Help", + "goto": "attrib_help"}, + {"desc": "Back to main menu", + "goto": "start"}) + return text, options + +def attrib_help(caller): + text = \ + """ + |yHow to select basic attributes|n + The basic attributes you select, will determine your abilities + including your strengths and weaknesses throughout the game. + Choose wisely. + + |yExplenation of the point system|n + Each attribute has a numerical point assigned to them. The more + points invested in the attribute, the better your characters + abilities towards that particular property will be: + + |c6 or less:|n An attribute with this few points severly + constraint your lifestyle. It is crippeling + + |c7:|n Anyone observing your character will immidiatly notice + your limitations in the attribute. It is considered the lowest + score you can have and still pass for "able bodied". + + |c8 or 9:|n This is considered below average. It is limiting, + but considered within the human norm. + + |c10:|n Most humans get by just fine with a score of 10, it's + considered average. 10 is the base starting stat of every + attribute of player characters. + + |c11 or 12:|n These scores are superior and considered above + average. But also within the norm + + |c13 or 14:|n We are starting to move into the realm of what + is considered exceptional with stats such as these. + Characters with these stats will draw attention from their + surroundings, and their qualities are apparent. + + |c15 and above:|n Characters who possess this much points in + any given attribute will immidiatly draw attention from + anyone, as they are considered amazing. + """ + options = ({"desc": "Back to attribute editor", + "goto": "set_attribs"}) + return text, options + +def set_align(caller): + text = \ + """ + Character attributes, abilities and preferences menu. + Choose the moral compass of your character + |wCurrent alignment:|n %s + """ % getCurrentAlignement(caller) + options = ({"desc": "My character is good", + "goto": _set_good}, + {"desc": "My character is neutral", + "goto": _set_neutral}, + {"desc": "My character is evil", + "goto": _set_evil}, + {"desc": "Back to main menu", + "goto": "start"}) + return text, options + +def set_sex(caller): + text = \ + """ + Character attributes, abilities and preferences menu. + Choose the gender of your character + |wCurrent gender:|n %s + """ % getCurrentGender(caller) + options = ({"desc": "Male sex", + "goto": _set_male}, + {"desc": "Female sex", + "goto": _set_female}, + {"desc": "Transgender", + "goto": _set_trans}, + {"desc": "Back to main menu", + "goto": "start"}) + return text, options + +def skill_menu(caller): + text = \ + """ + |wSkills menu|n + Here you can choose from a set of available skills for your + character to learn + """ + options = ({"desc": "Add skills", + "goto": "add_skills"}, + {"desc": "View current skills", + "goto": "view_current_skills"}, + {"desc": "Back to main menu", + "goto": "start"}) + return text, options + +def add_skills(caller, raw_string, **kwargs): + text = \ + """ + |wSkills menu|n + Choose which category of skills you want to learn + """ + options = ({"desc": "Animal skills", + "goto": "view_animal_skills"}, + {"desc": "Artistic skills", + "goto": "view_artistic_skills"}, + {"desc": "Athletic skills", + "goto": "view_athletic_skills"}, + {"desc": "Back to skill menu", + "goto": "skill_menu"}) + return text, options + +# Modify skill functions + +def enter_skill_typename(caller, raw_string, **kwargs): + skillId = kwargs.get("skillId") + typeName = kwargs.get("typeName") + cat = kwargs.get("category") + text = \ + """ + %s + + Type the name of the |y%s|n or to cancel + """ % (skillmodule.getTypeDescription(skillId), skillmodule.getTypenameOfSkill(skillId).lower()) + + options = { + "key": "_default", + "goto": (_enter_skill, { + "typeName": "Typed", + "skillId": skillId, + "category": cat + }) + } + + return text, options + +def _enter_skill(caller, raw_string, **kwargs): + typeSpecified = True + typeName = kwargs.get("typeName") + number = kwargs.get("skillId") + category = kwargs.get("category") + if not typeName: + typeSpecified = False + cat = kwargs.get("category") + if not raw_string: + return "add_skills" + else: + if not typeSpecified: + try: + number = int(raw_string.strip()) + except ValueError: + caller.msg("|rYou must enter a numerical value!|n") + return category + + if skillmodule.idInCategory(caller, cat, number): + if skillmodule.checkPrerequisite(caller, number): + if skillmodule.hasTypes(caller, number) and typeSpecified == False: + return "enter_skill_typename", {"skillId": number, "category": cat} + else: + caller.msg("Adding skill '|y%s|n' to your character" % skillmodule.getSkillName(number)) + skillmodule.addSkill(caller, number) + if typeSpecified: + skillmodule.addSkillTypeName(caller,number, typeName) + else: + caller.msg("The skill '|y%s|n' depends on having learned '|c%s|n'. Try adding it first" % (skillmodule.getSkillName(number), skillmodule.prerequisiteName(caller, number))) + return "add_skills" + else: + caller.msg("|rNo such skill in the table|n") + return category + +def view_animal_skills(caller, raw_string, **kwargs): + text = \ + """ + Choose the skill you want to add by typing in the numerical + (#) number from the table below or to return to the + category select menu + + %s + + You have |y%i|n points left to use on your skills + """ % (skillmodule.skillTable(caller, "Animal Skills"), caller.db.attribpoints) + + options = { + "key": "_default", + "goto": (_enter_skill, { + "category": "Animal Skills" + }) + } + return text, options + +def view_artistic_skills(caller, raw_string, **kwargs): + text = \ + """ + Choose the skill you want to add by typing in the numerical + (#) number from the table below or to return to the + category select menu + + %s + + You have |y%i|n points left to use on your skills + """ % (skillmodule.skillTable(caller, "Artistic Skills"), caller.db.attribpoints) + + options = { + "key": "_default", + "goto": (_enter_skill, { + "category": "Artistic Skills" + }) + } + return text, options + +def view_athletic_skills(caller, raw_string, **kwargs): + text = \ + """ + Choose the skill you want to add by typing in the numerical + (#) number from the table below or to return to the + category select menu + + %s + + You have |y%i|n points left to use on your skills + """ % (skillmodule.skillTable(caller, "Athletic Skills"), caller.db.attribpoints) + + options = { + "key": "_default", + "goto": (_enter_skill, { + "category": "Athletic Skills" + }) + } + return text, options + +def _remove_skill(caller, raw_string, **kwargs): + skillId = kwargs.get("skillId") + if skillmodule.removeFromCharacter(caller, skillId): + caller.msg("The skill: '|y%s|n' has been removed from your character" % skillmodule.getSkillName(skillId)) + else: + caller.msg("The skill '|y%s|n' is required by '|c%s|n'. Remove that skill first" % (skillmodule.getSkillName(skillId), skillmodule.requiredByName(caller,skillId))) + return "view_current_skills" + +def _set_level_skill(caller, raw_string, **kwargs): + number = kwargs.get("skillId") + updown = kwargs.get("updown") + if not raw_string: + return "view_current_skills" + + if not updown: + updownChoice = raw_string.strip() + if updownChoice.lower() not in ("d","down","u","up"): + caller.msg(f"|rYou must type either 'd' for down (or 'down') or 'u' for up (or 'up')|n") + return "level_skill" + elif updownChoice.lower() in ("d", "down") and caller.db.skillevel[number] < 2: + caller.msg(f"|rYour %s skill is already at the lowest permissable level|n" % skillmodule.getSkillName(number)) + return "level_skill" + elif updownChoice.lower() in ("u", "up") and caller.db.skillevel[number] > 9: + caller.msg(f"|rYour %s skill is already at the highest permissable level|n") + return "level_skill" + + return "level_skill", {"updown": updownChoice, "skillId": number} + + else: + try: + upDownNumber = int(raw_string.strip()) + except ValueError: + caller.msg(f"|rThe value you want to increase/decrease your level to, has to be a number|n") + return "level_skill" + + oldLevel = caller.db.skillevel[number] + if updown.lower() in ("d", "down"): + newLevel = oldLevel - upDownNumber + if newLevel < 1: + caller.msg(f"|rYou cannot decrease your level below 1|n") + return "level_skill" + + costDown = skillmodule.getSkillCost(number, oldLevel) - skillmodule.getSkillCost(number, newLevel) + caller.db.skillevel[number] = newLevel + caller.db.attribpoints = caller.db.attribpoints + costDown + caller.msg(f"You have |gsuccessfully|n changed the level of the skill |y%s|n to |c%i|n, refunding you |g%i|n skillpoints to your attribute point pool" + % (skillmodule.getSkillName(number), newLevel, costDown)) + else: + newLevel = oldLevel + upDownNumber + if newLevel > 10: + caller.msg(f"|rYou cannot increase your level above 10|n") + return "level_skill" + + costUp = skillmodule.getSkillCost(number, newLevel) - skillmodule.getSkillCost(number, oldLevel) + caller.db.skillevel[number] = newLevel + caller.db.attribpoints = caller.db.attribpoints - costUp + caller.msg(f"You have |gsuccessfully|n changed the level of the skill |y%s|n to |c%i|n, costing you |g%i|n skillpoints from your attribute point pool" + % (skillmodule.getSkillName(number), newLevel, costUp)) + + return "view_current_skills" + + +def level_skill(caller, raw_string, **kwargs): + number = kwargs.get("skillId") + updown = kwargs.get("updown") + text = "" + + if not updown: + text = \ + """ + Leveling the skill '|y%s|n' with |c%i|n points left to use + + Choose if you want to level this skill (|wu|n)p or (|wd|n)own + """ % (skillmodule.getSkillName(number), caller.db.attribpoints) + elif updown.lower() in ('u','up'): + text = \ + """ + Leveling the skill '|y%s|n' with |c%i|n points left to use + + Type in the number you want to |wincrease|n your level to or + to return to the skill overview menu + """ % (skillmodule.getSkillName(number), caller.db.attribpoints) + else: + text = \ + """ + Leveling the skill '|y%s|n' with |c%i|n points left to use + + Type in the number you want to |wdecrease|n your level to or + to return to the skill overview menu + """ % (skillmodule.getSkillName(number), caller.db.attribpoints) + + options = { + "key": "_default", + "goto": (_set_level_skill, { + "skillId": number, + "updown": updown + }) + } + return text, options + +def edit_skill(caller, raw_string, **kwargs): + number = kwargs.get("skillId") + text = \ + """ + Editing skill: '|y%s|n' + """ % skillmodule.getSkillName(number) + + options = ( + { + "desc": "Remove this skill", + "goto": (_remove_skill, { + "skillId": number + }) + }, + { + "desc": "Level skill up/down", + "goto": ("level_skill", { + "skillId": number + }) + }, + { + "desc": "Return to character skills menu", + "goto": "view_current_skills" + }, + { + "desc": "Return to main skill menu", + "goto": "skill_menu" + } + ) + + return text, options + +def _edit_skill_check(caller, raw_string, **kwargs): + if not raw_string: + return "skill_menu" + else: + try: + number = int(raw_string.strip()) + except ValueError: + caller.msg("|rYou must enter a numerical value!|n") + return "view_current_skills" + + if skillmodule.skillInCharacter(caller, number): + return "edit_skill", {"skillId": number} + else: + caller.msg("|rNo such skill in the table|n") + return "view_current_skills" + + +def view_current_skills(caller, raw_string, **kwargs): + text = \ + """ + These are the skills your character currently has. Type in + the number of the skill to edit it (edit the level of the + skill, other properties or just to remove it alltogether) + + Press to just cancel and return to the previous + menu + + %s + """ % skillmodule.characterSkills(caller) + + options = { + "key": "_default", + "goto": (_edit_skill_check, { + "category": "Animal Skills" + }) + } + return text, options + +# Modify attribute functions + +def _enter_points(caller, raw_string, **kwargs): + attr = kwargs.get("attr") + calcType = kwargs.get("type") + points = raw_string.strip() + newVal = 0 + cost = 0 + pointsGain = 0 + + try: + pointsNumber = int(points) + except ValueError: + caller.msg(f"You must supply a number") + return "set_attribs" + + if calcType == "add": + if validAddition(caller, attr, pointsNumber): + if caller.db.attribpoints == 0: + caller.msg(f"|wNo more attribute points to spend|n\nYou can remove points from other attributes and\nredistribute them later") + return "set_attribs" + + if attr == "ST": + cost = basestats.getStatCost(caller.db.strength, caller.db.strength + pointsNumber) + if cost > caller.db.attribpoints: + caller.msg(f"|rThe increase in the %s stat costs more than your available point pool \n(%i total cost|n)" % (attr, cost)) + return "set_attribs" + + caller.db.strength = caller.db.strength + pointsNumber + newVal = caller.db.strength + elif attr == "DX": + cost = basestats.getStatCost(caller.db.dexterity, caller.db.dexterity + pointsNumber) + if cost > caller.db.attribpoints: + caller.msg(f"|rThe increase in the %s stat costs more than your available point pool \n(%i total cost|n)" % (attr, cost)) + return "set_attribs" + + caller.db.dexterity = caller.db.dexterity + pointsNumber + newVal = caller.db.dexterity + elif attr == "IQ": + cost = basestats.getStatCost(caller.db.intelligence, caller.db.intelligence + pointsNumber) + if cost > caller.db.attribpoints: + caller.msg(f"|rThe increase in the %s stat costs more than your available point pool \n(%i total cost|n))" % (attr, cost)) + return "set_attribs" + + caller.db.intelligence = caller.db.intelligence + pointsNumber + newVal = caller.db.intelligence + elif attr == "HT": + cost = basestats.getStatCost(caller.db.health, caller.db.health + pointsNumber) + if cost > caller.db.attribpoints: + caller.msg(f"|rThe increase in the %s stat costs more than your available point pool \n(%i total cost|n)" % (attr, cost)) + return "set_attribs" + + caller.db.health = caller.db.health + pointsNumber + newVal = caller.db.health + if cost >= 0: + caller.db.attribpoints = caller.db.attribpoints - cost + else: + caller.db.attribpoints = caller.db.attribpoints + cost + else: + caller.msg(f"|rYou cannot increase your %s attribute by that much!|n" % attr) + return "set_attribs" + else: + if validSubtraction(caller, attr, pointsNumber): + if attr == "ST": + pointsGain = basestats.getStatCost(caller.db.strength, caller.db.strength - pointsNumber) + caller.db.strength = caller.db.strength - pointsNumber + newVal = caller.db.strength + elif attr == "DX": + pointsGain = basestats.getStatCost(caller.db.dexterity, caller.db.dexterity - pointsNumber) + caller.db.dexterity = caller.db.dexterity - pointsNumber + newVal = caller.db.dexterity + elif attr == "IQ": + pointsGain = basestats.getStatCost(caller.db.intelligence, caller.db.intelligence - pointsNumber) + caller.db.intelligence = caller.db.intelligence - pointsNumber + newVal = caller.db.intelligence + elif attr == "HT": + pointsGain = basestats.getStatCost(caller.db.health, caller.db.health - pointsNumber) + caller.db.health = caller.db.health - pointsNumber + newVal = caller.db.health + caller.db.attribpoints = caller.db.attribpoints + pointsGain + else: + caller.msg(f"|rYou cannot decrease your %s attribute by that much!|n" % attr) + return "set_attribs" + + caller.msg(f"Your |y%s|n stat has been changed to %i" % (attr, newVal)) + return "set_attribs" + +def startMod(caller, **kwargs): + attr = kwargs.get("attr") + text = "Do you want to |y(s)|nubtract or |y(a)|ndd to the attribute |w%s|n?" % attr + options = {"key": "_default", "goto": ("checkType", {"attr": attr})} + + return text, options + +def checkType(caller, raw_string, **kwargs): + attr = kwargs.get("attr") + if raw_string.lower() in ("s", "subtract"): + text = "Enter amount of |y%s|n points you want to subtract" % attr + options = {"key": "_default", "goto": (_enter_points, {"attr": attr, "type": "subtract"})} + return text, options + elif raw_string.lower() in ("a", "add"): + text = "Enter amount of |y%s|n points you want to add" % attr + options = {"key": "_default", "goto": (_enter_points, {"attr": attr, "type": "add"})} + return text, options + else: + text = "I did not understand that reply. Type 'a' or 'add' \nto add or 's' or 'subtract' to subtract" + options = {"key": "_default", "goto": ("startMod", {"attr": attr})} + return text, options + +# Modify alignement functions + +def _set_good(caller): + caller.db.alignment = 0 + return "set_align" + +def _set_neutral(caller): + caller.db.alignment = 1 + return "set_align" + +def _set_evil(caller): + caller.db.alignment = 2 + return "set_align" + +# Modify gender functions + +def _set_male(caller): + caller.db.gender = 0 + return "set_sex" + +def _set_female(caller): + caller.db.gender = 1 + return "set_sex" + +def _set_trans(caller): + caller.db.gender = 2 + return "set_sex" + +# Modify name functions + +def _set_name(caller, raw_string, **kwargs): + inp = raw_string.strip() + prev_entry = kwargs.get("prev_entry") + + if not inp: + # a blank input either means OK or Abort + if prev_entry: + caller.key = prev_entry + caller.msg("Name set to %s." % prev_entry) + caller.db.name = prev_entry + return "start" + else: + caller.msg("Aborted. No name set") + return "start" + else: + # re-run old node, but pass in the name given + return None, {"prev_entry": inp} + + +def enter_name(caller, raw_string, **kwargs): + # check if we already entered a name before + prev_entry = kwargs.get("prev_entry") + + if not prev_entry and caller.db.name != "NO_NAME": + prev_entry = caller.db.name + + if prev_entry: + text = "Current name: %s.\nEnter another name or to accept." % prev_entry + else: + text = "Enter your character's name or to abort." + + options = {"key": "_default", + "goto": (_set_name, {"prev_entry": prev_entry})} + + return text, options + +# Modify age functions + +def _set_age(caller, raw_string): + if not raw_string: + return "start" + try: + valueAge = int(raw_string) + except ValueError: + caller.msg(f"|rYou must type a number|n") + return "start" + + if validAge(caller, valueAge): + caller.msg(f"Age set to %i" % valueAge) + caller.db.age = valueAge + + return "start" + +def enter_age(caller, raw_string): + text = "Enter age or press to cancel" + options = {"key": "_default", "goto": _set_age} + return text,options + +# Helper functions + +def validSubtraction(caller, attribute, points): + value = 0 + if attribute == "ST": + value = caller.db.strength - points + elif attribute == "IQ": + value = caller.db.intelligence - points + elif attribute == "DX": + value = caller.db.dexterity - points + elif attribute == "HT": + value = caller.db.health - points + + if value < 1: + return False + else: + return True + +def validAddition(caller, attribute, points): + value = 0 + if attribute == "ST": + value = caller.db.strength + points + elif attribute == "IQ": + value = caller.db.intelligence + points + elif attribute == "DX": + value = caller.db.dexterity + points + elif attribute == "HT": + value = caller.db.health + points + + if value > 20: + return False + else: + return True + +def getCurrentAlignement(caller): + alignment = "" + if caller.db.alignment == 0: + alignement = "Good" + elif caller.db.alignment == 1: + alignement = "Neutral" + elif caller.db.alignment == 2: + alignement = "Evil" + else: + alignement = "|wUndefined|n" + + return alignement + +def getCurrentGender(caller): + gender = "" + if caller.db.gender == 0: + gender = "Male" + elif caller.db.gender == 1: + gender = "Female" + elif caller.db.gender == 2: + gender = "Transgender" + else: + gender = "Undefined" + + return gender + +def checkmark(caller, field): + if field == "name": + if caller.db.name == "NO_NAME": + return "|r[ ]|n No name for character set" + else: + return "|g[X]|n |wName: %s|n" % caller.db.name + elif field == "age": + if caller.db.age < 1: + return "|r[ ]|n Character has no age" + else: + return "|g[X]|n |wAge: %i|n" % caller.db.age + elif field == "sex": + if caller.db.gender == 4: + return "|r[ ]|n No gender defined" + else: + return "|g[X]|n |wGender: %s|n" % getCurrentGender(caller) + elif field == "alignment": + if caller.db.alignment == 4: + return "|r[ ]|n Character has no moral alignment" + else: + return "|g[X]|n |wAlignment: %s|n" % getCurrentAlignement(caller) + +def validAge(caller, age): + if age < 18 or age > 60: + caller.msg(f"|rInvalid value for age, it has to be between 18 and 60") + return False + else: + return True + +def requiredPropertiesSet(caller): + if caller.db.name == "NO_NAME": + return False + if caller.db.age < 1: + return False + if caller.db.gender == 4: + return False + if caller.db.alignment == 4: + return False + return True + +# Finalizing character creation functions + +def _save_prefs(caller, raw_string, **kwargs): + if not requiredPropertiesSet(caller): + caller.msg(f"|rYou have not filled out the required character fields|n") + return "start" + + form = EvForm("world/charsheetform.py") + form.map(cells = { + 1: caller.db.name, + 2: caller.db.account, + 3: "A placeholder description for this character", + 4: caller.db.strength, + 5: caller.db.dexterity, + 6: caller.db.intelligence, + 7: caller.db.health, + 8: caller.db.health + caller.db.dexterity / 4, + 9: math.floor(caller.db.health + caller.db.dexterity / 4), + 10: "10/08/2021", + 11: caller.db.attribpoints, + 12: caller.db.strength * 2, + 13: caller.db.strength * 4, + 14: caller.db.strength * 6, + 15: caller.db.strength * 12, + 16: caller.db.strength * 20, + 17: 0, + 18: 0, + 19: 0, + 20: math.floor(caller.db.health + caller.db.dexterity / 4), + 21: 0, + 22: 0 + }) + + tableA = EvTable("Item", "Value", border="incols") + tableB = EvTable("Skill", "Level", "Comp", border="incols") + for key in caller.db.skills: + tableB.add_row( + skillmodule.getSkillName(key), + caller.db.skillevel[key], + skillmodule.getSkillDescription(caller.db.skillevel[key]) + ) + form.map(tables = { + "A": tableA, + "B": tableB + }) + caller.msg(form) + return "start" \ No newline at end of file diff --git a/world/charsheetform.py b/world/charsheetform.py new file mode 100644 index 0000000..f515bcb --- /dev/null +++ b/world/charsheetform.py @@ -0,0 +1,46 @@ +FORMCHAR = "x" +TABLECHAR = "c" + +FORM = ''' +.------------------------------------------------. +| | +| Name: xxxxx1xxxxx Player: xxxxxxx2xxxxxxx | +| xxxxxxxxxxx | +| | +| DOB: xxxx10xxxxx Unspent points: xx11xx | + >----------------------------------------------< +| | Description/Appearance: | +| ST: x4x| xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | +| | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | +| DX: x5x| xxxxxxxxxxxxxxxxx3xxxxxxxxxxxxxxxxx | +| | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | +| IQ: x6x| >-----------------------------------< | +| | Movement | +| HT: x7x| Basic speed: x8x Move: x9x | +|________|______________________________________ | +| | +| Enc: None: x12x Passive defenses: | +| Light: x13x Armor: x17x | +| Med: x14x Shield: x18x | +| Hvy: x15x Total: x19x | +| X-hvy: x16x | +|________________________________________________| +| | | +| Active defenses: | Damage Resistance: | +| Dodge: x20x | | +| Parry: x21x | cccccccccccccccccccc | +| Block: x22x | cccccccccccccccccccc | +| | cccccccccccccccccccc | +| | cccccccccccccccccccc | +| | cccccccccAcccccccccc | +| | | + >----------------------------------------------< +| | +| cccccccccccccccccccccccccccccccccccccccccccccc | +| cccccccccccccccccccccccccccccccccccccccccccccc | +| cccccccccccccccccccccccccccccccccccccccccccccc | +| cccccccccccccccccccccccccccccccccccccccccccccc | +| ccccccccccccccccccBccccccccccccccccccccccccccc | +| | +------------------------------------------------- +''' \ No newline at end of file diff --git a/world/prototypes.py b/world/prototypes.py new file mode 100644 index 0000000..04aba09 --- /dev/null +++ b/world/prototypes.py @@ -0,0 +1,90 @@ +""" +Prototypes + +A prototype is a simple way to create individualized instances of a +given typeclass. It is dictionary with specific key names. + +For example, you might have a Sword typeclass that implements everything a +Sword would need to do. The only difference between different individual Swords +would be their key, description and some Attributes. The Prototype system +allows to create a range of such Swords with only minor variations. Prototypes +can also inherit and combine together to form entire hierarchies (such as +giving all Sabres and all Broadswords some common properties). Note that bigger +variations, such as custom commands or functionality belong in a hierarchy of +typeclasses instead. + +A prototype can either be a dictionary placed into a global variable in a +python module (a 'module-prototype') or stored in the database as a dict on a +special Script (a db-prototype). The former can be created just by adding dicts +to modules Evennia looks at for prototypes, the latter is easiest created +in-game via the `olc` command/menu. + +Prototypes are read and used to create new objects with the `spawn` command +or directly via `evennia.spawn` or the full path `evennia.prototypes.spawner.spawn`. + +A prototype dictionary have the following keywords: + +Possible keywords are: +- `prototype_key` - the name of the prototype. This is required for db-prototypes, + for module-prototypes, the global variable name of the dict is used instead +- `prototype_parent` - string pointing to parent prototype if any. Prototype inherits + in a similar way as classes, with children overriding values in their partents. +- `key` - string, the main object identifier. +- `typeclass` - string, if not set, will use `settings.BASE_OBJECT_TYPECLASS`. +- `location` - this should be a valid object or #dbref. +- `home` - valid object or #dbref. +- `destination` - only valid for exits (object or #dbref). +- `permissions` - string or list of permission strings. +- `locks` - a lock-string to use for the spawned object. +- `aliases` - string or list of strings. +- `attrs` - Attributes, expressed as a list of tuples on the form `(attrname, value)`, + `(attrname, value, category)`, or `(attrname, value, category, locks)`. If using one + of the shorter forms, defaults are used for the rest. +- `tags` - Tags, as a list of tuples `(tag,)`, `(tag, category)` or `(tag, category, data)`. +- Any other keywords are interpreted as Attributes with no category or lock. + These will internally be added to `attrs` (eqivalent to `(attrname, value)`. + +See the `spawn` command and `evennia.prototypes.spawner.spawn` for more info. + +""" + +## example of module-based prototypes using +## the variable name as `prototype_key` and +## simple Attributes + +# from random import randint +# +# GOBLIN = { +# "key": "goblin grunt", +# "health": lambda: randint(20,30), +# "resists": ["cold", "poison"], +# "attacks": ["fists"], +# "weaknesses": ["fire", "light"], +# "tags": = [("greenskin", "monster"), ("humanoid", "monster")] +# } +# +# GOBLIN_WIZARD = { +# "prototype_parent": "GOBLIN", +# "key": "goblin wizard", +# "spells": ["fire ball", "lighting bolt"] +# } +# +# GOBLIN_ARCHER = { +# "prototype_parent": "GOBLIN", +# "key": "goblin archer", +# "attacks": ["short bow"] +# } +# +# This is an example of a prototype without a prototype +# (nor key) of its own, so it should normally only be +# used as a mix-in, as in the example of the goblin +# archwizard below. +# ARCHWIZARD_MIXIN = { +# "attacks": ["archwizard staff"], +# "spells": ["greater fire ball", "greater lighting"] +# } +# +# GOBLIN_ARCHWIZARD = { +# "key": "goblin archwizard", +# "prototype_parent" : ("GOBLIN_WIZARD", "ARCHWIZARD_MIXIN") +# } diff --git a/world/skills.py b/world/skills.py new file mode 100644 index 0000000..f2e0597 --- /dev/null +++ b/world/skills.py @@ -0,0 +1,410 @@ +import array as arr + +from evennia.utils import evtable + +_GENERAL_SKILL_LEVEL_DESCRIPTIONS = { + 1: "Inept", + 2: "Mediocre", + 3: "Mediocre", + 4: "Average", + 5: "Average", + 6: "Rather skilled", + 7: "Rather skilled", + 8: "Well trained", + 9: "Well trained", + 10: "Expert" +} + +_WEAPON_SKILL_LEVEL_DESCRIPTIONS = { + 1: "Astoundingly bad", + 2: "Astoundingly bad", + 3: "Clumsy", + 4: "Unskilled", + 5: "Novice", + 6: "Veteran", + 7: "Expert", + 8: "Master", + 9: "Master", + 10: "Wizard" +} + +_SKILL_COST = { + 1: { + "Easy": 1, + "Average": 2, + "Hard": 4, + "Very Hard": 8 + }, + + 2: { + "Easy": 2, + "Average": 4, + "Hard": 8, + "Very Hard": 12 + }, + + 3: { + "Easy": 4, + "Average": 8, + "Hard": 12, + "Very Hard": 16 + }, + + 4: { + "Easy": 8, + "Average": 12, + "Hard": 16, + "Very Hard": 20 + }, + + 5: { + "Easy": 12, + "Average": 16, + "Hard": 20, + "Very Hard": 24 + }, + + 7: { + "Easy": 16, + "Average": 20, + "Hard": 24, + "Very Hard": 28 + }, + + 8: { + "Easy": 20, + "Average": 24, + "Hard": 28, + "Very Hard": 32 + }, + + 9: { + "Easy": 24, + "Average": 28, + "Hard": 32, + "Very Hard": 36 + }, + + 10: { + "Easy": 28, + "Average": 32, + "Hard": 36, + "Very Hard": 40 + } +} + +_SKILL_DATA = { + # Animal Skills + 1: { + "Name": "Animal Handling", + "Category": "Animal Skills", + "Difficulty": "Hard", + "Attribute": "Mental", + "Defaults": { + "IQ": 6 + }, + "Prerequisite": 0, + "Subskills": [3, 5], + "Description": "This is the ability to train and work with all types of animals" + }, + + 2: { + "Name": "Falconry", + "Category": "Animal Skills", + "Difficulty": "Average", + "Attribute": "Mental", + "Defaults": { + "IQ": 5 + }, + "Prerequisite": 0, + "Subskills": [0], + "Description": "Hunting small game with a trained hawk" + }, + + 3: { + "Name": "Packing", + "Category": "Animal Skills", + "Difficulty": "Hard", + "Attribute": "Mental", + "Prerequisite": 1, + "Subskills": [0], + "Defaults": { + "IQ": 6, + 1: 6 + }, + "Description": "Ability to efficiently and speedily get loads on and off pack animals" + }, + + 4: { + "Name": "Riding", + "Category": "Animal Skills", + "Difficulty": "Average", + "Attribute": "Physical", + "Prerequisite": 0, + "Subskills": [0], + "Defaults": { + "DX": 5, + 1: 3 + }, + "Description": "Riding tamed animals suited for travel" + }, + + 5: { + "Name": "Teamster", + "Category": "Animal Skills", + "Difficulty": "Average", + "Attribute": "Physical", + "Prerequisite": 1, + "Subskills": [0], + "Defaults": { + 1: 4, + 4: 2 + }, + "Description": "Skill of driving and harnessing teams/herds of animals, such as a wagon or gun teams" + }, + + 6: { + "Name": "Veterinary", + "Category": "Animal Skills", + "Difficulty": "Hard", + "Attribute": "Mental", + "Prerequisite": 0, + "Subskills": [0], + "Defaults": { + 0: { + "Typename": "Animal", + "Roll": 5, + "Create": "Specify the name of the animal species you are able to care for/doctor" + } + }, + "Description": "Skill at caring for sick or wounded animals" + }, + + # Artistic skills + + 7: { + "Name": "Artist", + "Category": "Artistic Skills", + "Difficulty": "Hard", + "Attribute": "Mental", + "Prerequisite": 0, + "Subskills": [0], + "Defaults": { + "IQ": 6 + }, + "Prerequisite": 0, + "Description": "Ability to paint and draw with both accuracy and beauty" + }, + + 8: { + "Name": "Bard", + "Category": "Artistic Skills", + "Difficulty": "Average", + "Attribute": "Mental", + "Defaults": { + "IQ": 5 + }, + "Prerequisite": 0, + "Subskills": [0], + "Description": "Ability to tell stories and to speak extemporaneously" + }, + + 9: { + "Name": "Musical Instrument", + "Category": "Artistic Skills", + "Difficulty": "Hard", + "Attribute": "Mental", + "Defaults": { + 0: { + "Typename": "Instrument", + "Roll": 3, + "Create": "Specify the name of the instrument you are able to play" + } + }, + "Prerequisite": 0, + "Subskills": [0], + "Description": "Skill at playing a specific instrument" + }, + + # Athletic skills + + 10: { + "Name": "Acrobatics", + "Category": "Athletic Skills", + "Difficulty": "Hard", + "Attribute": "Physical", + "Defaults": { + "DX": 6 + }, + "Prerequisite": 0, + "Subskills": [0], + "Description": "The art of pulling off acrobatic and gymnastic stunts, roll, take falls, balance and so on" + }, + + 11: { + "Name": "Jumping", + "Category": "Athletic Skills", + "Difficulty": "Easy", + "Attribute": "Physical", + "Defaults": 0, + "Prerequisite": 0, + "Subskills": [0], + "Description": "This is the trained ability to use your strength to its best advantage when you jump" + }, + + 12: { + "Name": "Running", + "Category": "Athletic Skills", + "Difficulty": "Hard", + "Attribute": "Physical", + "Defaults": { + "Special": { + 0: "Self", + 1: "Divide", + 2: 8, + 3: "Add", + 4: "Move" + } + }, + "Prerequisite": 0, + "Subskills": [0], + "Description": "This represents training in sprints and long distance running" + }, + + 13: { + "Name": "Swimming", + "Category": "Arhletic Skills", + "Difficulty": "Easy", + "Attribute": "Physical", + "Defaults": { + "ST": 5, + "DX": 4 + }, + "Prerequisite": 0, + "Subskills": [0], + "Description": "Ability to stay afloat and swim in water" + } +} + +def skillTable(caller, category, excludeCharacterSkills = True): + table = evtable.EvTable("#", "Skill name", "Attribute", "Difficulty", "Description") + for key in _SKILL_DATA: + if not excludeCharacterSkills: + if _SKILL_DATA[key]["Category"] == category: + table.add_row( + key, + _SKILL_DATA[key]["Name"], + _SKILL_DATA[key]["Attribute"], + _SKILL_DATA[key]["Difficulty"], + _SKILL_DATA[key]["Description"] + ) + else: + if _SKILL_DATA[key]["Category"] == category and key not in caller.db.skills: + table.add_row( + key, + _SKILL_DATA[key]["Name"], + _SKILL_DATA[key]["Attribute"], + _SKILL_DATA[key]["Difficulty"], + _SKILL_DATA[key]["Description"] + ) + + return table + +def idInCategory(caller, category, number): + legitimateValues = [] + for key in _SKILL_DATA: + if _SKILL_DATA[key]["Category"] == category: + legitimateValues.append(key) + + if number in legitimateValues: + return True + else: + return False + +def getSkillName(skillId): + return _SKILL_DATA[skillId]["Name"] + +def addSkill(caller, skillId): + cost = getSkillCost(skillId) + caller.db.skills.append(skillId) + caller.db.attribpoints = caller.db.attribpoints - cost + caller.db.skillevel[skillId] = 1 + +def addSkillTypeName(caller, skillId, typeName): + caller.db.skillproperties[skillId] = typeName + +def characterSkills(caller): + table = evtable.EvTable("#", "Skill name", "Level", "Attribute", "Difficulty", "Description") + for key in caller.db.skills: + table.add_row( + key, + _SKILL_DATA[key]["Name"], + "%i |w%s|n" % (caller.db.skillevel[key], getSkillDescription(caller.db.skillevel[key])), + _SKILL_DATA[key]["Attribute"], + _SKILL_DATA[key]["Difficulty"], + _SKILL_DATA[key]["Description"] + ) + + return table + +def skillInCharacter(caller, skillId): + for key in caller.db.skills: + if key == skillId: + return True + + return False + +def removeFromCharacter(caller, skillId): + for key in caller.db.skills: + if skillId == _SKILL_DATA[key]["Prerequisite"]: + return False + + level = caller.db.skillevel[skillId] + gain = getSkillCost(skillId, level) + caller.db.skills.remove(skillId) + caller.db.attribpoints = caller.db.attribpoints + gain + caller.db.skillevel[skillId] = 0 + caller.db.skillproperties[skillId] = 0 + return True + +def checkPrerequisite(caller, skillId): + if _SKILL_DATA[skillId]["Prerequisite"] == 0: + return True + elif _SKILL_DATA[skillId]["Prerequisite"] > 0 and _SKILL_DATA[skillId]["Prerequisite"] in caller.db.skills: + return True + return False + +def prerequisiteName(caller, skillId): + if _SKILL_DATA[skillId]["Prerequisite"] > 0: + prerequisiteId = _SKILL_DATA[skillId]["Prerequisite"] + return _SKILL_DATA[prerequisiteId]["Name"] + return None + +def requiredByName(caller, skillId): + skillNames = [] + for key in _SKILL_DATA[skillId]["Subskills"]: + for keyid in caller.db.skills: + if keyid == key: + skillNames.append(getSkillName(key)) + + return skillNames + +def hasTypes(caller, skillId): + if 0 in _SKILL_DATA[skillId]["Defaults"]: + return True + return False + +def getTypeDescription(skillId): + return _SKILL_DATA[skillId]["Defaults"][0]["Create"] + +def getTypenameOfSkill(skillId): + return _SKILL_DATA[skillId]["Defaults"][0]["Typename"] + +def getSkillCost(skillId, level = 1): + return _SKILL_COST[level][_SKILL_DATA[skillId]["Difficulty"]] + +def getSkillDescription(level, sType = "Generic"): + if sType == "Generic": + return _GENERAL_SKILL_LEVEL_DESCRIPTIONS[level] + else: + return _WEAPON_SKILL_LEVEL_DESCRIPTIONS[level] \ No newline at end of file diff --git a/world/testdict.py b/world/testdict.py new file mode 100644 index 0000000..c67a7ac --- /dev/null +++ b/world/testdict.py @@ -0,0 +1,5 @@ +# aDict = {1:2} + +class testClass: + testVar = 123 + testString = "Hello World" \ No newline at end of file