forked from pwgen2155/dawdle
Compare commits
40 commits
Author | SHA1 | Date | |
---|---|---|---|
249a16c3b5 | |||
e84dfe960c | |||
60228715fc | |||
9316e327c1 | |||
125adf6425 | |||
e55b031626 | |||
1a51747e08 | |||
pwgen2155 | d7cece2a58 | ||
pwgen2155 | 409511eb42 | ||
pwgen2155 | 5e448a6ff1 | ||
pwgen2155 | 9515c878a3 | ||
pwgen2155 | e00e4e5526 | ||
pwgen2155 | 2a26376364 | ||
41d72092a0 | |||
4767f2b1bf | |||
bf1a72323e | |||
f11e2aaf62 | |||
ea91606c63 | |||
1370f823ab | |||
fbc3261f45 | |||
ca05497fa0 | |||
7cba240eae | |||
pwgen2155 | 5d7374509e | ||
pwgen2155 | 66c9cb445d | ||
pwgen2155 | 42b51d25be | ||
Mediaman | e0e394462d | ||
pwgen2155 | fac81d8a0d | ||
pwgen2155 | b2b517a889 | ||
60128f45cc | |||
3b5ce16ced | |||
c8de3c2070 | |||
pwgen2155 | d6153956bf | ||
pwgen2155 | c6b2b8edba | ||
pwgen2155 | 6b2c711ee6 | ||
Mediaman | 852caf73e5 | ||
730c96191a | |||
f7dd063aab | |||
pwgen2155 | e320d01ca0 | ||
pwgen2155 | f28827d99d | ||
DataHoarder | d4c391bb38 |
|
@ -36,7 +36,7 @@ steps:
|
|||
key:
|
||||
from_secret: ssh_key
|
||||
script:
|
||||
- git fetch && git checkout main --force && git pull && sudo docker compose up -d --build
|
||||
- git fetch && git checkout main --force && git pull --force && sudo make prod
|
||||
depends_on:
|
||||
- test
|
||||
trigger:
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
FROM python:3.11
|
||||
FROM python:3.11@sha256:db07fba48daaf1c68c03676aadc73866414d25b4c278029f9873c784517613bf
|
||||
|
||||
COPY requirements.txt /data/
|
||||
RUN pip install --no-cache-dir -r /data/requirements.txt
|
||||
|
||||
VOLUME /data
|
||||
|
||||
CMD [ "python", "/data/dawdle.py", "/data/data/dawdle.conf"]
|
||||
|
|
12
Makefile
Normal file
12
Makefile
Normal file
|
@ -0,0 +1,12 @@
|
|||
SHELL: /bin/bash
|
||||
|
||||
phony: dev
|
||||
dev:
|
||||
@docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build
|
||||
|
||||
phony: prod
|
||||
prod:
|
||||
@docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
|
||||
|
||||
down:
|
||||
@docker compose down
|
58
README.md
58
README.md
|
@ -4,35 +4,45 @@ FriendsRPG is a modified version of DawdleRPG (an IdleRPG clone) written in Pyth
|
|||
|
||||
## 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.
|
||||
- Install python-django and docker
|
||||
- Copy dawdle.conf.example to af/dawdle.conf
|
||||
- Edit af/dawdle.conf to configure the bot
|
||||
- Run the following commands:
|
||||
|
||||
## 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>
|
||||
``` sh
|
||||
cd site
|
||||
./manage.py migrate --database=default
|
||||
./manage.py migrate --database=game
|
||||
./manage.py collectstatic --no-input
|
||||
cd ..
|
||||
./dawdle.py --setup af/dawdle.conf
|
||||
```
|
||||
|
||||
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.
|
||||
### Development:
|
||||
```
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build
|
||||
OR
|
||||
make dev
|
||||
```
|
||||
|
||||
## Migrating from IdleRPG
|
||||
Web is available via: http://localhost:8142
|
||||
|
||||
DawdleRPG is capable of being a drop-in replacement.
|
||||
### Production:
|
||||
```
|
||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
|
||||
OR
|
||||
make prod
|
||||
```
|
||||
|
||||
- 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 DawdleRPG
|
||||
- No longer a drop-in for IdleRPG and uses Sqlite database as a backend
|
||||
- Special items and Events are Anime themed
|
||||
- Mounts arrive at Level 45
|
||||
- Chatting is allowed in the channel
|
||||
- Sending commands to the bot via the #channel can be done via ! (ie: !status)
|
||||
- Debug logging of all randomness for "proof"
|
||||
- /timelapse via the web interface allows for a fun little gimmick (hard coded for us)
|
||||
- /players show IRCUser and other info at a glance
|
||||
|
||||
## Differences from IdleRPG
|
||||
|
||||
|
@ -40,7 +50,7 @@ need to replace them with the `-o key=value` option.
|
|||
- Output throttling allows configurable rate over a period.
|
||||
- Long messages are word wrapped.
|
||||
- Logging can be set to different levels.
|
||||
- Better IRC protocol support.
|
||||
- Better IRC protocol support. (SSL!)
|
||||
- More game numbers are configurable.
|
||||
- Quest pathfinding is much more efficient.
|
||||
- Fights caused by map collisions have chance of finding item.
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
# 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;
|
||||
}
|
||||
}
|
|
@ -1,31 +1,64 @@
|
|||
# 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 qt werewolf girl
|
||||
C kept asking for invites to the secreet club
|
||||
C kept asking for invites to the secret club
|
||||
C posted too many times in help
|
||||
C DIED... and isekai'd
|
||||
C stopped to smell the roses, byt was eaten by the Man Eating Roses
|
||||
C tried to bribe IdleFriend
|
||||
C re-enacted Lord of the Rings for the local orphanage, all twelve hours...
|
||||
C Left their boots in the pigsty and now they reek worse than the pigs
|
||||
C found out they were pregnant, with Cats
|
||||
C was divided by zero and caused the bot to restart
|
||||
C did nothing
|
||||
C panic[cpu0]/thread #ffffff01B00B1E5: dtrace: You're on your own...
|
||||
C clicked a link that referenced /gif/ >.>
|
||||
C rolled a 1* waifu
|
||||
C rolled a duplicate 4* waifu
|
||||
C uploaded 100 screenshots, of music
|
||||
C is dazed and confused, but trying to continue
|
||||
|
||||
# Godsends - these subtract from a player's time to level
|
||||
G was sprinkled with fairy dust, or something...
|
||||
G was sprinkled with fairy dust, or something that looked like fairy dust...
|
||||
G chose to bathed before seeing their husbando
|
||||
G found an unreleased Anime BD
|
||||
G found the glorious wonders of linux
|
||||
G met a friendly AB user, in the toilet
|
||||
G met a friendly AB user, in the toilet ^.^
|
||||
G rolled a 5* waifu
|
||||
G rewrote this Godsends in Go instead of Perl
|
||||
G bribed IdleFriend without AF knowing
|
||||
G fixed the RNG by using weighted die while no-one was looking
|
||||
G Installed a new LaserDisc player
|
||||
G uninstalled their CD+RWx16 player
|
||||
G found a way to use a HDMI "splitter"
|
||||
G made a breakthrough in one of life great mysteries
|
||||
G re-enacted the Lord of the Rings for the local orphanage, the cliff-notes version.
|
||||
G has discovered a perfect anison flac that was not uploaded to AB
|
||||
G bought vinyl music to the land and became an audiophile
|
||||
G managed to upload 1000 screenshots
|
||||
G was given a new mount by the name of Jigsy who was a masochist
|
||||
G understands the AnimeFriends source code and migrated some legacy code
|
||||
|
||||
# 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 girl from Grefog
|
||||
|
||||
# Quest mode 1 - these are simply timed quests. (are on a quest to ...)
|
||||
Q1 free the AB domain from the evil registrar. The 4 heroes must venture fourth without delay
|
||||
Q1 deliver a priceless scroll to the library of Chinese Anime, where they will upscale it for you
|
||||
Q1 rescue an imprisoned precure girl from the Fog of Grey
|
||||
Q1 find the hidden pics of famous Vtubers
|
||||
Q1 unearth the secret of the Twin Peeks (.)(.)
|
||||
Q1 throwing a party for Animefield
|
||||
Q1 decipher the fabled tablet of AnimeFriends
|
||||
Q1 throwing a party for the Field of Anime
|
||||
Q1 decipher the fabled tablet of 'AnimeFriends' Legends say it has more cultural significance than an old pair of pantsu
|
||||
Q1 stay connected longer than the bot (it isn't hard)
|
||||
Q1 walk around aimlessly waiting for new anime
|
||||
Q1 smell every flower of the land before spring ends
|
||||
Q1 mirror all of this week's airing uploads onto I2P
|
||||
|
||||
# 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 BakaBT to AnimeBytes
|
||||
Q2 304 269 70 417 secretly follow a Heroes part without being invited to join them saving the world
|
||||
Q2 304 269 70 417 secretly follow a Heroes party and help them save the world without being seen!
|
||||
Q2 447 432 359 318 travel to a tiny country village in Hokkaido to start a rebellion of Anime
|
||||
Q2 326 31 246 133 must spring trapped girls from the dungeons of Altis and return them to their rightful homes
|
||||
Q2 1 1 444 444 search for Ikargua's hammer and return it to him in one piece, or multiple pieces, or to just give him the shattered remains of a hammer, either is good.
|
||||
Q2 1 1 345 345 search for Ikargua's hammer and return it to him in one piece, or multiple pieces, or to just give him the shattered remains of a hammer, either is good.
|
||||
Q2 244 265 90 277 find the greatest coffee in the land. First to the Competition of Bean, then to the Grinder of Oblivion
|
||||
Q2 328 261 26 427 attempt the first-class mage examination
|
|
@ -11,13 +11,13 @@
|
|||
#die
|
||||
|
||||
# Superuser that cannot be DELADMINed.
|
||||
owner Animefield
|
||||
owner YourNameHere
|
||||
|
||||
# Server name:port.
|
||||
server irc.animefriends.moe:7000
|
||||
|
||||
# Bot's nickname
|
||||
botnick IdleFriend
|
||||
botnick ChaosFriend
|
||||
|
||||
# Bot's username
|
||||
botuser Idle
|
||||
|
@ -52,7 +52,7 @@ helpurl http://example.com/
|
|||
admincommurl http://example.com/admincomms.txt
|
||||
|
||||
# URL where users can reach the online quest map, if available.
|
||||
mapurl http://example.com/quest.php
|
||||
mapurl http://example.com/quest
|
||||
|
||||
# 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
|
||||
|
@ -98,7 +98,7 @@ penquest 15
|
|||
pennick 30
|
||||
|
||||
# Penalty for sending a message - this is per character!
|
||||
penmessage 1
|
||||
penmessage 0
|
||||
|
||||
# Penalty for leaving the channel.
|
||||
penpart 200
|
||||
|
@ -128,20 +128,20 @@ good_battle_pct 110
|
|||
evil_battle_pct 90
|
||||
|
||||
# Kick/ban users who mention a URL within seconds of joining channel.
|
||||
doban on
|
||||
doban off
|
||||
|
||||
# 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_min 6h
|
||||
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
|
||||
quest_min_login 3600
|
||||
|
||||
# These are URL hosts which are okay to mention early in channel. Multiples are fine.
|
||||
# okurl example.com
|
||||
|
@ -185,30 +185,14 @@ itemcolor yellow
|
|||
# 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
|
||||
dbfile ../site/game.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
|
16
dawdle.py
16
dawdle.py
|
@ -35,17 +35,15 @@ 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: ")
|
||||
pname = conf.get("owner")
|
||||
pclass = "Hidden Master"
|
||||
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: ")
|
||||
ppass = input("Password for owner account: ")
|
||||
finally:
|
||||
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, old)
|
||||
|
||||
|
@ -136,13 +134,7 @@ def start_bot() -> None:
|
|||
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)
|
||||
store = bot.Sqlite3GameStorage(bot.datapath(conf.get("dbfile")))
|
||||
|
||||
if conf.get("setup"):
|
||||
if store.exists():
|
||||
|
|
321
dawdle/bot.py
321
dawdle/bot.py
|
@ -390,250 +390,6 @@ class GameStorage(object):
|
|||
raise NotImplementedError
|
||||
|
||||
|
||||
class IdleRPGGameStorage(GameStorage):
|
||||
"""Implements a GameStorage compatible with the IdleRPG db.
|
||||
|
||||
Since the IdleRPG db is a tsv file, we can't reasonably update it
|
||||
piecemeal, so the playerbase is cached during reads, and written
|
||||
in its entirety on writes. The player objects are shared between
|
||||
the cache and the GameDB.
|
||||
|
||||
"""
|
||||
|
||||
IRPG_FIELDS = ["username", "pass", "is admin", "level", "class", "next ttl", "nick", "userhost", "online", "idled", "x pos", "y pos", "pen_mesg", "pen_nick", "pen_part", "pen_kick", "pen_quit", "pen_quest", "pen_logout", "created", "last login", "amulet", "charm", "helm", "boots", "gloves", "ring", "leggings", "shield", "tunic", "weapon", "alignment"]
|
||||
IRPG_FIELD_COUNT = len(IRPG_FIELDS)
|
||||
|
||||
# Instead of names, idlerpg decided to tack on codes to the number.
|
||||
ITEMCODES = {
|
||||
"Mattt's Omniscience Grand Crown":"a",
|
||||
"Juliet's Glorious Ring of Sparkliness":"h",
|
||||
"Res0's Protectorate Plate Mail":"b",
|
||||
"Dwyn's Storm Magic Amulet":"c",
|
||||
"Jotun's Fury Colossal Sword":"d",
|
||||
"Drdink's Cane of Blind Rage":"e",
|
||||
"Mrquick's Magical Boots of Swiftness":"f",
|
||||
"Jeff's Cluehammer of Doom":"g"
|
||||
}
|
||||
|
||||
_dbpath: str
|
||||
_cache: List[Player]
|
||||
|
||||
def _code_to_item(self, s: str) -> Tuple[int, str]:
|
||||
"""Converts an IdleRPG item code to a tuple of (level, name)."""
|
||||
match = re.match(r"(\d+)(.?)", s)
|
||||
if not match:
|
||||
log.error(f"invalid item code: {s}")
|
||||
return (int(s), "")
|
||||
lvl = int(match[1])
|
||||
if match[2]:
|
||||
return (lvl, [k for k,v in IdleRPGGameStorage.ITEMCODES.items() if v == match[2]][0])
|
||||
return (lvl, "")
|
||||
|
||||
|
||||
def _item_to_code(self, level: int, name: str) -> str:
|
||||
"""Converts an item level and name to an IdleRPG item code."""
|
||||
return f"{level}{IdleRPGGameStorage.ITEMCODES.get(name, '')}"
|
||||
|
||||
|
||||
def __init__(self, dbpath: str):
|
||||
self._dbpath = dbpath
|
||||
self._cache = []
|
||||
|
||||
|
||||
def create(self) -> None:
|
||||
"""Creates a new IdleRPG db."""
|
||||
self.write([])
|
||||
|
||||
|
||||
def exists(self) -> bool:
|
||||
"""Returns true if the db file exists."""
|
||||
return os.path.exists(self._dbpath)
|
||||
|
||||
|
||||
def close(self) -> None:
|
||||
"""Does nothing - no need to close the idlerpg file."""
|
||||
pass
|
||||
|
||||
|
||||
def backup(self) -> None:
|
||||
"""Backs up database to a directory."""
|
||||
os.makedirs(datapath(conf.get("backupdir")), exist_ok=True)
|
||||
backup_path = os.path.join(datapath(conf.get("backupdir")),
|
||||
f"{time.strftime('%Y-%m-%dT%H:%M:%S')}-{os.path.basename(conf.get('dbfile'))}")
|
||||
shutil.copyfile(self._dbpath, backup_path)
|
||||
|
||||
|
||||
def _player_to_record(self, p: Player) -> str:
|
||||
"""Converts the player to an IdleRPG db record."""
|
||||
return "\t".join([
|
||||
p.name,
|
||||
p.pw,
|
||||
"1" if p.isadmin else "0",
|
||||
str(p.level),
|
||||
p.cclass,
|
||||
str(p.nextlvl),
|
||||
p.nick,
|
||||
p.userhost,
|
||||
"1" if p.online else "0",
|
||||
str(p.idled),
|
||||
str(p.posx),
|
||||
str(p.posy),
|
||||
str(p.penmessage),
|
||||
str(p.pennick),
|
||||
str(p.penpart),
|
||||
str(p.penkick),
|
||||
str(p.penquit + p.pendropped),
|
||||
str(p.penquest),
|
||||
str(p.penlogout),
|
||||
str(int(p.created.timestamp())),
|
||||
str(int(p.lastlogin.timestamp())),
|
||||
self._item_to_code(p.item_level("amulet"), p.item_name("amulet")),
|
||||
self._item_to_code(p.item_level("charm"), p.item_name("charm")),
|
||||
self._item_to_code(p.item_level("helm"), p.item_name("helm")),
|
||||
self._item_to_code(p.item_level("boots"), p.item_name("boots")),
|
||||
self._item_to_code(p.item_level("gloves"), p.item_name("gloves")),
|
||||
self._item_to_code(p.item_level("ring"), p.item_name("ring")),
|
||||
self._item_to_code(p.item_level("leggings"), p.item_name("leggings")),
|
||||
self._item_to_code(p.item_level("shield"), p.item_name("shield")),
|
||||
self._item_to_code(p.item_level("tunic"), p.item_name("tunic")),
|
||||
self._item_to_code(p.item_level("weapon"), p.item_name("weapon")),
|
||||
str(p.alignment)
|
||||
]) + "\n"
|
||||
|
||||
|
||||
def readall(self) -> Iterable[Player]:
|
||||
"""Reads all the players into a dict."""
|
||||
self._cache = []
|
||||
with open(self._dbpath) as inf:
|
||||
for line in inf.readlines():
|
||||
if re.match(r'\s*(?:#|$)', line):
|
||||
continue
|
||||
parts = line.rstrip().split("\t")
|
||||
if len(parts) != IdleRPGGameStorage.IRPG_FIELD_COUNT:
|
||||
log.critical("line corrupt in player db - %d fields: %s", len(parts), repr(line))
|
||||
sys.exit(-1)
|
||||
|
||||
# This makes a mapping from irpg field to player field.
|
||||
d = dict(zip(["name", "pw", "isadmin", "level", "cclass", "nextlvl", "nick", "userhost", "online", "idled", "posx", "posy", "penmessage", "pennick", "penpart", "penkick", "penquit", "penquest", "penlogout", "created", "lastlogin", "amulet", "charm", "helm", "boots", "gloves", "ring", "leggings", "shield", "tunic", "weapon", "alignment"], parts))
|
||||
# convert items
|
||||
items = dict()
|
||||
for i in Item.SLOTS:
|
||||
level, name = self._code_to_item(d[i])
|
||||
if level > 0:
|
||||
items[i] = Item(level, name)
|
||||
del d[i]
|
||||
|
||||
# convert int fields
|
||||
for f in ["level", "nextlvl", "idled", "posx", "posy", "penmessage", "pennick", "penpart", "penkick", "penquit", "penquest", "penlogout", "created", "lastlogin"]:
|
||||
d[f] = round(float(d[f])) # type:ignore
|
||||
# convert boolean fields
|
||||
for f in ["isadmin", "online"]:
|
||||
d[f] = (d[f] == '1') # type:ignore
|
||||
|
||||
d['email'] = "" # needed to pass testing
|
||||
|
||||
d['pendropped'] = 0 # type: ignore
|
||||
|
||||
d['created'] = datetime.datetime.fromtimestamp(int(d['created'])) # type:ignore
|
||||
d['lastlogin'] = datetime.datetime.fromtimestamp(int(d['lastlogin'])) # type:ignore
|
||||
|
||||
p = Player.from_dict(d)
|
||||
p.items = items
|
||||
self._cache.append(p)
|
||||
return self._cache
|
||||
|
||||
|
||||
def write(self, players: Iterable[Player]) -> None:
|
||||
"""Writes players to an IdleRPG db."""
|
||||
with open(self._dbpath, "w") as ouf:
|
||||
ouf.write("# " + "\t".join(IdleRPGGameStorage.IRPG_FIELDS) + "\n")
|
||||
for p in self._cache:
|
||||
ouf.write(self._player_to_record(p))
|
||||
|
||||
|
||||
def new(self, p: Player) -> None:
|
||||
"""Creates a new player in the db."""
|
||||
self._cache.append(p)
|
||||
with open(self._dbpath, "a") as ouf:
|
||||
ouf.write(self._player_to_record(p))
|
||||
|
||||
|
||||
def rename_player(self, old_name: str, new_name: str) -> None:
|
||||
"""Renames a player in the db."""
|
||||
# Presumably the player in the cache has changed its name, so
|
||||
# just write out the file.
|
||||
self.write([])
|
||||
|
||||
|
||||
def delete_player(self, pname:str) -> None:
|
||||
"""Removes a player from the db."""
|
||||
self._cache = [p for p in self._cache if p.name != pname]
|
||||
self.write([])
|
||||
|
||||
|
||||
def add_history(self, pnames: List[str], text: str) -> None:
|
||||
"""Adds history text for the player.
|
||||
|
||||
IdleRPG dumps all history into a single modsfile, which is
|
||||
then grepped for by the website.
|
||||
|
||||
"""
|
||||
with open(datapath(conf.get("modsfile")), "a") as ouf:
|
||||
ouf.write(f"[{time.strftime('%m/%d/%y %H:%M:%S')}] {text}\n")
|
||||
|
||||
|
||||
def read_quest(self) -> Optional[Quest]:
|
||||
"""Returns stored Quest object or None."""
|
||||
if not conf.get("writequestfile"):
|
||||
return None
|
||||
if os.stat(datapath(conf.get("questfilename"))).st_size == 0:
|
||||
return None
|
||||
|
||||
with open(datapath(conf.get("questfilename"))) as inf:
|
||||
q = Quest()
|
||||
for line in inf.readlines():
|
||||
key, val = line.split(' ', 2)
|
||||
if key == "T":
|
||||
q.text = val
|
||||
elif key == "Y":
|
||||
q.mode = int(val)
|
||||
elif key == "S":
|
||||
if q.mode == 1:
|
||||
q.qtime = int(val)
|
||||
else:
|
||||
q.stage = int(val)
|
||||
elif key == "P":
|
||||
for pair in chunk.chunk(val.split(' '), 2):
|
||||
q.dests.append((int(pair[0]), int(pair[1])))
|
||||
elif re.match("P\d", key):
|
||||
q.questor_names.append(val)
|
||||
return q
|
||||
|
||||
|
||||
def update_quest(self, quest: Optional[Quest]) -> None:
|
||||
"""Updates quest information in store."""
|
||||
if not conf.get("writequestfile"):
|
||||
return
|
||||
with open(datapath(conf.get("questfilename")), 'w') as ouf:
|
||||
if not quest:
|
||||
# leave behind an empty quest file
|
||||
return
|
||||
|
||||
ouf.write(f"T {quest.text}\n"
|
||||
f"Y {quest.mode}\n")
|
||||
|
||||
if quest.mode == 1:
|
||||
ouf.write(f"S {quest.qtime}\n")
|
||||
elif quest.mode == 2:
|
||||
ouf.write(f"S {quest.stage:d}\n"
|
||||
f"P {' '.join([' '.join([str(c) for c in p]) for p in quest.dests])}\n")
|
||||
|
||||
ouf.write(f"P1 {quest.questors[0].name}\n"
|
||||
f"P2 {quest.questors[1].name}\n"
|
||||
f"P3 {quest.questors[2].name}\n"
|
||||
f"P4 {quest.questors[3].name}\n")
|
||||
|
||||
|
||||
class Sqlite3GameStorage(GameStorage):
|
||||
"""Player store using sqlite3."""
|
||||
|
||||
|
@ -763,6 +519,8 @@ class Sqlite3GameStorage(GameStorage):
|
|||
"""Rename player in db."""
|
||||
with self._connect() as con:
|
||||
con.execute("update dawdle_player set name = ? where name = ?", (new_name, old_name))
|
||||
con.execute("update dawdle_item set owner_id = ? where owner_id = ?", (new_name, old_name))
|
||||
con.execute("update dawdle_ally set owner_id = ? where owner_id = ?", (new_name, old_name))
|
||||
con.commit()
|
||||
|
||||
|
||||
|
@ -771,6 +529,7 @@ class Sqlite3GameStorage(GameStorage):
|
|||
with self._connect() as con:
|
||||
con.execute("delete from dawdle_history where owner_id = ?", (pname,))
|
||||
con.execute("delete from dawdle_item where owner_id = ?", (pname,))
|
||||
con.execute("delete from dawdle_ally where owner_id = ?", (pname,))
|
||||
con.execute("delete from dawdle_player where name = ?", (pname,))
|
||||
con.commit()
|
||||
|
||||
|
@ -1083,7 +842,7 @@ class DawdleBot(abstract.AbstractBot):
|
|||
return
|
||||
self._irc.chanmsg(text)
|
||||
|
||||
|
||||
|
||||
def logchanmsg(self, players: List[Player], text: str) -> None:
|
||||
"""Send a message to the bot channel and attach it to each player's history."""
|
||||
assert self._irc is not None
|
||||
|
@ -1181,10 +940,10 @@ class DawdleBot(abstract.AbstractBot):
|
|||
|
||||
|
||||
def channel_message(self, user: abstract.AbstractClient.User, text: str) -> None:
|
||||
"""Called when channel message received."""
|
||||
"""Treat channel messages like private messages."""
|
||||
player = self._db.from_user(user)
|
||||
if player:
|
||||
self.penalize(player, "message", text)
|
||||
if text[0] == '!' and player:
|
||||
self.private_message(player, text[1:])
|
||||
|
||||
|
||||
def channel_notice(self, user: abstract.AbstractClient.User, text: str) -> None:
|
||||
|
@ -1902,6 +1661,22 @@ class DawdleBot(abstract.AbstractBot):
|
|||
self._events.setdefault(line[0], []).append(line[1:].lstrip())
|
||||
|
||||
|
||||
def insult(self, text: str) -> None:
|
||||
"""Randomly chooses an insult based upon type"""
|
||||
short = ["baka",
|
||||
"horny baka",
|
||||
"ugh, typical"
|
||||
]
|
||||
long = ["You creep. I watched you do that. I can not believe you'd do something like that. Unforgiveable. (I like you)"
|
||||
]
|
||||
if text == "short":
|
||||
self.chanmsg(rand.choice("insult", short))
|
||||
elif text == "long":
|
||||
self.chanmsg(rand.choice("insult", long))
|
||||
else:
|
||||
self.chanmsg(rand.choice("insult", long+short))
|
||||
|
||||
|
||||
def expire_splits(self) -> None:
|
||||
"""Kick players offline if they were disconnected for too long."""
|
||||
assert self._irc is not None
|
||||
|
@ -1994,7 +1769,7 @@ class DawdleBot(abstract.AbstractBot):
|
|||
player.nextlvl = Player.time_to_next_level(player.level)
|
||||
|
||||
self.chanmsg(f"{C('name', player.name)}, the {player.cclass}, has attained level {player.level}! Next level in {duration(player.nextlvl)}.")
|
||||
if player.level >= 60 and 'mount' not in player.allies and rand.randomly('ally_find', 10):
|
||||
if player.level >= 45 and 'mount' not in player.allies and rand.randomly('ally_find', 10):
|
||||
self.find_mount(player)
|
||||
else:
|
||||
self.find_item(player)
|
||||
|
@ -2098,11 +1873,13 @@ class DawdleBot(abstract.AbstractBot):
|
|||
player.level / 5)
|
||||
nextlvl = Ally.time_to_next_level(level)
|
||||
|
||||
base_classes = ["bear", "eagle", "horse", "llama", "donkey", "ox", "snake", "elephant", "fox", "wolf", "squirrel", "camel"]
|
||||
base_classes = ["eagle", "ox", "nekomimi", "Gambunta", "Mr. Tadakichi", "crusty sock", "dakimakura", "Unit 001", "zoid", "broomstick"]
|
||||
if player.alignment == 'g':
|
||||
base_classes.extend(["pegasus", "unicorn", "hippogriff"])
|
||||
base_classes.extend(["unicorn", "flying Nimbus", "Toyota AE86"])
|
||||
elif player.alignment == 'e':
|
||||
base_classes.extend(["manticore", "chimera", "warg", "shark", "spider"])
|
||||
base_classes.extend(["manticore", "chimera", "Jigsy's FBI Party Van"])
|
||||
elif player.alignment == 'n':
|
||||
base_classes.extend(["Super Spot-Billed Duck", "yu-gi-oh's motorcycle", "catbus", "Cyclizar"])
|
||||
base_class = rand.choice("base_class", base_classes)
|
||||
full_class = self.random_mount_class(base_class, level, player.alignment)
|
||||
player.allies["mount"] = Ally("", base_class, full_class, player.alignment, level, nextlvl)
|
||||
|
@ -2119,7 +1896,14 @@ class DawdleBot(abstract.AbstractBot):
|
|||
# TODO: Convert to configuration
|
||||
# Note that order is important here - each item is less likely to be picked than the previous.
|
||||
special_items = [SpecialItem(25, 50, 25, 'helm', "Stone Mask from JoJo",
|
||||
"Your enemies quiver before your sacrificial device."),
|
||||
"Your enemies quiver before your sacrificial device. "),
|
||||
SpecialItem(25, 50, 25, 'helm', "Imouto pantsu",
|
||||
"Every day you take great care to don your your imouto pantsu. "
|
||||
"Unknown to everyone else, you choose to only wear these pantsu"),
|
||||
SpecialItem(25, 50, 25, 'ring', "Hazuki's nekomimi bow",
|
||||
"Into battle you go wearing a bow. Chastised by society for wearing "
|
||||
"such a flamboyant article on your hands. It reminds you of a simplier "
|
||||
"time, as you cut your enemies to shreds with its sharpened loops"),
|
||||
SpecialItem(25, 50, 25, 'ring', "Howl's Ring of Love",
|
||||
"Your lifeforce is bounded to a fair maiden "
|
||||
"What more could you ask for?"),
|
||||
|
@ -2138,9 +1922,9 @@ class DawdleBot(abstract.AbstractBot):
|
|||
SpecialItem(48, 250, 51, 'boots', "The Thigh Highs of Thighness",
|
||||
"Your enemies are left choking on your dust as you run from them "
|
||||
"very, very quickly."),
|
||||
SpecialItem(25, 300, 51, 'weapon', "Jeff's Cluelesshammer of Doom",
|
||||
SpecialItem(25, 300, 51, 'weapon', "Ikaru's Cluelesshammer of Doom",
|
||||
"It has brought doom to your enemies, your family, and your "
|
||||
"possible friends. God made a mistake, and you paid the price for it"
|
||||
"possible friends. God made a mistake, and you paid the price for it. "
|
||||
"In the end, it is just a hammer.")]
|
||||
|
||||
for si in special_items:
|
||||
|
@ -2157,7 +1941,7 @@ class DawdleBot(abstract.AbstractBot):
|
|||
self.notice(player.nick,
|
||||
f"The light of the gods shines down upon you! You have "
|
||||
f"found the S Tier {C('item')}level {ilvl} {si.name}{C()}! {si.flavor}")
|
||||
self.chanmsg(f"{C('name', player.name)} - S Tier Item Acquired"),
|
||||
self.chanmsg(f"{C('name', player.name)} - S Tier {C('item')}Level {ilvl}{C()} Item Acquired"),
|
||||
player.acquire_item(si.kind, ilvl, si.name)
|
||||
self._db.write_players([player])
|
||||
return
|
||||
|
@ -2204,14 +1988,14 @@ class DawdleBot(abstract.AbstractBot):
|
|||
if player.nextlvl > 0:
|
||||
self.chanmsg(f"{C('name', player.name)} reaches next level in {duration(player.nextlvl)}.")
|
||||
if opp is not None:
|
||||
if rand.randomly('pvp_critical', {'g': 50, 'n': 35, 'e': 20}[player.alignment]):
|
||||
if rand.randomly('pvp_critical', {'g': 50, 'n': 35, 'e': 15}[player.alignment]):
|
||||
penalty = int(((5 + rand.randint('pvp_cs_penalty_pct', 0, 20))/100 * opp.nextlvl))
|
||||
self.logchanmsg([opp], f"{C('name', player.name)} has dealt {C('name', opp.name)} a {CC('red')}Critical Strike{C()}! "
|
||||
f"{duration(penalty)} is added to {C('name', opp.name)}'s clock.")
|
||||
opp.nextlvl += penalty
|
||||
self.chanmsg(f"{C('name', opp.name)} reaches next level in {duration(opp.nextlvl)}.")
|
||||
if rand.randomly('insult', 2):
|
||||
self.chanmsg(f"the baka")
|
||||
self.insult("short")
|
||||
elif player.level > 19 and rand.randomly('pvp_swap_item', 25):
|
||||
slot = rand.choice('pvp_swap_itemtype', Item.SLOTS)
|
||||
playeritem = player.item_level(slot)
|
||||
|
@ -2237,7 +2021,7 @@ class DawdleBot(abstract.AbstractBot):
|
|||
f"{C('name', player.name)} notices something "
|
||||
f"in the mud. Upon investigation, they find an old lost item!")
|
||||
if rand.randomly('insult', 2):
|
||||
self.chanmsg(f"the lucky baka")
|
||||
self.insult("short")
|
||||
self.find_item(player)
|
||||
|
||||
|
||||
|
@ -2304,7 +2088,7 @@ class DawdleBot(abstract.AbstractBot):
|
|||
msg = f"{C('name', player.name)} stepped in some hot lava!"
|
||||
self.logchanmsg([player], msg + f" {C('name')}{player.name}'s{C()} {C('item', Item.DESC[slot])} loses 10% of its effectiveness.")
|
||||
if rand.randomly('insult', 2):
|
||||
self.chanmsg(f"baka")
|
||||
self.insult("short")
|
||||
player.items[slot].level = int(player.items[slot].level * 0.9)
|
||||
return
|
||||
|
||||
|
@ -2350,7 +2134,7 @@ class DawdleBot(abstract.AbstractBot):
|
|||
|
||||
self.logchanmsg([player], msg + f" {C('name')}{player.name}'s{C()} {C('item', Item.DESC[slot])} gains 10% effectiveness.")
|
||||
if rand.randomly('insult', 2):
|
||||
self.chanmsg(f"WTFBBQ")
|
||||
self.insult("short")
|
||||
player.items[slot].level = int(player.items[slot].level * 1.1)
|
||||
return
|
||||
|
||||
|
@ -2387,18 +2171,17 @@ class DawdleBot(abstract.AbstractBot):
|
|||
f"You made to steal {C('name', target.name)}'s {C('item', Item.DESC[slot])}, "
|
||||
f"but realized it was lower level than your own. You creep "
|
||||
f"back into the shadows.")
|
||||
if rand.randomly('insult', 2):
|
||||
self.notice(player.nick, (f"you creep."))
|
||||
if rand.randomly('insult', 5):
|
||||
self.insult("long")
|
||||
else:
|
||||
amount = int(player.nextlvl * rand.randint('evilness_penalty_pct', 1,6) / 100)
|
||||
player.nextlvl += amount
|
||||
self.logchanmsg([player], f"{C('name', player.name)} is forsaken by their evil god. {duration(amount)} is "
|
||||
f"added to their clock.")
|
||||
if rand.randomly('insult', 2):
|
||||
if rand.randomly('insult', 5):
|
||||
self.chanmsg(f"YOUR INSOLENCE WILL NOT BE FORGOTTEN {C('name', player.name)}")
|
||||
if player.nextlvl > 0:
|
||||
self.chanmsg(f"{C('name', player.name)} reaches next level in {duration(player.nextlvl)}.")
|
||||
self.chanmsg("weeb")
|
||||
|
||||
def goodness(self, op: List[Player]) -> None:
|
||||
"""Bring two good players closer to their next level."""
|
||||
|
@ -2467,13 +2250,13 @@ class DawdleBot(abstract.AbstractBot):
|
|||
if combatant.isadmin and rand.randomly('move_player_bow', 100):
|
||||
self.chanmsg(f"{C('name', p.name)} encounters {C('name', combatant.name)} and bows humbly, knowing the end is neigh")
|
||||
if rand.randomly('insult', 2):
|
||||
self.chanmsg(f"Their heads hit as if a comedy routine is playing out before our eyes"
|
||||
self.chanmsg(f"Their heads hit as if a comedy routine is playing out before our eyes. "
|
||||
"Hot and flustered their cheeks turn red from embarrassment")
|
||||
elif rand.randomly('move_player_combat', len(op)):
|
||||
self.pvp_battle(p, combatant,
|
||||
"come upon",
|
||||
"and took them for all they were worth (in combat), which wasn't very much",
|
||||
"and was defeated in combat, like a wet lily")
|
||||
"come upon ",
|
||||
"and took them for all they were worth (in combat) which wasn't very much ",
|
||||
"and was defeated in combat")
|
||||
del combatants[(p.posx, p.posy)]
|
||||
else:
|
||||
combatants[(p.posx, p.posy)] = p
|
||||
|
|
|
@ -39,7 +39,6 @@ def read_config(path: str) -> Dict[str, Any]:
|
|||
"okurls": [],
|
||||
"loggers": [],
|
||||
"localaddr": None,
|
||||
# Legacy idlerpg option
|
||||
"debug": False,
|
||||
# Non-idlerpg config needs defaults
|
||||
"confpath": os.path.realpath(path),
|
||||
|
@ -49,15 +48,14 @@ def read_config(path: str) -> Dict[str, Any]:
|
|||
"allylvlstep": 1.16,
|
||||
"allymaxexplvl": 60,
|
||||
"backupdir": ".dbbackup",
|
||||
"store_format": "idlerpg",
|
||||
"daemonize": True,
|
||||
"daemonize": False,
|
||||
"loglevel": "DEBUG",
|
||||
"throttle": True,
|
||||
"throttle_rate": 4,
|
||||
"throttle_period": 1,
|
||||
"throttle_rate": 5,
|
||||
"throttle_period": 10,
|
||||
"penquest": 15,
|
||||
"pennick": 30,
|
||||
"penmessage": 1,
|
||||
"penmessage": 0,
|
||||
"penpart": 200,
|
||||
"penkick": 250,
|
||||
"penquit": 20,
|
||||
|
@ -68,10 +66,10 @@ def read_config(path: str) -> Dict[str, Any]:
|
|||
"max_name_len": 16,
|
||||
"max_class_len": 30,
|
||||
"message_wrap_len": 400,
|
||||
"quest_interval_min": 12*3600,
|
||||
"quest_interval_min": 6*3600,
|
||||
"quest_interval_max": 24*3600,
|
||||
"quest_min_level": 24,
|
||||
"quest_min_login": 36000,
|
||||
"quest_min_login": 3600,
|
||||
"color": False,
|
||||
"namecolor": "cyan",
|
||||
"durationcolor": "green",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import random
|
||||
from typing import cast, Any, Dict, List, MutableSequence, Sequence, TypeVar
|
||||
from dawdle.log import log
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
@ -11,27 +12,62 @@ 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
|
||||
ans = random.randint(0, odds-1) < 1
|
||||
try:
|
||||
log.debug(f"Rand: {key}: {odds} A: {ans}")
|
||||
except Exception as e:
|
||||
log.debug(f"{e}")
|
||||
pass
|
||||
return ans
|
||||
|
||||
|
||||
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))
|
||||
ans = random.randint(int(bottom), int(top))
|
||||
try:
|
||||
# Very Very Verbose...
|
||||
if key not in [
|
||||
'move_player_x',
|
||||
'move_player_y',
|
||||
'hog_trigger',
|
||||
'team_battle_trigger',
|
||||
'calamity_trigger',
|
||||
'godsend_trigger',
|
||||
'evilness_trigger',
|
||||
'goodness_trigger'
|
||||
]:
|
||||
log.debug(f"Rand: {key}: {int(bottom)} to {int(top)} A: {ans}")
|
||||
except Exception as e:
|
||||
log.debug(f"{e}")
|
||||
pass
|
||||
return ans
|
||||
|
||||
|
||||
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))
|
||||
ans = int(random.gauss(mu, sigma))
|
||||
try:
|
||||
log.debug(f"Rand: {key}: A: {ans}")
|
||||
except Exception as e:
|
||||
log.debug(f"{e}")
|
||||
pass
|
||||
return ans
|
||||
|
||||
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)
|
||||
ans = random.sample(seq, count)
|
||||
try:
|
||||
log.debug(f"Rand: {key}: A:{ans}")
|
||||
except Exception as e:
|
||||
log.debug(f"{e}")
|
||||
pass
|
||||
return ans
|
||||
|
||||
|
||||
def choice(key: str, seq: Sequence[T]) -> T:
|
||||
|
@ -40,7 +76,13 @@ def choice(key: str, seq: Sequence[T]) -> T:
|
|||
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]
|
||||
ans = random.sample(seq, 1)[0]
|
||||
try:
|
||||
log.debug(f"Rand: {key}: {p for p in seq} A: {ans}")
|
||||
except Exception as e:
|
||||
log.debug(f"{e}")
|
||||
pass
|
||||
return ans
|
||||
|
||||
|
||||
def shuffle(key: str, seq: MutableSequence[Any]) -> None:
|
||||
|
|
6
docker-compose.dev.yml
Normal file
6
docker-compose.dev.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
nginx:
|
||||
ports:
|
||||
- 8142:80
|
38
docker-compose.prod.yml
Normal file
38
docker-compose.prod.yml
Normal file
|
@ -0,0 +1,38 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
web:
|
||||
networks:
|
||||
- web
|
||||
|
||||
nginx:
|
||||
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
|
|
@ -4,11 +4,17 @@ services:
|
|||
dawdle:
|
||||
build: .
|
||||
volumes:
|
||||
- ./:/data
|
||||
command:
|
||||
- 'python'
|
||||
- '/data/dawdle.py'
|
||||
- '/data/af/dawdle.conf'
|
||||
# Your specific data
|
||||
- ./af/backups:/data/af/backups
|
||||
- ./af/events.txt:/data/af/events.txt
|
||||
- ./af/dawdle.conf:/data/af/dawdle.conf
|
||||
- ./af/dawdle.log:/data/af/dawdle.log
|
||||
# Bot Code
|
||||
- ./dawdle:/data/dawdle
|
||||
- ./dawdle.py:/data/dawdle.py
|
||||
# Database
|
||||
- ./site:/data/site
|
||||
command: python /data/dawdle.py /data/af/dawdle.conf
|
||||
restart: unless-stopped
|
||||
|
||||
web:
|
||||
|
@ -18,8 +24,6 @@ services:
|
|||
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
|
||||
|
@ -27,33 +31,3 @@ services:
|
|||
- ./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
64
install.sh
|
@ -1,64 +0,0 @@
|
|||
#!/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.
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
rm /opt/dawdlerpg/site/static/map.mp4;
|
||||
|
||||
ffmpeg -framerate 3 -pattern_type glob -i '/opt/dawdlerpg/map/*.png' \
|
||||
ffmpeg -framerate 30 -pattern_type glob -i '/opt/dawdlerpg/map/*.png' \
|
||||
-c:v libx264 -pix_fmt yuv420p \
|
||||
/opt/dawdlerpg/site/static/map.mp4
|
||||
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
server {
|
||||
|
||||
location /static {
|
||||
#autoindex on;
|
||||
alias /usr/share/nginx/html/;
|
||||
alias /usr/share/nginx/html/;
|
||||
}
|
||||
location /favicon.ico {
|
||||
alias /usr/share/nginx/html/favicon.ico;
|
||||
}
|
||||
location /robots.txt {
|
||||
alias /usr/share/nginx/html/robots.txt;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://web:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_pass http://web:8000;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
access_log syslog;
|
||||
|
|
BIN
setup/dawdle.db
BIN
setup/dawdle.db
Binary file not shown.
|
@ -1 +0,0 @@
|
|||
1
|
|
@ -1,33 +0,0 @@
|
|||
# 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;
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
[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
|
|
@ -1,27 +0,0 @@
|
|||
# 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.
|
|
@ -1,130 +0,0 @@
|
|||
"""
|
||||
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")
|
|
@ -1,18 +0,0 @@
|
|||
[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
|
|
@ -1,13 +0,0 @@
|
|||
[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
|
|
@ -4,7 +4,8 @@ body {
|
|||
font-family: sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #eee;
|
||||
background: #121212;
|
||||
color: #e1cbd5;
|
||||
}
|
||||
#header-container {
|
||||
color: #e1cdb5;
|
||||
|
@ -51,12 +52,16 @@ body {
|
|||
}
|
||||
#content {
|
||||
margin: 1em auto;
|
||||
width: 75%;
|
||||
width: 95%;
|
||||
}
|
||||
.contentbox {
|
||||
background: white;
|
||||
background: #1f1f23;
|
||||
margin: 2rem;
|
||||
padding: 0.5rem;
|
||||
color: #e1cdb5;
|
||||
}
|
||||
.contentbox a {
|
||||
color: #e1cdb5;
|
||||
}
|
||||
.contentbox h1 {
|
||||
margin-top: 0;
|
||||
|
@ -105,9 +110,13 @@ body {
|
|||
#playerlist {
|
||||
display: inline-block;
|
||||
border-collapse: collapse;
|
||||
font-family: monospace;
|
||||
}
|
||||
#playerlist tr:nth-child(odd) {
|
||||
background: #90708C;
|
||||
}
|
||||
#playerlist tr:nth-child(even) {
|
||||
background: #E1CDB5;
|
||||
background: #90708CA8;
|
||||
}
|
||||
#playerlist thead tr {
|
||||
background: #90708C;
|
||||
|
@ -115,10 +124,14 @@ body {
|
|||
#playerlist thead tr td {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
padding: 0.5rem 1rem;
|
||||
padding: 0.3rem 0.3rem;
|
||||
}
|
||||
#playerlist td {
|
||||
padding: 0.1rem 1rem;
|
||||
padding: 0.1rem 0.3rem;
|
||||
color: white;
|
||||
}
|
||||
#playerlist a {
|
||||
color: white;
|
||||
}
|
||||
#pmap-container {
|
||||
display: inline-block;
|
||||
|
@ -127,7 +140,6 @@ body {
|
|||
}
|
||||
.online a {
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
.online a:hover {
|
||||
color: black;
|
||||
|
@ -143,8 +155,7 @@ body {
|
|||
.offline a:hover {
|
||||
color: gray;
|
||||
text-decoration: underline;
|
||||
}
|
||||
{% block extrastyles %}{% endblock %}
|
||||
}{% block extrastyles %}{% endblock %}
|
||||
</style>
|
||||
</head><body>
|
||||
<div id="header-container">
|
||||
|
|
|
@ -51,13 +51,13 @@
|
|||
<td>Quest failures:</td><td>{{player.penquest|duration}}</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
<p>Total penalties: {{total_penalties|duration}}</p>
|
||||
<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>
|
||||
<li>{{ item.slot }}: Level {{ item.level }} {{ item.name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p>Total item level: {{ total_items }}</p>
|
||||
|
@ -66,7 +66,7 @@
|
|||
<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>
|
||||
<li>{{ally.slot}}: {{ally.name}} the level {{ally.level}} {{ally.fullclass}}, Next level in {{ally.nextlvl|duration}}.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div><div class="contentbox">
|
||||
|
|
|
@ -10,7 +10,16 @@
|
|||
|
||||
<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>Item Total</td><td>Total Idled</td></tr>
|
||||
<tr style="text-align: center">
|
||||
<td>Rank</td>
|
||||
<td style="width:25rem">Name</td>
|
||||
<td style="width:5rem" title="Time to next level">Next level</td>
|
||||
<td style="width:5rem" title="STier Heroes">IRC User</td>
|
||||
<td style="width:5rem" title="Items">Items</td>
|
||||
<td style="width:5rem" title="Item Total">iTotal</td>
|
||||
<td style="width:15rem" title="Allies">Allies</td>
|
||||
<td style="width:5rem" title="Total Time Online">Total Idled</td>
|
||||
</tr>
|
||||
</thead><tbody>
|
||||
{% for p in object_list %}
|
||||
<tr class="{%if p.online %}online{% else %}offline{% endif %}">
|
||||
|
@ -30,10 +39,23 @@
|
|||
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: center"2>{{p.item_set | lvlsum}}</td>
|
||||
<td style="text-align: center"2>{{p.idled |duration}}</td>
|
||||
<td style="text-align: right">{{p.nextlvl|duration}}</td>
|
||||
<td style="text-align: center"><i>{{ p.userhost|split:"!"|first}}</i></td>
|
||||
<td style="text-align: center">
|
||||
[{% for item in p.item_set.all|dictsort:"slot" %}{% if item.level > 0 %}<span title={% if item.name|length > 0 %}"{{item.slot}}: {{item.name}} Lvl: {{item.level}}"{% else %}"{{item.slot}}: L{{item.level}}"{% endif %}>{%if item.name|length > 0 %}<font color="gold"><b>{%endif%}{{item.slot|slice:":1"}}{%if item.name|length > 0 %}</font></b>{%endif%}</span>{% endif %}{% endfor %}]
|
||||
</td>
|
||||
<td style="text-align: center">{{p.item_set | lvlsum}}</td>
|
||||
<td style="text-align: center">
|
||||
{% for ally in p.ally_set.all|dictsort:"slot" %}
|
||||
<span title="Level: {{ally.level}}">
|
||||
|
||||
{% if ally.name != "" %}
|
||||
{{ally.name}} the
|
||||
{%endif%}
|
||||
{{ally.fullclass}}
|
||||
</span>
|
||||
{% endfor %}</td>
|
||||
<td style="text-align: center">{{p.idled |duration}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody></table>
|
||||
|
|
|
@ -8,10 +8,11 @@
|
|||
Mon, Feb 26 00:05:24 UTC 2024 </p>
|
||||
{% with timelapse='DEFAULT' %}
|
||||
<p> Last Generated: {{ timelapse }} </p>
|
||||
<p> Video @ 30 FPS at 1 minute per frame. (1 second = 30 minutes of gameplay)</p>
|
||||
{% endwith %}
|
||||
<video controls width="500"> <source src="/static/map.mp4" type="video/mp4" />
|
||||
<p> Week 1: <p>
|
||||
<video controls width="500"> <source src="/static/map-week-1.mp4" type="video/mp4" />
|
||||
<video controls width="500"> <source src="/static/map.mp4" type="video/mp4" /></video>
|
||||
<p> Original Week 1: (1 frame is 15 minutes of gameplay) <p>
|
||||
<video controls width="500"> <source src="/static/map-week-1.mp4" type="video/mp4" /></video>
|
||||
|
||||
</div>
|
||||
|
||||
|
|
27
update.sh
27
update.sh
|
@ -1,27 +0,0 @@
|
|||
#!/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