Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

30 changed files with 780 additions and 328 deletions

View file

@ -36,7 +36,7 @@ steps:
key:
from_secret: ssh_key
script:
- git fetch && git checkout main --force && git pull --force && sudo make prod
- git fetch && git checkout main --force && git pull && sudo docker compose up -d --build
depends_on:
- test
trigger:

View file

@ -1,6 +1,8 @@
FROM python:3.11@sha256:db07fba48daaf1c68c03676aadc73866414d25b4c278029f9873c784517613bf
FROM python:3.11
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"]

View file

@ -1,12 +0,0 @@
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

View file

@ -4,45 +4,35 @@ FriendsRPG is a modified version of DawdleRPG (an IdleRPG clone) written in Pyth
## Basic Setup
- 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:
- 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.
``` 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
## 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>
```
### Development:
```
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build
OR
make dev
```
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.
Web is available via: http://localhost:8142
## Migrating from IdleRPG
### Production:
```
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
OR
make prod
```
DawdleRPG is capable of being a drop-in replacement.
## 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
- 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
@ -50,7 +40,7 @@ make prod
- Output throttling allows configurable rate over a period.
- Long messages are word wrapped.
- Logging can be set to different levels.
- Better IRC protocol support. (SSL!)
- 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.

33
af/dawdlerpg.nginx Normal file
View 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;
}
}

View file

@ -1,64 +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 qt werewolf girl
C kept asking for invites to the secret club
C kept asking for invites to the secreet 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 that looked like fairy dust...
G chose to bathed before seeing their husbando
G was sprinkled with fairy dust, or something...
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. (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
# 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
Q1 find the hidden pics of famous Vtubers
Q1 unearth the secret of the Twin Peeks (.)(.)
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
Q1 throwing a party for Animefield
Q1 decipher the fabled tablet of AnimeFriends
# 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 party and help them save the world without being seen!
Q2 304 269 70 417 secretly follow a Heroes part without being invited to join them saving the world
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 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
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.

View file

@ -35,15 +35,17 @@ import dawdle.log as dawdlelog
def first_setup(db: bot.GameDB) -> None:
"""Perform initialization of game."""
pname = conf.get("owner")
pclass = "Hidden Master"
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 owner account: ")
ppass = input("Password for this account: ")
finally:
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, old)
@ -134,7 +136,13 @@ def start_bot() -> None:
log.info("Bot %s starting.", bot.VERSION)
store: bot.GameStorage
store = bot.Sqlite3GameStorage(bot.datapath(conf.get("dbfile")))
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():

View file

