Compare commits

...

40 commits
main ... main

Author SHA1 Message Date
pwgen2155 249a16c3b5 fix: enforce our colors on single player page
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-20 09:48:17 +10:00
pwgen2155 e84dfe960c feat: darkmode
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-19 10:18:16 +10:00
pwgen2155 60228715fc fix: insult lists & proper logging
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-10 16:35:33 +10:00
pwgen2155 9316e327c1 feat: critical strike: Evil: 1/20 -> 1/15
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-10 13:27:46 +10:00
pwgen2155 125adf6425 fix: show level on allies
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-10 13:09:08 +10:00
pwgen2155 e55b031626 chore: compact mode
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-10 13:08:03 +10:00
pwgen2155 1a51747e08 fix: fonts & item description tooltips
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-10 12:52:35 +10:00
pwgen2155 d7cece2a58
feat: ally on player list (#23)
All checks were successful
continuous-integration/drone/push Build is passing
fixes: #18
Co-authored-by: pwgen2155 <git@example.com>
Reviewed-on: pwgen2155/dawdle#23
2024-05-10 02:00:48 +00:00
pwgen2155 409511eb42
Update dawdle/bot.py
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-14 02:48:32 +00:00
pwgen2155 5e448a6ff1
Update af/events.txt
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-12 11:04:10 +00:00
pwgen2155 9515c878a3
fix: pin python version so we don't rebuild everytime they push a new version
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-12 06:05:38 +00:00
pwgen2155 e00e4e5526
Update af/events.txt
Some checks failed
continuous-integration/drone/push Build is failing
2024-04-12 05:25:05 +00:00
pwgen2155 2a26376364
Update af/events.txt
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-30 02:23:10 +00:00
pwgen2155 41d72092a0 fix: allow for renaming and proper deletion
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-21 21:11:38 +11:00
pwgen2155 4767f2b1bf fix: build & deploy
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-21 20:41:43 +11:00
pwgen2155 bf1a72323e fix: yaml is hard
Some checks failed
continuous-integration/drone/push Build is failing
2024-03-21 20:35:02 +11:00
pwgen2155 f11e2aaf62 feat: anime themed mounts
Some checks failed
continuous-integration/drone/push Build is failing
2024-03-21 20:29:23 +11:00
pwgen2155 ea91606c63 feat: Makefile support & better readme 2024-03-21 20:28:56 +11:00
pwgen2155 1370f823ab fix: no pid nonsense 2024-03-21 20:28:29 +11:00
pwgen2155 fbc3261f45 feat: dev & prod compose files 2024-03-21 20:15:32 +11:00
pwgen2155 ca05497fa0 chore: sane defaults & more removals of dead features 2024-03-21 19:47:58 +11:00
pwgen2155 7cba240eae chore: remove dead files 2024-03-21 19:45:16 +11:00
pwgen2155 5d7374509e
Update af/events.txt
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-21 00:49:39 +00:00
pwgen2155 66c9cb445d
feat: LVL 45 Mounts (#20)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: pwgen2155/dawdle#20
2024-03-17 06:16:03 +00:00
pwgen2155 42b51d25be
fix: events and grammer
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-09 20:53:47 +00:00
Mediaman e0e394462d
Setup and readme improvement and clarification (#15)
All checks were successful
continuous-integration/drone/push Build is passing
Removed the setup folder and install.sh script, changed readme to actually show how to set up the bot, included a "dawdle.conf.example" to act as the template config, changed dawdle.py --setup to just pull the owner character's name from config and use a default "Hidden Master" class since admins can change class whenever (but it still prompts for password because there's not much reason not to)

Reviewed-on: pwgen2155/dawdle#15
Co-authored-by: Mediaman <mediaman@riseup.net>
Co-committed-by: Mediaman <mediaman@riseup.net>
2024-03-07 05:46:46 +00:00
pwgen2155 fac81d8a0d
fix: is ! to == !
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-07 01:00:26 +00:00
pwgen2155 b2b517a889
fix: special item formatting
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-06 00:32:29 +00:00
pwgen2155 60128f45cc fix: spelling of quests is hard
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-04 22:10:55 +11:00
pwgen2155 3b5ce16ced feat: hotfix channel messages & random
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-04 21:31:40 +11:00
pwgen2155 c8de3c2070 fix: only look if it has !
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-04 21:24:45 +11:00
pwgen2155 d6153956bf
feat: new quests, treat public channel as private, prove randomness (#8)
All checks were successful
continuous-integration/drone/push Build is passing
Co-authored-by: pwgen2155 <git@example.com>
Reviewed-on: pwgen2155/dawdle#8
2024-03-04 10:20:24 +00:00
pwgen2155 c6b2b8edba
revert 6b2c711ee6
All checks were successful
continuous-integration/drone/push Build is passing
revert feat: display all randomness (#7)

Co-authored-by: pwgen2155 <git@example.com>
Reviewed-on: pwgen2155/dawdle#7
2024-03-04 09:29:22 +00:00
pwgen2155 6b2c711ee6
feat: display all randomness (#7)
All checks were successful
continuous-integration/drone/push Build is passing
Co-authored-by: pwgen2155 <git@example.com>
Reviewed-on: pwgen2155/dawdle#7
2024-03-04 09:06:51 +00:00
Mediaman 852caf73e5
feat: Changes for the removal of IdleRPG db support (#6)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: pwgen2155/dawdle#6
Reviewed-by: pwgen2155 <pwgen2155@noreply.gammaspectra.live>
Co-authored-by: Mediaman <mediaman@riseup.net>
Co-committed-by: Mediaman <mediaman@riseup.net>
2024-03-04 08:57:25 +00:00
pwgen2155 730c96191a feat: timelapse rework
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-04 15:37:31 +11:00
pwgen2155 f7dd063aab fix: not enough frames
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-04 10:31:41 +11:00
pwgen2155 e320d01ca0
fix: why is pid here
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-03 22:06:05 +00:00
pwgen2155 f28827d99d
fix: rude
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-03 22:05:21 +00:00
DataHoarder d4c391bb38
Include S tier level on channel notice (#5)
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: pwgen2155/dawdle#5
Co-authored-by: DataHoarder <weebdatahoarder@noreply.gammaspectra.live>
Co-committed-by: DataHoarder <weebdatahoarder@noreply.gammaspectra.live>
2024-03-02 20:59:47 +00:00
30 changed files with 328 additions and 780 deletions

View file

@ -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:

View file

@ -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
View 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

View file

@ -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.

View file

@ -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;
}
}

View file

@ -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

View file

@ -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

View file

@ -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():

View file

@ -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

View file

@ -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",

View file

@ -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
View file

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

38
docker-compose.prod.yml Normal file
View 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

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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;

Binary file not shown.

View file

@ -1 +0,0 @@
1

View file

@ -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;
}
}

View file

@ -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

View file

@ -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.

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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">

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}}: 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">

View file

@ -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>

View file

@ -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>

View file

@ -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