forked from pwgen2155/dawdle
feat: working
This commit is contained in:
commit
e6cb475b6d
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Django #
|
||||
*.log
|
||||
*.pot
|
||||
*.pyc
|
||||
__pycache__
|
||||
*~
|
||||
*/__pycache__
|
||||
*/*/__pycache__
|
||||
*/*/*/__pycache__
|
||||
db.sqlite3
|
||||
media
|
||||
|
||||
# Backup files #
|
||||
*.bak
|
||||
*.sqlite3
|
||||
|
||||
site/static/
|
177
COPYING
Normal file
177
COPYING
Normal file
|
@ -0,0 +1,177 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
7
Dockerfile
Normal file
7
Dockerfile
Normal file
|
@ -0,0 +1,7 @@
|
|||
FROM python:3.11
|
||||
|
||||
RUN pip install --no-cache-dir django uwsgi Pillow
|
||||
|
||||
VOLUME /data
|
||||
|
||||
CMD [ "python", "/data/dawdle.py", "/data/data/dawdle.conf"]
|
49
README.md
Normal file
49
README.md
Normal file
|
@ -0,0 +1,49 @@
|
|||
# DawdleRPG
|
||||
|
||||
DawdleRPG is an IdleRPG clone written in Python.
|
||||
|
||||
## Basic Setup
|
||||
|
||||
- Edit `dawdle.conf` to configure your bot.
|
||||
- Run `dawdle.py <path to dawdle.conf>`
|
||||
- The data directory defaults to the parent directory of the
|
||||
configuration file, and dawdlerpg expects files to be in that
|
||||
directory.
|
||||
|
||||
## Setup with Website
|
||||
|
||||
The included `install.sh` script will set up the dawdlerpg bot and
|
||||
website on a freshly installed Debian system. It uses nginx, uwsgi,
|
||||
and django for the site. At some point, you should be prompted to
|
||||
edit the dawdle.conf file, and you'll need to edit some configuration
|
||||
parameters explained by the comments in the file.
|
||||
|
||||
```sh
|
||||
./install.sh <hostname>
|
||||
```
|
||||
|
||||
If you don't have a clean install, you should instead look at the
|
||||
`install.sh` script and use the pieces that work for your setup.
|
||||
|
||||
## Migrating from IdleRPG
|
||||
|
||||
DawdleRPG is capable of being a drop-in replacement.
|
||||
|
||||
- Run `dawdle.py <path to old irpg.conf>`
|
||||
|
||||
If you have any command line overrides to the configuration, you will
|
||||
need to replace them with the `-o key=value` option.
|
||||
|
||||
## Differences from IdleRPG
|
||||
|
||||
- Names, items, and durations are in different colors.
|
||||
- Output throttling allows configurable rate over a period.
|
||||
- Long messages are word wrapped.
|
||||
- Logging can be set to different levels.
|
||||
- Better IRC protocol support.
|
||||
- More game numbers are configurable.
|
||||
- Quest pathfinding is much more efficient.
|
||||
- Fights caused by map collisions have chance of finding item.
|
||||
- All worn items have a chance to get buffs/debuffs instead of a subset.
|
||||
- High level character can still get special items.
|
||||
- Special items are always buffs.
|
33
af/dawdlerpg.nginx
Normal file
33
af/dawdlerpg.nginx
Normal file
|
@ -0,0 +1,33 @@
|
|||
# DawdleRPG nginx configuration
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
|
||||
# SSL configuration
|
||||
#
|
||||
# listen 443 ssl default_server;
|
||||
# listen [::]:443 ssl default_server;
|
||||
# gzip off
|
||||
#
|
||||
|
||||
root DAWDLERPG_DIR/site/static;
|
||||
charset utf-8;
|
||||
server_name _;
|
||||
|
||||
location /media {
|
||||
alias DAWDLERPG_DIR/site/media;
|
||||
}
|
||||
location /static {
|
||||
alias DAWDLERPG_DIR/site/static;
|
||||
}
|
||||
location /favicon.ico {
|
||||
alias DAWDLERPG_DIR/site/static/favicon.ico;
|
||||
}
|
||||
location /robots.txt {
|
||||
alias DAWDLERPG_DIR/site/static/robots.txt;
|
||||
}
|
||||
location / {
|
||||
uwsgi_pass unix:///tmp/dawdlerpg-uwsgi.sock;
|
||||
include uwsgi_params;
|
||||
}
|
||||
}
|
31
af/events.txt
Normal file
31
af/events.txt
Normal file
|
@ -0,0 +1,31 @@
|
|||
# Calamities - these add to a player's time to level
|
||||
C was struck in the head by a falling anime box set
|
||||
C was mauled by a werewolf
|
||||
C kept asking for invites to the secreet club
|
||||
C posted too many times in help
|
||||
C DIED... a bit
|
||||
|
||||
# Godsends - these subtract from a player's time to level
|
||||
G was sprinkled with fairy dust, or something...
|
||||
G found a pot of gold with an unreleased Anime BD
|
||||
G found the glorious wonders of linux
|
||||
G met a friendly AB user
|
||||
G found a magical spring, that wasn't poisioned
|
||||
G drank a potent elixir
|
||||
G rewrote this Godsends in Go instead of Perl
|
||||
|
||||
# Quest mode 1 - these are simply timed quests
|
||||
Q1 solve the theft of the AB domain
|
||||
Q1 deliver a priceless scroll to the library of Chinese Anime
|
||||
Q1 rescue an imprisoned precure from the palace of Grefog
|
||||
Q1 impersonate cultists to infiltrate a dark temple
|
||||
Q1 unearth the secret of the Twin Peeks (.)(.)
|
||||
Q1 throwing a party for Animefield
|
||||
Q1 decipher the fabled tablet of AnimeBytes
|
||||
|
||||
# Quest mode 2 - these are two-stage navigation quests
|
||||
Q2 464 23 113 187 search for a hostage nymph and heal her broken spring
|
||||
Q2 321 488 137 56 complete the Pisan pilgrimage from the shine of Katu to Mount Irta
|
||||
Q2 304 269 70 417 secretly follow a Esteri assassin and report on her activities
|
||||
Q2 447 432 359 318 travel to the Orod volcano to retrieve a lava-proof circlet and bring it to a country village
|
||||
Q2 326 31 246 133 must spring a Jonast noble from a prison and escort him back to his ancestral lands.
|
220
dawdle.py
Executable file
220
dawdle.py
Executable file
|
@ -0,0 +1,220 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# Copyright 2021 Daniel Lowe
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import asyncio
|
||||
import atexit
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import re
|
||||
import resource
|
||||
import signal
|
||||
import sys
|
||||
import termios
|
||||
import time
|
||||
|
||||
from dawdle import bot
|
||||
from dawdle import conf
|
||||
from dawdle import irc
|
||||
from dawdle.log import log
|
||||
import dawdle.log as dawdlelog
|
||||
|
||||
|
||||
def first_setup(db: bot.GameDB) -> None:
|
||||
"""Perform initialization of game."""
|
||||
pname = input(f"Initializing dbfile {bot.datapath(conf.get('dbfile'))}. Give an account name that you would like to have admin access [{conf.get('owner')}]: ")
|
||||
if pname == "":
|
||||
pname = conf.get("owner")
|
||||
pclass = input("Enter a character class for this account: ")
|
||||
pclass = pclass[:conf.get("max_class_len")]
|
||||
try:
|
||||
old = termios.tcgetattr(sys.stdin.fileno())
|
||||
new = old.copy()
|
||||
new[3] = new[3] & ~termios.ECHO
|
||||
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, new)
|
||||
ppass = input("Password for this account: ")
|
||||
finally:
|
||||
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, old)
|
||||
|
||||
if db.exists():
|
||||
db.clear()
|
||||
else:
|
||||
db.create()
|
||||
p = db.new_player(pname, pclass, ppass)
|
||||
p.isadmin = True
|
||||
db.write_players()
|
||||
|
||||
print(f"\n\nOK, wrote you into {bot.datapath(conf.get('dbfile'))}\n")
|
||||
|
||||
|
||||
def check_pidfile(pidfile: str) -> None:
|
||||
"""Exit if pid in pidfile is still active."""
|
||||
try:
|
||||
with open(pidfile) as inf:
|
||||
pid = int(inf.readline().rstrip())
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
sys.stderr.write(f"The pidfile at {pidfile} indicates that dawdle is still running at pid {pid}. Remove the file or kill the process.\n")
|
||||
sys.exit(1)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def daemonize() -> None:
|
||||
"""Daemonize the process."""
|
||||
# python-daemon on pip would do this better.
|
||||
|
||||
# set core limit to 0
|
||||
core_limit = (0, 0)
|
||||
resource.setrlimit(resource.RLIMIT_CORE, core_limit)
|
||||
os.umask(0)
|
||||
|
||||
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
os._exit(0)
|
||||
os.setsid()
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
os._exit(0)
|
||||
os.chdir("/")
|
||||
signal.signal(signal.SIGTSTP, signal.SIG_IGN)
|
||||
signal.signal(signal.SIGTTIN, signal.SIG_IGN)
|
||||
signal.signal(signal.SIGTTOU, signal.SIG_IGN)
|
||||
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
||||
os.dup2(os.open(os.devnull, os.O_RDWR), sys.stdin.fileno())
|
||||
os.dup2(os.open(os.devnull, os.O_RDWR), sys.stdout.fileno())
|
||||
os.dup2(os.open(os.devnull, os.O_RDWR), sys.stderr.fileno())
|
||||
|
||||
|
||||
async def mainloop(client: irc.IRCClient) -> None:
|
||||
"""Connect to servers repeatedly."""
|
||||
while not client.quitting:
|
||||
addr, port = conf.get("servers")[0].split(':')
|
||||
await client.connect(addr, port)
|
||||
if client.quitting or not conf.get("reconnect"):
|
||||
break
|
||||
await asyncio.sleep(conf.get("reconnect_wait"))
|
||||
|
||||
|
||||
def start_bot() -> None:
|
||||
"""Main entry point for bot."""
|
||||
conf.init()
|
||||
|
||||
# Legacy IdleRPG logging config
|
||||
if conf.get("debug"):
|
||||
dawdlelog.add_handler("DEBUG", "/dev/stderr", "%(asctime)s %(message)s")
|
||||
|
||||
if conf.has("logfile"):
|
||||
dawdlelog.add_handler(conf.get("loglevel"),
|
||||
bot.datapath(conf.get("logfile")),
|
||||
"%(asctime)s %(message)s")
|
||||
|
||||
# DawdleRPG logging config
|
||||
for logger in conf.get("loggers"):
|
||||
if len(logger) != 3:
|
||||
sys.stderr.write(f"Invalid log configuration {logger}.")
|
||||
sys.exit(2)
|
||||
dawdlelog.add_handler(logger[0], bot.datapath(logger[1]), logger[2])
|
||||
|
||||
log.info("Bot %s starting.", bot.VERSION)
|
||||
|
||||
store: bot.GameStorage
|
||||
if conf.get("store_format") == "idlerpg":
|
||||
store = bot.IdleRPGGameStorage(bot.datapath(conf.get("dbfile")))
|
||||
elif conf.get("store_format") == "sqlite3":
|
||||
store = bot.Sqlite3GameStorage(bot.datapath(conf.get("dbfile")))
|
||||
else:
|
||||
sys.stderr.write(f"Invalid configuration store_format={conf.get('store_format')}. Configuration must be idlerpg or sqlite3.")
|
||||
sys.exit(2)
|
||||
|
||||
if conf.get("setup"):
|
||||
if store.exists():
|
||||
store.clear()
|
||||
first_setup(bot.GameDB(store))
|
||||
sys.exit(0)
|
||||
|
||||
db = bot.GameDB(store)
|
||||
if not db.exists():
|
||||
sys.stderr.write("Game db doesn't exist. Run with --setup.")
|
||||
sys.exit(6)
|
||||
|
||||
db.backup_store()
|
||||
db.load_state()
|
||||
|
||||
if conf.get("migrate"):
|
||||
new_store = bot.Sqlite3GameStorage(conf.get("migrate"))
|
||||
if new_store.exists():
|
||||
new_store.clear()
|
||||
else:
|
||||
new_store.create()
|
||||
print(f"Writing {db.count_players()} players.")
|
||||
new_store.write(db._players.values())
|
||||
print(f"Writing quest.")
|
||||
new_store.update_quest(db._quest)
|
||||
# Update history from modsfile.
|
||||
print(f"Writing history.")
|
||||
names = set(db._players.keys())
|
||||
history = []
|
||||
with open(bot.datapath(conf.get("modsfile")), "rb") as inf:
|
||||
for linebytes in inf.readlines():
|
||||
try:
|
||||
line = str(linebytes, encoding='utf8')
|
||||
except UnicodeDecodeError:
|
||||
line = str(linebytes, encoding='latin-1')
|
||||
|
||||
match = re.match(r'\[(\d\d)/(\d\d)/(\d\d) (.*?)\] (.*)', line)
|
||||
if not match:
|
||||
print(f"Line didn't parse: {line}")
|
||||
continue
|
||||
mon, day, year, timeofday, text = match.groups()
|
||||
for word in re.findall(r"\w+", text):
|
||||
if word in names:
|
||||
history.append((word, f"20{year}-{mon}-{day} {timeofday}", text))
|
||||
if len(history) > 10000:
|
||||
new_store.bulk_history_insert(history)
|
||||
history = []
|
||||
new_store.bulk_history_insert(history)
|
||||
print("Done.")
|
||||
sys.exit(0)
|
||||
|
||||
if db.count_players() == 0:
|
||||
sys.stderr.write(f"Zero players in {conf.get('dbfile')}. Do you need to run with --setup?\n")
|
||||
sys.exit(6)
|
||||
|
||||
if conf.has("pidfile"):
|
||||
check_pidfile(bot.datapath(conf.get("pidfile")))
|
||||
|
||||
if conf.get("daemonize"):
|
||||
daemonize()
|
||||
|
||||
if conf.has("pidfile"):
|
||||
with open(bot.datapath(conf.get("pidfile")), "w") as ouf:
|
||||
ouf.write(f"{os.getpid()}\n")
|
||||
atexit.register(os.remove, bot.datapath(conf.get("pidfile")))
|
||||
|
||||
mybot = bot.DawdleBot(db)
|
||||
client = irc.IRCClient(mybot)
|
||||
|
||||
log.info("Starting main async loop.")
|
||||
asyncio.run(mainloop(client))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
start_bot()
|
0
dawdle/__init__.py
Normal file
0
dawdle/__init__.py
Normal file
144
dawdle/abstract.py
Normal file
144
dawdle/abstract.py
Normal file
|
@ -0,0 +1,144 @@
|
|||
from abc import abstractmethod, ABC
|
||||
from typing import Iterable, Optional, Set
|
||||
|
||||
|
||||
class AbstractClient(ABC):
|
||||
|
||||
class User(ABC):
|
||||
nick: str
|
||||
userhost: str
|
||||
modes: Set[str]
|
||||
joined: float
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def chanmsg(self, text: str) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def notice(self, target: str, text: str) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def match_user(self, nick: str, userhost: str) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def bot_has_ops(self) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def grant_voice(self, *targets: str) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def revoke_voice(self, *targets: str) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_channel_voices(self, voiced_nicks: Iterable[str]) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def writeq_len(self) -> int:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def writeq_bytes(self) -> int:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def clear_writeq(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def servername(self) -> str:
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def bytes_sent(self) -> int:
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def bytes_received(self) -> int:
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def user_exists(self, nick: str) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def nick_userhost(self, nick: str) -> Optional[str]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_bot_nick(self, nick: str) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def quit(self, text: str) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class AbstractBot(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def connected(self, client: AbstractClient) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def disconnected(self) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def ready(self) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def acquired_ops(self) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def nick_parted(self, user: AbstractClient.User) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def nick_kicked(self, user: AbstractClient.User) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def netsplit(self, user: AbstractClient.User) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def nick_dropped(self, user: AbstractClient.User) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def nick_quit(self, user: AbstractClient.User) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def nick_changed(self, user: AbstractClient.User, new_nick: str) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def private_message(self, user: AbstractClient.User, text: str) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def channel_message(self, user: AbstractClient.User, text: str) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def channel_notice(self, user: AbstractClient.User, text: str) -> None:
|
||||
pass
|
||||
|
||||
|
||||
AbstractClient.register(tuple)
|
||||
AbstractBot.register(tuple)
|
2547
dawdle/bot.py
Executable file
2547
dawdle/bot.py
Executable file
File diff suppressed because it is too large
Load diff
34
dawdle/chunk.py
Normal file
34
dawdle/chunk.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
import itertools
|
||||
from typing import Iterable, Iterator, Tuple, TypeVar
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def chunk(iterable: Iterable[T], n: int) -> Iterator[Tuple[T, ...]]:
|
||||
"""Collect data into chunks of size not more than n.
|
||||
|
||||
The last chunk may be a different length than n.
|
||||
chunk('ABCDEFG', 3) --> ABC DEF G
|
||||
"""
|
||||
iterator = iter(iterable)
|
||||
while True:
|
||||
group = tuple(itertools.islice(iterator, 0, n))
|
||||
if not group:
|
||||
break
|
||||
yield group
|
||||
|
||||
|
||||
def padded_chunk(iterable: Iterable[T], n: int, pad: T) -> Iterator[Tuple[T, ...]]:
|
||||
"""Collect data into chunks of exactly n elements.
|
||||
|
||||
The chunks will all be the same size, padded with the pad value.
|
||||
padded_chunk('ABCDEFG', 3, 'x') --> ABC DEF Gxx
|
||||
"""
|
||||
iterator = iter(iterable)
|
||||
while True:
|
||||
group = list(itertools.islice(iterator, 0, n))
|
||||
if not group:
|
||||
break
|
||||
group.extend(itertools.repeat(pad, n - len(group)))
|
||||
yield tuple(group)
|
153
dawdle/conf.py
Normal file
153
dawdle/conf.py
Normal file
|
@ -0,0 +1,153 @@
|
|||
import argparse
|
||||
import logging
|
||||
import os.path
|
||||
import re
|
||||
import sys
|
||||
|
||||
from dawdle.log import log
|
||||
from typing import cast, Any, Dict, List, Union
|
||||
|
||||
DURATION_RE = re.compile(r"(\d+)([dhms])")
|
||||
NUMERIC_RE = re.compile(r"[+-]?\d+(?:(\.)\d*)?")
|
||||
|
||||
_conf = dict()
|
||||
|
||||
|
||||
def parse_val(s: str) -> Union[bool, float, str]:
|
||||
"""Parse values used in the configuration file."""
|
||||
if s in ["on", "yes", "true"]:
|
||||
return True
|
||||
if s in ["off", "no", "false"]:
|
||||
return False
|
||||
istime = DURATION_RE.match(s)
|
||||
if istime:
|
||||
return int(istime[1]) * {"d":86400, "h": 3600, "m": 60, "s": 1}[istime[2]]
|
||||
|
||||
isnum = NUMERIC_RE.match(s)
|
||||
if isnum:
|
||||
if isnum[1]:
|
||||
return float(s)
|
||||
return int(s)
|
||||
return s
|
||||
|
||||
|
||||
def read_config(path: str) -> Dict[str, Any]:
|
||||
"""Return dict with contents of configuration file."""
|
||||
newconf = {
|
||||
"setup": False,
|
||||
"servers": [],
|
||||
"okurls": [],
|
||||
"loggers": [],
|
||||
"localaddr": None,
|
||||
# Legacy idlerpg option
|
||||
"debug": False,
|
||||
# Non-idlerpg config needs defaults
|
||||
"confpath": os.path.realpath(path),
|
||||
"datadir": os.path.realpath(os.path.dirname(path)),
|
||||
"rpmaxexplvl": 60,
|
||||
"allylvlbase": 200,
|
||||
"allylvlstep": 1.16,
|
||||
"allymaxexplvl": 60,
|
||||
"backupdir": ".dbbackup",
|
||||
"store_format": "idlerpg",
|
||||
"daemonize": True,
|
||||
"loglevel": "DEBUG",
|
||||
"throttle": True,
|
||||
"throttle_rate": 4,
|
||||
"throttle_period": 1,
|
||||
"penquest": 15,
|
||||
"pennick": 30,
|
||||
"penmessage": 1,
|
||||
"penpart": 200,
|
||||
"penkick": 250,
|
||||
"penquit": 20,
|
||||
"pendropped": 20,
|
||||
"penlogout": 20,
|
||||
"good_battle_pct": 110,
|
||||
"evil_battle_pct": 90,
|
||||
"max_name_len": 16,
|
||||
"max_class_len": 30,
|
||||
"message_wrap_len": 400,
|
||||
"quest_interval_min": 12*3600,
|
||||
"quest_interval_max": 24*3600,
|
||||
"quest_min_level": 24,
|
||||
"quest_min_login": 36000,
|
||||
"color": False,
|
||||
"namecolor": "cyan",
|
||||
"durationcolor": "green",
|
||||
"itemcolor": "olive",
|
||||
}
|
||||
|
||||
ignore_line_re = re.compile(r"^\s*(?:#|$)")
|
||||
config_line_re = re.compile(r"^\s*(\S+)\s*(.*)$")
|
||||
try:
|
||||
with open(path) as inf:
|
||||
for line in inf:
|
||||
if ignore_line_re.match(line):
|
||||
continue
|
||||
match = config_line_re.match(line)
|
||||
if not match:
|
||||
log.warning("Invalid config line: "+line)
|
||||
continue
|
||||
key, val = match[1].lower(), match[2].rstrip()
|
||||
if key == "die":
|
||||
log.critical(f"Please edit {path} to setup your bot's options.")
|
||||
sys.exit(1)
|
||||
elif key == "server":
|
||||
cast(List[str], newconf["servers"]).append(val)
|
||||
elif key == "okurl":
|
||||
cast(List[str], newconf["okurls"]).append(val)
|
||||
elif key == "log":
|
||||
cast(List[List[str]], newconf["loggers"]).append(val.split(" ", 2))
|
||||
else:
|
||||
newconf[key] = parse_val(val)
|
||||
except OSError as err:
|
||||
log.critical(f"Unable to read {path}: {err}")
|
||||
sys.exit(1)
|
||||
return newconf
|
||||
|
||||
|
||||
def init() -> None:
|
||||
global _conf
|
||||
parser = argparse.ArgumentParser(description="IdleRPG clone")
|
||||
parser.add_argument("-o", "--override", action='append', default=[], help="Override config option in k=v format.")
|
||||
parser.add_argument("--setup", default=False, action="store_true", help="Begin initial setup.")
|
||||
parser.add_argument("--migrate", help="Migrate game to slqite3 db at path.")
|
||||
parser.add_argument("config_file", help="Path to configuration file. You must specify this.")
|
||||
|
||||
args = parser.parse_args()
|
||||
_conf.update(read_config(args.config_file))
|
||||
|
||||
_conf['setup'] = args.setup
|
||||
_conf['migrate'] = args.migrate
|
||||
|
||||
# override configurations from command line
|
||||
server_overrides = []
|
||||
okurl_overrides = []
|
||||
for pair in args.override:
|
||||
if "=" not in pair:
|
||||
sys.stderr.write("Overrides must be in k=v format.\n")
|
||||
sys.exit(1)
|
||||
k,v = pair.split('=', 1)
|
||||
if k == "server":
|
||||
server_overrides.append(v)
|
||||
elif k == "okurl":
|
||||
okurl_overrides.append(v)
|
||||
else:
|
||||
_conf[k] = parse_val(v)
|
||||
if server_overrides:
|
||||
_conf["servers"] = server_overrides
|
||||
if okurl_overrides:
|
||||
_conf["okurls"] = okurl_overrides
|
||||
|
||||
# Debug flag turns off daemonization, sets loglevel to debug, and logs to stderr
|
||||
if _conf["debug"]:
|
||||
_conf["daemonize"] = False
|
||||
_conf["loglevel"] = logging.DEBUG
|
||||
|
||||
|
||||
def get(key: str) -> Any:
|
||||
return _conf[key]
|
||||
|
||||
def has(key: str) -> Any:
|
||||
return key in _conf
|
589
dawdle/irc.py
Normal file
589
dawdle/irc.py
Normal file
|
@ -0,0 +1,589 @@
|
|||
import asyncio
|
||||
import collections
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import textwrap
|
||||
import time
|
||||
|
||||
from dawdle import abstract
|
||||
from dawdle import chunk
|
||||
from dawdle import conf
|
||||
from dawdle.log import log
|
||||
from typing import Dict, Iterable, List, Optional, Set, TypeVar
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class IRCClient(abstract.AbstractClient):
|
||||
"""IRCClient acts as a layer between the IRC protocol and the bot protocol.
|
||||
|
||||
This class has the following responsibilities:
|
||||
- Connection and disconnection
|
||||
- Nick recovery
|
||||
- Output throttling
|
||||
- NickServ authentication
|
||||
- IRC message decoding and parsing
|
||||
- Tracking users in the channel
|
||||
- Calling methods on the bot interface
|
||||
"""
|
||||
MESSAGE_RE = re.compile(r'^(?:@(\S*) )?(?::([^ !]*)(?:!([^ @]*)(?:@([^ ]*))?)?\s+)?(\S+)\s*((?:[^:]\S*(?:\s+|$))*)(?::(.*))?')
|
||||
|
||||
Message = collections.namedtuple('Message', ['tags', 'src', 'user', 'host', 'cmd', 'args', 'trailing', 'line', 'time'])
|
||||
|
||||
|
||||
@staticmethod
|
||||
def parse_message(line: str) -> Optional[Message]:
|
||||
"""Parse IRC line into a Message."""
|
||||
# Parse IRC message with a regular expression
|
||||
match = IRCClient.MESSAGE_RE.match(line)
|
||||
if not match:
|
||||
return None
|
||||
rawtags, src, user, host, cmd, argstr, trailing = match.groups()
|
||||
# IRCv3 supports tags
|
||||
tags: Dict[str, Optional[str]] = {}
|
||||
if rawtags is not None and rawtags != "":
|
||||
for pairstr in rawtags.split(';'):
|
||||
pair = pairstr.split('=')
|
||||
if len(pair) == 2:
|
||||
tags[pair[0]] = re.sub(r"\\(.)",
|
||||
lambda m: {":": ";", "s": " ", "r": "\r", "n": "\n"}.get(m[1], m[1]),
|
||||
pair[1])
|
||||
else:
|
||||
tags[pair[0]] = None
|
||||
# Arguments before the trailing argument (after the colon) are space-delimited
|
||||
args = [] if argstr == "" else argstr.rstrip().split(' ')
|
||||
# There's nothing special about the trailing argument except it can have spaces.
|
||||
if trailing is not None:
|
||||
args.append(trailing)
|
||||
# Numeric responses specify a useless target afterwards
|
||||
if re.match(r'\d+', cmd):
|
||||
args = args[1:]
|
||||
# Support time tag, which allows servers and bouncers to send history
|
||||
if 'time' in tags and tags["time"] is not None:
|
||||
msgtime = time.mktime(time.strptime(tags['time'], "%Y-%m-%dT%H:%M:%S"))
|
||||
else:
|
||||
msgtime = time.time()
|
||||
return IRCClient.Message(tags, src, user, host, cmd, args, trailing, line, msgtime)
|
||||
|
||||
|
||||
class User(abstract.AbstractClient.User):
|
||||
"""An IRC user in the channel."""
|
||||
|
||||
def __init__(self, nick: str, userhost: str, modes: List[str], joined: float):
|
||||
self.nick = nick
|
||||
self.userhost = userhost
|
||||
self.modes = set(modes)
|
||||
self.joined = joined
|
||||
|
||||
|
||||
_bot: abstract.AbstractBot
|
||||
_writer: Optional[asyncio.StreamWriter]
|
||||
_nick: str
|
||||
_bytes_sent: int
|
||||
_bytes_received: int
|
||||
_caps: Set[str]
|
||||
_server: Optional[str]
|
||||
_messages_sent: int
|
||||
_writeq: List[bytes]
|
||||
_flushq_task: Optional[asyncio.Task] # type: ignore
|
||||
_prefixmodes: Dict[str, str]
|
||||
_maxmodes: int
|
||||
_modetypes: Dict[str, int]
|
||||
_users: Dict[str, abstract.AbstractClient.User]
|
||||
quitting: bool
|
||||
|
||||
def __init__(self, bot: abstract.AbstractBot):
|
||||
self._bot = bot
|
||||
self._writer = None
|
||||
self._nick = conf.get("botnick")
|
||||
self._bytes_sent = 0
|
||||
self._bytes_received = 0
|
||||
self._caps = set()
|
||||
self._server = None
|
||||
self._messages_sent = 0
|
||||
self._writeq = []
|
||||
self._flushq_task = None
|
||||
self._prefixmodes = {}
|
||||
self._maxmodes = 3
|
||||
self._modetypes = {}
|
||||
self._users = {}
|
||||
self.quitting = False
|
||||
|
||||
|
||||
async def connect(self, addr: str, port: str) -> None:
|
||||
"""Connect to IRC network and handle messages."""
|
||||
reader, self._writer = await asyncio.open_connection(addr, port, ssl=True, local_addr=conf.get("localaddr"))
|
||||
self._server = addr
|
||||
self._connected = True
|
||||
self._messages_sent = 0
|
||||
self._writeq = []
|
||||
self._flushq_task = None
|
||||
self._prefixmodes = {}
|
||||
self._maxmodes = 3
|
||||
self._modetypes = {}
|
||||
self._users = {}
|
||||
self._caps = set() # all enabled capabilities
|
||||
self.sendnow("CAP REQ :multi-prefix userhost-in-names")
|
||||
self.sendnow("CAP END")
|
||||
if 'BOTPASS' in os.environ:
|
||||
self.sendnow(f"PASS {os.environ['BOTPASS']}")
|
||||
self.sendnow(f"NICK {conf.get('botnick')}")
|
||||
self.sendnow(f"USER {conf.get('botuser')} 0 * :{conf.get('botrlnm')}")
|
||||
self._bot.connected(self)
|
||||
while True:
|
||||
linebytes = await reader.readline()
|
||||
if not linebytes:
|
||||
if self._flushq_task:
|
||||
self._flushq_task.cancel()
|
||||
self._bot.disconnected()
|
||||
self._writer.close()
|
||||
await self._writer.wait_closed()
|
||||
self._writer = None
|
||||
break
|
||||
self._bytes_received += len(linebytes)
|
||||
# Assume utf-8 encoding, fall back to latin-1, which has no invalid encodings from bytes.
|
||||
try:
|
||||
line = str(linebytes, encoding='utf8')
|
||||
except UnicodeDecodeError:
|
||||
line = str(linebytes, encoding='latin-1')
|
||||
line = line.rstrip('\r\n')
|
||||
loglevel = 5 if re.match(r"^PING ", line) else logging.DEBUG
|
||||
log.log(loglevel, "<- %s", line)
|
||||
msg = IRCClient.parse_message(line)
|
||||
if msg:
|
||||
self.dispatch(msg)
|
||||
|
||||
|
||||
def send(self, s: str, loglevel: int=logging.DEBUG) -> None:
|
||||
"""Send throttled messages."""
|
||||
assert self._writer is not None
|
||||
b = bytes(s+"\r\n", encoding='utf8')
|
||||
|
||||
if not conf.get("throttle"):
|
||||
log.log(loglevel, "-> %s", s)
|
||||
self._writer.write(b)
|
||||
self._bytes_sent += len(b)
|
||||
return
|
||||
|
||||
if self._messages_sent < conf.get("throttle_rate"):
|
||||
log.log(loglevel, "(%d)-> %s", self._messages_sent, s)
|
||||
self._writer.write(b)
|
||||
self._messages_sent += 1
|
||||
self._bytes_sent += len(b)
|
||||
else:
|
||||
self._writeq.append(b)
|
||||
|
||||
# The flushq task will reset messages_sent after the throttle period.
|
||||
if not self._flushq_task:
|
||||
self._flushq_task = asyncio.create_task(self.flushq_task())
|
||||
|
||||
|
||||
def sendnow(self, s: str, loglevel:int=logging.DEBUG) -> None:
|
||||
"""Send messages ignoring throttle."""
|
||||
assert self._writer is not None
|
||||
log.log(loglevel, "=> %s", s)
|
||||
b = bytes(s+"\r\n", encoding='utf8')
|
||||
self._writer.write(b)
|
||||
self._messages_sent += 1
|
||||
self._bytes_sent += len(b)
|
||||
if conf.get("throttle") and not self._flushq_task:
|
||||
self._flushq_task = asyncio.create_task(self.flushq_task())
|
||||
|
||||
|
||||
async def flushq_task(self) -> None:
|
||||
"""Flush send queue and release throttle."""
|
||||
assert self._writer is not None
|
||||
await asyncio.sleep(conf.get("throttle_period"))
|
||||
self._messages_sent = max(0, self._messages_sent - conf.get("throttle_rate"))
|
||||
while self._writeq:
|
||||
while self._writeq and self._messages_sent < conf.get("throttle_rate"):
|
||||
log.debug("(%d)~> %s", self._messages_sent, str(self._writeq[0], encoding='utf8').rstrip())
|
||||
self._writer.write(self._writeq[0])
|
||||
self._messages_sent += 1
|
||||
self._bytes_sent += len(self._writeq[0])
|
||||
self._writeq = self._writeq[1:]
|
||||
if self._writeq:
|
||||
await asyncio.sleep(conf.get("throttle_period"))
|
||||
self._messages_sent = max(0, self._messages_sent - conf.get("throttle_rate"))
|
||||
|
||||
self._flushq_task = None
|
||||
|
||||
|
||||
def servername(self) -> str:
|
||||
if self._server:
|
||||
return self._server
|
||||
return "<disconnected>"
|
||||
|
||||
|
||||
def bytes_sent(self) -> int:
|
||||
return self._bytes_sent
|
||||
|
||||
|
||||
def bytes_received(self) -> int:
|
||||
return self._bytes_received
|
||||
|
||||
|
||||
def writeq_len(self) -> int:
|
||||
"""Returns number of messages in the write queue."""
|
||||
return sum([len(b) for b in self._writeq])
|
||||
|
||||
|
||||
def writeq_bytes(self) -> int:
|
||||
"""Returns number of bytes in the write queue."""
|
||||
return sum([len(b) for b in self._writeq])
|
||||
|
||||
|
||||
def clear_writeq(self) -> None:
|
||||
self._writeq.clear()
|
||||
|
||||
|
||||
def dispatch(self, msg: Message) -> None:
|
||||
"""Dispatch the IRC command to a handler method."""
|
||||
if hasattr(self, "handle_"+msg.cmd.lower()):
|
||||
getattr(self, "handle_"+msg.cmd.lower())(msg)
|
||||
|
||||
|
||||
def handle_ping(self, msg: Message) -> None:
|
||||
"""PING - sends PONG back to server for keepalive."""
|
||||
self.sendnow(f"PONG :{msg.trailing}", loglevel=5)
|
||||
|
||||
|
||||
def handle_005(self, msg: Message) -> None:
|
||||
"""RPL_ISUPPORT - server features and information"""
|
||||
self._server = msg.src
|
||||
params = dict([arg.split('=') if '=' in arg else (arg, arg) for arg in msg.args])
|
||||
if 'MODES' in params:
|
||||
self._maxmodes = int(params['MODES'])
|
||||
if 'PREFIX' in params:
|
||||
m = re.match(r'\(([^)]*)\)(.*)', params['PREFIX'])
|
||||
if m:
|
||||
self._prefixmodes.update(zip(m[2], m[1]))
|
||||
for mode in m[1]:
|
||||
self._modetypes[mode] = 2
|
||||
if 'CHANMODES' in params:
|
||||
m = re.match(r'([^,]*),([^,]*),([^,]*),(.*)', params['CHANMODES'])
|
||||
if m:
|
||||
for mode in m[1]:
|
||||
self._modetypes[mode] = 1 # adds to a list and always has a parameter
|
||||
for mode in m[2]:
|
||||
self._modetypes[mode] = 2 # changes a setting and always has param
|
||||
for mode in m[3]:
|
||||
self._modetypes[mode] = 3 # only has a parameter when set
|
||||
for mode in m[4]:
|
||||
self._modetypes[mode] = 4 # never has a parameter
|
||||
|
||||
|
||||
def handle_376(self, msg: Message) -> None:
|
||||
"""RPL_ENDOFMOTD - server is ready"""
|
||||
self.mode(conf.get("botnick"), conf.get("botmodes"))
|
||||
if conf.has("botident"):
|
||||
self.send(conf.get("botident"))
|
||||
if conf.has("botlogin"):
|
||||
self.sendnow(conf.get("botlogin"))
|
||||
self.join(conf.get("botchan"))
|
||||
|
||||
|
||||
def handle_422(self, msg: Message) -> None:
|
||||
"""ERR_NOTMOTD - server is ready, but without a MOTD"""
|
||||
self.mode(conf.get("botnick"), conf.get("botmodes"))
|
||||
if conf.has("botident"):
|
||||
self.sendnow(f"{conf.get('botident')}")
|
||||
if conf.has("botlogin"):
|
||||
self.sendnow(conf.get("botlogin"))
|
||||
self.join(conf.get("botchan"))
|
||||
|
||||
|
||||
def handle_352(self, msg: Message) -> None:
|
||||
"""RPL_WHOREPLY - Response to WHO command"""
|
||||
self.add_user(msg.args[4],
|
||||
f"{msg.src}!{msg.args[1]}@{msg.args[2]}",
|
||||
[self._prefixmodes[p] for p in msg.args[5][1:]], # Format is [GH]\S*
|
||||
msg.time)
|
||||
|
||||
|
||||
def handle_315(self, msg: Message) -> None:
|
||||
"""RPL_ENDOFWHO - End of WHO command response"""
|
||||
self._bot.ready()
|
||||
|
||||
|
||||
def handle_353(self, msg: Message) -> None:
|
||||
"""RPL_NAMREPLY - names in the channel"""
|
||||
if 'userhost-in-names' not in self._caps:
|
||||
return
|
||||
prefixes=''.join(self._prefixmodes.keys())
|
||||
userhost_re = re.compile(f"([{prefixes}]*)" + r"((\S+)!\S+@\S+)")
|
||||
for u in msg.trailing.split(' '):
|
||||
m = userhost_re.match(u)
|
||||
if m:
|
||||
self.add_user(m[3], m[2], [self._prefixmodes[p] for p in m[1]], msg.time)
|
||||
|
||||
|
||||
def handle_366(self, msg: Message) -> None:
|
||||
"""RPL_ENDOFNAMES - the actual end of channel joining"""
|
||||
# We know who is in the channel now
|
||||
if conf.has("botopcmd"):
|
||||
self.sendnow(re.sub(r'%botnick%', self._nick, conf.get("botopcmd")))
|
||||
if 'userhost-in-names' in self._caps:
|
||||
self._bot.ready()
|
||||
else:
|
||||
self.send(f"WHO {conf.get('botchan')}")
|
||||
|
||||
|
||||
def handle_433(self, msg: Message) -> None:
|
||||
"""ERR_NICKNAME_IN_USE - try another nick"""
|
||||
self._nick = self._nick + "0"
|
||||
self.nick(self._nick)
|
||||
if conf.has("botghostcmd"):
|
||||
self.sendnow(conf.get("botghostcmd"))
|
||||
|
||||
|
||||
def handle_444(self, msg: Message) -> None:
|
||||
"""ERR_NOLOGIN - """
|
||||
if conf.has("botident"):
|
||||
self.sendnow(conf.get("botident"))
|
||||
if conf.has("botlogin"):
|
||||
self.sendnow(conf.get("botlogin"))
|
||||
|
||||
|
||||
def handle_cap(self, msg: Message) -> None:
|
||||
"""CAP - notification of capability"""
|
||||
# We only care about enabled capabilities.
|
||||
if msg.args[1] == "ACK":
|
||||
self._caps.update(msg.args[2].split(' '))
|
||||
|
||||
|
||||
def handle_join(self, msg: Message) -> None:
|
||||
"""JOIN - bot or user joined the channel."""
|
||||
self.add_user(msg.src, f"{msg.src}!{msg.user}@{msg.host}", [], msg.time)
|
||||
|
||||
|
||||
def handle_part(self, msg: Message) -> None:
|
||||
"""PART - bot or user left the channel."""
|
||||
user = self.remove_user(msg.src)
|
||||
self._bot.nick_parted(user)
|
||||
|
||||
|
||||
def handle_kick(self, msg: Message) -> None:
|
||||
"""KICK - user was kicked from the channel."""
|
||||
user = self.remove_user(msg.args[1])
|
||||
self._bot.nick_kicked(user)
|
||||
|
||||
|
||||
def handle_mode(self, msg: Message) -> None:
|
||||
"""MODE - bot or channel changed its mode."""
|
||||
# ignore mode changes to everything except the bot channel
|
||||
if msg.args[0] != conf.get("botchan"):
|
||||
return
|
||||
changes = []
|
||||
params = []
|
||||
for arg in msg.args[1:]:
|
||||
m = re.match(r'([-+])(.*)', arg)
|
||||
if m:
|
||||
changes.extend([(m[1], term) for term in m[2]])
|
||||
else:
|
||||
params.append(arg)
|
||||
for change in changes:
|
||||
# all this modetype machinery is required to accurately parse modelines
|
||||
modetype = self._modetypes[change[1]]
|
||||
if modetype == 1 or modetype == 2 or (modetype == 3 and change[0] == '+'):
|
||||
param = params.pop()
|
||||
if modetype != 2:
|
||||
continue
|
||||
if change[0] == '+':
|
||||
self._users[param].modes.add(change[1])
|
||||
if param == self._nick and change[1] == 'o':
|
||||
# Acquiring op is special to the bot
|
||||
self._bot.acquired_ops()
|
||||
else:
|
||||
self._users[param].modes.discard(change[1])
|
||||
|
||||
|
||||
def handle_nick(self, msg: Message) -> None:
|
||||
"""NICK - bot or user had its nick changed."""
|
||||
|
||||
# Do this first so that the user still matches the player.
|
||||
self._bot.nick_changed(self._users[msg.src], msg.args[0])
|
||||
|
||||
self._users[msg.args[0]] = self._users[msg.src]
|
||||
self._users[msg.args[0]].nick = msg.args[0]
|
||||
del self._users[msg.src]
|
||||
|
||||
if msg.src == self._nick:
|
||||
# Update my nick
|
||||
self._nick = msg.args[0]
|
||||
return
|
||||
|
||||
if msg.src == conf.get("botnick"):
|
||||
# Grab my nick that someone left
|
||||
self.nick(conf.get("botnick"))
|
||||
|
||||
|
||||
def handle_quit(self, msg: Message) -> None:
|
||||
"""QUIT - bot or user was disconnected."""
|
||||
if msg.src == conf.get("botnick"):
|
||||
# Grab my nick that someone left
|
||||
self.nick(conf.get("botnick"))
|
||||
user = self.remove_user(msg.src)
|
||||
if conf.get("detectsplits") and re.match(r'\S+\.\S+ \S+\.\S+', msg.trailing):
|
||||
# Don't penalize on netsplit
|
||||
self._bot.netsplit(user)
|
||||
elif re.match(r"Read error|Ping timeout", msg.trailing):
|
||||
self._bot.nick_dropped(user)
|
||||
else:
|
||||
self._bot.nick_quit(user)
|
||||
|
||||
|
||||
def handle_notice(self, msg: Message) -> None:
|
||||
"""NOTICE - Message sent, used to prevent loops in bots."""
|
||||
if msg.args[0] != self._nick and msg.src in self._users and self.user_is_ok(msg):
|
||||
# we ignore private notices
|
||||
self._bot.channel_notice(self._users[msg.src], msg.trailing)
|
||||
|
||||
|
||||
def handle_privmsg(self, msg: Message) -> None:
|
||||
"""PRIVMSG - Message sent."""
|
||||
if msg.src not in self._users:
|
||||
# Server messages
|
||||
return
|
||||
if msg.args[0] == self._nick:
|
||||
self._bot.private_message(self._users[msg.src], msg.trailing)
|
||||
elif self.user_is_ok(msg):
|
||||
self._bot.channel_message(self._users[msg.src], msg.trailing)
|
||||
|
||||
|
||||
def add_user(self, nick: str, userhost: str, modes: List[str], joined: float) -> None:
|
||||
"""Adds channel user with the given properties."""
|
||||
self._users[nick] = IRCClient.User(nick, userhost, modes, joined)
|
||||
|
||||
|
||||
def remove_user(self, nick: str) -> "abstract.AbstractClient.User":
|
||||
"""Remove user with the given nick. Returns that user."""
|
||||
user = self._users[nick]
|
||||
del self._users[nick]
|
||||
if len(self._users) == 1 and not self.bot_has_ops():
|
||||
# Try to acquire ops by leaving and joining
|
||||
self.sendnow(f"PART {conf.get('botchan')} :Acquiring ops")
|
||||
self.sendnow(f"JOIN {conf.get('botchan')}")
|
||||
return user
|
||||
|
||||
|
||||
def user_exists(self, nick: str) -> bool:
|
||||
return nick in self._users
|
||||
|
||||
|
||||
def user_is_ok(self, msg: Message) -> bool:
|
||||
"""Check to see if msg should cause user to be kickbanned."""
|
||||
if not conf.get("doban"):
|
||||
# Bot doesn't do bans
|
||||
return True
|
||||
if not self.bot_has_ops():
|
||||
# Bot can't do bans
|
||||
return True
|
||||
if msg.src == self._nick:
|
||||
# Bot is always ok
|
||||
return True
|
||||
if msg.src not in self._users:
|
||||
# Not in channel - maybe channel could use mode +n
|
||||
return False
|
||||
if msg.time > self._users[msg.src].joined + conf.get("bannable_time"):
|
||||
# Been in channel for a while, prob ok?
|
||||
return True
|
||||
|
||||
for host in re.findall(r"https?://([^/]+)/", msg.trailing):
|
||||
if host not in conf.get("okurls"):
|
||||
# User not okay
|
||||
self.kickban(msg.src)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def match_user(self, nick: str, userhost: str) -> bool:
|
||||
"""Return True if the nick and userhost match an existing user."""
|
||||
return nick in self._users and userhost == self._users[nick].userhost
|
||||
|
||||
|
||||
def is_bot_nick(self, nick: str) -> bool:
|
||||
return nick == self._nick or nick == conf.get("botnick")
|
||||
|
||||
|
||||
def bot_has_ops(self) -> bool:
|
||||
"""Return True if the bot has ops in the channel."""
|
||||
return self._nick in self._users and 'o' in self._users[self._nick].modes
|
||||
|
||||
|
||||
def nick_userhost(self, nick: str) -> Optional[str]:
|
||||
if nick not in self._users:
|
||||
return None
|
||||
return self._users[nick].userhost
|
||||
|
||||
|
||||
def kickban(self, nick: str) -> None:
|
||||
"""Kick a nick from the channel and ban them."""
|
||||
self.sendnow(f"MODE {conf.get('botchan')} +b {nick}")
|
||||
self.sendnow(f"KICK {conf.get('botchan')} {nick} :No advertising")
|
||||
|
||||
|
||||
def nick(self, nick: str) -> None:
|
||||
"""Send nick change request."""
|
||||
self.sendnow(f"NICK {nick}")
|
||||
|
||||
|
||||
def join(self, channel: str) -> None:
|
||||
"""Send channel join request."""
|
||||
time.sleep(5)
|
||||
self.send(f"JOIN {channel}")
|
||||
|
||||
|
||||
def grant_voice(self, *targets: str) -> None:
|
||||
for subset in chunk.chunk(targets, self._maxmodes):
|
||||
self.send(f"MODE {conf.get('botchan')} +{'v' * len(subset)} {' '.join(subset)}")
|
||||
|
||||
|
||||
def revoke_voice(self, *targets: str) -> None:
|
||||
for subset in chunk.chunk(targets, self._maxmodes):
|
||||
self.send(f"MODE {conf.get('botchan')} -{'v' * len(subset)} {' '.join(subset)}")
|
||||
|
||||
|
||||
def set_channel_voices(self, voiced_nicks: Iterable[str]) -> None:
|
||||
add_voice = []
|
||||
remove_voice = []
|
||||
for u in self._users.keys():
|
||||
if 'v' in self._users[u].modes:
|
||||
if u not in voiced_nicks:
|
||||
remove_voice.append(u)
|
||||
else:
|
||||
if u in voiced_nicks:
|
||||
add_voice.append(u)
|
||||
if add_voice:
|
||||
self.grant_voice(*add_voice)
|
||||
if remove_voice:
|
||||
self.revoke_voice(*remove_voice)
|
||||
|
||||
|
||||
|
||||
def mode(self, target: str, *modeinfo: str) -> None:
|
||||
"""Send mode change request."""
|
||||
for modes in chunk.chunk(modeinfo, self._maxmodes):
|
||||
self.send(f"MODE {target} {' '.join(modes)}")
|
||||
|
||||
|
||||
def notice(self, target: str, text: str) -> None:
|
||||
"""Send notice text to target."""
|
||||
for line in textwrap.wrap(text, width=conf.get("message_wrap_len")):
|
||||
self.send(f"NOTICE {target} :{line}")
|
||||
|
||||
|
||||
def chanmsg(self, text: str) -> None:
|
||||
"""Send message text to bot channel."""
|
||||
for line in textwrap.wrap(text, width=conf.get("message_wrap_len")):
|
||||
self.send(f"PRIVMSG {conf.get('botchan')} :{line}")
|
||||
|
||||
|
||||
def quit(self, text: str) -> None:
|
||||
"""Send quit request to server."""
|
||||
self.quitting = True
|
||||
if text:
|
||||
self.sendnow(f"QUIT :{text}")
|
||||
else:
|
||||
self.sendnow("QUIT")
|
23
dawdle/log.py
Normal file
23
dawdle/log.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
import logging
|
||||
import sys
|
||||
|
||||
from typing import Union
|
||||
|
||||
logging.addLevelName(5, "SPAMMY")
|
||||
|
||||
|
||||
log = logging.getLogger()
|
||||
log.setLevel(0)
|
||||
|
||||
|
||||
def add_handler(loglevel:Union[str, int] , logfile: str, template: str) -> None:
|
||||
h: Union[logging.StreamHandler, logging.FileHandler]
|
||||
if logfile == "/dev/stdout":
|
||||
h = logging.StreamHandler(sys.stdout)
|
||||
elif logfile == "/dev/stderr":
|
||||
h = logging.StreamHandler(sys.stderr)
|
||||
else:
|
||||
h = logging.FileHandler(logfile)
|
||||
h.setLevel(loglevel)
|
||||
h.setFormatter(logging.Formatter(template))
|
||||
log.addHandler(h)
|
53
dawdle/rand.py
Normal file
53
dawdle/rand.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
import random
|
||||
from typing import cast, Any, Dict, List, MutableSequence, Sequence, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
overrides: Dict[str,Any] = {}
|
||||
|
||||
|
||||
def randomly(key: str, odds: int) -> bool:
|
||||
"""Overrideable random func which returns true at 1:ODDS odds."""
|
||||
if key in overrides:
|
||||
return cast(bool, overrides[key])
|
||||
return random.randint(0, odds-1) < 1
|
||||
|
||||
|
||||
def randint(key: str, bottom: int, top: int) -> int:
|
||||
"""Overrideable random func which returns an integer bottom <= i <= top."""
|
||||
if key in overrides:
|
||||
return cast(int, overrides[key])
|
||||
return random.randint(int(bottom), int(top))
|
||||
|
||||
|
||||
def gauss(key: str, mu: float, sigma: float) -> int:
|
||||
"""Overrideable func which returns an random int with gaussian distribution."""
|
||||
if key in overrides:
|
||||
return cast(int, overrides[key])
|
||||
return int(random.gauss(mu, sigma))
|
||||
|
||||
def sample(key: str, seq: Sequence[T], count: int) -> List[T]:
|
||||
"""Overrideable random func which returns random COUNT elements of SEQ."""
|
||||
if key in overrides:
|
||||
return cast(List[T], overrides[key])
|
||||
return random.sample(seq, count)
|
||||
|
||||
|
||||
def choice(key: str, seq: Sequence[T]) -> T:
|
||||
"""Overrideable random func which returns one random element of SEQ."""
|
||||
if key in overrides:
|
||||
return cast(T, overrides[key])
|
||||
# Don't use random.choice here - it uses random access, which
|
||||
# is unsupported by the dict_keys view.
|
||||
return random.sample(seq, 1)[0]
|
||||
|
||||
|
||||
def shuffle(key: str, seq: MutableSequence[Any]) -> None:
|
||||
"""Overrideable random func which does an in-place shuffle of SEQ."""
|
||||
if key in overrides:
|
||||
seq.clear()
|
||||
seq.extend(overrides[key])
|
||||
return None
|
||||
random.shuffle(seq)
|
||||
return None
|
917
dawdle/test_bot.py
Executable file
917
dawdle/test_bot.py
Executable file
|
@ -0,0 +1,917 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import datetime
|
||||
import os.path
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
|
||||
from dawdle import bot
|
||||
from dawdle import conf
|
||||
from dawdle import irc
|
||||
from dawdle import rand
|
||||
|
||||
|
||||
class TestGameDBSqlite3(unittest.TestCase):
|
||||
def test_db(self):
|
||||
conf._conf['rpbase'] = 600
|
||||
conf._conf['rpstep'] = 1.16
|
||||
conf._conf['rpmaxexplvl'] = 60
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db = bot.GameDB(bot.Sqlite3GameStorage(os.path.join(tmpdir, 'dawdle_test.db')))
|
||||
self.assertFalse(db.exists())
|
||||
db.create()
|
||||
p = db.new_player('foo', 'bar', 'baz')
|
||||
p.online = True
|
||||
p.lastlogin = datetime.datetime.now() + datetime.timedelta(days=5)
|
||||
db.write_players()
|
||||
self.assertTrue(db.exists())
|
||||
db.load_state()
|
||||
self.assertEqual(db['foo'].name, 'foo')
|
||||
self.assertEqual(db['foo'].online, True)
|
||||
self.assertEqual(db['foo'].created, p.created)
|
||||
self.assertEqual(db['foo'].lastlogin, p.lastlogin)
|
||||
db.close()
|
||||
|
||||
def test_passwords(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db = bot.GameDB(bot.Sqlite3GameStorage(os.path.join(tmpdir, 'dawdle_test.db')))
|
||||
self.assertFalse(db.exists())
|
||||
db.create()
|
||||
p = db.new_player('foo', 'bar', 'baz')
|
||||
self.assertTrue(db.check_login('foo', 'baz'))
|
||||
self.assertFalse(db.check_login('foo', 'azb'))
|
||||
p.set_password('azb')
|
||||
self.assertTrue(db.check_login('foo', 'azb'))
|
||||
db.close()
|
||||
|
||||
|
||||
class TestGameDBIdleRPG(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
|
||||
|
||||
def test_db(self):
|
||||
conf._conf['botnick'] = ''
|
||||
conf._conf['writequestfile'] = True
|
||||
conf._conf['questfilename'] = "/tmp/testquestfile.txt"
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db = bot.GameDB(bot.IdleRPGGameStorage(os.path.join(tmpdir, 'dawdle_test.db')))
|
||||
db.create()
|
||||
db.update_quest(None)
|
||||
op = db.new_player('foo', 'bar', 'baz')
|
||||
op.items['amulet'] = bot.Item(55, '')
|
||||
op.items['helm'] = bot.Item(42, "Jeff's Cluehammer of Doom")
|
||||
db.write_players()
|
||||
db.load_state()
|
||||
p = db['foo']
|
||||
self.assertEqual(vars(op), vars(p))
|
||||
db.close()
|
||||
|
||||
|
||||
class FakeIRCClient(object):
|
||||
def __init__(self):
|
||||
self._nick = 'dawdlerpg'
|
||||
self._users = {}
|
||||
self.server = "irc.example.com"
|
||||
self.chanmsgs = []
|
||||
self.notices = {}
|
||||
|
||||
|
||||
def user_exists(self, nick):
|
||||
return nick in self._users
|
||||
|
||||
|
||||
def nick_userhost(self, nick):
|
||||
return self._users[nick]
|
||||
|
||||
|
||||
def match_user(self, nick, userhost):
|
||||
return nick in self._users and userhost == self._users[nick].userhost
|
||||
|
||||
|
||||
def bot_has_ops(self):
|
||||
return False
|
||||
|
||||
|
||||
def resetmsgs(self):
|
||||
self.chanmsgs = []
|
||||
self.notices = {}
|
||||
|
||||
|
||||
def chanmsg(self, text):
|
||||
self.chanmsgs.append(text)
|
||||
|
||||
|
||||
def notice(self, nick, text):
|
||||
self.notices.setdefault(nick, []).append(text)
|
||||
|
||||
|
||||
def servername(self):
|
||||
return "irc.example.com"
|
||||
|
||||
|
||||
class FakeGameStorage(bot.GameStorage):
|
||||
|
||||
def __init__(self):
|
||||
self._mem = {}
|
||||
|
||||
def backup(self):
|
||||
pass
|
||||
|
||||
def create(self):
|
||||
pass
|
||||
|
||||
def readall(self):
|
||||
pass
|
||||
|
||||
def write(self, p):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def new(self, p):
|
||||
self._mem[p.name] = p
|
||||
|
||||
def rename(self, old, new):
|
||||
self._mem[new] = self._mem[old]
|
||||
self._mem.pop(old)
|
||||
|
||||
def delete_player(self, pname):
|
||||
self._mem.pop(pname)
|
||||
|
||||
|
||||
def add_history(self, players, text):
|
||||
pass
|
||||
|
||||
|
||||
def update_quest(self, quest):
|
||||
pass
|
||||
|
||||
|
||||
class TestBot(unittest.TestCase):
|
||||
|
||||
def test_nick_change(self):
|
||||
conf._conf['botnick'] = 'dawdlerpg'
|
||||
conf._conf['botchan'] = '#dawdlerpg'
|
||||
conf._conf['message_wrap_len'] = 400
|
||||
conf._conf['throttle'] = False
|
||||
conf._conf['pennick'] = 10
|
||||
conf._conf['penquit'] = 20
|
||||
conf._conf['rppenstep'] = 1.14
|
||||
|
||||
testbot = bot.DawdleBot(bot.GameDB(FakeGameStorage()))
|
||||
testirc = FakeIRCClient()
|
||||
testbot.connected(testirc)
|
||||
testirc._users['foo'] = irc.IRCClient.User("foo", "foo!foo@example.com", [], 1)
|
||||
a = testbot._db.new_player('a', 'b', 'c')
|
||||
a.online = True
|
||||
a.nick = 'foo'
|
||||
a.userhost = 'foo!foo@example.com'
|
||||
self.assertEqual('foo', a.nick)
|
||||
testbot.nick_changed(testirc._users['foo'], 'bar')
|
||||
self.assertEqual('bar', a.nick)
|
||||
testbot.nick_quit(testirc._users['foo'])
|
||||
|
||||
|
||||
def test_dropped_conn(self):
|
||||
conf._conf['splitwait'] = 10
|
||||
conf._conf['pendropped'] = 20
|
||||
conf._conf['rppenstep'] = 1.14
|
||||
testbot = bot.DawdleBot(bot.GameDB(FakeGameStorage()))
|
||||
testirc = FakeIRCClient()
|
||||
testbot.connected(testirc)
|
||||
testirc._users['foo'] = irc.IRCClient.User("foo", "foo!foo@example.com", [], 1)
|
||||
a = testbot._db.new_player('a', 'b', 'c')
|
||||
a.online = True
|
||||
a.nick = 'foo'
|
||||
a.userhost = 'foo!foo@example.com'
|
||||
self.assertTrue(testbot._db._players['a'].nick, 'foo')
|
||||
testbot.nick_dropped(testirc._users['foo'])
|
||||
del testirc._users['foo']
|
||||
testbot.expire_splits()
|
||||
self.assertTrue(a.online)
|
||||
a.lastlogin -= datetime.timedelta(seconds=11)
|
||||
testbot.expire_splits()
|
||||
self.assertFalse(a.online)
|
||||
|
||||
def test_find_mount(self):
|
||||
conf._conf['allylvlbase'] = 200
|
||||
conf._conf['allylvlstep'] = 1.16
|
||||
conf._conf['allymaxexplvl'] = 60
|
||||
testbot = bot.DawdleBot(bot.GameDB(FakeGameStorage()))
|
||||
testirc = FakeIRCClient()
|
||||
testbot.connected(testirc)
|
||||
testirc._users['foo'] = irc.IRCClient.User("foo", "foo!foo@example.com", [], 1)
|
||||
a = testbot._db.new_player('a', 'b', 'c')
|
||||
a.level = 40
|
||||
a.online = True
|
||||
a.nick = 'foo'
|
||||
a.userhost = 'foo!foo@example.com'
|
||||
testbot.find_mount(a)
|
||||
self.assertIn("mount", a.allies)
|
||||
|
||||
|
||||
class TestGameDB(unittest.TestCase):
|
||||
|
||||
def test_top_players(self):
|
||||
db = bot.GameDB(FakeGameStorage())
|
||||
a = db.new_player('a', 'waffle', 'c')
|
||||
a.level, a.nextlvl = 30, 100
|
||||
b = db.new_player('b', 'doughnut', 'c')
|
||||
b.level, b.nextlvl = 20, 1000
|
||||
c = db.new_player('c', 'bagel', 'c')
|
||||
c.level, c.nextlvl = 10, 10
|
||||
self.assertEqual(['a', 'b', 'c'], [p.name for p in db.top_players()])
|
||||
|
||||
|
||||
class TestPvPBattle(unittest.TestCase):
|
||||
|
||||
|
||||
def setUp(self):
|
||||
conf._conf['rpbase'] = 600
|
||||
conf._conf['rpstep'] = 1.16
|
||||
conf._conf['rpmaxexplvl'] = 60
|
||||
conf._conf['modsfile'] = '/tmp/modsfile.txt'
|
||||
conf._conf['color'] = False
|
||||
self.bot = bot.DawdleBot(bot.GameDB(FakeGameStorage()))
|
||||
self.irc = FakeIRCClient()
|
||||
self.bot.connected(self.irc)
|
||||
|
||||
|
||||
def test_player_battle_win(self):
|
||||
a = self.bot._db.new_player('a', 'b', 'c')
|
||||
a.items['amulet'] = bot.Item(20, '')
|
||||
b = self.bot._db.new_player('b', 'c', 'd')
|
||||
b.items['amulet'] = bot.Item(40, '')
|
||||
rand.overrides = {
|
||||
'pvp_player_roll': 20,
|
||||
'pvp_opp_roll': 10,
|
||||
'pvp_critical': False,
|
||||
'pvp_swap_item': False,
|
||||
'pvp_find_item': False
|
||||
}
|
||||
self.bot.pvp_battle(a, b, "fought", "and has won", "and has lost")
|
||||
self.assertListEqual(self.irc.chanmsgs, [
|
||||
"a [20/20] has fought b [10/40] and has won! 0 days, 00:00:42 is removed from a's clock.",
|
||||
"a reaches next level in 0 days, 00:09:18."
|
||||
])
|
||||
self.assertEqual(a.nextlvl, 558)
|
||||
|
||||
|
||||
def test_player_battle_bot(self):
|
||||
conf._conf['botnick'] = 'dawdlerpg'
|
||||
a = self.bot._db.new_player('a', 'b', 'c')
|
||||
a.items['amulet'] = bot.Item(20, '')
|
||||
rand.overrides = {
|
||||
'pvp_player_roll': 20,
|
||||
'pvp_opp_roll': 10,
|
||||
'pvp_critical': False,
|
||||
'pvp_swap_item': False,
|
||||
'pvp_find_item': False
|
||||
}
|
||||
self.bot.pvp_battle(a, None, "fought", "and has won", "and has lost")
|
||||
self.assertListEqual(self.irc.chanmsgs, [
|
||||
"a [20/20] has fought dawdlerpg [10/21] and has won! 0 days, 00:02:00 is removed from a's clock.",
|
||||
"a reaches next level in 0 days, 00:08:00."
|
||||
])
|
||||
self.assertEqual(a.nextlvl, 480)
|
||||
|
||||
|
||||
def test_player_battle_lose(self):
|
||||
a = self.bot._db.new_player('a', 'b', 'c')
|
||||
a.items['amulet'] = bot.Item(20, '')
|
||||
b = self.bot._db.new_player('b', 'c', 'd')
|
||||
b.items['amulet'] = bot.Item(40, '')
|
||||
rand.overrides = {
|
||||
'pvp_player_roll': 10,
|
||||
'pvp_opp_roll': 20,
|
||||
'pvp_critical': False,
|
||||
'pvp_swap_item': False,
|
||||
'pvp_find_item': False
|
||||
}
|
||||
self.bot.pvp_battle(a, b, "fought", "and has won", "and has lost")
|
||||
self.assertListEqual(self.irc.chanmsgs, [
|
||||
"a [10/20] has fought b [20/40] and has lost! 0 days, 00:00:42 is added to a's clock.",
|
||||
"a reaches next level in 0 days, 00:10:42."
|
||||
])
|
||||
self.assertEqual(a.nextlvl, 642)
|
||||
|
||||
|
||||
def test_player_battle_critical(self):
|
||||
a = self.bot._db.new_player('a', 'b', 'c')
|
||||
a.items['amulet'] = bot.Item(20, '')
|
||||
b = self.bot._db.new_player('b', 'c', 'd')
|
||||
b.items['amulet'] = bot.Item(40, '')
|
||||
rand.overrides = {
|
||||
'pvp_player_roll': 20,
|
||||
'pvp_opp_roll': 10,
|
||||
'pvp_critical': True,
|
||||
'pvp_cs_penalty_pct': 10,
|
||||
'pvp_swap_item': False,
|
||||
'pvp_find_item': False
|
||||
}
|
||||
self.bot.pvp_battle(a, b, "fought", "and has won", "and has lost")
|
||||
self.assertListEqual(self.irc.chanmsgs, [
|
||||
"a [20/20] has fought b [10/40] and has won! 0 days, 00:00:42 is removed from a's clock.",
|
||||
"a reaches next level in 0 days, 00:09:18.",
|
||||
"a has dealt b a Critical Strike! 0 days, 00:01:30 is added to b's clock.",
|
||||
"b reaches next level in 0 days, 00:11:30."
|
||||
])
|
||||
self.assertEqual(a.nextlvl, 558)
|
||||
self.assertEqual(b.nextlvl, 690)
|
||||
|
||||
|
||||
def test_player_battle_swapitem(self):
|
||||
a = self.bot._db.new_player('a', 'b', 'c')
|
||||
a.level = 20
|
||||
a.items['amulet'] = bot.Item(20, '')
|
||||
b = self.bot._db.new_player('b', 'c', 'd')
|
||||
b.items['amulet'] = bot.Item(40, '')
|
||||
rand.overrides = {
|
||||
'pvp_player_roll': 20,
|
||||
'pvp_opp_roll': 10,
|
||||
'pvp_critical': False,
|
||||
'pvp_swap_item': True,
|
||||
'pvp_swap_itemtype': 'amulet',
|
||||
'pvp_find_item': False
|
||||
}
|
||||
self.bot.pvp_battle(a, b, "fought", "and has won", "and has lost")
|
||||
self.assertListEqual(self.irc.chanmsgs, [
|
||||
"a [20/20] has fought b [10/40] and has won! 0 days, 00:00:42 is removed from a's clock.",
|
||||
"a reaches next level in 0 days, 00:09:18.",
|
||||
"In the fierce battle, b dropped their level 40 amulet! a picks it up, tossing their old level 20 amulet to b."
|
||||
])
|
||||
self.assertEqual(a.nextlvl, 558)
|
||||
self.assertEqual(a.items['amulet'].level, 40)
|
||||
self.assertEqual(b.items['amulet'].level, 20)
|
||||
|
||||
|
||||
def test_player_battle_finditem(self):
|
||||
a = self.bot._db.new_player('a', 'b', 'c')
|
||||
a.nick = 'a'
|
||||
a.items['amulet'] = bot.Item(20, '')
|
||||
b = self.bot._db.new_player('b', 'c', 'd')
|
||||
b.items['amulet'] = bot.Item(40, '')
|
||||
rand.overrides = {
|
||||
'pvp_player_roll': 20,
|
||||
'pvp_opp_roll': 10,
|
||||
'pvp_critical': False,
|
||||
'pvp_swap_item': False,
|
||||
'pvp_find_item': True,
|
||||
'specitem_find': False,
|
||||
'find_item_slot': 'charm',
|
||||
'find_item_level': 5
|
||||
}
|
||||
self.bot.pvp_battle(a, b, "fought", "and has won", "and has lost")
|
||||
self.assertListEqual(self.irc.chanmsgs, [
|
||||
"a [20/20] has fought b [10/40] and has won! 0 days, 00:00:42 is removed from a's clock.",
|
||||
"a reaches next level in 0 days, 00:09:18.",
|
||||
"While recovering from battle, a notices a glint in the mud. Upon investigation, they find an old lost item!"
|
||||
])
|
||||
self.assertListEqual(self.irc.notices['a'], [
|
||||
"You found a level 5 charm! Your current charm is only level 0, so it seems Luck is with you!"
|
||||
])
|
||||
|
||||
self.assertEqual(a.nextlvl, 558)
|
||||
|
||||
|
||||
class TestTeamBattle(unittest.TestCase):
|
||||
|
||||
|
||||
def setUp(self):
|
||||
conf._conf['rpbase'] = 600
|
||||
conf._conf['rpstep'] = 1.16
|
||||
conf._conf['rpmaxexplvl'] = 60
|
||||
conf._conf['modsfile'] = '/tmp/modsfile.txt'
|
||||
conf._conf['color'] = False
|
||||
self.bot = bot.DawdleBot(bot.GameDB(FakeGameStorage()))
|
||||
self.irc = FakeIRCClient()
|
||||
self.bot.connected(self.irc)
|
||||
|
||||
|
||||
def test_setup_insufficient_players(self):
|
||||
op = [self.bot._db.new_player(pname, 'a', 'b') for pname in "abcde"]
|
||||
self.bot.team_battle(op)
|
||||
self.assertEqual(self.irc.chanmsgs, [])
|
||||
|
||||
|
||||
def test_win(self):
|
||||
op = [self.bot._db.new_player(pname, 'a', 'b') for pname in "abcdef"]
|
||||
op[0].items['amulet'] = bot.Item(20, "")
|
||||
op[1].items['amulet'] = bot.Item(20, "")
|
||||
op[2].items['amulet'] = bot.Item(20, "")
|
||||
op[3].items['amulet'] = bot.Item(40, "")
|
||||
op[4].items['amulet'] = bot.Item(40, "")
|
||||
op[5].items['amulet'] = bot.Item(40, "")
|
||||
op[0].nextlvl = 1200
|
||||
op[1].nextlvl = 3600
|
||||
op[2].nextlvl = 3600
|
||||
op[3].nextlvl = 3600
|
||||
op[4].nextlvl = 3600
|
||||
op[5].nextlvl = 3600
|
||||
rand.overrides = {
|
||||
'team_battle_members': op,
|
||||
'team_a_roll': 60,
|
||||
'team_b_roll': 30
|
||||
}
|
||||
self.bot.team_battle(op)
|
||||
self.assertEqual(self.irc.chanmsgs[0], "a, b, and c [60/60] have team battled d, e, and f [30/120] and won! 0 days, 00:04:00 is removed from their clocks.")
|
||||
|
||||
def test_loss(self):
|
||||
op = [self.bot._db.new_player(pname, 'a', 'b') for pname in "abcdef"]
|
||||
op[0].items['amulet'] = bot.Item(20, "")
|
||||
op[1].items['amulet'] = bot.Item(20, "")
|
||||
op[2].items['amulet'] = bot.Item(20, "")
|
||||
op[3].items['amulet'] = bot.Item(40, "")
|
||||
op[4].items['amulet'] = bot.Item(40, "")
|
||||
op[5].items['amulet'] = bot.Item(40, "")
|
||||
op[0].nextlvl = 1200
|
||||
op[1].nextlvl = 3600
|
||||
op[2].nextlvl = 3600
|
||||
op[3].nextlvl = 3600
|
||||
op[4].nextlvl = 3600
|
||||
op[5].nextlvl = 3600
|
||||
rand.overrides = {
|
||||
'team_battle_members': op,
|
||||
'team_a_roll': 30,
|
||||
'team_b_roll': 60
|
||||
}
|
||||
self.bot.team_battle(op)
|
||||
self.assertEqual(self.irc.chanmsgs[0], "a, b, and c [30/60] have team battled d, e, and f [60/120] and lost! 0 days, 00:04:00 is added to their clocks.")
|
||||
|
||||
|
||||
class TestEvilness(unittest.TestCase):
|
||||
|
||||
|
||||
def setUp(self):
|
||||
conf._conf['rpbase'] = 600
|
||||
conf._conf['rpstep'] = 1.16
|
||||
conf._conf['rpmaxexplvl'] = 60
|
||||
conf._conf['modsfile'] = '/tmp/modsfile.txt'
|
||||
conf._conf['color'] = False
|
||||
self.bot = bot.DawdleBot(bot.GameDB(FakeGameStorage()))
|
||||
self.irc = FakeIRCClient()
|
||||
self.bot.connected(self.irc)
|
||||
|
||||
|
||||
def test_theft(self):
|
||||
op = [self.bot._db.new_player('a', 'b', 'c'), self.bot._db.new_player('b', 'c', 'd')]
|
||||
op[0].alignment = 'e'
|
||||
op[1].alignment = 'g'
|
||||
op[1].items['amulet'] = bot.Item(20, "")
|
||||
rand.overrides = {
|
||||
'evilness_theft': True,
|
||||
'evilness_slot': 'amulet'
|
||||
}
|
||||
self.bot.evilness(op)
|
||||
self.assertEqual(self.irc.chanmsgs[0], "a stole b's level 20 amulet while they were sleeping! a leaves their old level 0 amulet behind, which b then takes.")
|
||||
|
||||
|
||||
def test_penalty(self):
|
||||
op = [self.bot._db.new_player('a', 'b', 'c')]
|
||||
op[0].alignment = 'e'
|
||||
rand.overrides = {
|
||||
'evilness_theft': False,
|
||||
'evilness_penalty_pct': 5
|
||||
}
|
||||
self.bot.evilness(op)
|
||||
self.assertEqual(self.irc.chanmsgs[0], "a is forsaken by their evil god. 0 days, 00:00:30 is added to their clock.")
|
||||
|
||||
|
||||
class TestGoodness(unittest.TestCase):
|
||||
|
||||
|
||||
def setUp(self):
|
||||
conf._conf['rpbase'] = 600
|
||||
conf._conf['rpstep'] = 1.16
|
||||
conf._conf['rpmaxexplvl'] = 60
|
||||
conf._conf['modsfile'] = '/tmp/modsfile.txt'
|
||||
conf._conf['color'] = False
|
||||
self.bot = bot.DawdleBot(bot.GameDB(FakeGameStorage()))
|
||||
self.irc = FakeIRCClient()
|
||||
self.bot.connected(self.irc)
|
||||
|
||||
|
||||
def test_goodness(self):
|
||||
op = [self.bot._db.new_player('a', 'b', 'c'), self.bot._db.new_player('b', 'c', 'd')]
|
||||
op[0].alignment = 'g'
|
||||
op[1].alignment = 'g'
|
||||
rand.overrides = {
|
||||
'goodness_players': op,
|
||||
'goodness_gain_pct': 10,
|
||||
}
|
||||
self.bot.goodness(op)
|
||||
self.assertListEqual(self.irc.chanmsgs, [
|
||||
"a and b have not let the iniquities of evil people poison them. Together have they prayed to their god, and light now shines down upon them. 10% of their time is removed from their clocks.",
|
||||
"a reaches next level in 0 days, 00:09:00.",
|
||||
"b reaches next level in 0 days, 00:09:00."
|
||||
])
|
||||
self.assertEqual(op[0].nextlvl, 540)
|
||||
self.assertEqual(op[1].nextlvl, 540)
|
||||
|
||||
|
||||
class TestHandOfGod(unittest.TestCase):
|
||||
|
||||
|
||||
def setUp(self):
|
||||
conf._conf['rpbase'] = 600
|
||||
conf._conf['rpstep'] = 1.16
|
||||
conf._conf['rpmaxexplvl'] = 60
|
||||
conf._conf['color'] = False
|
||||
self.bot = bot.DawdleBot(bot.GameDB(FakeGameStorage()))
|
||||
self.irc = FakeIRCClient()
|
||||
self.bot.connected(self.irc)
|
||||
|
||||
|
||||
def test_forward(self):
|
||||
op = [self.bot._db.new_player('a', 'b', 'c')]
|
||||
rand.overrides = {
|
||||
'hog_effect': False,
|
||||
'hog_amount': 10
|
||||
}
|
||||
self.bot.hand_of_god(op)
|
||||
self.assertEqual(self.irc.chanmsgs[0], "Verily I say unto thee, the Heavens have burst forth, and the blessed hand of God carried a 0 days, 00:01:30 toward level 1.")
|
||||
self.assertEqual(self.irc.chanmsgs[1], "a reaches next level in 0 days, 00:08:30.")
|
||||
|
||||
|
||||
def test_back(self):
|
||||
op = [self.bot._db.new_player('a', 'b', 'c')]
|
||||
rand.overrides = {
|
||||
'hog_effect': True,
|
||||
'hog_amount': 10
|
||||
}
|
||||
self.bot.hand_of_god(op)
|
||||
self.assertEqual(self.irc.chanmsgs[0], "Thereupon He stretched out His little finger among them and consumed a with fire, slowing the heathen 0 days, 00:01:30 from level 1.")
|
||||
self.assertEqual(self.irc.chanmsgs[1], "a reaches next level in 0 days, 00:11:30.")
|
||||
|
||||
|
||||
class TestQuest(unittest.TestCase):
|
||||
|
||||
|
||||
def setUp(self):
|
||||
conf._conf['rpbase'] = 600
|
||||
conf._conf['rpstep'] = 1.16
|
||||
conf._conf['rpmaxexplvl'] = 60
|
||||
conf._conf['datadir'] = os.path.join(os.path.dirname(os.path.dirname(__file__)), "setup")
|
||||
conf._conf['eventsfile'] = "events.txt"
|
||||
conf._conf['writequestfile'] = True
|
||||
conf._conf['questfilename'] = "/tmp/testquestfile.txt"
|
||||
conf._conf['quest_interval_min'] = 6*3600
|
||||
conf._conf['quest_interval_max'] = 12*3600
|
||||
conf._conf['quest_min_level'] = 24
|
||||
conf._conf['quest_min_login'] = 36000
|
||||
conf._conf['penquest'] = 15
|
||||
conf._conf['penlogout'] = 20
|
||||
conf._conf['color'] = False
|
||||
self.bot = bot.DawdleBot(bot.GameDB(FakeGameStorage()))
|
||||
self.irc = FakeIRCClient()
|
||||
self.bot.connected(self.irc)
|
||||
self.bot._state = "ready"
|
||||
self.bot.refresh_events()
|
||||
|
||||
|
||||
def test_questing_mode_1(self):
|
||||
users = [irc.IRCClient.User(uname, f"{uname}!{uname}@irc.example.com", [], 0) for uname in "abcd"]
|
||||
op = [self.bot._db.new_player(pname, 'a', 'b') for pname in "abcd"]
|
||||
now = time.time()
|
||||
for u,p in zip(users,op):
|
||||
p.online = True
|
||||
p.level = 25
|
||||
p.lastlogin = datetime.datetime.fromtimestamp(now - 36001)
|
||||
p.nick = u.nick
|
||||
p.userhost = u.userhost
|
||||
rand.overrides = {
|
||||
"quest_members": op,
|
||||
"quest_selection": "1 locate the centuries-lost tomes of the grim prophet Haplashak Mhadhu",
|
||||
"quest_time": 12
|
||||
}
|
||||
self.bot.quest_start(now)
|
||||
self.bot.private_message(users[0], 'quest')
|
||||
# time passes
|
||||
self.bot._db._quest.qtime = now-1
|
||||
self.bot.quest_check(now)
|
||||
|
||||
self.assertListEqual(self.irc.chanmsgs, [
|
||||
"a, b, c, and d have been chosen by the gods to locate the centuries-lost tomes of the grim prophet Haplashak Mhadhu. Quest to end in 0 days, 12:00:00.",
|
||||
"a, b, c, and d have blessed the realm by completing their quest! 25% of their burden is eliminated."
|
||||
])
|
||||
self.assertListEqual(self.irc.notices['a'], [
|
||||
"a, b, c, and d are on a quest to locate the centuries-lost tomes of the grim prophet Haplashak Mhadhu. Quest to complete in 0 days, 11:59:59."
|
||||
])
|
||||
self.assertEqual(op[0].nextlvl, 450)
|
||||
self.assertEqual(op[1].nextlvl, 450)
|
||||
self.assertEqual(op[2].nextlvl, 450)
|
||||
self.assertEqual(op[3].nextlvl, 450)
|
||||
|
||||
|
||||
def test_questing_mode_2(self):
|
||||
conf._conf['mapurl'] = "https://example.com/"
|
||||
users = [irc.IRCClient.User(uname, f"{uname}!{uname}@irc.example.com", [], 0) for uname in "abcd"]
|
||||
op = [self.bot._db.new_player(pname, 'a', 'b') for pname in "abcd"]
|
||||
now = time.time()
|
||||
for u,p in zip(users,op):
|
||||
p.online = True
|
||||
p.level = 25
|
||||
p.lastlogin = datetime.datetime.fromtimestamp(now - 36001)
|
||||
p.nick = u.nick
|
||||
p.userhost = u.userhost
|
||||
rand.overrides = {
|
||||
"quest_members": op,
|
||||
"quest_selection": "2 400 475 480 380 explore and chart the dark lands of T'rnalvph",
|
||||
}
|
||||
self.bot._db._online = op
|
||||
self.bot.quest_start(now)
|
||||
self.bot.private_message(users[0], 'quest')
|
||||
for p in op:
|
||||
p.posx, p.posy = 400, 475
|
||||
self.bot.quest_check(1)
|
||||
for p in op:
|
||||
p.posx, p.posy = 480, 380
|
||||
self.bot.quest_check(2)
|
||||
|
||||
self.assertEqual(self.irc.chanmsgs, [
|
||||
"a, b, c, and d have been chosen by the gods to explore and chart the dark lands of T'rnalvph. Participants must first reach (400,475), then (480,380). See https://example.com/ to monitor their journey's progress.",
|
||||
"a, b, c, and d have reached a landmark on their journey! 1 landmark remains.",
|
||||
"a, b, c, and d have completed their journey! 25% of their burden is eliminated."
|
||||
])
|
||||
self.assertListEqual(self.irc.notices['a'], [
|
||||
"a, b, c, and d are on a quest to explore and chart the dark lands of T'rnalvph. Participants must first reach (400, 475), then (480, 380). See https://example.com/ to monitor their journey's progress."
|
||||
])
|
||||
self.assertEqual(op[0].nextlvl, 450)
|
||||
self.assertEqual(op[1].nextlvl, 450)
|
||||
self.assertEqual(op[2].nextlvl, 450)
|
||||
self.assertEqual(op[3].nextlvl, 450)
|
||||
|
||||
|
||||
def test_questing_failure(self):
|
||||
conf._conf['rppenstep'] = 1.14
|
||||
op = [self.bot._db.new_player(pname, 'a', 'b') for pname in "abcd"]
|
||||
now = time.time()
|
||||
for p in op:
|
||||
p.online = True
|
||||
p.nick = p.name
|
||||
p.level = 25
|
||||
p.lastlogin = datetime.datetime.fromtimestamp(now - 36001)
|
||||
rand.overrides = {
|
||||
"quest_members": op,
|
||||
"quest_selection": "1 locate the centuries-lost tomes of the grim prophet Haplashak Mhadhu",
|
||||
"quest_time": 12
|
||||
}
|
||||
self.bot.quest_start(now)
|
||||
self.bot.penalize(op[0], 'logout')
|
||||
|
||||
self.assertListEqual(self.irc.chanmsgs, [
|
||||
"a, b, c, and d have been chosen by the gods to locate the centuries-lost tomes of the grim prophet Haplashak Mhadhu. Quest to end in 0 days, 12:00:00.",
|
||||
"a's insolence has brought the wrath of the gods down upon them. Your great wickedness burdens you like lead, drawing you downwards with great force towards hell. Thereby have you plunged 15 steps closer to that gaping maw."
|
||||
])
|
||||
self.assertListEqual(self.irc.notices['a'],
|
||||
["Penalty of 0 days, 00:08:40 added to your timer for LOGOUT command."])
|
||||
self.assertEqual(op[0].nextlvl, 1516)
|
||||
self.assertEqual(op[1].nextlvl, 996)
|
||||
self.assertEqual(op[2].nextlvl, 996)
|
||||
self.assertEqual(op[3].nextlvl, 996)
|
||||
|
||||
def test_quest_autologin_failure(self):
|
||||
users = [irc.IRCClient.User(uname, f"{uname}!{uname}@irc.example.com", [], 0) for uname in "abcd"]
|
||||
op = [self.bot._db.new_player(pname, 'a', 'b') for pname in "abcd"]
|
||||
now = time.time()
|
||||
for u,p in zip(users,op):
|
||||
p.online = True
|
||||
p.level = 25
|
||||
p.lastlogin = datetime.datetime.fromtimestamp(now - 36001)
|
||||
p.nick = u.nick
|
||||
p.userhost = u.userhost
|
||||
self.irc._users = dict((user.nick, user) for user in users)
|
||||
rand.overrides = {
|
||||
"quest_members": op,
|
||||
"quest_selection": "2 10 20 30 40 locate the centuries-lost tomes of the grim prophet Haplashak Mhadhu",
|
||||
}
|
||||
self.bot.quest_start(now)
|
||||
self.bot.disconnected()
|
||||
self.bot.connected(self.irc)
|
||||
del self.irc._users["d"]
|
||||
self.bot._autologin()
|
||||
self.bot.move_players()
|
||||
self.bot.quest_check(now+50000)
|
||||
|
||||
|
||||
def test_cmd_trigger_quest(self):
|
||||
op = [self.bot._db.new_player(pname, "a", "b") for pname in "abcd"]
|
||||
now = time.time()
|
||||
for p in op:
|
||||
p.online = True
|
||||
p.nick = p.name
|
||||
p.level = 25
|
||||
p.lastlogin = datetime.datetime.fromtimestamp(now - 36001)
|
||||
rand.overrides = {
|
||||
"quest_members": op,
|
||||
"quest_selection": "1 locate the centuries-lost tomes of the grim prophet Haplashak Mhadhu",
|
||||
"quest_time": 12
|
||||
}
|
||||
op[0].admin = True
|
||||
self.bot.cmd_trigger(op[0], op[0].nick, "quest")
|
||||
|
||||
|
||||
class TestAdminCommands(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
conf._conf['rpbase'] = 600
|
||||
conf._conf['rpstep'] = 1.16
|
||||
conf._conf['rpmaxexplvl'] = 60
|
||||
conf._conf['color'] = False
|
||||
self.bot = bot.DawdleBot(bot.GameDB(FakeGameStorage()))
|
||||
self.irc = FakeIRCClient()
|
||||
self.bot.connected(self.irc)
|
||||
|
||||
def test_delold(self):
|
||||
op = [self.bot._db.new_player(pname, 'a', 'b') for pname in "abcd"]
|
||||
expired = time.time() - 9 * 86400
|
||||
for p in op[:2]:
|
||||
p.lastlogin = datetime.datetime.fromtimestamp(expired)
|
||||
op[3].online = True
|
||||
op[3].isadmin = True
|
||||
self.bot.cmd_delold(op[3], op[3].nick, "7")
|
||||
self.assertListEqual(self.irc.chanmsgs, [
|
||||
"2 accounts not accessed in the last 7 days removed by d."
|
||||
])
|
||||
self.assertNotIn(op[0].name, self.bot._db)
|
||||
self.assertNotIn(op[1].name, self.bot._db)
|
||||
self.assertIn(op[2].name, self.bot._db)
|
||||
self.assertIn(op[3].name, self.bot._db)
|
||||
|
||||
|
||||
class TestPlayerCommands(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
conf._conf['rpbase'] = 600
|
||||
conf._conf['rpstep'] = 1.16
|
||||
conf._conf['rpmaxexplvl'] = 60
|
||||
conf._conf['color'] = False
|
||||
conf._conf['allowuserinfo'] = True
|
||||
conf._conf['helpurl'] = "http://example.com/"
|
||||
conf._conf['botchan'] = "#dawdlerpg"
|
||||
conf._conf["voiceonlogin"] = False
|
||||
self.bot = bot.DawdleBot(bot.GameDB(FakeGameStorage()))
|
||||
self.irc = FakeIRCClient()
|
||||
self.bot.connected(self.irc)
|
||||
|
||||
def test_unrestricted_commands_without_player(self):
|
||||
self.irc._server = 'irc.example.com'
|
||||
for cmd in bot.DawdleBot.ALLOWALL:
|
||||
# We don't care what it does, as long as it doesn't crash.
|
||||
getattr(self.bot, f"cmd_{cmd}")(None, "foo", "")
|
||||
|
||||
def test_cmd_info(self):
|
||||
self.irc._server = 'irc.example.com'
|
||||
self.bot.cmd_info(None, "foo", "")
|
||||
self.assertIn("DawdleRPG v", self.irc.notices["foo"][0])
|
||||
player = self.bot._db.new_player("bar", 'a', 'b')
|
||||
self.bot.cmd_info(player, "bar", "")
|
||||
self.assertIn("DawdleRPG v", self.irc.notices["bar"][0])
|
||||
|
||||
def test_cmd_login(self):
|
||||
|
||||
self.bot.cmd_login(None, "foo", "bar baz")
|
||||
self.irc._users['foo'] = irc.IRCClient.User("foo", "foo@example.com", [], 1)
|
||||
self.assertEqual("Sorry, you aren't on #dawdlerpg.", self.irc.notices["foo"][0])
|
||||
self.irc.resetmsgs()
|
||||
player = self.bot._db.new_player("bar", 'a', 'b')
|
||||
player.set_password("baz")
|
||||
self.bot.cmd_login(None, "foo", "bar baz")
|
||||
self.assertIn("foo", self.irc.chanmsgs[0])
|
||||
|
||||
def test_cmd_email_no_arg_no_email(self):
|
||||
player = self.bot._db.new_player("foo", 'a', 'b')
|
||||
self.bot.cmd_email(player, "foo", "")
|
||||
self.assertEqual("Your account does not have an email set. You can set it with EMAIL <email>.", self.irc.notices["foo"][0])
|
||||
|
||||
def test_cmd_email_no_arg_with_email(self):
|
||||
player = self.bot._db.new_player("foo", 'a', 'b')
|
||||
player.email = "foo@example.com"
|
||||
self.bot.cmd_email(player, "foo", "")
|
||||
self.assertEqual("Your account email is foo@example.com.", self.irc.notices["foo"][0])
|
||||
|
||||
def test_cmd_email_with_arg_bad_email(self):
|
||||
player = self.bot._db.new_player("foo", 'a', 'b')
|
||||
player.email = "foo@example.com"
|
||||
self.bot.cmd_email(player, "foo", "bar")
|
||||
self.assertEqual(player.email, "foo@example.com")
|
||||
self.assertEqual(self.bot._db._store._mem["foo"].email, "foo@example.com")
|
||||
self.assertEqual("That doesn't look like an email address.", self.irc.notices["foo"][0])
|
||||
|
||||
def test_cmd_email_with_arg_good_email(self):
|
||||
player = self.bot._db.new_player("foo", 'a', 'b')
|
||||
player.email = "foo@example.com"
|
||||
self.bot.cmd_email(player, "foo", "bar@example.com")
|
||||
self.assertEqual(player.email, "bar@example.com")
|
||||
self.assertEqual(self.bot._db._store._mem["foo"].email, "bar@example.com")
|
||||
self.assertEqual("Your email is now set to bar@example.com.", self.irc.notices["foo"][0])
|
||||
|
||||
|
||||
class TestFindItem(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
conf._conf['rpbase'] = 600
|
||||
conf._conf['rpstep'] = 1.16
|
||||
conf._conf['rpmaxexplvl'] = 60
|
||||
conf._conf['modsfile'] = '/tmp/modsfile.txt'
|
||||
conf._conf['color'] = False
|
||||
self.bot = bot.DawdleBot(bot.GameDB(FakeGameStorage()))
|
||||
self.irc = FakeIRCClient()
|
||||
self.bot.connected(self.irc)
|
||||
|
||||
def test_special_item(self):
|
||||
a = self.bot._db.new_player('a', 'b', 'c')
|
||||
a.nick = 'a'
|
||||
a.items['helm'] = bot.Item(20, '')
|
||||
a.level = 25
|
||||
rand.overrides = {
|
||||
'specitem_find': True,
|
||||
'specitem_level': 5,
|
||||
}
|
||||
self.bot.find_item(a)
|
||||
self.assertIn("Your enemies fall before you", self.irc.notices["a"][0])
|
||||
self.assertEqual(a.items['helm'].level, 55)
|
||||
self.assertEqual(a.items['helm'].name, "Mattt's Omniscience Grand Crown")
|
||||
|
||||
def test_special_has_bigger_item(self):
|
||||
a = self.bot._db.new_player('a', 'b', 'c')
|
||||
a.nick = 'a'
|
||||
a.items['helm'] = bot.Item(60, '')
|
||||
a.level = 25
|
||||
rand.overrides = {
|
||||
'specitem_find': True,
|
||||
'specitem_level': 5,
|
||||
}
|
||||
self.bot.find_item(a)
|
||||
self.assertIn("Your enemies fall before you", self.irc.notices["a"][0])
|
||||
# Note that the level is always higher than the current item.
|
||||
self.assertEqual(a.items['helm'].level, 65)
|
||||
self.assertEqual(a.items['helm'].name, "Mattt's Omniscience Grand Crown")
|
||||
|
||||
def test_higher_level_item(self):
|
||||
a = self.bot._db.new_player('a', 'b', 'c')
|
||||
a.nick = 'a'
|
||||
a.items['amulet'] = bot.Item(60, '')
|
||||
rand.overrides = {
|
||||
'specitem_find': False,
|
||||
'specitem_level': 5,
|
||||
'find_item_slot': 'amulet',
|
||||
'find_item_level': 70,
|
||||
}
|
||||
self.bot.find_item(a)
|
||||
self.assertIn("Luck is with you!", self.irc.notices["a"][0])
|
||||
self.assertEqual(a.items['amulet'].level, 70)
|
||||
self.assertEqual(a.items['amulet'].name, '')
|
||||
|
||||
def test_lower_level_item(self):
|
||||
a = self.bot._db.new_player('a', 'b', 'c')
|
||||
a.nick = 'a'
|
||||
a.items['amulet'] = bot.Item(60, '')
|
||||
rand.overrides = {
|
||||
'specitem_find': False,
|
||||
'specitem_level': 5,
|
||||
'find_item_slot': 'amulet',
|
||||
'find_item_level': 50,
|
||||
}
|
||||
self.bot.find_item(a)
|
||||
self.assertIn("Luck is against you.", self.irc.notices["a"][0])
|
||||
self.assertEqual(a.items['amulet'].level, 60)
|
||||
self.assertEqual(a.items['amulet'].name, '')
|
||||
|
||||
|
||||
class TestGameTick(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
conf._conf['rpbase'] = 600
|
||||
conf._conf['rpstep'] = 1.16
|
||||
conf._conf['rpmaxexplvl'] = 60
|
||||
conf._conf['detectsplits'] = True
|
||||
conf._conf['splitwait'] = 300
|
||||
conf._conf['datadir'] = os.path.join(os.path.dirname(os.path.dirname(__file__)), "setup")
|
||||
conf._conf['eventsfile'] = "events.txt"
|
||||
conf._conf['writequestfile'] = True
|
||||
conf._conf['questfilename'] = "/tmp/testquestfile.txt"
|
||||
conf._conf['quest_min_level'] = 24
|
||||
conf._conf['quest_min_login'] = 0
|
||||
conf._conf['self_clock'] = 1
|
||||
conf._conf['mapx'] = 500
|
||||
conf._conf['mapy'] = 500
|
||||
conf._conf['color'] = False
|
||||
self.bot = bot.DawdleBot(bot.GameDB(FakeGameStorage()))
|
||||
self.irc = FakeIRCClient()
|
||||
self.bot.connected(self.irc)
|
||||
|
||||
def test_gametick(self):
|
||||
op = [self.bot._db.new_player(pname, 'a', 'b') for pname in "abcd"]
|
||||
level = 25
|
||||
for p in op:
|
||||
p.online = True
|
||||
p.level = level
|
||||
level += 3
|
||||
self.bot.gametick(0, 0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
138
dawdle/test_irc.py
Normal file
138
dawdle/test_irc.py
Normal file
|
@ -0,0 +1,138 @@
|
|||
import unittest
|
||||
|
||||
from dawdle import abstract
|
||||
from dawdle import conf
|
||||
from dawdle import irc
|
||||
|
||||
class FakeBot(abstract.AbstractBot):
|
||||
|
||||
|
||||
def connected(self, client: abstract.AbstractClient) -> None:
|
||||
pass
|
||||
|
||||
def disconnected(self) -> None:
|
||||
pass
|
||||
|
||||
def ready(self) -> None:
|
||||
pass
|
||||
|
||||
def acquired_ops(self) -> None:
|
||||
pass
|
||||
|
||||
def nick_parted(self, user: abstract.AbstractClient.User) -> None:
|
||||
pass
|
||||
|
||||
def nick_kicked(self, user: abstract.AbstractClient.User) -> None:
|
||||
pass
|
||||
|
||||
def netsplit(self, user: abstract.AbstractClient.User) -> None:
|
||||
pass
|
||||
|
||||
def nick_dropped(self, user: abstract.AbstractClient.User) -> None:
|
||||
pass
|
||||
|
||||
def nick_quit(self, user: abstract.AbstractClient.User) -> None:
|
||||
pass
|
||||
|
||||
def nick_changed(self, user: abstract.AbstractClient.User, new_nick: str) -> None:
|
||||
pass
|
||||
|
||||
def private_message(self, user: abstract.AbstractClient.User, text: str) -> None:
|
||||
pass
|
||||
|
||||
def channel_message(self, user: abstract.AbstractClient.User, text: str) -> None:
|
||||
pass
|
||||
|
||||
def channel_notice(self, user: abstract.AbstractClient.User, text: str) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class TestIRCMessage(unittest.TestCase):
|
||||
def test_basic(self) -> None:
|
||||
line = "@time=2021-07-31T13:55:00;bar=baz :nick!example@example.com PART #example :later!"
|
||||
msg = irc.IRCClient.parse_message(line)
|
||||
self.assertIsNotNone(msg)
|
||||
if msg is None:
|
||||
return
|
||||
self.assertEqual(msg.tags, {"time": "2021-07-31T13:55:00", "bar": "baz"})
|
||||
self.assertEqual(msg.src, "nick")
|
||||
self.assertEqual(msg.user, "example")
|
||||
self.assertEqual(msg.host, "example.com")
|
||||
self.assertEqual(msg.cmd, "PART")
|
||||
self.assertEqual(msg.args, ["#example", "later!"])
|
||||
self.assertEqual(msg.trailing, "later!")
|
||||
self.assertEqual(msg.line, line)
|
||||
self.assertEqual(msg.time, 1627754100)
|
||||
|
||||
def test_one_trailing_arg(self) -> None:
|
||||
line = ":foo!bar@example.com NICK :baz"
|
||||
msg = irc.IRCClient.parse_message(line)
|
||||
self.assertIsNotNone(msg)
|
||||
if msg is None:
|
||||
return
|
||||
self.assertEqual(msg.tags, {})
|
||||
self.assertEqual(msg.src, "foo")
|
||||
self.assertEqual(msg.user, "bar")
|
||||
self.assertEqual(msg.host, "example.com")
|
||||
self.assertEqual(msg.cmd, "NICK")
|
||||
self.assertEqual(msg.args, ["baz"])
|
||||
self.assertEqual(msg.trailing, "baz")
|
||||
self.assertEqual(msg.line, line)
|
||||
|
||||
|
||||
def test_complextags(self) -> None:
|
||||
line = "@keyone=one\\sbig\\:value;keytwo=t\\wo\\rbig\\n\\\\values :nick!example@example.com PART #example :later!"
|
||||
msg = irc.IRCClient.parse_message(line)
|
||||
self.assertIsNotNone(msg)
|
||||
if msg is None:
|
||||
return
|
||||
self.assertEqual(msg.tags, {
|
||||
"keyone": "one big;value",
|
||||
"keytwo": "two\rbig\n\\values",
|
||||
})
|
||||
|
||||
|
||||
def test_notags(self) -> None:
|
||||
line = ":nick!example@example.com PART #example :later!"
|
||||
msg = irc.IRCClient.parse_message(line)
|
||||
self.assertIsNotNone(msg)
|
||||
if msg is None:
|
||||
return
|
||||
self.assertEqual(msg.tags, {})
|
||||
self.assertEqual(msg.src, "nick")
|
||||
|
||||
def test_badtags(self) -> None:
|
||||
line = "@asdf :nick!example@example.com PART #example :later!"
|
||||
msg = irc.IRCClient.parse_message(line)
|
||||
self.assertIsNotNone(msg)
|
||||
if msg is None:
|
||||
return
|
||||
self.assertEqual(msg.tags, {'asdf': None})
|
||||
self.assertEqual(msg.src, "nick")
|
||||
|
||||
line = "@ :nick!example@example.com PART #example :later!"
|
||||
msg = irc.IRCClient.parse_message(line)
|
||||
self.assertIsNotNone(msg)
|
||||
if msg is None:
|
||||
return
|
||||
self.assertEqual(msg.tags, {})
|
||||
self.assertEqual(msg.src, "nick")
|
||||
|
||||
|
||||
class TestIRCClient(unittest.TestCase):
|
||||
|
||||
def test_handle_cap(self) -> None:
|
||||
conf._conf['botnick'] = 'foo'
|
||||
client = irc.IRCClient(FakeBot())
|
||||
client.handle_cap(irc.IRCClient.Message(tags={}, src='tungsten.libera.chat', user=None, host=None, cmd='CAP', args=['*', 'ACK', 'multi-prefix'], trailing='multi-prefix', line=':tungsten.libera.chat CAP * ACK :multi-prefix', time=1629501206))
|
||||
self.assertIn("multi-prefix", client._caps)
|
||||
|
||||
|
||||
def test_nick_change(self) -> None:
|
||||
conf._conf['botnick'] = 'dawdlerpg'
|
||||
testbot = FakeBot()
|
||||
client = irc.IRCClient(testbot)
|
||||
client.handle_join(irc.IRCClient.Message(tags={}, src='foo', user=None, host=None, cmd='NICK', args=['#dawdlerpg'], trailing='', line='', time=0))
|
||||
client.handle_nick(irc.IRCClient.Message(tags={}, src='foo', user=None, host=None, cmd='NICK', args=['bar'], trailing='bar', line='', time=0))
|
||||
self.assertNotIn('foo', client._users)
|
||||
self.assertIn('bar', client._users)
|
59
docker-compose.yml
Normal file
59
docker-compose.yml
Normal file
|
@ -0,0 +1,59 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
dawdle:
|
||||
build: .
|
||||
volumes:
|
||||
- ./:/data
|
||||
command:
|
||||
- 'python'
|
||||
- '/data/dawdle.py'
|
||||
- '/data/af/dawdle.conf'
|
||||
restart: unless-stopped
|
||||
|
||||
web:
|
||||
build: .
|
||||
volumes:
|
||||
- ./site:/site
|
||||
working_dir: /site
|
||||
restart: unless-stopped
|
||||
command: python manage.py runserver 0.0.0.0:8000
|
||||
networks:
|
||||
- web
|
||||
|
||||
nginx:
|
||||
image: docker.io/library/nginx:stable-alpine
|
||||
volumes:
|
||||
- ./site/static:/usr/share/nginx/html:ro
|
||||
- ./nginx.conf.template:/etc/nginx/templates/default.conf.template
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- web
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.middlewares.https-compress-ircl3.compress=true
|
||||
|
||||
- traefik.http.middlewares.header_ircl3.headers.sslRedirect=true
|
||||
- traefik.http.middlewares.header_ircl3.headers.sslHost=irc.l3.lv
|
||||
- traefik.http.middlewares.header_ircl3.headers.stsSeconds=3600
|
||||
- traefik.http.middlewares.header_ircl3.headers.stsPreload=true
|
||||
- traefik.http.middlewares.header_ircl3.headers.forceSTSHeader=true
|
||||
- traefik.http.middlewares.header_ircl3.headers.frameDeny=true
|
||||
- traefik.http.middlewares.header_ircl3.headers.contentTypeNosniff=true
|
||||
- traefik.http.middlewares.header_ircl3.headers.customRequestHeaders.Cache-Control='public, max-age=3600'
|
||||
|
||||
- traefik.http.routers.ircl3.rule=Host(`irc.l3.lv`)
|
||||
- traefik.http.routers.ircl3.tls=true
|
||||
- traefik.http.routers.ircl3.tls.certresolver=le
|
||||
- traefik.http.routers.ircl3.entrypoints=secure
|
||||
- traefik.http.routers.ircl3.middlewares=header_ircl3,https-compress-ircl3
|
||||
- traefik.http.services.ircl3.loadbalancer.server.port=80
|
||||
|
||||
- traefik.http.routers.ircl3_http.rule=Host(`irc.l3.lv`)
|
||||
- traefik.http.routers.ircl3_http.service=ircl3
|
||||
- traefik.http.routers.ircl3_http.entrypoints=web
|
||||
- traefik.http.routers.ircl3_http.middlewares=header_ircl3,https-compress-ircl3
|
||||
|
||||
networks:
|
||||
web:
|
||||
external: true
|
64
install.sh
Executable file
64
install.sh
Executable file
|
@ -0,0 +1,64 @@
|
|||
#!/bin/bash
|
||||
|
||||
if [[ "$UID" != "0" ]]; then
|
||||
echo This script must be run as root.
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "x$1" == "x" ]]; then
|
||||
echo "Usage: install.sh <hostname>" >/dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
HOST="$1"
|
||||
|
||||
DIR="$(readlink -f $(dirname $0))"
|
||||
echo "Using $DIR as dawdlerpg directory."
|
||||
|
||||
echo Installing services.
|
||||
apt install -y nginx python3-pip
|
||||
pip install django uwsgi Pillow
|
||||
|
||||
echo Configuring services.
|
||||
sed "s#DAWDLERPG_DIR#${DIR}#g" setup/uwsgi.service >/etc/systemd/system/uwsgi.service
|
||||
sed "s#DAWDLERPG_DIR#${DIR}#g" setup/uwsgi.ini >/etc/uwsgi.ini
|
||||
sed "s#DAWDLERPG_DIR#${DIR}#g" setup/dawdlerpg.service >/etc/systemd/system/dawdlerpg.service
|
||||
sed "s#DAWDLERPG_DIR#${DIR}#g" setup/dawdlerpg.nginx >/etc/nginx/sites-available/dawdlerpg
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
ln -sf /etc/nginx/sites-available/dawdlerpg /etc/nginx/sites-enabled/
|
||||
|
||||
systemctl enable uwsgi
|
||||
systemctl enable dawdlerpg
|
||||
systemctl enable nginx
|
||||
|
||||
echo Setting up dawdlerpg.
|
||||
cp setup/dawdle.conf "$DIR/data"
|
||||
cp setup/events.txt "$DIR/data"
|
||||
pushd "$DIR/site/"
|
||||
SECRET_KEY="$(openssl rand -base64 45)"
|
||||
sed -e "/^SECRET_KEY/ c \\SECRET_KEY = '${SECRET_KEY}'" \
|
||||
-e "/^ALLOWED_HOSTS/ c \\ALLOWED_HOSTS = ['${HOST}']" \
|
||||
-e "/^DEBUG/ c \\DEBUG = False" \
|
||||
setup/project-settings.py \
|
||||
>project/settings.py
|
||||
./manage.py migrate --database=default
|
||||
./manage.py migrate --database=game
|
||||
./manage.py collectstatic --no-input
|
||||
cd "$DIR"
|
||||
echo -n "You should now edit the dawdle.conf file for your particular game. Press RETURN to begin editing."
|
||||
read
|
||||
"$EDITOR" "$DIR/data/dawdle.conf"
|
||||
if [[ $! != 0 ]]; then
|
||||
exit $!
|
||||
fi
|
||||
"$DIR/dawdle.py" --setup "$DIR/data/dawdle.conf"
|
||||
popd
|
||||
|
||||
chown -R www-data:www-data "$DIR"
|
||||
|
||||
echo Starting systems.
|
||||
systemctl start uwsgi
|
||||
systemctl restart nginx
|
||||
systemctl start dawdlerpg
|
||||
|
||||
echo Done.
|
3
map/get.sh
Executable file
3
map/get.sh
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
curl https://irc.l3.lv/map -o "/opt/dawdlerpg/map/map-$(date +%s).png"
|
11
map/make-mp4
Executable file
11
map/make-mp4
Executable file
|
@ -0,0 +1,11 @@
|
|||
#!/bin/bash
|
||||
|
||||
rm /opt/dawdlerpg/site/static/map.mp4;
|
||||
|
||||
ffmpeg -framerate 3 -pattern_type glob -i '/opt/dawdlerpg/map/*.png' \
|
||||
-c:v libx264 -pix_fmt yuv420p \
|
||||
/opt/dawdlerpg/site/static/map.mp4
|
||||
|
||||
now=$(date -u)
|
||||
|
||||
sed -i "s/{% with timelapse='.*' %}/{% with timelapse='${now}' %}/" /opt/dawdlerpg/site/dawdle/templates/dawdle/timelapse.html
|
15
nginx.conf.template
Normal file
15
nginx.conf.template
Normal file
|
@ -0,0 +1,15 @@
|
|||
server {
|
||||
|
||||
location /static {
|
||||
#autoindex on;
|
||||
alias /usr/share/nginx/html/;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://web:8000;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
access_log syslog;
|
||||
error_log stderr warn;
|
||||
}
|
258
setup/dawdle.conf
Normal file
258
setup/dawdle.conf
Normal file
|
@ -0,0 +1,258 @@
|
|||
# Configuration file for the DawdleRPG bot.
|
||||
#
|
||||
# This is backwards-compatible with the IdleRPG bot's configuration.
|
||||
# Some of the old directives are ignored.
|
||||
|
||||
|
||||
# Basic configuration - you should set all these
|
||||
################################################
|
||||
|
||||
# Remove this line so the bot knows you edited the file.
|
||||
#die
|
||||
|
||||
# Superuser that cannot be DELADMINed.
|
||||
owner Animefield
|
||||
|
||||
# Server name:port.
|
||||
server irc.animefriends.moe:7000
|
||||
|
||||
# Bot's nickname
|
||||
botnick IdleFriend
|
||||
|
||||
# Bot's username
|
||||
botuser Idle
|
||||
|
||||
# Bot's real name
|
||||
botrlnm DawdleRPG Bot
|
||||
|
||||
# Bot joins this channel
|
||||
botchan #idlerpg
|
||||
|
||||
# Bot changes its modes to this on connection
|
||||
botmodes +Bix
|
||||
|
||||
# Bot identifies itself with this command - %botpass% string is
|
||||
# replaced by the BOTPASS environment variable.
|
||||
# Commented out because it requires registration by the server.
|
||||
botident PRIVMSG NickServ :identify %botpass%
|
||||
|
||||
# Bot gains ops with this command after joining channel. The channel
|
||||
# name and bot nick are replaced with their configuration.
|
||||
# Commented out because it requires registration by the server.
|
||||
# botopcmd PRIVMSG ChanServ :op %botchan% %botnick%
|
||||
|
||||
# Bot sends this command to attempt to retrieve its nick.
|
||||
# Commented out because it requires registration by the server.
|
||||
botghostcmd PRIVMSG NickServ :ghost %botnick% %botpass%
|
||||
|
||||
# URL that shows up in the help command output.
|
||||
helpurl http://example.com/
|
||||
|
||||
# URL where admins can find help.
|
||||
admincommurl http://example.com/admincomms.txt
|
||||
|
||||
# URL where users can reach the online quest map, if available.
|
||||
mapurl http://example.com/quest.php
|
||||
|
||||
# Daemonize the bot. This will make the bot detach from the terminal
|
||||
# and act as an independent server. If you are running this from a
|
||||
# command line or an init script, this should be on. If you are
|
||||
# running it from systemd or some other service manager, this should
|
||||
# probably be off.
|
||||
daemonize off
|
||||
|
||||
# Gameplay configuration - you might want to tweak these.
|
||||
#########################################################
|
||||
|
||||
# Base amount of time to level.
|
||||
rpbase 600
|
||||
|
||||
# Time to next level is rpbase * (rpstep ** current level)
|
||||
rpstep 1.16
|
||||
|
||||
# Penalty time = penalty * (rppenstep ** current level)
|
||||
rppenstep 1.14
|
||||
|
||||
# Maximum level for ttl to be exponentially computed.
|
||||
rpmaxexplvl 60
|
||||
|
||||
# Ally base time to level
|
||||
allylvlbase 200
|
||||
|
||||
# Time to next ally level is allylvlbase * (allylvlstep ** level)
|
||||
allylvlstep 1.16
|
||||
|
||||
# Maximum level for ally ttl to be exponentially computed
|
||||
allymaxexplvl 60
|
||||
|
||||
# Allow non-admin users to access info command.
|
||||
allowuserinfo on
|
||||
|
||||
# Time penalty limit.
|
||||
limitpen 24192200
|
||||
|
||||
# Penalty for losing the quest. This is applied to all questors!
|
||||
penquest 15
|
||||
|
||||
# Penalty for changing nick.
|
||||
pennick 30
|
||||
|
||||
# Penalty for sending a message - this is per character!
|
||||
penmessage 1
|
||||
|
||||
# Penalty for leaving the channel.
|
||||
penpart 200
|
||||
|
||||
# Penalty for being kicked from the channel.
|
||||
penkick 250
|
||||
|
||||
# Penalty for quitting.
|
||||
penquit 20
|
||||
|
||||
# Penalty for dropping connection.
|
||||
pendropped 20
|
||||
|
||||
# Penalty for using the LOGOUT command.
|
||||
penlogout 20
|
||||
|
||||
# Width of map.
|
||||
mapx 500
|
||||
|
||||
# Height of map.
|
||||
mapy 500
|
||||
|
||||
# Percent change in battle power for good.
|
||||
good_battle_pct 110
|
||||
|
||||
# Percent change in battle power for evil.
|
||||
evil_battle_pct 90
|
||||
|
||||
# Kick/ban users who mention a URL within seconds of joining channel.
|
||||
doban on
|
||||
|
||||
# Time after joining that users can mention a URL without being banned.
|
||||
bannable_time 90s
|
||||
|
||||
# Minimum time between quests.
|
||||
quest_interval_min 12h
|
||||
quest_interval_max 24h
|
||||
|
||||
# Minimum level that a player must be to quest.
|
||||
quest_min_level 24
|
||||
|
||||
# Minimum login time in seconds that a player must have to quest.
|
||||
quest_min_login 36000
|
||||
|
||||
# These are URL hosts which are okay to mention early in channel. Multiples are fine.
|
||||
# okurl example.com
|
||||
# okurl example.org
|
||||
|
||||
# Write quest file to be picked up from the website.
|
||||
writequestfile on
|
||||
|
||||
# Voice users on login.
|
||||
voiceonlogin on
|
||||
|
||||
# Allow users to view information on another user?
|
||||
statuscmd on
|
||||
|
||||
# Disallow registration of filenames existing in a different case?
|
||||
casematters on
|
||||
|
||||
# Detect netsplits. Servers are supposed to disallow netsplit-like
|
||||
# messages, so this shouldn't be a cheat vector.
|
||||
detectsplits on
|
||||
|
||||
# Time to wait after netsplit for disconnection.
|
||||
splitwait 10m
|
||||
|
||||
# Enable mIRC color codes
|
||||
color on
|
||||
|
||||
# Color of player names
|
||||
namecolor cyan
|
||||
|
||||
# Color of durations (mostly time to level)
|
||||
durationcolor green
|
||||
|
||||
# Color of items
|
||||
itemcolor yellow
|
||||
|
||||
# Technical details
|
||||
###################
|
||||
|
||||
# Directory to look for game files. This is set to this configuration
|
||||
# file's directory by default. Uncomment if you need to specify it.
|
||||
# datadir <data directory>
|
||||
|
||||
# Filename for player database. This should be set to the
|
||||
# game.sqlite3 in the site folder if using the django website.
|
||||
dbfile dawdle.db
|
||||
# To integrate with website, use this. Create this file with:
|
||||
# manager.py migrate --database=game
|
||||
# before running with --setup
|
||||
# dbfile ../site/game.sqlite3
|
||||
|
||||
# Game store format - "idlerpg" is fully compatible with the original
|
||||
# perl idlerpg bot and website so dawdlerpg can be used as a drop-in
|
||||
# replacement. "sqlite3" allows some of the newer features and works
|
||||
# with the django website.
|
||||
# store_format idlerpg
|
||||
store_format sqlite3
|
||||
|
||||
# Filename for events file.
|
||||
eventsfile events.txt
|
||||
|
||||
# Filename for quest file. Used by website.
|
||||
questfilename questinfo.txt
|
||||
|
||||
# Game events are saved to this filename. Used by website.
|
||||
modsfile modifiers.txt
|
||||
|
||||
# Logging output - format is log <log level> <path> <template>.
|
||||
# Levels are CRITICAL, WARNING, INFO, DEBUG, and SPAMMY. The path is
|
||||
# relative to the data directory. The template uses the python logger
|
||||
# format documented in
|
||||
# https://docs.python.org/3/library/logging.html#logrecord-attributes
|
||||
#
|
||||
# Examples:
|
||||
# For debugging: tslog DEBUG /dev/stderr %(asctime)s %(message)s
|
||||
# For systemd journal: log INFO /dev/stdout %(message)s
|
||||
# For saving to a log file: tslog WARNING dawdle.log %(asctime)s %(message)s
|
||||
log INFO dawdle.log %(message)s
|
||||
|
||||
# PID file for ensuring that dawdlerpg isn't running twice.
|
||||
pidfile dawdle.pid
|
||||
|
||||
# Backup directory.
|
||||
backupdir backups
|
||||
|
||||
# Maximum player name length.
|
||||
max_player_len 16
|
||||
|
||||
# Maximum player class length.
|
||||
max_class_len 30
|
||||
|
||||
# Does the bot reconnect when disconnected?
|
||||
reconnect on
|
||||
|
||||
# How long to wait after disconnection to reconnect.
|
||||
reconnect_wait 30s
|
||||
|
||||
# Throttle output?
|
||||
throttle on
|
||||
|
||||
# Maximum messages to send during throttle period. Any more messages
|
||||
# will be queued until next period.
|
||||
throttle_rate 5
|
||||
throttle_period 10s
|
||||
|
||||
# Address to use for outgoing connection. This is used to select a
|
||||
# specific interface. Uses default interface when unset.
|
||||
# localaddr <local address>
|
||||
|
||||
# IRC max message width to use for word wrap.
|
||||
message_wrap_len 400
|
||||
|
||||
# Interval in seconds to check for game events.
|
||||
self_clock 1s
|
BIN
setup/dawdle.db
Normal file
BIN
setup/dawdle.db
Normal file
Binary file not shown.
1
setup/dawdle.pid
Normal file
1
setup/dawdle.pid
Normal file
|
@ -0,0 +1 @@
|
|||
1
|
33
setup/dawdlerpg.nginx
Normal file
33
setup/dawdlerpg.nginx
Normal file
|
@ -0,0 +1,33 @@
|
|||
# DawdleRPG nginx configuration
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
|
||||
# SSL configuration
|
||||
#
|
||||
# listen 443 ssl default_server;
|
||||
# listen [::]:443 ssl default_server;
|
||||
# gzip off
|
||||
#
|
||||
|
||||
root DAWDLERPG_DIR/site/static;
|
||||
charset utf-8;
|
||||
server_name _;
|
||||
|
||||
location /media {
|
||||
alias DAWDLERPG_DIR/site/media;
|
||||
}
|
||||
location /static {
|
||||
alias DAWDLERPG_DIR/site/static;
|
||||
}
|
||||
location /favicon.ico {
|
||||
alias DAWDLERPG_DIR/site/static/favicon.ico;
|
||||
}
|
||||
location /robots.txt {
|
||||
alias DAWDLERPG_DIR/site/static/robots.txt;
|
||||
}
|
||||
location / {
|
||||
uwsgi_pass unix:///tmp/dawdlerpg-uwsgi.sock;
|
||||
include uwsgi_params;
|
||||
}
|
||||
}
|
14
setup/dawdlerpg.service
Normal file
14
setup/dawdlerpg.service
Normal file
|
@ -0,0 +1,14 @@
|
|||
[Unit]
|
||||
Description=DawdleRPG IRC Bot
|
||||
After=network.target auditd.service
|
||||
|
||||
[Service]
|
||||
User=www-data
|
||||
Group=www-data
|
||||
ExecStart=DAWDLERPG_DIR/dawdle.py -o daemonize=off DAWDLERPG_DIR/data/dawdle.conf
|
||||
Restart=on-failure
|
||||
RestartPreventExitStatus=255
|
||||
Type=simple
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
27
setup/events.txt
Normal file
27
setup/events.txt
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Calamities - these add to a player's time to level
|
||||
C was struck by lightning
|
||||
C was mauled by a bear
|
||||
C was cursed by a swamp witch
|
||||
C fell into a bog
|
||||
C DIED... a bit
|
||||
|
||||
# Godsends - these subtract from a player's time to level
|
||||
G was sprinkled with fairy dust
|
||||
G found a pot of gold
|
||||
G met a friendly bard
|
||||
G found a magical spring
|
||||
G drank a potent elixir
|
||||
|
||||
# Quest mode 1 - these are simply timed quests
|
||||
Q1 solve the theft of the five rings of Macala
|
||||
Q1 deliver a priceless scroll to the Azar library
|
||||
Q1 rescue an imprisoned genie from the palace of the Thutharks
|
||||
Q1 impersonate cultists to infiltrate a dark temple
|
||||
Q1 unearth the secret of the Twin Pines
|
||||
|
||||
# Quest mode 2 - these are two-stage navigation quests
|
||||
Q2 464 23 113 187 search for a hostage nymph and heal her broken tree
|
||||
Q2 321 488 137 56 complete the Pisan pilgrimage from the shine of Katu to Mount Irta
|
||||
Q2 304 269 70 417 secretly follow a Esteri assassin and report on her activities
|
||||
Q2 447 432 359 318 travel to the Orod volcano to retrieve a lava-proof circlet and bring it to a country village
|
||||
Q2 326 31 246 133 must spring a Jonast noble from a prison and escort him back to his ancestral lands.
|
130
setup/project-settings.py
Normal file
130
setup/project-settings.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
"""
|
||||
Django settings for dawdle project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 2.2.12.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/2.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/2.2/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = ''
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'dawdle.apps.DawdleConfig',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'project.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'project.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
},
|
||||
'game': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'game.sqlite3'),
|
||||
}
|
||||
}
|
||||
|
||||
# Routers
|
||||
# https://docs.djangoproject.com/en/3.2/topics/db/multi-db/
|
||||
DATABASE_ROUTERS = ['dawdle.router.DawdleRouter']
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
18
setup/uwsgi.ini
Normal file
18
setup/uwsgi.ini
Normal file
|
@ -0,0 +1,18 @@
|
|||
[uwsgi]
|
||||
# base directory (full path)
|
||||
chdir = DAWDLERPG_DIR/site/
|
||||
# Django's wsgi file
|
||||
module = project.wsgi:application
|
||||
# Set uid/gid to www-data so nginx can access
|
||||
uid = www-data
|
||||
gid = www-data
|
||||
# master
|
||||
master = true
|
||||
# maximum number of worker processes
|
||||
processes = 3
|
||||
# the socket (use the full path to be safe)
|
||||
socket = /tmp/dawdlerpg-uwsgi.sock
|
||||
# ... with appropriate permissions - may be needed
|
||||
chmod-socket = 664
|
||||
# clear environment on exit
|
||||
vacuum = true
|
13
setup/uwsgi.service
Normal file
13
setup/uwsgi.service
Normal file
|
@ -0,0 +1,13 @@
|
|||
[Unit]
|
||||
Description=uWSGI Service
|
||||
After=network.target auditd.service
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=-/etc/default/uwsgi
|
||||
ExecStart=/usr/local/bin/uwsgi --ini /etc/uwsgi.ini
|
||||
Restart=on-failure
|
||||
RestartPreventExitStatus=255
|
||||
type=notify
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
0
site/dawdle/__init__.py
Normal file
0
site/dawdle/__init__.py
Normal file
8
site/dawdle/admin.py
Normal file
8
site/dawdle/admin.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import Player,Item,History,Quest
|
||||
|
||||
admin.site.register(Player)
|
||||
admin.site.register(Item)
|
||||
admin.site.register(History)
|
||||
admin.site.register(Quest)
|
5
site/dawdle/apps.py
Normal file
5
site/dawdle/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
class DawdleConfig(AppConfig):
|
||||
name = 'dawdle'
|
||||
default_auto_field = 'django.db.models.AutoField'
|
52
site/dawdle/migrations/0001_initial.py
Normal file
52
site/dawdle/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
# Generated by Django 3.2.6 on 2021-08-28 11:50
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Player',
|
||||
fields=[
|
||||
('name', models.CharField(max_length=50, primary_key=True, serialize=False)),
|
||||
('cclass', models.CharField(max_length=30)),
|
||||
('pw', models.CharField(max_length=64)),
|
||||
('alignment', models.CharField(choices=[('g', 'Good'), ('n', 'Neutral'), ('e', 'Evil')], default='n', max_length=1)),
|
||||
('isadmin', models.BooleanField(default=False)),
|
||||
('online', models.BooleanField(default=False)),
|
||||
('nick', models.CharField(max_length=32)),
|
||||
('level', models.IntegerField()),
|
||||
('nextlvl', models.IntegerField()),
|
||||
('userhost', models.CharField(max_length=255)),
|
||||
('posx', models.IntegerField()),
|
||||
('posy', models.IntegerField()),
|
||||
('idled', models.IntegerField()),
|
||||
('penmessage', models.IntegerField()),
|
||||
('pennick', models.IntegerField()),
|
||||
('penpart', models.IntegerField()),
|
||||
('penkick', models.IntegerField()),
|
||||
('penquit', models.IntegerField()),
|
||||
('pendropped', models.IntegerField()),
|
||||
('penquest', models.IntegerField()),
|
||||
('penlogout', models.IntegerField()),
|
||||
('created', models.DateTimeField(editable=False)),
|
||||
('lastlogin', models.DateTimeField(editable=False)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Item',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('level', models.IntegerField()),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dawdle.player')),
|
||||
],
|
||||
),
|
||||
]
|
68
site/dawdle/migrations/0002_auto_20210828_1158.py
Normal file
68
site/dawdle/migrations/0002_auto_20210828_1158.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
# Generated by Django 3.2.6 on 2021-08-28 11:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dawdle', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='player',
|
||||
name='idled',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='player',
|
||||
name='level',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='player',
|
||||
name='nextlvl',
|
||||
field=models.IntegerField(default=600),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='player',
|
||||
name='pendropped',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='player',
|
||||
name='penkick',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='player',
|
||||
name='penlogout',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='player',
|
||||
name='penmessage',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='player',
|
||||
name='pennick',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='player',
|
||||
name='penpart',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='player',
|
||||
name='penquest',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='player',
|
||||
name='penquit',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
]
|
23
site/dawdle/migrations/0003_auto_20210828_1202.py
Normal file
23
site/dawdle/migrations/0003_auto_20210828_1202.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 3.2.6 on 2021-08-28 12:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dawdle', '0002_auto_20210828_1158'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='player',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='player',
|
||||
name='lastlogin',
|
||||
field=models.DateTimeField(auto_now_add=True),
|
||||
),
|
||||
]
|
19
site/dawdle/migrations/0004_item_slot.py
Normal file
19
site/dawdle/migrations/0004_item_slot.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 3.2.6 on 2021-09-01 22:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dawdle', '0003_auto_20210828_1202'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='slot',
|
||||
field=models.CharField(default='helm', max_length=10),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
23
site/dawdle/migrations/0005_history.py
Normal file
23
site/dawdle/migrations/0005_history.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 3.2.6 on 2021-09-09 10:55
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dawdle', '0004_item_slot'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='History',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('time', models.DateTimeField(auto_now_add=True)),
|
||||
('text', models.CharField(max_length=400)),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dawdle.player')),
|
||||
],
|
||||
),
|
||||
]
|
31
site/dawdle/migrations/0006_quest.py
Normal file
31
site/dawdle/migrations/0006_quest.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
# Generated by Django 3.2.6 on 2021-09-09 11:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dawdle', '0005_history'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Quest',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('mode', models.IntegerField(default=0)),
|
||||
('p1', models.CharField(max_length=50)),
|
||||
('p2', models.CharField(max_length=50)),
|
||||
('p3', models.CharField(max_length=50)),
|
||||
('p4', models.CharField(max_length=50)),
|
||||
('text', models.CharField(max_length=400)),
|
||||
('qtime', models.IntegerField(default=0)),
|
||||
('stage', models.IntegerField(default=0)),
|
||||
('dest1x', models.IntegerField(default=0)),
|
||||
('dest1y', models.IntegerField(default=0)),
|
||||
('dest2x', models.IntegerField(default=0)),
|
||||
('dest2y', models.IntegerField(default=0)),
|
||||
],
|
||||
),
|
||||
]
|
17
site/dawdle/migrations/0007_item_unique_item_owner_slot.py
Normal file
17
site/dawdle/migrations/0007_item_unique_item_owner_slot.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 3.2.6 on 2021-09-10 10:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dawdle', '0006_quest'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddConstraint(
|
||||
model_name='item',
|
||||
constraint=models.UniqueConstraint(fields=('owner', 'slot'), name='unique_item_owner_slot'),
|
||||
),
|
||||
]
|
28
site/dawdle/migrations/0008_ally.py
Normal file
28
site/dawdle/migrations/0008_ally.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 3.2.6 on 2021-10-27 11:07
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dawdle', '0007_item_unique_item_owner_slot'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Ally',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slot', models.CharField(max_length=10)),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('baseclass', models.CharField(max_length=30)),
|
||||
('fullclass', models.CharField(max_length=30)),
|
||||
('alignment', models.CharField(choices=[('g', 'Good'), ('n', 'Neutral'), ('e', 'Evil')], default='n', max_length=1)),
|
||||
('level', models.IntegerField(default=0)),
|
||||
('nextlvl', models.IntegerField(default=600)),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dawdle.player')),
|
||||
],
|
||||
),
|
||||
]
|
19
site/dawdle/migrations/0009_player_email.py
Normal file
19
site/dawdle/migrations/0009_player_email.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 4.0.7 on 2023-04-10 17:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dawdle', '0008_ally'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='player',
|
||||
name='email',
|
||||
field=models.CharField(default='', max_length=64),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
19
site/dawdle/migrations/0010_player_penrealign.py
Normal file
19
site/dawdle/migrations/0010_player_penrealign.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 4.0.7 on 2023-04-10 17:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dawdle', '0009_player_email'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='player',
|
||||
name='penrealign',
|
||||
field=models.IntegerField(),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
0
site/dawdle/migrations/__init__.py
Normal file
0
site/dawdle/migrations/__init__.py
Normal file
83
site/dawdle/models.py
Normal file
83
site/dawdle/models.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
from django.db import models
|
||||
|
||||
class AlignmentChoices(models.TextChoices):
|
||||
GOOD = "g"
|
||||
NEUTRAL = "n"
|
||||
EVIL = "e"
|
||||
|
||||
|
||||
class Player(models.Model):
|
||||
name = models.CharField(primary_key=True, max_length=50)
|
||||
cclass = models.CharField(max_length=30)
|
||||
pw = models.CharField(max_length=64)
|
||||
email = models.CharField(max_length=64)
|
||||
alignment = models.CharField(
|
||||
max_length=1,
|
||||
choices=AlignmentChoices.choices,
|
||||
default=AlignmentChoices.NEUTRAL
|
||||
)
|
||||
isadmin = models.BooleanField(default=False)
|
||||
online = models.BooleanField(default=False)
|
||||
nick = models.CharField(max_length=32)
|
||||
level = models.IntegerField(default=0)
|
||||
nextlvl = models.IntegerField(default=600)
|
||||
userhost = models.CharField(max_length=255)
|
||||
posx = models.IntegerField()
|
||||
posy = models.IntegerField()
|
||||
idled = models.IntegerField(default=0)
|
||||
penmessage = models.IntegerField(default=0)
|
||||
pennick = models.IntegerField(default=0)
|
||||
penpart = models.IntegerField(default=0)
|
||||
penkick = models.IntegerField(default=0)
|
||||
penquit = models.IntegerField(default=0)
|
||||
pendropped = models.IntegerField(default=0)
|
||||
penquest = models.IntegerField(default=0)
|
||||
penlogout = models.IntegerField(default=0)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
lastlogin = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class Ally(models.Model):
|
||||
owner = models.ForeignKey(Player, on_delete=models.CASCADE)
|
||||
slot = models.CharField(max_length=10) # mount, sidekick, henchman
|
||||
name = models.CharField(max_length=50) # player selectable name - may be empty
|
||||
baseclass = models.CharField(max_length=30) # lizard
|
||||
fullclass = models.CharField(max_length=30) # sparkly lizard of the beyond
|
||||
alignment = models.CharField(
|
||||
max_length=1,
|
||||
choices=AlignmentChoices.choices,
|
||||
default=AlignmentChoices.NEUTRAL
|
||||
)
|
||||
level = models.IntegerField(default=0)
|
||||
nextlvl = models.IntegerField(default=600)
|
||||
|
||||
|
||||
class Item(models.Model):
|
||||
class Meta:
|
||||
# We use this constraint for the sqlite3 REPLACE command
|
||||
constraints = [models.UniqueConstraint(name="unique_item_owner_slot", fields=["owner", "slot"])]
|
||||
owner = models.ForeignKey(Player, on_delete=models.CASCADE)
|
||||
slot = models.CharField(max_length=10)
|
||||
name = models.CharField(max_length=50)
|
||||
level = models.IntegerField()
|
||||
|
||||
|
||||
class History(models.Model):
|
||||
owner = models.ForeignKey(Player, on_delete=models.CASCADE)
|
||||
time = models.DateTimeField(auto_now_add=True)
|
||||
text = models.CharField(max_length=400)
|
||||
|
||||
|
||||
class Quest(models.Model):
|
||||
mode = models.IntegerField(default=0)
|
||||
p1 = models.CharField(max_length=50)
|
||||
p2 = models.CharField(max_length=50)
|
||||
p3 = models.CharField(max_length=50)
|
||||
p4 = models.CharField(max_length=50)
|
||||
text = models.CharField(max_length=400)
|
||||
qtime = models.IntegerField(default=0)
|
||||
stage = models.IntegerField(default=0)
|
||||
dest1x = models.IntegerField(default=0)
|
||||
dest1y = models.IntegerField(default=0)
|
||||
dest2x = models.IntegerField(default=0)
|
||||
dest2y = models.IntegerField(default=0)
|
26
site/dawdle/router.py
Normal file
26
site/dawdle/router.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
class DawdleRouter(object):
|
||||
def db_for_read(self, model, **hints):
|
||||
"Point all operations on dawdle models to 'dawdledb'"
|
||||
if model._meta.app_label == 'dawdle':
|
||||
return 'game'
|
||||
return 'default'
|
||||
|
||||
def db_for_write(self, model, **hints):
|
||||
"Point all operations on dawdle models to 'dawdledb'"
|
||||
if model._meta.app_label == 'dawdle':
|
||||
return 'game'
|
||||
return 'default'
|
||||
|
||||
def allow_relation(self, obj1, obj2, **hints):
|
||||
"Allow any relation if a both models in dawdle app"
|
||||
if obj1._meta.app_label == 'dawdle' and obj2._meta.app_label == 'dawdle':
|
||||
return True
|
||||
# Allow if neither is dawdle app
|
||||
elif 'dawdle' not in [obj1._meta.app_label, obj2._meta.app_label]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def allow_migrate(self, db, app_label, model_name=None, **hints):
|
||||
if app_label == 'dawdle':
|
||||
return db == 'game'
|
||||
return db == 'default'
|
135
site/dawdle/static/dawdle/Blue_sphere.svg
Normal file
135
site/dawdle/static/dawdle/Blue_sphere.svg
Normal file
|
@ -0,0 +1,135 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.0"
|
||||
width="128"
|
||||
height="128"
|
||||
id="svg2"
|
||||
sodipodi:docname="Blue_sphere.svg"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
|
||||
<metadata
|
||||
id="metadata1057">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1025"
|
||||
id="namedview1055"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.84375"
|
||||
inkscape:cx="-32.813559"
|
||||
inkscape:cy="64"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
<defs
|
||||
id="defs5">
|
||||
<linearGradient
|
||||
id="linearGradient2816">
|
||||
<stop
|
||||
style="stop-color:#003de4;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop2818" />
|
||||
<stop
|
||||
style="stop-color:white;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop2820" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient2794">
|
||||
<stop
|
||||
style="stop-color:#545454;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop2796" />
|
||||
<stop
|
||||
style="stop-color:#545454;stop-opacity:1"
|
||||
offset="0.9464286"
|
||||
id="stop2804" />
|
||||
<stop
|
||||
style="stop-color:#c5c5c5;stop-opacity:0"
|
||||
offset="1"
|
||||
id="stop2798" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient2760">
|
||||
<stop
|
||||
style="stop-color:#80bbff;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop2762" />
|
||||
<stop
|
||||
style="stop-color:#001ee9;stop-opacity:1"
|
||||
offset="0.61160713"
|
||||
id="stop2768" />
|
||||
<stop
|
||||
style="stop-color:#a6d1ff;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop2764" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
x1="6.9292336"
|
||||
y1="121.07077"
|
||||
x2="6.9292336"
|
||||
y2="8.9609833"
|
||||
id="linearGradient2766"
|
||||
xlink:href="#linearGradient2760"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<radialGradient
|
||||
cx="62.984127"
|
||||
cy="65.015877"
|
||||
r="54.179893"
|
||||
fx="62.984127"
|
||||
fy="65.015877"
|
||||
id="radialGradient2812"
|
||||
xlink:href="#linearGradient2794"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
x1="31.5"
|
||||
y1="63"
|
||||
x2="31.5"
|
||||
y2="12"
|
||||
id="linearGradient2822"
|
||||
xlink:href="#linearGradient2816"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<path
|
||||
d="M 117.16402 65.015877 A 54.179893 54.179893 0 1 1 8.8042336,65.015877 A 54.179893 54.179893 0 1 1 117.16402 65.015877 z"
|
||||
transform="translate(5.724864,4.031744)"
|
||||
style="opacity:1;fill:url(#radialGradient2812);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.75;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path2790" />
|
||||
<path
|
||||
d="M 117.16402 65.015877 A 54.179893 54.179893 0 1 1 8.8042336,65.015877 A 54.179893 54.179893 0 1 1 117.16402 65.015877 z"
|
||||
transform="translate(1.69312,0)"
|
||||
style="opacity:1;fill:url(#linearGradient2766);fill-opacity:1;fill-rule:nonzero;stroke:#002274;stroke-width:3.75;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path1873" />
|
||||
<path
|
||||
d="M 95.5 37.5 A 32 25.5 0 1 1 31.5,37.5 A 32 25.5 0 1 1 95.5 37.5 z"
|
||||
transform="translate(1,2)"
|
||||
style="opacity:1;fill:url(#linearGradient2822);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.75;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path2814" />
|
||||
</svg>
|
After Width: | Height: | Size: 4.5 KiB |
135
site/dawdle/static/dawdle/Gray_sphere.svg
Normal file
135
site/dawdle/static/dawdle/Gray_sphere.svg
Normal file
|
@ -0,0 +1,135 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.0"
|
||||
width="128"
|
||||
height="128"
|
||||
id="svg2"
|
||||
sodipodi:docname="Gray_sphere.svg"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
|
||||
<metadata
|
||||
id="metadata952">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1025"
|
||||
id="namedview950"
|
||||
showgrid="false"
|
||||
inkscape:zoom="6.6875"
|
||||
inkscape:cx="64"
|
||||
inkscape:cy="64"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
<defs
|
||||
id="defs5">
|
||||
<linearGradient
|
||||
id="linearGradient2816">
|
||||
<stop
|
||||
style="stop-color:#534f4f;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop2818" />
|
||||
<stop
|
||||
style="stop-color:white;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop2820" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient2794">
|
||||
<stop
|
||||
style="stop-color:#545454;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop2796" />
|
||||
<stop
|
||||
style="stop-color:#545454;stop-opacity:1"
|
||||
offset="0.9464286"
|
||||
id="stop2804" />
|
||||
<stop
|
||||
style="stop-color:#c5c5c5;stop-opacity:0"
|
||||
offset="1"
|
||||
id="stop2798" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient2760">
|
||||
<stop
|
||||
style="stop-color:#808080;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop2762" />
|
||||
<stop
|
||||
style="stop-color:#525050;stop-opacity:1"
|
||||
offset="0.61160713"
|
||||
id="stop2768" />
|
||||
<stop
|
||||
style="stop-color:#b2b2b2;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop2764" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
x1="6.9292336"
|
||||
y1="121.07077"
|
||||
x2="6.9292336"
|
||||
y2="8.9609833"
|
||||
id="linearGradient2766"
|
||||
xlink:href="#linearGradient2760"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<radialGradient
|
||||
cx="62.984127"
|
||||
cy="65.015877"
|
||||
r="54.179893"
|
||||
fx="62.984127"
|
||||
fy="65.015877"
|
||||
id="radialGradient2812"
|
||||
xlink:href="#linearGradient2794"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
x1="31.5"
|
||||
y1="63"
|
||||
x2="31.5"
|
||||
y2="12"
|
||||
id="linearGradient2822"
|
||||
xlink:href="#linearGradient2816"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<path
|
||||
d="M 117.16402 65.015877 A 54.179893 54.179893 0 1 1 8.8042336,65.015877 A 54.179893 54.179893 0 1 1 117.16402 65.015877 z"
|
||||
transform="translate(5.724864,4.031744)"
|
||||
style="opacity:1;fill:url(#radialGradient2812);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.75;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path2790" />
|
||||
<path
|
||||
d="M 117.16402 65.015877 A 54.179893 54.179893 0 1 1 8.8042336,65.015877 A 54.179893 54.179893 0 1 1 117.16402 65.015877 z"
|
||||
style="opacity:1;fill:url(#linearGradient2766);fill-opacity:1;fill-rule:nonzero;stroke:#3a3a3a;stroke-width:3.75;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path1873"
|
||||
transform="translate(1.69312,0)" />
|
||||
<path
|
||||
d="M 95.5 37.5 A 32 25.5 0 1 1 31.5,37.5 A 32 25.5 0 1 1 95.5 37.5 z"
|
||||
transform="translate(1,2)"
|
||||
style="opacity:1;fill:url(#linearGradient2822);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.75;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path2814" />
|
||||
</svg>
|
After Width: | Height: | Size: 4.5 KiB |
95
site/dawdle/static/dawdle/Green_sphere.svg
Normal file
95
site/dawdle/static/dawdle/Green_sphere.svg
Normal file
|
@ -0,0 +1,95 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
version="1.0"
|
||||
width="128"
|
||||
height="128"
|
||||
id="svg2">
|
||||
<defs
|
||||
id="defs5">
|
||||
<linearGradient
|
||||
id="linearGradient2816">
|
||||
<stop
|
||||
style="stop-color:#00b000;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop2818" />
|
||||
<stop
|
||||
style="stop-color:#ffffff;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop2820" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient2794">
|
||||
<stop
|
||||
style="stop-color:#545454;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop2796" />
|
||||
<stop
|
||||
style="stop-color:#545454;stop-opacity:1"
|
||||
offset="0.9464286"
|
||||
id="stop2804" />
|
||||
<stop
|
||||
style="stop-color:#c5c5c5;stop-opacity:0"
|
||||
offset="1"
|
||||
id="stop2798" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient2760">
|
||||
<stop
|
||||
style="stop-color:#00ff00;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop2762" />
|
||||
<stop
|
||||
style="stop-color:#00a200;stop-opacity:1"
|
||||
offset="0.61160713"
|
||||
id="stop2768" />
|
||||
<stop
|
||||
style="stop-color:#66ff66;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop2764" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
x1="6.9292336"
|
||||
y1="121.07077"
|
||||
x2="6.9292336"
|
||||
y2="8.9609833"
|
||||
id="linearGradient2766"
|
||||
xlink:href="#linearGradient2760"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<radialGradient
|
||||
cx="62.984127"
|
||||
cy="65.015877"
|
||||
r="54.179893"
|
||||
fx="62.984127"
|
||||
fy="65.015877"
|
||||
id="radialGradient2812"
|
||||
xlink:href="#linearGradient2794"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
x1="31.5"
|
||||
y1="63"
|
||||
x2="31.5"
|
||||
y2="12"
|
||||
id="linearGradient2822"
|
||||
xlink:href="#linearGradient2816"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<path
|
||||
d="M 117.16402,65.015877 C 117.16402,94.938606 92.906856,119.19577 62.984127,119.19577 C 33.061398,119.19577 8.8042336,94.938606 8.8042336,65.015877 C 8.8042336,35.093148 33.061398,10.835984 62.984127,10.835984 C 92.906856,10.835984 117.16402,35.093148 117.16402,65.015877 z "
|
||||
transform="translate(5.724864,4.031744)"
|
||||
style="opacity:1;fill:url(#radialGradient2812);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.75;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path2790" />
|
||||
<path
|
||||
d="M 117.16402,65.015877 C 117.16402,94.938606 92.906856,119.19577 62.984127,119.19577 C 33.061398,119.19577 8.8042336,94.938606 8.8042336,65.015877 C 8.8042336,35.093148 33.061398,10.835984 62.984127,10.835984 C 92.906856,10.835984 117.16402,35.093148 117.16402,65.015877 z "
|
||||
transform="translate(1.69312,0)"
|
||||
style="opacity:1;fill:url(#linearGradient2766);fill-opacity:1;fill-rule:nonzero;stroke:#007400;stroke-width:3.75;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path1873" />
|
||||
<path
|
||||
d="M 95.5,37.5 C 95.5,51.583261 81.173112,63 63.5,63 C 45.826888,63 31.5,51.583261 31.5,37.5 C 31.5,23.416739 45.826888,12 63.5,12 C 81.173112,12 95.5,23.416739 95.5,37.5 z "
|
||||
transform="translate(1,2)"
|
||||
style="opacity:1;fill:url(#linearGradient2822);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.75;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path2814" />
|
||||
</svg>
|
After Width: | Height: | Size: 3.7 KiB |
95
site/dawdle/static/dawdle/Red_sphere.svg
Normal file
95
site/dawdle/static/dawdle/Red_sphere.svg
Normal file
|
@ -0,0 +1,95 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
version="1.0"
|
||||
width="128"
|
||||
height="128"
|
||||
id="svg2">
|
||||
<defs
|
||||
id="defs5">
|
||||
<linearGradient
|
||||
id="linearGradient2816">
|
||||
<stop
|
||||
style="stop-color:#a20000;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop2818" />
|
||||
<stop
|
||||
style="stop-color:white;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop2820" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient2794">
|
||||
<stop
|
||||
style="stop-color:#545454;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop2796" />
|
||||
<stop
|
||||
style="stop-color:#545454;stop-opacity:1"
|
||||
offset="0.9464286"
|
||||
id="stop2804" />
|
||||
<stop
|
||||
style="stop-color:#c5c5c5;stop-opacity:0"
|
||||
offset="1"
|
||||
id="stop2798" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient2760">
|
||||
<stop
|
||||
style="stop-color:red;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop2762" />
|
||||
<stop
|
||||
style="stop-color:#a20000;stop-opacity:1"
|
||||
offset="0.61160713"
|
||||
id="stop2768" />
|
||||
<stop
|
||||
style="stop-color:#f66;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop2764" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
x1="6.9292336"
|
||||
y1="121.07077"
|
||||
x2="6.9292336"
|
||||
y2="8.9609833"
|
||||
id="linearGradient2766"
|
||||
xlink:href="#linearGradient2760"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<radialGradient
|
||||
cx="62.984127"
|
||||
cy="65.015877"
|
||||
r="54.179893"
|
||||
fx="62.984127"
|
||||
fy="65.015877"
|
||||
id="radialGradient2812"
|
||||
xlink:href="#linearGradient2794"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
x1="31.5"
|
||||
y1="63"
|
||||
x2="31.5"
|
||||
y2="12"
|
||||
id="linearGradient2822"
|
||||
xlink:href="#linearGradient2816"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<path
|
||||
d="M 117.16402 65.015877 A 54.179893 54.179893 0 1 1 8.8042336,65.015877 A 54.179893 54.179893 0 1 1 117.16402 65.015877 z"
|
||||
transform="translate(5.724864,4.031744)"
|
||||
style="opacity:1;fill:url(#radialGradient2812);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.75;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path2790" />
|
||||
<path
|
||||
d="M 117.16402 65.015877 A 54.179893 54.179893 0 1 1 8.8042336,65.015877 A 54.179893 54.179893 0 1 1 117.16402 65.015877 z"
|
||||
transform="translate(1.69312,0)"
|
||||
style="opacity:1;fill:url(#linearGradient2766);fill-opacity:1;fill-rule:nonzero;stroke:#740000;stroke-width:3.75;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path1873" />
|
||||
<path
|
||||
d="M 95.5 37.5 A 32 25.5 0 1 1 31.5,37.5 A 32 25.5 0 1 1 95.5 37.5 z"
|
||||
transform="translate(1,2)"
|
||||
style="opacity:1;fill:url(#linearGradient2822);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.75;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path2814" />
|
||||
</svg>
|
After Width: | Height: | Size: 3.3 KiB |
BIN
site/dawdle/static/dawdle/map.png
Normal file
BIN
site/dawdle/static/dawdle/map.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 494 KiB |
19
site/dawdle/templates/dawdle/about.html
Normal file
19
site/dawdle/templates/dawdle/about.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
{% extends "dawdle/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="contentbox">
|
||||
<h1>About DawdleRPG</h1>
|
||||
|
||||
<p>DawdleRPG was made by Daniel Lowe in loving imitation of the venerable
|
||||
<a href="https://idlerpg.net/">IdleRPG</a> of IRC fame,
|
||||
by <a href="mailto:jotun@idlerpg.net">jotun@idlerpg.net</a>.</p>
|
||||
|
||||
<p>Source code for both the bot and the website can be found at:
|
||||
<a href="https://github.com/dlowe-net/dawdlerpg/">https://github.com/dlowe-net/dawdlerpg/</a>.
|
||||
The code is licensed under the Apache License 2.0.</p>
|
||||
|
||||
<p> Modified for the AnimeFriends community </p>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
162
site/dawdle/templates/dawdle/base.html
Normal file
162
site/dawdle/templates/dawdle/base.html
Normal file
|
@ -0,0 +1,162 @@
|
|||
<html><head><title>{% block title %}Friends IdleRPG{% endblock %}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #eee;
|
||||
}
|
||||
#header-container {
|
||||
color: #e1cdb5;
|
||||
background: #49306b;
|
||||
margin: 0;
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
#header {
|
||||
text-align: left;
|
||||
#height: 67px;
|
||||
padding: 0;
|
||||
margin: 0 auto;
|
||||
width: 70%;
|
||||
position: relative;
|
||||
}
|
||||
#header h1 {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
line-height: 67px;
|
||||
}
|
||||
#header ul {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
height: 100%:
|
||||
}
|
||||
#header li {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
line-height: 67px;
|
||||
text-align: center;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
#header li:hover {
|
||||
background-color: #AF97AC;
|
||||
}
|
||||
#header a {
|
||||
color: #e1cdb5;
|
||||
text-decoration: none;
|
||||
}
|
||||
#content {
|
||||
margin: 1em auto;
|
||||
width: 75%;
|
||||
}
|
||||
.contentbox {
|
||||
background: white;
|
||||
margin: 2rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.contentbox h1 {
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.contentbox h2 {
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.good-align {
|
||||
color: #00f;
|
||||
}
|
||||
.evil-align {
|
||||
color: #500;
|
||||
}
|
||||
@media only screen and (max-width: 1200px) {
|
||||
body {
|
||||
font-size: 200%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#header-container {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
#header h1 {
|
||||
display: block;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
#header ul {
|
||||
display: block;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
#content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.contentbox {
|
||||
margin: 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
#playerlist {
|
||||
display: inline-block;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
#playerlist tr:nth-child(even) {
|
||||
background: #E1CDB5;
|
||||
}
|
||||
#playerlist thead tr {
|
||||
background: #90708C;
|
||||
}
|
||||
#playerlist thead tr td {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
#playerlist td {
|
||||
padding: 0.1rem 1rem;
|
||||
}
|
||||
#pmap-container {
|
||||
display: inline-block;
|
||||
padding: 1rem;
|
||||
vertical-align: top;
|
||||
}
|
||||
.online a {
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
.online a:hover {
|
||||
color: black;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.offline {
|
||||
color: gray;
|
||||
}
|
||||
.offline a {
|
||||
text-decoration: none;
|
||||
color: gray;
|
||||
}
|
||||
.offline a:hover {
|
||||
color: gray;
|
||||
text-decoration: underline;
|
||||
}
|
||||
{% block extrastyles %}{% endblock %}
|
||||
</style>
|
||||
</head><body>
|
||||
<div id="header-container">
|
||||
<div id="header">
|
||||
<h1><a href="/">AnimeFriends IdleRPG</a></h1>
|
||||
<ul>
|
||||
<a href="{% url 'dawdle:about' %}"><li>About</li></a>
|
||||
<a href="{% url 'dawdle:player-list' %}"><li>Players</li></a>
|
||||
<a href="/quest"><li>Quest</li></a>
|
||||
<a href="/timelapse"><li>TimeLapse</li></a>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div id="content">{% block content %}{% endblock %}</div>
|
||||
</body></html>
|
112
site/dawdle/templates/dawdle/front.html
Normal file
112
site/dawdle/templates/dawdle/front.html
Normal file
|
@ -0,0 +1,112 @@
|
|||
{% extends "dawdle/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="contentbox">
|
||||
<h1>Welcome to IdleRPG (DawdleRPG)</h1>
|
||||
|
||||
<p>IdleRPG is an IRC game where your alter-ego goes adventuring
|
||||
across a rich land full of magic and adventure, while players lurk
|
||||
silently in a channel. You register your character, and sit back
|
||||
and enjoy the ride while shit talking your friends.</p>
|
||||
|
||||
<p>Originally based on the DawdleRPG code, (one of the only bots with
|
||||
ssl support) but has been modified slightly for a more "enjoyable" time
|
||||
See all <strong>(New)</strong> items below</p>
|
||||
|
||||
<h2>Getting Started</h2>
|
||||
|
||||
<p>To start, login to your favorite IRC networ and join the
|
||||
#idleRPG channel. Then, send a private message to the bot:</p>
|
||||
|
||||
<p><code>/msg IdleFriend register <name> <password>
|
||||
<character class></code></p>
|
||||
|
||||
<p>If all goes well, your character will start at level 0 and begin
|
||||
wandering the lands. You may explore the other commands by
|
||||
messaging <code>help</code> to the bot.</p>
|
||||
|
||||
<p>If you lose connection to IRC, you should login quickly with the
|
||||
command: <code>/msg IdleFriend login <name> <password></code></p>
|
||||
|
||||
<h2>Advancement & Penalties</h2>
|
||||
|
||||
<p>Your character advances through the passage of time. You will be
|
||||
kept updated regularly on how long your character has until the
|
||||
next level. Events may happen to your character which will push
|
||||
you forward or back along your advancement path.</p>
|
||||
|
||||
<p>As much as doing nothing on the channel is rewarded, doing
|
||||
something on the channel is punished. Changing your nick,
|
||||
parting the channel, getting kicked from the channel,
|
||||
quitting IRC, and using the LOGOUT command are all penalized to
|
||||
different degrees. If you are disconnected or netsplit, your
|
||||
character will <em>not</em> be penalized as long as you login
|
||||
again within 15 minutes.</p>
|
||||
|
||||
<p>Incurring a penalty may cause you to fail a quest, penalizing
|
||||
everyone online! See below for details.</p>
|
||||
<p><strong>(NEW)</strong> No penalty is incurred for Chatting in the channel!</p>
|
||||
|
||||
<h2>Combat and Equipment</h2>
|
||||
|
||||
<p>Characters will frequently engage in combat. Combat is resolved
|
||||
by taking the sum of item levels of each combatant, then randomly
|
||||
picking a number between 0 and that sum. The side with the higher
|
||||
sum wins the combat.</p>
|
||||
|
||||
<p>In one-on-one fights, the winner may take an item from the loser,
|
||||
giving the loser a replacement item in a moment of pity. The
|
||||
winner might also deal a Critical Strike, which slows the loser's
|
||||
progess to the next level. Regardless of outcome, the challenger
|
||||
may randomly find an item, and pick it up.</p>
|
||||
|
||||
<p>The level of equipment found is usually limited by the level of
|
||||
the character, but there are certain rare artifacts of great
|
||||
power. The level of these artifacts can be far beyond what a
|
||||
character may usually possess.</p>
|
||||
|
||||
<h2>Alignment</h2>
|
||||
|
||||
<p>Your character starts out as neutral. By using
|
||||
the <code>ALIGN</code> command, you may change it to good or
|
||||
evil (or back to neutral, if you tire of the struggle).</p>
|
||||
|
||||
<p>Good characters get a bonus to their combat power, and
|
||||
occasionally help each other to the next level. They are more
|
||||
likely to find an item after battling. However, they are much less
|
||||
likely to deal a Critical Strike.</p>
|
||||
|
||||
<p>Evil characters have a penalty to their combat power, but are
|
||||
much more likely to deal a Critical Strike. They are less likely to
|
||||
find an item after the battle. They will steal an item from a
|
||||
good character if they can, but are frequently forsaken by their
|
||||
evil god.</p>
|
||||
|
||||
<p><strong>(New)</strong> Once you align, removing your alignment
|
||||
or forsaking your god and changing will incur a penalty that fits your
|
||||
crimes. (Same penalty as leaving the channel)</p>
|
||||
|
||||
<h2>Mounts</h2>
|
||||
|
||||
<p>When your character is powerful enough, they might attract a
|
||||
mount. This mount will level with your character, but at a faster
|
||||
rate. The mount will match your alignment, however —
|
||||
changing your alignment will cause them to leave in disgust. Your
|
||||
mount's level adds to your prowess in battle.</p>
|
||||
|
||||
<h2>Quests</h2>
|
||||
|
||||
<p>The gods will occasionally pick out four characters to go on a
|
||||
Quest. The nature of this quest may vary - sometimes a quest will
|
||||
take a certain length of time. Sometimes the characters must
|
||||
journey together to a far-off land. On completion of the quest,
|
||||
each questor will advance 25% of their time to next level.</p>
|
||||
|
||||
<p>If any questor fails the quest, however, if they are ever
|
||||
penalized, the quest has failed, and the gods will
|
||||
punish <em>all</em> characters with a stiff penalty.</p>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
82
site/dawdle/templates/dawdle/player_detail.html
Normal file
82
site/dawdle/templates/dawdle/player_detail.html
Normal file
|
@ -0,0 +1,82 @@
|
|||
{% extends "dawdle/base.html" %}
|
||||
{% load duration %}
|
||||
{% load alignment %}
|
||||
|
||||
{% block content %}
|
||||
<div class="contentbox">
|
||||
<h1>{{player.name}}, the level {{player.level}} {{player.cclass}}</h1>
|
||||
<table><tbody>
|
||||
<tr>
|
||||
<td>Level:</td><td>{{player.level}}</td>
|
||||
<td>Current Position:</td><td>{{player.posx}}, {{player.posy}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Account created:</td><td>{{player.created}}</td>
|
||||
<td>Last Login:</td><td>{{player.lastlogin}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total time idled:</td><td>{{player.idled|duration}}</td>
|
||||
<td>Time to next level:</td><td>{{player.nextlvl|duration}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alignment:</td><td><span class="{{player.alignment|alignment}}-align">{{player.alignment|alignment}}</span></td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
<img src="{% url 'dawdle:player-map' player.name %}" alt="Player Map" usemap="#playermap">
|
||||
<map name="playermap">
|
||||
<area shape="circle"
|
||||
coords="{{player.posx}},{{player.posy}},3"
|
||||
alt="{{player.name}}"
|
||||
href="{% url 'dawdle:player-detail' player.name %}">
|
||||
</map>
|
||||
|
||||
|
||||
</div><div class="contentbox">
|
||||
<h2>Penalties</h2>
|
||||
<table><tbody>
|
||||
<tr>
|
||||
<td>Being kicked:</td><td>{{player.penkick|duration}}</td>
|
||||
<td>Leaving channel:</td><td>{{player.penpart|duration}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Quitting:</td><td>{{player.penquit|duration}}</td>
|
||||
<td>Dropping link:</td><td>{{player.pendropped|duration}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Changing nicks:</td><td>{{player.pennick|duration}}</td>
|
||||
<td>Messaging:</td><td>{{player.penmessage|duration}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Logout command:</td><td>{{player.penlogout|duration}}</td>
|
||||
<td>Quest failures:</td><td>{{player.penquest|duration}}</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
<p>Total penalties: {{total_penalties|duration}}</p>
|
||||
|
||||
</div><div class="contentbox">
|
||||
<h2>Items</h2>
|
||||
<ul>
|
||||
{% for item in player.item_set.all|dictsort:"slot" %}
|
||||
<li>{{item.slot}}: Level {{ item.level }} {{item.name}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p>Total item level: {{ total_items }}</p>
|
||||
</div><div class="contentbox">
|
||||
{% if player.ally_set %}
|
||||
<h2>Allies</h2>
|
||||
<ul>
|
||||
{% for ally in player.ally_set.all|dictsort:"slot" %}
|
||||
<li>{{ally.slot}}: level {{ ally.level }} {{ally.fullclass}}, Next level in {{ally.nextlvl|duration}}.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div><div class="contentbox">
|
||||
{% endif %}
|
||||
<h2>History</h2>
|
||||
<ul>
|
||||
{% for event in player.history_set.all|dictsortreversed:"time" %}
|
||||
<li>{{event.time}}: {{event.text}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
49
site/dawdle/templates/dawdle/player_items.html
Normal file
49
site/dawdle/templates/dawdle/player_items.html
Normal file
|
@ -0,0 +1,49 @@
|
|||
{% extends "dawdle/base.html" %}
|
||||
{% load static %}
|
||||
{% load duration %}
|
||||
{% load alignment %}
|
||||
|
||||
{% block content %}
|
||||
<div class="contentbox">
|
||||
<h1>Players</h1>
|
||||
|
||||
<table id="playerlist">
|
||||
<thead>
|
||||
<tr><td>Rank</td><td style="width:30rem">Name</td><td>Time to next level</td></tr>
|
||||
</thead><tbody>
|
||||
{% for p in object_list %}
|
||||
<tr class="{%if p.online %}online{% else %}offline{% endif %}">
|
||||
<td style="text-align:right">#{{forloop.counter}}</td>
|
||||
<td><img style="height: 0.7em; width: 0.7em"
|
||||
src="{% if p.online %}
|
||||
{% static 'dawdle/Blue_sphere.svg' %}
|
||||
{% else %}
|
||||
{% static 'dawdle/Gray_sphere.svg' %}
|
||||
{% endif %}"
|
||||
alt="{% if p.online %}
|
||||
On
|
||||
{% else %}
|
||||
Off
|
||||
{% endif %}">
|
||||
<a href="{% url 'dawdle:player-detail' p.name %}">{{p.name}},
|
||||
the <span class="{{p.alignment|alignment}}-align">{{p.alignment|alignment}}</span>
|
||||
level {{p.level}} {{p.cclass}}</a>
|
||||
</td>
|
||||
<td style="text-align: right"2>{{p.nextlvl|duration}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody></table>
|
||||
|
||||
<div id="pmap-container">
|
||||
<img id="pmap" src="{% url 'dawdle:map' %}" alt="Player Map" usemap="#playermap">
|
||||
</div>
|
||||
<map name="playermap">
|
||||
{% for p in object_list %}
|
||||
<area shape="circle"
|
||||
coords="{{p.posx}},{{p.posy}},3"
|
||||
alt="{{p.name}}"
|
||||
href="{% url 'dawdle:player-detail' p.name %}">
|
||||
{% endfor %}
|
||||
</map>
|
||||
</div>
|
||||
{% endblock %}
|
53
site/dawdle/templates/dawdle/player_list.html
Normal file
53
site/dawdle/templates/dawdle/player_list.html
Normal file
|
@ -0,0 +1,53 @@
|
|||
{% extends "dawdle/base.html" %}
|
||||
{% load static %}
|
||||
{% load duration %}
|
||||
{% load alignment %}
|
||||
{% load split %}
|
||||
{% load lvlsum %}
|
||||
{% block content %}
|
||||
<div class="contentbox">
|
||||
<h1>Players</h1>
|
||||
|
||||
<table id="playerlist">
|
||||
<thead>
|
||||
<tr><td>Rank</td><td style="width:30rem">Name</td><td>Time to next level</td><td>IRC User</td><td>Level Total</td></tr>
|
||||
</thead><tbody>
|
||||
{% for p in object_list %}
|
||||
<tr class="{%if p.online %}online{% else %}offline{% endif %}">
|
||||
<td style="text-align:right">#{{forloop.counter}}</td>
|
||||
<td><img style="height: 0.7em; width: 0.7em"
|
||||
src="{% if p.online %}
|
||||
{% static 'dawdle/Blue_sphere.svg' %}
|
||||
{% else %}
|
||||
{% static 'dawdle/Gray_sphere.svg' %}
|
||||
{% endif %}"
|
||||
alt="{% if p.online %}
|
||||
On
|
||||
{% else %}
|
||||
Off
|
||||
{% endif %}">
|
||||
<a href="{% url 'dawdle:player-detail' p.name %}">{{p.name}},
|
||||
the <span class="{{p.alignment|alignment}}-align">{{p.alignment|alignment}}</span>
|
||||
level {{p.level}} {{p.cclass}}</a>
|
||||
</td>
|
||||
<td style="text-align: right"2>{{p.nextlvl|duration}}</td>
|
||||
<td><i>{{ p.userhost|split:"!"|first}}</i></td>
|
||||
<td style="text-align: right"2>{{p.item_set | lvlsum}}</td>
|
||||
{#{% for i in p.item_set.aggregate(Sum('level'))['level__sum'] %}#}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody></table>
|
||||
|
||||
<div id="pmap-container">
|
||||
<img id="pmap" src="{% url 'dawdle:map' %}" alt="Player Map" usemap="#playermap">
|
||||
</div>
|
||||
<map name="playermap">
|
||||
{% for p in object_list %}
|
||||
<area shape="circle"
|
||||
coords="{{p.posx}},{{p.posy}},3"
|
||||
alt="{{p.name}}"
|
||||
href="{% url 'dawdle:player-detail' p.name %}">
|
||||
{% endfor %}
|
||||
</map>
|
||||
</div>
|
||||
{% endblock %}
|
63
site/dawdle/templates/dawdle/quest.html
Normal file
63
site/dawdle/templates/dawdle/quest.html
Normal file
|
@ -0,0 +1,63 @@
|
|||
{% extends "dawdle/base.html" %}
|
||||
{% load static %}
|
||||
{% load duration %}
|
||||
{% load alignment %}
|
||||
|
||||
{% block content %}
|
||||
<div class="contentbox">
|
||||
{% if quest and quest.mode != 0%}
|
||||
<h1>The sacred quest to {{quest.text}}</h1>
|
||||
|
||||
{% if quest.mode == 1 %}
|
||||
<p>Time left in quest: {{qtime_remaining|duration}}</p>
|
||||
{% elif quest.stage == 1 %}
|
||||
<p>Questor's next destination: ({{quest.dest1x}}, {{quest.dest1y}})</p>
|
||||
{% else %}
|
||||
<p>Questor's next destination: ({{quest.dest2x}}, {{quest.dest2y}})</p>
|
||||
{% endif %}
|
||||
|
||||
<table id="playerlist">
|
||||
<thead>
|
||||
<tr><td style="width:30rem">Questor</td><td>Time to next level</td></tr>
|
||||
</thead><tbody>
|
||||
{% for p in questors %}
|
||||
<tr class="{%if p.online %}online{% else %}offline{% endif %}">
|
||||
<td><img style="height: 0.7em; width: 0.7em"
|
||||
src="{% if p.online %}
|
||||
{% static 'dawdle/Blue_sphere.svg' %}
|
||||
{% else %}
|
||||
{% static 'dawdle/Gray_sphere.svg' %}
|
||||
{% endif %}"
|
||||
alt="{% if p.online %}
|
||||
On
|
||||
{% else %}
|
||||
Off
|
||||
{% endif %}">
|
||||
<a href="{% url 'dawdle:player-detail' p.name %}">{{p.name}},
|
||||
the <span class="{{p.alignment|alignment}}-align">{{p.alignment|alignment}}</span>
|
||||
level {{p.level}} {{p.cclass}}</a>
|
||||
</td>
|
||||
<td style="text-align: right"2>{{p.nextlvl|duration}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody></table>
|
||||
|
||||
<div id="pmap-container">
|
||||
<img id="pmap" src="{% url 'dawdle:map' %}" alt="Player Map" usemap="#playermap">
|
||||
</div>
|
||||
<map name="playermap">
|
||||
{% for p in questors %}
|
||||
<area shape="circle"
|
||||
coords="{{p.posx}},{{p.posy}},3"
|
||||
alt="{{p.name}}"
|
||||
href="{% url 'dawdle:player-detail' p.name %}">
|
||||
{% endfor %}
|
||||
</map>
|
||||
|
||||
{% else %}
|
||||
<h1>No current quest</h1>
|
||||
<p>There is no decreed quest at this time.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
16
site/dawdle/templates/dawdle/timelapse.html
Normal file
16
site/dawdle/templates/dawdle/timelapse.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
{% extends "dawdle/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="contentbox">
|
||||
<h1>Timelapse of movements</h1>
|
||||
<p>A Timelapse of everyone since:
|
||||
Mon, Feb 26 00:05:24 UTC 2024 </p>
|
||||
{% with timelapse='Wed Feb 28 04:01:29 UTC 2024' %}
|
||||
<p> Last Generated: {{ timelapse }} </p>
|
||||
{% endwith %}
|
||||
<video controls width="500"> <source src="/static/map.mp4" type="video/mp4" />
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
0
site/dawdle/templatetags/__init__.py
Normal file
0
site/dawdle/templatetags/__init__.py
Normal file
8
site/dawdle/templatetags/alignment.py
Normal file
8
site/dawdle/templatetags/alignment.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
def alignment(align):
|
||||
return {"g": "good", "n":"neutral", "e":"evil"}[align]
|
||||
|
||||
register.filter(alignment)
|
17
site/dawdle/templatetags/duration.py
Normal file
17
site/dawdle/templatetags/duration.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
def plural(num, singlestr='', pluralstr='s'):
|
||||
"""Return singlestr when num is 1, otherwise pluralstr."""
|
||||
if num == 1:
|
||||
return singlestr
|
||||
return pluralstr
|
||||
|
||||
def duration(secs):
|
||||
d, secs = int(secs / 86400), secs % 86400
|
||||
h, secs = int(secs / 3600), secs % 3600
|
||||
m, secs = int(secs / 60), secs % 60
|
||||
return f"{d} day{plural(d)}, {h:02d}:{m:02d}:{int(secs):02d}"
|
||||
|
||||
register.filter(duration)
|
9
site/dawdle/templatetags/lvlsum.py
Normal file
9
site/dawdle/templatetags/lvlsum.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from django import template
|
||||
from django.db.models import Sum
|
||||
|
||||
register = template.Library()
|
||||
|
||||
def lvlsum(values):
|
||||
return values.aggregate(Sum(('level')))['level__sum']
|
||||
|
||||
register.filter(lvlsum)
|
10
site/dawdle/templatetags/split.py
Normal file
10
site/dawdle/templatetags/split.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
def split(value, key):
|
||||
|
||||
value.split("key")
|
||||
return value.split(key)
|
||||
|
||||
register.filter(split)
|
3
site/dawdle/tests.py
Normal file
3
site/dawdle/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
17
site/dawdle/urls.py
Normal file
17
site/dawdle/urls.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
from dawdle import views
|
||||
|
||||
|
||||
app_name = 'dawdle'
|
||||
urlpatterns = [
|
||||
path('', views.FrontView.as_view(), name='front'),
|
||||
path('about', views.AboutView.as_view(), name='about'),
|
||||
path('map', views.MapView.as_view(), name='map'),
|
||||
path('map/<path:player>', views.MapView.as_view(), name='player-map'),
|
||||
path('map/quest', views.MapView.as_view(), {'quest': 'quest'}, name='quest-map'),
|
||||
path('players', views.PlayerListView.as_view(), name='player-list'),
|
||||
path('player/<path:pk>', views.PlayerDetailView.as_view(), name='player-detail'),
|
||||
path('quest', views.QuestView.as_view(), name='quest'),
|
||||
path('timelapse', views.Timelapse.as_view(), name='timelapse'),
|
||||
]
|
152
site/dawdle/views.py
Normal file
152
site/dawdle/views.py
Normal file
|
@ -0,0 +1,152 @@
|
|||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
from xml.dom import minidom
|
||||
|
||||
from django.db.models import Sum
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.views import generic, View
|
||||
from django.views.decorators.vary import vary_on_headers
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from .models import Player, Item, Quest
|
||||
from project.settings import BASE_DIR
|
||||
|
||||
class FrontView(generic.TemplateView):
|
||||
template_name = "dawdle/front.html"
|
||||
|
||||
|
||||
class AboutView(generic.TemplateView):
|
||||
template_name = "dawdle/about.html"
|
||||
|
||||
|
||||
class MapView(View):
|
||||
|
||||
def _colorplayer(self, player, questors):
|
||||
if player.name in questors:
|
||||
return (0x00, 0xff, 0xe9), (0x80, 0xbb, 0xff)
|
||||
if player.online:
|
||||
return (0x00, 0x1e, 0xe9), (0x80, 0xbb, 0xff)
|
||||
return (0xaa, 0xaa, 0xaa), (0xee, 0xee, 0xee)
|
||||
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
base_map = Image.open(os.path.join(BASE_DIR, "dawdle/static/dawdle/map.png"))
|
||||
full_map = base_map.copy()
|
||||
draw = ImageDraw.Draw(full_map)
|
||||
|
||||
questors = []
|
||||
q = Quest.objects.get()
|
||||
if q and q.mode != 0:
|
||||
questors = [q.p1, q.p2, q.p3, q.p4]
|
||||
|
||||
if 'player' in kwargs:
|
||||
pquery = Player.objects.filter(name=kwargs['player'])
|
||||
dotsize = 5
|
||||
elif 'quest' in kwargs and questors:
|
||||
pquery = Player.objects.filter(name__in=questors)
|
||||
else:
|
||||
pquery = Player.objects
|
||||
dotsize = 3
|
||||
|
||||
if q and q.mode == 2:
|
||||
if q.stage == 1:
|
||||
draw.ellipse([q.dest1x-dotsize, q.dest1y-dotsize, q.dest1x+dotsize, q.dest1y+dotsize], fill=(0xff, 0xff, 0x00))
|
||||
else:
|
||||
draw.ellipse([q.dest2x-dotsize, q.dest2y-dotsize, q.dest2x+dotsize, q.dest2y+dotsize], fill=(0xff, 0xff, 0x00))
|
||||
|
||||
for p in pquery.all():
|
||||
fillcolor, strokecolor = self._colorplayer(p, questors)
|
||||
draw.ellipse([p.posx-dotsize, p.posy-dotsize, p.posx+dotsize, p.posy+dotsize],
|
||||
outline=strokecolor,
|
||||
fill=fillcolor)
|
||||
|
||||
map_bytes = io.BytesIO()
|
||||
full_map.save(map_bytes, format="png")
|
||||
return HttpResponse(map_bytes.getvalue(), content_type='image/png')
|
||||
|
||||
|
||||
class PlayerListView(generic.ListView):
|
||||
model = Player
|
||||
queryset = Player.objects.order_by('-level', 'nextlvl')
|
||||
|
||||
#def get_context_data(self, **kwargs):
|
||||
# #context = super().get_context_data(**kwargs)
|
||||
# context['players'] = p.order_by('-level', 'nextlvl')
|
||||
# context['total_items'] = p.item_set.aggregate(Sum('level'))['level__sum']
|
||||
# return context
|
||||
|
||||
|
||||
class PlayerDetailView(generic.DetailView):
|
||||
model = Player
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
p = self.object
|
||||
context['total_penalties'] = sum([p.penkick, p.penpart, p.penquit, p.pendropped, p.pennick, p.penmessage, p.penlogout, p.penquest])
|
||||
context['total_items'] = p.item_set.aggregate(Sum('level'))['level__sum']
|
||||
return context
|
||||
|
||||
|
||||
class PlayerDumpView(generic.ListView):
|
||||
|
||||
@vary_on_headers('Accept')
|
||||
def get(self, request, *args, **kwargs):
|
||||
response = HttpResponse()
|
||||
plist = []
|
||||
for p in Player.objects.all():
|
||||
plist.append({
|
||||
"name": p.name,
|
||||
"cclass": p.cclass,
|
||||
"idled": p.idled,
|
||||
"level": p.level,
|
||||
"nick": p.nick,
|
||||
"userhost": p.userhost,
|
||||
"email": p.email,
|
||||
})
|
||||
if request.accepts('text/plain') or request.accepts('text/csv'):
|
||||
response.content_type = 'text/csv'
|
||||
writer = csv.DictWriter(response, ('name', 'cclass', 'idled', 'level', 'nick', 'userhost', 'email'))
|
||||
writer.writeheader()
|
||||
for p in plist:
|
||||
writer.writerow(p)
|
||||
elif request.accepts('application/json'):
|
||||
response.content_type = 'application/json'
|
||||
json.dump(plist, response, separators=(',',':'))
|
||||
elif request.accepts('application/xml'):
|
||||
response.content_type = 'application/xml'
|
||||
root = minidom.Document()
|
||||
players_el = root.createElement('players')
|
||||
root.appendChild(players_el)
|
||||
for p in plist:
|
||||
el = root.createElement('player')
|
||||
for k,v in p.items():
|
||||
el.setAttribute(k, str(v))
|
||||
players_el.appendChild(el)
|
||||
root.writexml(response)
|
||||
return response
|
||||
|
||||
|
||||
class QuestView(generic.TemplateView):
|
||||
template_name = "dawdle/quest.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
try:
|
||||
quest = Quest.objects.get()
|
||||
questors = Player.objects.filter(name__in=[quest.p1,quest.p2,quest.p3,quest.p4]).all()
|
||||
context['quest'] = quest
|
||||
context['questors'] = questors
|
||||
context['qtime_remaining'] = quest.qtime - time.time()
|
||||
except Quest.DoesNotExist:
|
||||
context['quest'] = None
|
||||
return context
|
||||
|
||||
class Timelapse(generic.TemplateView):
|
||||
template_name = "dawdle/timelapse.html"
|
21
site/manage.py
Executable file
21
site/manage.py
Executable file
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
0
site/project/__init__.py
Normal file
0
site/project/__init__.py
Normal file
129
site/project/settings.py
Normal file
129
site/project/settings.py
Normal file
|
@ -0,0 +1,129 @@
|
|||
"""
|
||||
Django settings for dawdle project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 2.2.12.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/2.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/2.2/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'GKF+J8ifZXIcW0sMP4+EOxD4DHTAZSN0FXbw/l9qP8Zchy1Xd1Z5wPbIEhOj'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = False
|
||||
|
||||
ALLOWED_HOSTS = ['irc.l3.lv']
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'dawdle.apps.DawdleConfig',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'project.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'project.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
},
|
||||
'game': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'game.sqlite3'),
|
||||
}
|
||||
}
|
||||
|
||||
# Routers
|
||||
# https://docs.djangoproject.com/en/3.2/topics/db/multi-db/
|
||||
DATABASE_ROUTERS = ['dawdle.router.DawdleRouter']
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/2.2/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
22
site/project/urls.py
Normal file
22
site/project/urls.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
"""dawdle URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/2.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
path('', include('dawdle.urls')),
|
||||
path('admin/', admin.site.urls),
|
||||
]
|
16
site/project/wsgi.py
Normal file
16
site/project/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for dawdle project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
|
||||
|
||||
application = get_wsgi_application()
|
27
update.sh
Normal file
27
update.sh
Normal file
|
@ -0,0 +1,27 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -x
|
||||
set -e
|
||||
|
||||
DIR="$(readlink -f $(dirname $0))"
|
||||
echo "Using $DIR as dawdlerpg directory"
|
||||
|
||||
echo "Updating source tree"
|
||||
cd "$DIR"
|
||||
git fetch -a
|
||||
git stash
|
||||
git merge origin/main
|
||||
git stash pop
|
||||
|
||||
echo "Migrating db"
|
||||
cd "$DIR/site"
|
||||
./manage.py --database=default
|
||||
./manage.py --database=game
|
||||
./manage collectstatic --no-input
|
||||
cd "$DIR"
|
||||
|
||||
chown -R www-data:www-data "$DIR"
|
||||
|
||||
echo "Restart bot and website"
|
||||
systemctl restart dawdlerpg
|
||||
systemctl restart uwsgi
|
Loading…
Reference in a new issue