@ -390,6 +390,250 @@ 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."""
@ -519,8 +763,6 @@ 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()
@ -529,7 +771,6 @@ 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()
@ -842,7 +1083,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
@ -940,10 +1181,10 @@ class DawdleBot(abstract.AbstractBot):
def channel_message(self, user: abstract.AbstractClient.User, text: str) -> None:
"""Treat channel messages like private messages."""
"""Called when channel message received."""
player = self._db.from_user(user)
if text[0] == '!' and player:
self.private_message(player, text[1:])
if player:
self.penalize(player, "message", text)
def channel_notice(self, user: abstract.AbstractClient.User, text: str) -> None:
@ -1661,22 +1902,6 @@ 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
@ -1769,7 +1994,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 >= 45 and 'mount' not in player.allies and rand.randomly('ally_find', 10):
if player.level >= 60 and 'mount' not in player.allies and rand.randomly('ally_find', 10):
self.find_mount(player)
else:
self.find_item(player)
@ -1873,13 +2098,11 @@ class DawdleBot(abstract.AbstractBot):
player.level / 5)
nextlvl = Ally.time_to_next_level(level)
base_classes = ["eagle", "ox", "nekomimi", "Gambunta", "Mr. Tadakichi", "crusty sock", "dakimakura", "Unit 001", "zoid", "broomstick"]
base_classes = ["bear", "eagle", "horse", "llama", "donkey", "ox", "snake", "elephant", "fox", "wolf", "squirrel", "camel"]
if player.alignment == 'g':
base_classes.extend(["unicorn", "flying Nimbus", "Toyota AE86"])
base_classes.extend(["pegasus", "unicorn", "hippogriff"])
elif player.alignment == 'e':
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_classes.extend(["manticore", "chimera", "warg", "shark", "spider"])
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)
@ -1896,14 +2119,7 @@ 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. "),
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"),
"Your enemies quiver before your sacrificial device."),
SpecialItem(25, 50, 25, 'ring', "Howl's Ring of Love",
"Your lifeforce is bounded to a fair maiden "
"What more could you ask for?"),
@ -1922,9 +2138,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', "Ikaru's Cluelesshammer of Doom",
SpecialItem(25, 300, 51, 'weapon', "Jeff'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:
@ -1941,7 +2157,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 {C('item')}Level {ilvl}{C()} Item Acquired"),
self.chanmsg(f"{C('name', player.name)} - S Tier Item Acquired"),
player.acquire_item(si.kind, ilvl, si.name)
self._db.write_players([player])
return
@ -1988,14 +2204,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': 15}[player.alignment]):
if rand.randomly('pvp_critical', {'g': 50, 'n': 35, 'e': 20}[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.insult("short")
self.chanmsg(f"the baka")
elif player.level > 19 and rand.randomly('pvp_swap_item', 25):
slot = rand.choice('pvp_swap_itemtype', Item.SLOTS)
playeritem = player.item_level(slot)
@ -2021,7 +2237,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.insult("short")
self.chanmsg(f"the lucky baka")
self.find_item(player)
@ -2088,7 +2304,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.insult("short")
self.chanmsg(f"baka")
player.items[slot].level = int(player.items[slot].level * 0.9)
return
@ -2134,7 +2350,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.insult("short")
self.chanmsg(f"WTFBBQ")
player.items[slot].level = int(player.items[slot].level * 1.1)
return
@ -2171,17 +2387,18 @@ 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', 5):
self.insult("long")
if rand.randomly('insult', 2):
self.notice(player.nick, (f"you creep."))
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', 5):
if rand.randomly('insult', 2):
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."""
@ -2250,13 +2467,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")
"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")
del combatants[(p.posx, p.posy)]
else:
combatants[(p.posx, p.posy)] = p

View file

@ -39,6 +39,7 @@ 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),
@ -48,14 +49,15 @@ def read_config(path: str) -> Dict[str, Any]:
"allylvlstep": 1.16,
"allymaxexplvl": 60,
"backupdir": ".dbbackup",
"daemonize": False,
"store_format": "idlerpg",
"daemonize": True,
"loglevel": "DEBUG",
"throttle": True,
"throttle_rate": 5,
"throttle_period": 10,
"throttle_rate": 4,
"throttle_period": 1,
"penquest": 15,
"pennick": 30,
"penmessage": 0,
"penmessage": 1,
"penpart": 200,
"penkick": 250,
"penquit": 20,
@ -66,10 +68,10 @@ def read_config(path: str) -> Dict[str, Any]:
"max_name_len": 16,
"max_class_len": 30,
"message_wrap_len": 400,
"quest_interval_min": 6*3600,
"quest_interval_min": 12*3600,
"quest_interval_max": 24*3600,
"quest_min_level": 24,
"quest_min_login": 3600,
"quest_min_login": 36000,
"color": False,
"namecolor": "cyan",
"durationcolor": "green",

View file

@ -1,6 +1,5 @@
import random
from typing import cast, Any, Dict, List, MutableSequence, Sequence, TypeVar
from dawdle.log import log
T = TypeVar("T")
@ -12,62 +11,27 @@ 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])
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
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])
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
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])
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
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])
ans = random.sample(seq, count)
try:
log.debug(f"Rand: {key}: A:{ans}")
except Exception as e:
log.debug(f"{e}")
pass
return ans
return random.sample(seq, count)
def choice(key: str, seq: Sequence[T]) -> T:
@ -76,13 +40,7 @@ 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.
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
return random.sample(seq, 1)[0]
def shuffle(key: str, seq: MutableSequence[Any]) -> None:

View file

@ -1,6 +0,0 @@
version: "3"
services:
nginx:
ports:
- 8142:80

View file

@ -1,38 +0,0 @@
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

View file

@ -4,17 +4,11 @@ services:
dawdle:
build: .
volumes:
# 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
- ./:/data
command:
- 'python'
- '/data/dawdle.py'
- '/data/af/dawdle.conf'
restart: unless-stopped
web:
@ -24,6 +18,8 @@ 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
@ -31,3 +27,33 @@ 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 Executable file
View 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.

View file

