"""Scriptabit plugin that implements various health-modification effects.
"""
# Ensure backwards compatibility with Python 2
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals)
from builtins import *
import itertools
import logging
import math
from datetime import datetime, timedelta
import pytz
import scriptabit
[docs]class HealthEffects(scriptabit.IPlugin):
""" Implements the health effects plugin.
"""
[docs] def __init__(self):
""" Initialises the plugin.
"""
super().__init__()
self.__stats = None
[docs] def get_arg_parser(self):
"""Gets the argument parser containing any CLI arguments for the plugin.
"""
parser = super().get_arg_parser()
parser.add(
'-hp24',
'--max-hp-change-per-day',
required=False,
default=10.0,
type=float,
help='Health Effects: Max amount of health change per day')
parser.add(
'--sun-power',
required=False,
default=2.0,
type=float,
help='Health Effects: Sun HP damage multiplier for vampire mode.')
parser.add(
'--moon-power',
required=False,
default=1.0,
type=float,
help='Health Effects: Moonlight HP restoration\
multiplier for vampire mode.')
parser.add(
'--health-drain',
required=False,
action='store_true',
help='Drains your health over time')
parser.add(
'--health-regen',
required=False,
action='store_true',
help='Restores your health over time')
parser.add(
'--vampire',
required=False,
action='store_true',
help='Enables Vampire mode')
self.print_help = parser.print_help
return parser
[docs] def initialise(self, configuration, habitica_service, data_dir):
""" Initialises the plugin.
Args:
configuration (ArgParse.Namespace): The application configuration.
habitica_service: the Habitica Service instance.
data_dir (str): A writeable directory that the plugin can use for
persistent data.
"""
super().initialise(configuration, habitica_service, data_dir)
[docs] @staticmethod
def supports_dry_runs():
""" The HealthEffects plugin supports dry runs.
Returns:
bool: True
"""
return True
[docs] def get_health_delta(self, hp24=None):
""" Gets the health delta for the current update """
hp24 = abs(hp24 or self._config.max_hp_change_per_day)
interval = self.update_interval_minutes()
return interval * hp24 / (24 * 60)
[docs] def apply_health_delta(self, hp24=None, up=True):
""" Applies the health delta.
Args:
hp24 (float): The health change per 24 hours.
up (bool): If True, then health is increased, otherwise health is
decreased.
Returns:
float: the signed health delta that was applied.
"""
delta = self.get_health_delta(hp24)
old_hp = self.__stats['hp']
if up:
new_hp = min(50, old_hp + delta)
else:
new_hp = max(0, old_hp - delta)
if not self.dry_run:
self._hs.set_hp(new_hp)
return delta if up else -delta
[docs] def poisoned(self):
""" Simple health drain/poisoning """
delta = abs(self.apply_health_delta(up=False))
self.notify('Poisoned. Lost {0:.2} HP'.format(delta), panel=False)
return True
[docs] def regenerating(self):
""" Simple health regeneration """
delta = self.apply_health_delta(up=True)
self.notify('Regenerated {0:.2} HP'.format(delta), panel=False)
return True
[docs] def vampire(self):
""" Vampire mode.
Lose health during daylight hours.
Gain small amounts of health at night, and large amounts by feeding.
"""
logging.getLogger(__name__).debug('You are a Vampire!')
moon_power = self._config.moon_power
sun_power = self._config.sun_power
hp24 = self._config.max_hp_change_per_day
hp24_night = hp24 * moon_power
hp24_day = hp24 * sun_power
print('Moon power multiplier: {0:.4}'.format(moon_power))
print('Sun power multiplier: {0:.4}'.format(sun_power))
print('HP Change per 24 hours: {0:.4}'.format(hp24))
print('Max HP loss during the day: {0:.4}'.format(hp24_day / 2))
print('Max HP gain during the night: {0:.4}'.format(hp24_night / 2))
# determine day or night mode
# Night is 6pm (1800) to 6am (0600)
now = datetime.now()
night = now.hour < 6 or now.hour >= 18
# Are we regenerating or taking damage?
if night:
hp24 = hp24_night
msg = ':full_moon: Ahh, sweet moonlight. {hp:.2} HP per hour'
else:
hp24 = hp24_day
msg = ':sunny: The Sun! It burns! {hp:.2} HP per hour'
# apply the health change
delta = self.apply_health_delta(hp24=hp24, up=night)
delta_per_hour = hp24 / 24
if not night:
delta_per_hour *= -1
print('applied delta', delta)
print('delta_per_hour', delta_per_hour)
# Notifications
self.notify(
msg.format(hp=delta_per_hour),
alias='vampire_notification_panel')
return True
[docs] def summarise_task_score(self, task, now, window):
""" Summarises the task score changes within a time window.
Args:
task (dict): The task.
now (datetime): The most recent time to consider.
window (timedelta): The time window to consider prior to now.
Returns:
float: Total of the score changes within the time window.
int: Number of times the score went up.
int: Number of times the score went down.
"""
if task['type'] == 'todo':
if task['completed']:
return float(task['value']) * float(task['priority']), 1, 0
else:
return 0, 0, 0
if 'history' not in task:
return 0, 0, 0
def pairwise(iterable):
""" pairwise iteration of a sequence.
s -> (s0,s1), (s1,s2), (s2, s3), ...
"""
a, b = itertools.tee(iterable)
next(b, None)
return zip(a, b)
class PairCounter(object):
""" Tracks score movements for a pair of values. """
def __init__(self, scale=1):
""" Initialises the PairCounter.
Args:
multiplier (float): Optional score scale factor.
"""
self.up = 0
self.down = 0
self.sum_delta = 0
self.scale = scale
def count(self, a, b):
""" Count a pair of values. """
delta = (b - a) * self.scale
# print(a, b, delta)
self.sum_delta += delta
if delta > 0:
self.up += 1
elif delta < 0:
self.down += 1
counter = PairCounter()
# counter = PairCounter(3 if task['type'] == 'daily' else 1)
history = [{'date': task['createdAt'], 'value': 0}]
history.extend(task['history'])
# print()
# print(task['text'])
# pprint(history)
for a, b in pairwise(history):
date = scriptabit.parse_date_utc(b['date'])
if date <= now and now - date < window:
counter.count(a['value'], b['value'])
return counter.sum_delta, counter.up, counter.down
[docs] def logistic_growth(
self,
x,
a=50,
b=0.5,
k_x_positive=0.2,
k_x_negative=4.8):
""" Returns the logistic growth function value for a given input x.
y = a / (1 + b * e^(kx))
For k > 0, larger values give a greater rate of change while smaller
values lead to a slower approach to the function asymptotes.
For positive k, the return values is bounded by a to the left
(large negative x) and by 0 to the right (large positive x). For
negative k this is reversed.
Additionally, this method allows different k terms for positive and
negative x, which allows finer-grained tuning of the output.
The y-intercept is given by a / (1 + b).
Args:
x (float): The input value
a (float): The upper limit on the function value.
b (float): Influences the steepness of the function curve, and also
contributes to the y-intercept. High values give a slower change
and a lower y-intercept.
k_x_positive (float): The k term used when x >= 0
k_x_negative (float): The k term used when x < 0. Passing None will
cause k_x_positive to be used for all x.
Returns:
float: The delta.
"""
assert b >= 0
k = k_x_positive if x >= 0 or not k_x_negative else k_x_negative
y = a / (1 + b * math.exp(k * x))
return y
[docs] def update(self):
""" Update the health effects plugin.
Returns: bool: True if further updates are required; False if the plugin
is finished and the application should shut down.
"""
super().update()
# get some user data
self.__stats = self._hs.get_stats()
if self._config.health_drain:
return self.poisoned()
elif self._config.health_regen:
return self.regenerating()
elif self._config.vampire:
return self.vampire()
# elif self._config.test:
# try:
# return self.test()
# except Exception as e:
# logging.getLogger(__name__).error(e)
# import traceback
# traceback.print_exc()
# If no other functions ran, just print the help and exit
self.print_help()
return False