@ -2,7 +2,7 @@
rm /opt/dawdlerpg/site/static/map.mp4;
ffmpeg -framerate 30 -pattern_type glob -i '/opt/dawdlerpg/map/*.png' \
ffmpeg -framerate 3 -pattern_type glob -i '/opt/dawdlerpg/map/*.png' \
-c:v libx264 -pix_fmt yuv420p \
/opt/dawdlerpg/site/static/map.mp4

View file

@ -1,17 +1,13 @@
server {
location /static {
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;
#autoindex on;
alias /usr/share/nginx/html/;
}
location / {
proxy_pass http://web:8000;
proxy_set_header Host $host;
proxy_pass http://web:8000;
proxy_set_header Host $host;
}
access_log syslog;

View file

@ -11,13 +11,13 @@
#die
# Superuser that cannot be DELADMINed.
owner YourNameHere
owner Animefield
# Server name:port.
server irc.animefriends.moe:7000
# Bot's nickname
botnick ChaosFriend
botnick IdleFriend
# 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
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
@ -98,7 +98,7 @@ penquest 15
pennick 30
# Penalty for sending a message - this is per character!
penmessage 0
penmessage 1
# 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 off
doban on
# Time after joining that users can mention a URL without being banned.
bannable_time 90s
# Minimum time between quests.
quest_interval_min 6h
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 3600
quest_min_login 36000
# These are URL hosts which are okay to mention early in channel. Multiples are fine.
# okurl example.com
@ -185,14 +185,30 @@ 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
# 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

BIN
setup/dawdle.db Normal file

Binary file not shown.

1
setup/dawdle.pid Normal file
View file

@ -0,0 +1 @@
1

33
setup/dawdlerpg.nginx Normal file
View 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
View 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
View 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
View 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
View 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
View 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

View file

@ -4,8 +4,7 @@ body {
font-family: sans-serif;
margin: 0;
padding: 0;
background: #121212;
color: #e1cbd5;
background: #eee;
}
#header-container {
color: #e1cdb5;
@ -52,16 +51,12 @@ body {
}
#content {
margin: 1em auto;
width: 95%;
width: 75%;
}
.contentbox {
background: #1f1f23;
background: white;
margin: 2rem;
padding: 0.5rem;
color: #e1cdb5;
}
.contentbox a {
color: #e1cdb5;
}
.contentbox h1 {
margin-top: 0;
@ -110,13 +105,9 @@ body {
#playerlist {
display: inline-block;
border-collapse: collapse;
font-family: monospace;
}
#playerlist tr:nth-child(odd) {
background: #90708C;
}
#playerlist tr:nth-child(even) {
background: #90708CA8;
background: #E1CDB5;
}
#playerlist thead tr {
background: #90708C;
@ -124,14 +115,10 @@ body {
#playerlist thead tr td {
color: white;
font-weight: bold;
padding: 0.3rem 0.3rem;
padding: 0.5rem 1rem;
}
#playerlist td {
padding: 0.1rem 0.3rem;
color: white;
}
#playerlist a {
color: white;
padding: 0.1rem 1rem;
}
#pmap-container {
display: inline-block;
@ -140,6 +127,7 @@ body {
}
.online a {
text-decoration: none;
color: black;
}
.online a:hover {
color: black;
@ -155,7 +143,8 @@ body {
.offline a:hover {
color: gray;
text-decoration: underline;
}{% block extrastyles %}{% endblock %}
}
{% block extrastyles %}{% endblock %}
</style>
</head><body>
<div id="header-container">

View file

@ -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}}: {{ally.name}} the level {{ally.level}} {{ally.fullclass}}, Next level in {{ally.nextlvl|duration}}.</li>
<li>{{ally.slot}}: level {{ ally.level }} {{ally.fullclass}}, Next level in {{ally.nextlvl|duration}}.</li>
{% endfor %}
</ul>
</div><div class="contentbox">

View file

@ -10,16 +10,7 @@
<table id="playerlist">
<thead>
<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>
<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>
</thead><tbody>
{% for p in object_list %}
<tr class="{%if p.online %}online{% else %}offline{% endif %}">
@ -39,23 +30,10 @@
the <span class="{{p.alignment|alignment}}-align">{{p.alignment|alignment}}</span>
level {{p.level}} {{p.cclass}}</a>
</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>
<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>
</tr>
{% endfor %}
</tbody></table>

View file

@ -8,11 +8,10 @@
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" /></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>
<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" />
</div>

27
update.sh Normal file
View 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