class DragonForm(Action): def __init__(self, name, conf, adv): self.name = name self.conf = conf self.adv = adv self.cancel_by = [] self.interrupt_by = [] self.disabled = False self.shift_event = Event('dragon') self.act_event = Event('dact') self.end_event = Event('dragon_end') self.delayed = set() self.ds_reset() self.act_list = [] self.act_sum = [] self.repeat_act = False self.dx_list = [dx for dx, _ in self.conf.find(r'^dx\d+$')] self.ds_event = Event('ds') self.ds_event.name = 'ds' self.dx_event = Event('dx') self.action_timer = None self.shift_start_time = 0 self.shift_end_timer = Timer(self.d_shift_end) self.idle_event = Event('idle') self.c_act_name = None self.c_act_conf = None self.dracolith_mod = self.adv.Modifier('dracolith', 'att', 'dragon', 0) self.dracolith_mod.get = self.ddamage self.dracolith_mod.off() self.shift_mods = [self.dracolith_mod] self.shift_spd_mod = None self.off_ele_mod = None if self.adv.slots.c.ele != self.adv.slots.d.ele: self.off_ele_mod = self.adv.Modifier('off_ele', 'att', 'dragon', -1/3) self.off_ele_mod.off() self.dragon_gauge = 0 self.dragon_gauge_val = self.conf.gauge_val self.conf.gauge_iv = min(int(self.adv.duration/12), 15) self.dragon_gauge_timer = Timer(self.auto_gauge, timeout=max(1, self.conf.gauge_iv), repeat=1).on() self.dragon_gauge_pause_timer = None self.dragon_gauge_timer_diff = 0 self.max_gauge = 1000 self.shift_cost = 500 self.shift_count = 0 self.shift_silence = False self.is_dragondrive = False self.can_end = True self.allow_end_cd = self.conf.allow_end + (self.conf.ds.startup + self.conf.ds.startup) / self.speed() self.allow_force_end_timer = Timer(self.set_allow_end, timeout=self.allow_end_cd) self.allow_end = False def reset_allow_end(self): if self.is_dragondrive: self.allow_end = True else: log('allow_end', self.allow_end_cd) self.allow_end = False self.allow_force_end_timer = Timer(self.set_allow_end, timeout=self.allow_end_cd) self.allow_force_end_timer.on() self.allow_end_cd += self.conf.allow_end_step def set_allow_end(self, _): self.allow_end = True def set_dragondrive(self, dd_buff, max_gauge=3000, shift_cost=1200, drain=150): self.disabled = False self.is_dragondrive = True self.shift_event = Event('dragondrive') self.dragondrive_end_event = Event('dragondrive_end') ratio = max_gauge / self.max_gauge self.dragon_gauge *= ratio self.dragon_gauge_val *= ratio self.max_gauge = max_gauge self.shift_cost = shift_cost # does not deduct, but need to have this much pt to shift self.drain = drain self.dragondrive_buff = dd_buff self.dragondrive_timer = Timer(self.d_dragondrive_end) return self.dragondrive_buff def set_dragonbattle(self, duration): self.disabled = False self.dragon_gauge = self.max_gauge self.conf.duration = duration self.can_end = False self.repeat_act = True if self.conf.ds['sp_db']: self.skill_sp = self.conf.ds['sp_db'] else: self.skill_sp = self.conf.ds.sp+15 self.skill_spc = self.skill_sp self.skill_use = -1 def end_silence(self, t): self.shift_silence = False def dodge_cancel(self): if len(self.dx_list) <= 0: return False combo = self.conf[self.dx_list[-1]].recovery / self.speed() dodge = self.conf.dodge.startup + self.conf.dodge.recovery return combo > dodge def auto_gauge(self, t): self.charge_gauge(self.dragon_gauge_val) def pause_auto_gauge(self): if self.dragon_gauge_pause_timer is None: self.dragon_gauge_timer_diff = self.dragon_gauge_timer.timing - now() else: self.dragon_gauge_timer_diff = self.dragon_gauge_pause_timer.timing - now() self.dragon_gauge_timer.off() def resume_auto_gauge(self, t): self.dragon_gauge_pause_timer = None self.auto_gauge(t) self.dragon_gauge_timer.on() def add_drive_gauge_time(self, delta, skill_pause=False): max_duration = self.max_gauge/self.drain duration = self.dragondrive_timer.timing - now() max_add = max_duration - duration if skill_pause: add_time = min(delta, max_add) else: add_time = min(delta/self.drain, max_add) duration = self.dragondrive_timer.add(add_time) if duration <= 0: self.d_dragondrive_end('<gauge deplete>') else: self.dragon_gauge = (duration/max_duration)*self.max_gauge if add_time != 0: log('drive_time' if not skill_pause else 'skill_pause', f'{add_time:+2.4}', f'{duration:2.4}', f'{int(self.dragon_gauge)}/{int(self.max_gauge)}') def charge_gauge(self, value, utp=False, dhaste=True): # if dhaste is None: # dhaste = not utp dh = self.adv.mod('dh') if dhaste else 1 value = self.adv.sp_convert(dh, value) delta = min(self.dragon_gauge+value, self.max_gauge) - self.dragon_gauge if self.is_dragondrive and self.dragondrive_buff.get(): self.add_drive_gauge_time(delta) elif delta != 0: self.dragon_gauge += delta if utp: log('dragon_gauge', '{:+} utp'.format(int(delta)), f'{int(self.dragon_gauge)}/{int(self.max_gauge)}', value) else: log('dragon_gauge', '{:+.2f}%'.format(delta/self.max_gauge*100), '{:.2f}%'.format(self.dragon_gauge/self.max_gauge*100)) @allow_acl def dtime(self): return self.conf.dshift.startup + self.conf.duration * self.adv.mod('dt') + self.conf.exhilaration * (self.off_ele_mod is None) @allow_acl def ddamage(self): return self.conf.dracolith + self.adv.mod('da') - 1 def ds_check(self): return self.skill_use != 0 and self.skill_spc >= self.skill_sp def ds_charge(self, value): if self.skill_use != 0 and self.skill_spc < self.skill_sp: self.skill_spc += self.adv.sp_convert(self.adv.sp_mod('x'), value) if self.skill_spc > self.skill_sp: self.skill_spc = self.skill_sp log(self.c_act_name, 'sp', f'{self.skill_spc}/{self.skill_sp}') def ds_reset(self): self.skill_use = self.conf.ds.uses self.skill_sp = self.conf.ds.sp self.skill_spc = self.skill_sp def d_shift_end(self, t): if self.action_timer is not None: self.action_timer.off() self.action_timer = None duration = now()-self.shift_start_time shift_dmg = g_logs.shift_dmg g_logs.log_shift_dmg(False) count = self.clear_delayed() if count > 0: log('cancel', self.c_act_name, f'by shift end', f'lost {count} hit{"s" if count > 1 else ""}') log(self.name, '{:.2f}dmg / {:.2f}s, {:.2f} dps'.format(shift_dmg, duration, shift_dmg/duration), ' '.join(self.act_sum)) self.act_sum = [] self.act_list = [] if self.off_ele_mod is not None: self.off_ele_mod.off() if self.shift_spd_mod is not None: self.shift_spd_mod.off() self.ds_reset() if not self.is_dragondrive: self.shift_silence = True Timer(self.end_silence).on(10) self.dragon_gauge_pause_timer = Timer(self.resume_auto_gauge).on(self.dragon_gauge_timer_diff) self.status = Action.OFF self._setprev() # turn self from doing to prev self._static.doing = self.nop self.end_event() self.idle_event() def d_dragondrive_end(self, t): self.dragon_gauge = 0 log('dragondrive', 'end', t if isinstance(t, str) else '<timeout>') self.dragondrive_buff.off() self.shift_silence = True Timer(self.end_silence).on(10) self.status = Action.OFF self._setprev() # turn self from doing to prev self._static.doing = self.nop self.dragondrive_end_event() self.idle_event() def act_timer(self, act, time, next_action=None): if self.c_act_name == 'dodge': self.action_timer = Timer(act, time) else: self.action_timer = Timer(act, time / self.speed()) self.action_timer.next_action = next_action return self.action_timer.on() def d_act_start_t(self, t): self.action_timer = None self.d_act_start(t.next_action) def d_act_start(self, name): if name in self.conf and self._static.doing == self and self.action_timer is None: self.prev_act = self.c_act_name self.prev_conf = self.c_act_conf self.c_act_name = name self.c_act_conf = self.conf[name] self.act_timer(self.d_act_do, self.c_act_conf.startup) def d_act_do(self, t): if self.c_act_name == 'end': self.d_shift_end(None) self.shift_end_timer.off() return actconf = self.conf[self.c_act_name] e = self.act_event e.name = self.c_act_name e.base = self.c_act_name e.group = 'dragon' self.adv.actmod_on(e) try: getattr(self.adv, f'{self.c_act_name}_before')(e) except AttributeError: pass final_mt = self.adv.schedule_hits(e, self.conf[self.c_act_name]) if final_mt: final_mt.actmod = True final_mt.actmod = True try: final_mt.proc = getattr(self.adv, f'{self.c_act_name}_proc') except AttributeError: pass else: self.adv.actmod_off(e) try: getattr(self.adv, f'{self.c_act_name}_proc')(e) except AttributeError: pass if self.c_act_name == 'ds': self.skill_use -= 1 self.skill_spc = 0 self.act_sum.append('s') self.ds_event() self.shift_end_timer.add(actconf.startup+actconf.recovery) elif self.c_act_name.startswith('dx'): if len(self.act_sum) > 0 and self.act_sum[-1][0] == 'c' and int(self.act_sum[-1][1]) < int(self.c_act_name[-1]): self.act_sum[-1] = 'c'+self.c_act_name[-1] else: self.act_sum.append('c'+self.c_act_name[-1]) self.dx_event.index = int(self.c_act_name[-1]) self.dx_event() self.d_act_next() def d_act_next(self): nact = None if self.repeat_act and not self.act_list: self.parse_act(self.conf.act) if self.act_list: if self.act_list[0] != 'ds' or self.ds_check(): if self.act_list[0] == 'end' and not self.allow_end: nact = None else: nact = self.act_list.pop(0) # print('CHOSE BY LIST', nact, self.c_act_name) if nact is None: if self.c_act_name[0:2] == 'dx': nact = 'dx{}'.format(int(self.c_act_name[2])+1) if not nact in self.dx_list: if self.ds_check(): nact = 'ds' elif self.dodge_cancel(): nact = 'dodge' else: nact = 'dx1' else: nact = 'dx1' # print('CHOSE BY DEFAULT', nact, self.c_act_name) if nact == 'ds' or nact == 'dodge' or (nact == 'end' and self.c_act_name != 'ds'): # cancel count = self.clear_delayed() if count > 0: log('cancel', self.c_act_name, f'by {nact}', f'lost {count} hit{"s" if count > 1 else ""}') self.act_timer(self.d_act_start_t, self.conf.latency, nact) else: # regular recovery self.act_timer(self.d_act_start_t, self.c_act_conf.recovery, nact) def parse_act(self, act_str): if self.status != Action.OFF and not self.repeat_act: return act_str = act_str.strip() self.act_list = [] skill_usage = 0 for a in act_str.split('-'): if a[0] == 'c' or a[0] == 'x': for i in range(1, int(a[1])+1): dxseq = 'dx{}'.format(i) if dxseq in self.dx_list: self.act_list.append(dxseq) try: if self.dodge_cancel() or self.act_list[-1] != self.dx_list[-1]: self.act_list.append('dodge') except IndexError: pass else: try: if len(self.act_list) > 0 and self.act_list[-1] == 'dodge': self.act_list.pop() except IndexError: pass if (a == 's' or a == 'ds') and (self.skill_use <= -1 or skill_usage < self.skill_use): self.act_list.append('ds') skill_usage += 1 elif a == 'end' and self.can_end: self.act_list.append('end') elif a == 'dodge': self.act_list.append('dodge') def act(self, act_str): self.parse_act(act_str) return self() @allow_acl def check(self, dryrun=True): if self.disabled or self.shift_silence: return False if self.dragon_gauge < self.shift_cost and not (self.is_dragondrive and self.dragondrive_buff.get()): return False doing = self.getdoing() if not doing.idle: if isinstance(doing, S) or isinstance(doing, DragonForm): return False if dryrun == False: if doing.status == Action.STARTUP: doing.startup_timer.off() log('interrupt', doing.name , 'by '+self.name, 'after {:.2f}s'.format(now()-doing.startup_start)) elif doing.status == Action.RECOVERY: doing.recovery_timer.off() log('cancel', doing.name , 'by '+self.name, 'after {:.2f}s'.format(now()-doing.recover_start)) return True def __call__(self): if not self.check(dryrun=False): return False if self.is_dragondrive: self.act_list = ['end'] if self.dragondrive_buff.get(): self.d_dragondrive_end('<turn off>') return True else: log('cast', 'dragondrive', self.name, f'base duration {self.dragon_gauge/self.drain:.4}s') self.dragondrive_timer.on(self.dragon_gauge/self.drain) self.dragondrive_buff.on() else: log('cast', 'dshift', self.name) if len(self.act_list) == 0: self.parse_act(self.conf.act) self.dragon_gauge -= self.shift_cost if self.off_ele_mod is not None: self.off_ele_mod.on() if self.shift_spd_mod is not None: self.shift_spd_mod.on() self.pause_auto_gauge() self.shift_count += 1 self.status = Action.STARTUP self._setdoing() g_logs.log_shift_dmg(True) self.shift_start_time = now() self.shift_end_timer.on(self.dtime()) self.reset_allow_end() self.shift_event() self.d_act_start('dshift') return True
class Buff(object): MAXHP_CAP = 1.30 _static = Static({ 'all_buffs': [], 'adv': None }) def __init__(self, name='<buff_noname>', value=0, duration=0, mtype='att', morder=None, modifier=None, hidden=False): self.name = name self.__value = value self.duration = duration self.mod_type = mtype self.mod_order = morder or ('chance' if self.mod_type == 'crit' else 'buff') self.bufftype = 'misc' if hidden else 'self' self.bufftime = self._bufftime if self.duration > 0 else self._no_bufftime self.buff_end_timer = Timer(self.buff_end_proc) if modifier: self.modifier = modifier self.get = self.modifier.get elif mtype != 'effect': self.modifier = Modifier('mod_' + self.name, self.mod_type, self.mod_order, 0) self.modifier.get = self.get else: self.modifier = None self.dmg_test_event = Event('dmg_formula') self.dmg_test_event.dmg_coef = 1 self.dmg_test_event.dname = 'test' self.hidden = hidden self.__stored = 0 self.__active = 0 # self.on() def logwrapper(self, *args): if not self.hidden: log('buff', *args) def _no_bufftime(self): return 1 def _ex_bufftime(self): return 1 + self._static.adv.sub_mod('buff', 'ex') def _bufftime(self): return self._static.adv.mod('buff', operator=operator.add) def _debufftime(self): return self._static.adv.mod('debuff', operator=operator.add) def no_bufftime(self): self.bufftime = self._no_bufftime return self def ex_bufftime(self): self.bufftime = self._ex_bufftime return self def value(self, newvalue=None): if newvalue: self.logwrapper(self.name, f'{self.mod_type}({self.mod_order}): {newvalue:.02f}', 'buff value change') return self.set(newvalue) else: return self.get() def get(self): if self.__active: return self.__value else: return 0 def set(self, v, d=None): self.__value = v if d != None: self.duration = d return self def stack(self): stack = 0 for i in self._static.all_buffs: if i.name == self.name: if i.__active != 0: stack += 1 return stack def valuestack(self): stack = 0 value = 0 for i in self._static.all_buffs: if i.name == self.name: if i.__active != 0: stack += 1 value += i.__value return value, stack def effect_on(self): return self.modifier and self.modifier.on() def effect_off(self): return self.modifier and self.modifier.off() def buff_end_proc(self, e): self.logwrapper(self.name, f'{self.mod_type}({self.mod_order}): {self.value():.02f}', 'buff end <timeout>') self.__active = 0 if self.__stored: idx = len(self._static.all_buffs) while 1: idx -= 1 if idx < 0: break if self == self._static.all_buffs[idx]: self._static.all_buffs.pop(idx) break self.__stored = 0 value, stack = self.valuestack() if stack > 0: self.logwrapper(self.name, f'{self.mod_type}({self.mod_order}): {value:.02f}', f'buff stack <{stack}>') self.effect_off() def count_team_buff(self): base_mods = [ Modifier('base_cc', 'crit', 'chance', 0.12), Modifier('base_killer', 'killer','passive', 0.30) ] self.dmg_test_event.modifiers = ModifierDict() for mod in base_mods: self.dmg_test_event.modifiers.append(mod) for b in filter(lambda b: b.get() and b.bufftype == 'simulated_def', self._static.all_buffs): self.dmg_test_event.modifiers.append(b.modifier) self.dmg_test_event() no_team_buff_dmg = self.dmg_test_event.dmg placeholders = [] for b in filter(lambda b: b.get() and b.bufftype in ('team', 'debuff'), self._static.all_buffs): placehold = None if b.modifier.mod_type == 's': placehold = Modifier('placehold_sd', 'att', 'sd', b.modifier.get() / 2) elif b.modifier.mod_type == 'spd': placehold = Modifier('placehold_spd', 'att', 'spd', b.modifier.get()) elif b.modifier.mod_type.endswith('_killer'): placehold = Modifier('placehold_k', 'killer', 'passive', b.modifier.get()) if placehold: self.dmg_test_event.modifiers.append(placehold) placeholders.append(placehold) else: self.dmg_test_event.modifiers.append(b.modifier) self.dmg_test_event() team_buff_dmg = self.dmg_test_event.dmg log('buff', 'team', team_buff_dmg / no_team_buff_dmg - 1) for mod in chain(base_mods, placeholders): mod.off() def on(self, duration=None): if self.mod_type == 'maxhp': max_hp = self._static.adv.mod('maxhp') if max_hp >= Buff.MAXHP_CAP: return self value = self.__value mod_val = min(value, max(Buff.MAXHP_CAP-max_hp, 0)) self._static.adv.set_hp((self._static.adv.hp*max_hp+value*100)/(max_hp+mod_val)) d = (duration or self.duration) * self.bufftime() if self.__active == 0: self.__active = 1 if self.__stored == 0: self._static.all_buffs.append(self) self.__stored = 1 if d >= 0: self.buff_end_timer.on(d) proc_type = 'start' else: if d >= 0: self.buff_end_timer.on(d) proc_type = 'refresh' self.logwrapper(self.name, f'{self.mod_type}({self.mod_order}): {self.value():.02f}', f'buff {proc_type} <{d:.02f}s>') value, stack = self.valuestack() if stack > 1: log('buff', self.name, f'{self.mod_type}({self.mod_order}): {value:.02f}', f'buff stack <{stack}>') if self.mod_type == 'defense': Event('defchain').on() if self.bufftype == 'team': log('buff', 'team_defense', 'proc team doublebuffs') if self.mod_type == 'regen': # may need to make this part global since game always regen all stacks at same ticks self.set_hp_event = Event('set_hp') self.set_hp_event.delta = self.get() self.regen_timer = Timer(self.hp_regen, 3.9, True) else: self.effect_on() return self def hp_regen(self, t): self.set_hp_event() def off(self): if self.__active == 0: return self.logwrapper(self.name, f'{self.mod_type}({self.mod_order}): {self.value():.02f}', f'buff end <turn off>') self.__active = 0 self.effect_off() self.buff_end_timer.off() return self def timeleft(self): return -1 if self.duration == -1 else (self.buff_end_timer.timing-now()) def add_time(self, delta): self.buff_end_timer.add(delta)
class DragonForm: def __init__(self, name, conf, adv, dragon, dform_mode=-1, unique_dform=False): self.name = name self.conf = conf self.adv = adv self.dragon = dragon self.dform_mode = dform_mode self.unique_dform = unique_dform self.shift_start_proc = None self.shift_end_proc = None self.config_actions() # events self.status = False self.disabled_reasons = set() self.dp_event = Event("dp") self.shift_end_timer = Timer(self.d_shift_end) self.shift_silence_timer = Timer(None, 10) self.can_end = True self.l_shift = Listener("dshift", self.d_shift_start, order=2) self.l_s = Listener("s", self.l_ds_pause, order=0) self.l_s_end = Listener("s_end", self.l_ds_resume, order=0) self.l_s_final_end = Listener("s_end", self.d_shift_end, order=0) self.l_s.off() self.l_s_end.off() self.l_s_final_end.off() self.shift_skills = ("ds1", "ds2") self.allow_end_cd = self.conf.allow_end + self.dstime() self.shift_end_reason = None # mods self.dracolith_mod = self.adv.Modifier("dracolith", "ex", "dragon", 0) self.dracolith_mod.get = self.ddamage self.dracolith_mod.off() self.shift_mods = [self.dracolith_mod] self.shift_spd_mod = None self.off_ele = self.adv.slots.c.ele != self.conf.d.ele self.previous_x = DEFAULT # gauge self.auto_gauge_val = 0.1 # percent self.auto_gauge_iv = min(int(self.adv.duration / 12), 15) self.dragon_gauge_timer = Timer(self.auto_gauge, timeout=max(1, self.auto_gauge_iv), repeat=1).on() self.dragon_gauge_pause_timer = None self.dragon_gauge_timer_diff = 0 self.dragon_gauge = 0 self.max_dragon_gauge = 1000 self.shift_cost = 500 self.log_utp = False # dragonbattle self.is_dragonbattle = False def set_dragonbattle(self): self.is_dragonbattle = True if self.dform_mode == 0: # ddrive chara cant do dragon battle self.adv.stop() return for skey in self.shift_skills: self.adv.a_s_dict[skey].dragonbattle_skill = True self.dragon_gauge += self.shift_cost g_logs.log_dact_as_act = True def config_actions(self): # merge confs into adv conf self.dx_max = 0 for dx, dxconf in self.conf.find(r"^dx\d+$"): self.adv.conf[dx] = dxconf self.dx_max = max(self.dx_max, int(dx[2:])) self.default_x_loop = self.conf["default_x_loop"] or self.dx_max # the default combo idx to end combo on for fs, fsconf in self.conf.find(r"^dfs\d*(_[A-Za-z0-9]+)?$"): self.adv.conf[fs] = fsconf if not self.unique_dform: for sfn in ("before", "proc"): self.adv.rebind_function(self.dragon, f"{fs}_{sfn}", f"{fs}_{sfn}", overwrite=False) self.ds_final = None for ds, dsconf in self.conf.find(r"^ds\d*(_[A-Za-z0-9]+)?$"): self.adv.conf[ds] = dsconf ds_base = ds.split("_")[0] if not self.unique_dform: for sfn in ("before", "proc"): self.adv.rebind_function(self.dragon, f"{ds_base}_{sfn}", f"{ds_base}_{sfn}", overwrite=False) if ds.startswith("ds99") or dsconf.get("final"): self.ds_final = ds.split("_")[0] # make separate dodge action, may want to handle forward/backward later self.d_dodge = Dodge("dodge", self.conf.dodge) self.d_shift = Shift("dshift", self.name, self.conf.dshift) self.d_end = Shift("dend", self.name, self.conf.dend) self.shift_event = Event("dragon") self.end_event = Event("dragon_end") if abs(self.dform_mode) == 1: try: self.shift_start_proc = self.dragon.shift_start_proc except AttributeError: pass try: self.shift_end_proc = self.dragon.shift_end_proc except AttributeError: pass def in_dform(self): return self.status def in_ddrive(self): return False @property def shift_silence(self): return bool(self.shift_silence_timer.online) def set_disabled(self, reason): if self.disabled_reasons is not None: self.disabled_reasons.add(reason) def unset_disabled(self, reason): if self.disabled_reasons is not None: self.disabled_reasons.discard(reason) @property def disabled(self): return bool(self.disabled_reasons) def set_dacts_enabled(self, enabled): for xact in self.adv.a_x_dict[DRG].values(): xact.enabled = enabled fsact = self.adv.a_fs_dict.get("dfs") if fsact: fsact.set_enabled(enabled) for skey in self.shift_skills: self.adv.a_s_dict[skey].set_enabled(enabled) def auto_dodge(self, index=None): index = index or self.dx_max d_combo = self.adv.a_x_dict[DRG][index] if "dodge" not in d_combo.conf.cancel: return False dodge_t = d_combo.conf.cancel["dodge"] + self.d_dodge.getstartup() + self.d_dodge.getrecovery() if d_combo.conf.cancel["dx1"]: combo_t = d_combo.conf.cancel["dx1"] / d_combo.speed() else: combo_t = d_combo.getrecovery() return dodge_t < combo_t def dx_dodge_or_wait(self, index): # log("dx_dodge_or_skill", "x{}: c on {}, s on {}".format(index, self.default_ds_x, self.default_x_loop), self.adv.ds1.check()) if index == self.default_x_loop: return self.d_dodge if self.auto_dodge(index=index) else False return False def pause_auto_gauge(self): if self.is_dragonbattle: return if self.dragon_gauge_pause_timer is None: self.dragon_gauge_timer_diff = self.dragon_gauge_timer.timing - now() else: self.dragon_gauge_timer_diff = self.dragon_gauge_pause_timer.timing - now() self.dragon_gauge_timer.off() def resume_auto_gauge(self, t): self.dragon_gauge_pause_timer = None self.auto_gauge(t) self.dragon_gauge_timer.on() def l_ds_pause(self, e): if self.status and e.base in self.shift_skills: self.shift_end_timer.pause() log("shift_end_timer", "pause", self.shift_end_timer.timing, self.shift_end_timer.pause_time) def l_ds_resume(self, e): if self.status and e.act.base in self.shift_skills: self.shift_end_timer.resume() log("shift_end_timer", "resume", self.shift_end_timer.timing, self.shift_end_timer.timing - now()) @allow_acl def dtime(self): return self.conf.duration * self.adv.mod("dt") + self.conf.exhilaration * int(not self.off_ele) def dstime(self): try: return (self.conf.ds.startup + self.conf.ds.recovery) / self.d_shift.speed() except TypeError: return 0 def reset_allow_end(self): self.allow_force_end_timer = Timer(None, timeout=self.allow_end_cd) self.allow_force_end_timer.on() self.allow_end_cd = min(self.allow_end_cd + self.conf.allow_end_step, self.dtime()) @property def allow_end(self): return not bool(self.allow_force_end_timer.online) def dhaste(self): return self.adv.mod("dh", operator=operator.add) def _charge_dp(self, name, value): gauge_before = self.dragon_gauge self.dragon_gauge = max(0, min(self.max_dragon_gauge, self.dragon_gauge + value)) delta = self.dragon_gauge - gauge_before if delta != 0 and not self.log_utp: log( "dgauge", name, f"{int(delta):+}", f"{int(self.dragon_gauge)}/{int(self.max_dragon_gauge)} [{self.dragon_gauge//self.shift_cost}]", ) self.dp_event.name = name self.dp_event.value = value self.dp_event.delta = delta self.dp_event.gauge = self.dragon_gauge self.dp_event() return value def charge_dprep(self, value): return self._charge_dp("dprep", float_ceil(self.max_dragon_gauge, value / 100)) def charge_dp(self, name, value): return self._charge_dp(name, float_ceil(value, self.dhaste())) def auto_gauge(self, t): self._charge_dp("auto", float_ceil(self.max_dragon_gauge * self.auto_gauge_val, self.dhaste())) @allow_acl def ddamage(self): return self.conf.dracolith + self.adv.mod("da", operator=operator.add, initial=0) def extend_shift_time(self, value, percent=True): max_d = self.dtime() - self.conf.dshift.startup cur_d = self.shift_end_timer.timeleft() delta_t = value if percent: delta_t *= max_d delta_t = min(max_d, cur_d + delta_t) - cur_d if cur_d + delta_t > 0: self.shift_end_timer.add(delta_t) log("shift_time", f"{delta_t:+2.4}", f"{cur_d+delta_t:2.4}") elif self.can_end: self.d_shift_end() self.shift_end_timer.off() def d_shift_start(self, _=None): self.status = True self.dragon_gauge -= self.shift_cost g_logs.set_log_shift(shift_name=self.name) if self.shift_start_proc: self.shift_start_proc() if self.off_ele: self.adv.element = self.adv.slots.d.ele if self.shift_spd_mod is not None: self.shift_spd_mod.on() self.pause_auto_gauge() for s in self.adv.dskills: s.reset_uses() self.adv.set_dacl(True) self.l_s.on() self.l_s_end.on() self.set_dacts_enabled(True) self.adv.charge_p("dshift", 1.0, dragon_sp=True) log("shift_end_timer", "on", self.dtime()) self.shift_end_timer.on(self.dtime()) self.reset_allow_end() self.shift_event() def d_shift_end(self, e=None, reason="timeout"): if not self.status or self.is_dragonbattle: return False doing = self.d_shift.getdoing() if self.ds_final and not (isinstance(doing, S) and doing.base in self.shift_skills) and not self.l_s_final_end.get(): ds_final = self.adv.a_s_dict[self.ds_final] if ds_final.ac.conf.final and ds_final.ac.enabled: self.shift_end_reason = reason self.l_s_final_end.on() ds_final.reset_uses() ds_final.charged = ds_final.sp ds_final() self.set_dacts_enabled(False) return False if self.off_ele: self.adv.element = self.adv.slots.c.ele if self.shift_spd_mod is not None: self.shift_spd_mod.off() self.shift_silence_timer.on() if self.dragon_gauge_timer_diff > 0: self.dragon_gauge_pause_timer = Timer(self.resume_auto_gauge).on(self.dragon_gauge_timer_diff) self.l_s.off() self.l_s_end.off() self.l_s_final_end.off() self.set_dacts_enabled(False) self.adv.set_dacl(False) self.end_event() self.d_end() self.status = False g_logs.set_log_shift(end_reason=self.shift_end_reason or reason) self.shift_end_reason = None if self.shift_end_proc: self.shift_end_proc() return True def d_shift_partial_end(self): if self.status and abs(self.dform_mode) == 1: g_logs.set_log_shift(end_reason="partial") @allow_acl def check(self): if self.disabled_reasons or self.shift_silence: return False if self.dragon_gauge < self.shift_cost: return False if self.status: return False doing = self.d_shift.getdoing() if not doing.idle: if isinstance(doing, S): return False return True def shift(self): if not self.check(): return False return self.d_shift() def sack(self): if self.status and not self.l_s_final_end.get() and self.allow_end: self.d_shift_end(reason="forced") self.shift_end_timer.off() return True return False
class DragonForm(Action): def __init__(self, name, conf, adv, ds_proc): self.name = name self.conf = conf self.adv = adv self.cancel_by = [] self.interrupt_by = [] self.disabled = False self.shift_event = Event('dragon') self.end_event = Event('dragon_end') self.ds_proc = ds_proc self.skill_use = self.conf.skill_use self.act_list = [] self.act_sum = [] self.dx_list = [ 'dx{}'.format(i) for i in range(1, 6) if 'dmg' in self.conf['dx{}'.format(i)] ] self.ds_event = Event('s') self.ds_event.name = 'ds' self.action_timer = None self.shift_start_time = 0 self.shift_damage_sum = 0 self.shift_end_timer = Timer(self.d_shift_end) self.idle_event = Event('idle') self.c_act_name = None self.c_act_conf = None self.dracolith_mod = self.adv.Modifier('dracolith', 'att', 'dragon', 0) self.dracolith_mod.off() self.off_ele_mod = None if self.adv.slots.c.ele != self.adv.slots.d.ele: self.off_ele_mod = self.adv.Modifier('off_ele', 'att', 'dragon', -1 / 3) self.off_ele_mod.off() self.dragon_gauge = 0 self.dragon_gauge_timer = Timer(self.auto_gauge, repeat=1).on( max(1, self.conf.gauge_iv)) self.max_gauge = 1000 self.shift_cost = 500 self.shift_count = 0 self.shift_silence = False self.is_dragondrive = False def set_dragondrive(self, dd_buff): self.is_dragondrive = True self.shift_event = Event('dragondrive') self.dragondrive_end_event = Event('dragondrive_end') self.max_gauge = 3000 self.shift_cost = 1200 # does not deduct, but need to have this much pt to shift self.dragondrive_buff = dd_buff self.dragondrive_timer = Timer(self.d_dragondrive_end) def end_silence(self, t): self.shift_silence = False def dodge_cancel(self): if len(self.dx_list) <= 0: return False combo = self.conf[self.dx_list[-1]].recovery / self.speed() dodge = self.conf.dodge.startup return combo > dodge def auto_gauge(self, t): self.charge_gauge(self.conf.gauge_val) def add_drive_gauge_time(self, delta): duration = self.dragondrive_timer.timing - now() max_add = 20 - duration add_time = min((20 * delta) / self.max_gauge, max_add) self.dragondrive_timer.add(add_time) duration = self.dragondrive_timer.timing - now() if duration <= 0: self.d_dragondrive_end(None) self.dragon_gauge = 0 else: self.dragon_gauge = (duration / 20) * self.max_gauge log('drive_time', f'{add_time:+2.4}', f'{duration:2.4}', '{:.2f}%'.format(self.dragon_gauge / self.max_gauge * 100)) def charge_gauge(self, value, percent=True): # if self.status != -1: # ignore dragonform blocking gauge (as it would in game) to avoid break-pointy bullshit dh = 1 if self.is_dragondrive else self.adv.mod('dh') if percent: value *= 10 value = self.adv.sp_convert(dh, value) delta = min(self.dragon_gauge + value, self.max_gauge) - self.dragon_gauge if self.is_dragondrive and self.dragondrive_buff.get(): self.add_drive_gauge_time(delta) elif delta > 0: self.dragon_gauge += delta log('dragon_gauge', '{:+.2f}%'.format(delta / self.max_gauge * 100), '{:.2f}%'.format(self.dragon_gauge / self.max_gauge * 100)) def dtime(self): return self.conf.dshift.startup + self.conf.duration * self.adv.mod( 'dt') + self.conf.exhilaration * (self.off_ele_mod is None) def ddamage(self): return self.conf.dracolith + self.adv.mod('da') - 1 def d_shift_end(self, t): if self.action_timer is not None: self.action_timer.off() self.action_timer = None duration = now() - self.shift_start_time log( self.name, '{:.2f}dmg / {:.2f}s, {:.2f} dps'.format( self.shift_damage_sum, duration, self.shift_damage_sum / duration), ' '.join(self.act_sum)) self.act_sum = [] self.act_list = [] self.dracolith_mod.off() if self.off_ele_mod is not None: self.off_ele_mod.off() self.skill_use = self.conf.skill_use self.shift_silence = True Timer(self.end_silence).on(10) self.status = -2 self._setprev() # turn self from doing to prev self._static.doing = self.nop self.end_event() self.idle_event() def d_dragondrive_end(self, t): log('dragondrive', 'end') self.dragondrive_buff.off() Timer(self.end_silence).on(10) self.status = -2 self._setprev() # turn self from doing to prev self._static.doing = self.nop self.dragondrive_end_event() self.idle_event() def act_timer(self, act, time, next_action=None): if self.c_act_name == 'dodge': self.action_timer = Timer(act, time) else: self.action_timer = Timer(act, time / self.speed()) self.action_timer.next_action = next_action return self.action_timer.on() def d_act_start_t(self, t): self.action_timer = None self.d_act_start(t.next_action) def d_act_start(self, name): if name in self.conf and self._static.doing == self and self.action_timer is None: self.prev_act = self.c_act_name self.prev_conf = self.c_act_conf self.c_act_name = name self.c_act_conf = self.conf[name] self.act_timer(self.d_act_do, self.c_act_conf.startup) def d_act_do(self, t): if self.c_act_name == 'ds': self.skill_use -= 1 self.ds_event() self.shift_damage_sum += self.ds_proc() or 0 self.shift_end_timer.add(self.conf.ds.startup + self.conf.ds.recovery) self.act_sum.append('s') elif self.c_act_name == 'end': self.d_shift_end(None) self.shift_end_timer.off() return elif self.c_act_name != 'dodge': # dname = self.c_act_name[:-1] if self.c_act_name != 'dshift' else self.c_act_name self.shift_damage_sum += self.adv.dmg_make(self.c_act_name, self.c_act_conf.dmg) if self.c_act_name.startswith('dx'): if len(self.act_sum ) > 0 and self.act_sum[-1][0] == 'c' and int( self.act_sum[-1][1]) < int(self.c_act_name[-1]): self.act_sum[-1] = 'c' + self.c_act_name[-1] else: self.act_sum.append('c' + self.c_act_name[-1]) if self.c_act_conf.hit > -1: self.adv.hits += self.c_act_conf.hit else: self.adv.hits = -self.c_act_conf.hit self.d_act_next() def d_act_next(self): if len(self.act_list) > 0: nact = self.act_list.pop(0) else: if self.c_act_name[0:2] == 'dx': nact = 'dx{}'.format(int(self.c_act_name[2]) + 1) if not nact in self.dx_list: if self.skill_use > 0: nact = 'ds' elif self.dodge_cancel(): nact = 'dodge' else: nact = 'dx1' else: nact = 'dx1' if nact == 'ds' or nact == 'dodge' or ( nact == 'end' and self.c_act_name != 'ds'): # cancel self.act_timer(self.d_act_start_t, self.conf.latency, nact) else: # regular recovery self.act_timer(self.d_act_start_t, self.c_act_conf.recovery, nact) def parse_act(self, act_str): act_str = act_str.strip() self.act_list = [] skill_usage = 0 for a in act_str.split(' '): if a[0] == 'c' or a[0] == 'x': for i in range(1, int(a[1]) + 1): dxseq = 'dx{}'.format(i) if dxseq in self.dx_list: self.act_list.append(dxseq) if self.dodge_cancel( ) or self.act_list[-1] != self.dx_list[-1]: self.act_list.append('dodge') else: if len(self.act_list) > 0 and self.act_list[-1] == 'dodge': self.act_list.pop() if (a == 's' or a == 'ds') and skill_usage < self.skill_use: self.act_list.append('ds') skill_usage += 1 elif a == 'end': self.act_list.append('end') elif a == 'dodge': self.act_list.append('dodge') def act(self, act_str): self.parse_act(act_str) return self() def __call__(self): if self.disabled or self.shift_silence or self.dragon_gauge < self.shift_cost: return False doing = self.getdoing() if not doing.idle: if isinstance(doing, S) or isinstance(doing, DragonForm): return False if doing.status == -1: doing.startup_timer.off() log('interrupt', doing.name, 'by ' + self.name, 'after {:.2f}s'.format(now() - doing.startup_start)) elif doing.status == 1: doing.recovery_timer.off() log('cancel', doing.name, 'by ' + self.name, 'after {:.2f}s'.format(now() - doing.recover_start)) self.shift_count += 1 if self.is_dragondrive: self.act_list = ['end'] if self.dragondrive_buff.get(): self.d_dragondrive_end(None) return True else: self.dragondrive_timer.on(20) self.dragondrive_buff.on() else: if len(self.act_list) == 0: self.parse_act(self.conf.act) self.dragon_gauge -= self.shift_cost self.dracolith_mod.mod_value = self.ddamage() self.dracolith_mod.on() if self.off_ele_mod is not None: self.off_ele_mod.on() self.shift_damage_sum = 0 self.status = -1 self._setdoing() self.shift_start_time = now() self.shift_end_timer.on(self.dtime()) self.shift_event() log('cast', 'dshift', self.name) self.d_act_start('dshift') return True
class DragonForm(Action): def __init__(self, name, conf, adv, ds_proc): self.name = name self.conf = conf self.adv = adv self.cancel_by = [] self.interrupt_by = [] self.disabled = False self.shift_event = Event('dragon') self.end_event = Event('dragon_end') self.ds_proc = ds_proc self.ds_reset() self.act_list = [] self.act_sum = [] self.dx_list = [ 'dx{}'.format(i) for i in range(1, 6) if 'dmg' in self.conf['dx{}'.format(i)] ] self.ds_event = Event('s') self.ds_event.name = 'ds' self.action_timer = None self.shift_start_time = 0 self.shift_damage_sum = 0 self.shift_end_timer = Timer(self.d_shift_end) self.idle_event = Event('idle') self.c_act_name = None self.c_act_conf = None self.dracolith_mod = self.adv.Modifier('dracolith', 'att', 'dragon', 0) self.dracolith_mod.off() self.off_ele_mod = None if self.adv.slots.c.ele != self.adv.slots.d.ele: self.off_ele_mod = self.adv.Modifier('off_ele', 'att', 'dragon', -1 / 3) self.off_ele_mod.off() self.dragon_gauge = 0 self.dragon_gauge_val = self.conf.gauge_val self.dragon_gauge_timer = Timer(self.auto_gauge, timeout=max(1, self.conf.gauge_iv), repeat=1).on() self.dragon_gauge_timer_diff = 0 self.max_gauge = 1000 self.shift_cost = 500 self.shift_count = 0 self.shift_silence = False self.is_dragondrive = False def set_dragondrive(self, dd_buff, max_gauge=3000, shift_cost=1200, drain=150): self.disabled = False self.is_dragondrive = True self.shift_event = Event('dragondrive') self.dragondrive_end_event = Event('dragondrive_end') ratio = max_gauge / self.max_gauge self.dragon_gauge *= ratio self.dragon_gauge_val *= ratio self.max_gauge = max_gauge self.shift_cost = shift_cost # does not deduct, but need to have this much pt to shift self.drain = drain self.dragondrive_buff = dd_buff self.dragondrive_timer = Timer(self.d_dragondrive_end) return self.dragondrive_buff def end_silence(self, t): self.shift_silence = False def dodge_cancel(self): if len(self.dx_list) <= 0: return False combo = self.conf[self.dx_list[-1]].recovery / self.speed() dodge = self.conf.dodge.startup return combo > dodge def auto_gauge(self, t): self.charge_gauge(self.dragon_gauge_val) def pause_auto_gauge(self): self.dragon_gauge_timer_diff = self.dragon_gauge_timer.timing - now() self.dragon_gauge_timer.off() def resume_auto_gauge(self, t): self.auto_gauge(t) self.dragon_gauge_timer.on() def add_drive_gauge_time(self, delta, skill_pause=False): max_duration = self.max_gauge / self.drain duration = self.dragondrive_timer.timing - now() max_add = max_duration - duration if skill_pause: add_time = min(delta, max_add) else: add_time = min(delta / self.drain, max_add) self.dragondrive_timer.add(add_time) duration = self.dragondrive_timer.timing - now() if duration <= 0: self.d_dragondrive_end('<gauge deplete>') self.dragon_gauge = 0 else: self.dragon_gauge = (duration / max_duration) * self.max_gauge if add_time > 0: log('drive_time' if not skill_pause else 'skill_pause', f'{add_time:+2.4}', f'{duration:2.4}', f'{int(self.dragon_gauge)}/{int(self.max_gauge)}') def charge_gauge(self, value, utp=False, dhaste=True): dhaste = not utp dh = self.adv.mod('dh') if dhaste else 1 if not utp: value *= 10 value = self.adv.sp_convert(dh, value) delta = min(self.dragon_gauge + value, self.max_gauge) - self.dragon_gauge if self.is_dragondrive and self.dragondrive_buff.get(): self.add_drive_gauge_time(delta) elif delta > 0: self.dragon_gauge += delta if utp: log('dragon_gauge', '{:+} utp'.format(int(delta)), f'{int(self.dragon_gauge)}/{int(self.max_gauge)}') else: log('dragon_gauge', '{:+.2f}%'.format(delta / self.max_gauge * 100), '{:.2f}%'.format(self.dragon_gauge / self.max_gauge * 100)) def dtime(self): return self.conf.dshift.startup + self.conf.duration * self.adv.mod( 'dt') + self.conf.exhilaration * (self.off_ele_mod is None) def ddamage(self): return self.conf.dracolith + self.adv.mod('da') - 1 def ds_check(self): return self.skill_use > 0 and self.skill_spc >= self.skill_sp def ds_reset(self): self.skill_use = self.conf.skill_use self.skill_sp = 0 if self.conf.skill_use == 1 else 30 self.skill_spc = self.skill_sp def d_shift_end(self, t): if self.action_timer is not None: self.action_timer.off() self.action_timer = None duration = now() - self.shift_start_time log( self.name, '{:.2f}dmg / {:.2f}s, {:.2f} dps'.format( self.shift_damage_sum, duration, self.shift_damage_sum / duration), ' '.join(self.act_sum)) self.act_sum = [] self.act_list = [] self.dracolith_mod.off() if self.off_ele_mod is not None: self.off_ele_mod.off() self.ds_reset() if not self.is_dragondrive: self.shift_silence = True Timer(self.end_silence).on(10) Timer(self.resume_auto_gauge).on(self.dragon_gauge_timer_diff) self.status = -2 self._setprev() # turn self from doing to prev self._static.doing = self.nop self.end_event() self.idle_event() def d_dragondrive_end(self, t): log('dragondrive', 'end', t if isinstance(t, str) else '<timeout>') self.dragondrive_buff.off() self.shift_silence = True Timer(self.end_silence).on(10) self.status = -2 self._setprev() # turn self from doing to prev self._static.doing = self.nop self.dragondrive_end_event() self.idle_event() def act_timer(self, act, time, next_action=None): if self.c_act_name == 'dodge': self.action_timer = Timer(act, time) else: self.action_timer = Timer(act, time / self.speed()) self.action_timer.next_action = next_action return self.action_timer.on() def d_act_start_t(self, t): self.action_timer = None self.d_act_start(t.next_action) def d_act_start(self, name): if name in self.conf and self._static.doing == self and self.action_timer is None: self.prev_act = self.c_act_name self.prev_conf = self.c_act_conf self.c_act_name = name self.c_act_conf = self.conf[name] self.act_timer(self.d_act_do, self.c_act_conf.startup) def d_act_do(self, t): if self.c_act_name == 'ds': self.skill_use -= 1 self.skill_spc = 0 self.ds_event() self.shift_damage_sum += self.ds_proc() or 0 self.shift_end_timer.add(self.conf.ds.startup + self.conf.ds.recovery) self.act_sum.append('s') elif self.c_act_name == 'end': self.d_shift_end(None) self.shift_end_timer.off() return elif self.c_act_name != 'dodge': # dname = self.c_act_name[:-1] if self.c_act_name != 'dshift' else self.c_act_name self.shift_damage_sum += self.adv.dmg_make( self.c_act_name, self.c_act_conf.dmg, 'x' if self.c_act_name != 'dshift' else self.c_act_name) if self.c_act_name.startswith('dx'): if len(self.act_sum ) > 0 and self.act_sum[-1][0] == 'c' and int( self.act_sum[-1][1]) < int(self.c_act_name[-1]): self.act_sum[-1] = 'c' + self.c_act_name[-1] else: self.act_sum.append('c' + self.c_act_name[-1]) if self.skill_sp > 0 and self.skill_spc < self.skill_sp: self.skill_spc += self.adv.sp_convert( self.adv.sp_mod('x'), 5) if self.skill_spc > self.skill_sp: self.skill_spc = self.skill_sp log(self.c_act_name, 'sp', f'{self.skill_spc}/{self.skill_sp}') if self.c_act_conf.hit > -1: self.adv.hits += self.c_act_conf.hit else: self.adv.hits = -self.c_act_conf.hit self.d_act_next() def d_act_next(self): nact = None if len(self.act_list) > 0: if self.act_list[0] != 'ds' or self.ds_check(): nact = self.act_list.pop(0) if nact is None: if self.c_act_name[0:2] == 'dx': nact = 'dx{}'.format(int(self.c_act_name[2]) + 1) if not nact in self.dx_list: if self.ds_check(): nact = 'ds' elif self.dodge_cancel(): nact = 'dodge' else: nact = 'dx1' else: nact = 'dx1' if nact == 'ds' or nact == 'dodge' or ( nact == 'end' and self.c_act_name != 'ds'): # cancel self.act_timer(self.d_act_start_t, self.conf.latency, nact) else: # regular recovery self.act_timer(self.d_act_start_t, self.c_act_conf.recovery, nact) def parse_act(self, act_str): act_str = act_str.strip() self.act_list = [] skill_usage = 0 for a in act_str.split(' '): if a[0] == 'c' or a[0] == 'x': for i in range(1, int(a[1]) + 1): dxseq = 'dx{}'.format(i) if dxseq in self.dx_list: self.act_list.append(dxseq) if self.dodge_cancel( ) or self.act_list[-1] != self.dx_list[-1]: self.act_list.append('dodge') else: if len(self.act_list) > 0 and self.act_list[-1] == 'dodge': self.act_list.pop() if (a == 's' or a == 'ds') and skill_usage < self.skill_use: self.act_list.append('ds') skill_usage += 1 elif a == 'end': self.act_list.append('end') elif a == 'dodge': self.act_list.append('dodge') def act(self, act_str): self.parse_act(act_str) return self() def check(self, dryrun=True): if self.disabled or self.shift_silence: return False if self.dragon_gauge < self.shift_cost and not ( self.is_dragondrive and self.dragondrive_buff.get()): return False doing = self.getdoing() if not doing.idle: if isinstance(doing, S) or isinstance(doing, DragonForm): return False if not dryrun: if doing.status == -1: doing.startup_timer.off() log('interrupt', doing.name, 'by ' + self.name, 'after {:.2f}s'.format(now() - doing.startup_start)) elif doing.status == 1: doing.recovery_timer.off() log('cancel', doing.name, 'by ' + self.name, 'after {:.2f}s'.format(now() - doing.recover_start)) return True def __call__(self): if not self.check(dryrun=False): return False self.shift_count += 1 if self.is_dragondrive: self.act_list = ['end'] if self.dragondrive_buff.get(): self.d_dragondrive_end('<turn off>') return True else: self.dragondrive_timer.on(self.max_gauge / self.drain) self.dragondrive_buff.on() else: if len(self.act_list) == 0: self.parse_act(self.conf.act) self.dragon_gauge -= self.shift_cost self.dracolith_mod.mod_value = self.ddamage() self.dracolith_mod.on() if self.off_ele_mod is not None: self.off_ele_mod.on() self.pause_auto_gauge() self.shift_damage_sum = 0 self.status = -1 self._setdoing() self.shift_start_time = now() self.shift_end_timer.on(self.dtime()) self.shift_event() log('cast', 'dshift', self.name) self.d_act_start('dshift') return True
class DragonForm(Action): def __init__(self, name, conf, adv): self.name = name self.conf = conf self.adv = adv self.disabled = False self.shift_event = Event("dragon") self.act_event = Event("dact") self.end_event = Event("dragon_end") self.delayed = set() self.ds_reset() self.act_list = [] self.act_sum = [] self.repeat_act = False self.dx_list = [dx for dx, _ in self.conf.find(r"^dx\d+$")] self.ds_event = Event("ds") self.ds_event.name = "ds" self.dx_event = Event("dx") self.action_timer = None self.shift_start_time = 0 self.shift_end_timer = Timer(self.d_shift_end) self.idle_event = Event("idle") self.c_act_name = None self.c_act_conf = None self.dracolith_mod = self.adv.Modifier("dracolith", "ex", "dragon", 0) self.dracolith_mod.get = self.ddamage self.dracolith_mod.off() self.shift_mods = [self.dracolith_mod] self.shift_spd_mod = None self.off_ele = self.adv.slots.c.ele != self.adv.slots.d.ele self.dragon_gauge = 0 self.dragon_gauge_val = self.conf.gauge_val self.conf.gauge_iv = min(int(self.adv.duration / 12), 15) self.dragon_gauge_timer = Timer(self.auto_gauge, timeout=max(1, self.conf.gauge_iv), repeat=1).on() self.dragon_gauge_pause_timer = None self.dragon_gauge_timer_diff = 0 self.max_gauge = 1000 self.shift_cost = 500 self.shift_count = 0 self.shift_silence = False self.is_dragondrive = False self.can_end = True self.allow_end_cd = self.conf.allow_end + self.dstime() self.allow_force_end_timer = Timer(self.set_allow_end, timeout=self.allow_end_cd) self.allow_end = False def set_shift_end(self, value, percent=True): if self.can_end: max_d = self.dtime() - self.conf.dshift.startup cur_d = self.shift_end_timer.timing - now() - ( self.conf.ds.uses - self.skill_use) * self.dstime() delta_t = value if percent: delta_t *= max_d delta_t = min(max_d, cur_d + delta_t) - cur_d if cur_d + delta_t > 0: self.shift_end_timer.add(delta_t) log("shift_time", f"{delta_t:+2.4}", f"{cur_d+delta_t:2.4}") else: self.d_shift_end(None) self.shift_end_timer.off() def can_interrupt(self, *args): return None def can_cancel(self, *args): return None def getstartup(self): return 0 def getrecovery(self): return 0 def reset_allow_end(self): if self.is_dragondrive: self.allow_end = True else: log("allow_end", self.allow_end_cd) self.allow_end = False self.allow_force_end_timer = Timer(self.set_allow_end, timeout=self.allow_end_cd) self.allow_force_end_timer.on() self.allow_end_cd = min( self.allow_end_cd + self.conf.allow_end_step, self.dtime()) def set_allow_end(self, _): self.allow_end = True def set_dragondrive(self, dd_buff, max_gauge=3000, shift_cost=1200, drain=150): self.disabled = False self.is_dragondrive = True self.shift_event = Event("dragondrive") self.end_event = Event("dragondrive_end") ratio = max_gauge / self.max_gauge self.dragon_gauge *= ratio self.max_gauge = max_gauge self.shift_cost = shift_cost # does not deduct, but need to have this much pt to shift self.drain = drain self.dragondrive_buff = dd_buff self.dragondrive_timer = Timer(self.d_dragondrive_end) return self.dragondrive_buff def set_dragonbattle(self, duration): self.disabled = False self.dragon_gauge = self.max_gauge self.conf.duration = duration self.can_end = False self.repeat_act = True if self.conf.ds["sp_db"]: self.skill_sp = self.conf.ds["sp_db"] else: self.skill_sp = self.conf.ds.sp + 15 self.skill_spc = self.skill_sp self.skill_use = -1 self.skill_use_final = -1 def end_silence(self, t): self.shift_silence = False def dodge_cancel(self): if len(self.dx_list) <= 0: return False combo = self.conf[self.dx_list[-1]].recovery / self.speed() dodge = self.conf.dodge.startup + self.conf.dodge.recovery return combo > dodge def auto_gauge(self, t): self.charge_gauge(self.dragon_gauge_val, percent=True, auto=True) def pause_auto_gauge(self): if self.dragon_gauge_pause_timer is None: self.dragon_gauge_timer_diff = self.dragon_gauge_timer.timing - now( ) else: self.dragon_gauge_timer_diff = self.dragon_gauge_pause_timer.timing - now( ) self.dragon_gauge_timer.off() def resume_auto_gauge(self, t): self.dragon_gauge_pause_timer = None self.auto_gauge(t) self.dragon_gauge_timer.on() def add_drive_gauge_time(self, delta, skill_pause=False): max_duration = self.max_gauge / self.drain duration = self.dragondrive_timer.timing - now() max_add = max_duration - duration if skill_pause: add_time = min(delta, max_add) else: add_time = min(delta / self.drain, max_add) duration = self.dragondrive_timer.add(add_time) if duration <= 0: self.d_dragondrive_end("<gauge deplete>") else: self.dragon_gauge = (duration / max_duration) * self.max_gauge if add_time != 0: log( "drive_time" if not skill_pause else "skill_pause", f"{add_time:+2.4}", f"{duration:2.4}", f"{int(self.dragon_gauge)}/{int(self.max_gauge)}", ) def charge_gauge(self, value, utp=False, dhaste=True, percent=False, auto=False): # if dhaste is None: # dhaste = not utp dh = self.dhaste() if dhaste else 1 if utp: dh *= self.adv.mod("utph", operator=operator.add) if percent: value *= self.max_gauge / 100 value = self.adv.sp_convert(dh, value) delta = min(self.dragon_gauge + value, self.max_gauge) - self.dragon_gauge if self.is_dragondrive and self.dragondrive_buff.get(): self.add_drive_gauge_time(delta) elif delta != 0: self.dragon_gauge += delta log( "dragon_gauge", "{:+} ({:+.2f}%)".format(int(delta), delta / self.max_gauge * 100), f"{int(self.dragon_gauge)}/{int(self.max_gauge)}", "{:.2f}%".format(self.dragon_gauge / self.max_gauge * 100), "auto" if auto else "", ) return value @allow_acl def dtime(self): return self.conf.dshift.startup + self.conf.duration * self.adv.mod( "dt") + self.conf.exhilaration * int(not self.off_ele) def dstime(self): try: return (self.conf.ds.startup + self.conf.ds.recovery) / self.speed() except TypeError: return 0 def dhaste(self): return self.adv.mod("dh", operator=operator.add) def chain_dhaste(self): return self.adv.sub_mod("dh", "chain") + 1 @allow_acl def ddamage(self): return self.conf.dracolith + self.adv.mod( "da", operator=operator.add, initial=0) def ds_check(self): return self.skill_use != 0 and self.skill_spc >= self.skill_sp and self.shift_end_timer.elapsed( ) >= 1.9 def ds_charge(self, value): if self.skill_use != 0 and self.skill_spc < self.skill_sp: self.skill_spc += self.adv.sp_convert(self.adv.sp_mod("x"), value) if self.skill_spc > self.skill_sp: self.skill_spc = self.skill_sp log(self.c_act_name, "sp", f"{self.skill_spc}/{self.skill_sp}") def ds_reset(self): self.skill_use = self.conf.ds.uses self.skill_use_final = 1 if self.conf["ds_final"] else 0 self.skill_sp = self.conf.ds.sp self.skill_spc = self.skill_sp def d_shift_partial_end(self): if not self.is_dragondrive and self.status != Action.OFF: duration = now() - self.shift_start_time shift_dmg = g_logs.shift_dmg g_logs.log_shift_dmg(False) log( self.name, "{:.2f}dmg / {:.2f}s, {:.2f} dps".format( shift_dmg, duration, shift_dmg / duration), " ".join(self.act_sum), ) def d_shift_end(self, t): if self.action_timer is not None: self.action_timer.off() self.action_timer = None if not self.is_dragondrive and self.prev_act != "ds" and self.skill_use_final > 0: self.skill_use_final -= 1 self.d_act_start("ds_final") self.act_list = ["end"] return False duration = now() - self.shift_start_time shift_dmg = g_logs.shift_dmg g_logs.log_shift_dmg(False) count = self.clear_delayed() if count > 0: log( "cancel", self.c_act_name, f"by shift end", f'lost {count} hit{"s" if count > 1 else ""}', ) log( self.name, "{:.2f}dmg / {:.2f}s, {:.2f} dps".format(shift_dmg, duration, shift_dmg / duration), " ".join(self.act_sum), ) self.act_sum = [] self.act_list = [] if self.off_ele: self.adv.element = self.adv.slots.c.ele if self.shift_spd_mod is not None: self.shift_spd_mod.off() self.ds_reset() if not self.is_dragondrive: self.shift_silence = True Timer(self.end_silence).on(10) self.dragon_gauge_pause_timer = Timer(self.resume_auto_gauge).on( self.dragon_gauge_timer_diff) self.status = Action.OFF self._setprev() # turn self from doing to prev self._static.doing = self.nop if not self.is_dragondrive: self.end_event() self.idle_event() return True def d_dragondrive_end(self, t): self.dragon_gauge = 0 log("dragondrive", "end", t if isinstance(t, str) else "<timeout>") self.dragondrive_buff.off() self.shift_silence = True Timer(self.end_silence).on(10) self.status = Action.OFF self._setprev() # turn self from doing to prev self._static.doing = self.nop self.end_event() self.idle_event() def act_timer(self, act, time, next_action=None): if self.c_act_name == "dodge": self.action_timer = Timer(act, time) else: self.action_timer = Timer(act, time / self.speed()) self.action_timer.next_action = next_action return self.action_timer.on() def d_act_start_t(self, t): self.action_timer = None self.d_act_start(t.next_action) def d_act_start(self, name): if name in self.conf and self._static.doing == self and self.action_timer is None: log("d_act", name) self.prev_act = self.c_act_name self.prev_conf = self.c_act_conf self.c_act_name = name self.c_act_conf = self.conf[name] self.act_timer(self.d_act_do, self.c_act_conf.startup) def d_act_do_hitattr(self, act_name): actconf = self.conf[act_name] e = self.act_event e.name = act_name e.base = act_name e.group = "dragon" self.adv.actmod_on(e) try: getattr(self.adv, f"{act_name}_before")(e) except AttributeError: pass final_mt = self.adv.schedule_hits(e, self.conf[act_name]) if final_mt: final_mt.actmod = True final_mt.actmod = True try: final_mt.proc = getattr(self.adv, f"{act_name}_proc") except AttributeError: pass else: self.adv.actmod_off(e) try: getattr(self.adv, f"{act_name}_proc")(e) except AttributeError: pass def d_act_do(self, t): if self.c_act_name == "end": if self.d_shift_end(None): self.shift_end_timer.off() return self.d_act_do_hitattr(self.c_act_name) if self.c_act_name in ("ds", "ds_final"): self.skill_use -= 1 self.skill_spc = 0 self.act_sum.append("s") self.ds_event() self.shift_end_timer.add(self.dstime()) elif self.c_act_name.startswith("dx"): if len(self.act_sum) > 0 and self.act_sum[-1][0] == "c" and int( self.act_sum[-1][1]) < int(self.c_act_name[-1]): self.act_sum[-1] = "c" + self.c_act_name[-1] else: self.act_sum.append("c" + self.c_act_name[-1]) self.dx_event.index = int(self.c_act_name[-1]) self.dx_event() self.d_act_next() def d_act_next(self): nact = None if self.repeat_act and not self.act_list: self.parse_act(self.conf.act) if self.act_list: if self.act_list[0] not in ("ds", "dsf") or self.ds_check(): if self.act_list[0] == "end" and not self.allow_end: nact = None else: nact = self.act_list.pop(0) # print('CHOSE BY LIST', nact, self.c_act_name) if nact is None: if self.c_act_name[0:2] == "dx": nact = "dx{}".format(int(self.c_act_name[2]) + 1) if not nact in self.dx_list: if self.ds_check(): nact = "ds" elif self.dodge_cancel(): nact = "dodge" else: nact = "dx1" else: nact = "dx1" # print('CHOSE BY DEFAULT', nact, self.c_act_name) if self.has_delayed and nact == "dsf": nact = "ds" count = self.clear_delayed() if count > 0: log( "cancel", self.c_act_name, f"by {nact}", f'lost {count} hit{"s" if count > 1 else ""}', ) return self.act_timer(self.d_act_start_t, self.conf.latency, nact) if nact in ("ds", "dsf", "dodge") or (nact == "end" and self.c_act_name not in ("ds", "ds_final", "dshift")): # cancel if nact == "dsf": nact = "ds" self.act_timer(self.d_act_start_t, self.max_delayed + self.conf.latency, nact) else: # regular recovery self.act_timer(self.d_act_start_t, self.c_act_conf.recovery, nact) def parse_act(self, act_str): if self.status != Action.OFF and not self.repeat_act: return act_str = act_str.strip() self.act_list = [] skill_usage = 0 for a in act_str.split("-"): if a[0] == "c" or a[0] == "x": for i in range(1, int(a[1]) + 1): dxseq = "dx{}".format(i) if dxseq in self.dx_list: self.act_list.append(dxseq) try: if self.dodge_cancel( ) or self.act_list[-1] != self.dx_list[-1]: self.act_list.append("dodge") except IndexError: pass else: nact = None if (a in ("s", "ds", "sf", "dsf")) and (self.skill_use <= -1 or skill_usage < self.skill_use): if a[-1] == "f": # self.act_list.append('dsf') nact = "dsf" else: nact = "ds" skill_usage += 1 elif a == "end" and self.can_end: nact = "end" elif a == "dodge": nact = "dodge" if nact: try: if len(self.act_list ) > 0 and self.act_list[-1] == "dodge": self.act_list.pop() except IndexError: pass self.act_list.append(nact) def act(self, act_str): self.parse_act(act_str) return self() @allow_acl def check(self, dryrun=True): if self.disabled or self.shift_silence: return False if self.dragon_gauge < self.shift_cost and not ( self.is_dragondrive and self.dragondrive_buff.get()): return False doing = self.getdoing() if not doing.idle: if isinstance(doing, S) or isinstance(doing, DragonForm): return False if dryrun == False: if doing.status == Action.STARTUP: doing.startup_timer.off() doing.end_event() log( "interrupt", doing.name, "by " + self.name, "after {:.2f}s".format(now() - doing.startup_start), ) elif doing.status == Action.RECOVERY: doing.recovery_timer.off() doing.end_event() log( "cancel", doing.name, "by " + self.name, "after {:.2f}s".format(now() - doing.recover_start), ) return True def __call__(self): if not self.check(dryrun=False): return False if self.is_dragondrive: self.act_list = ["end"] if self.dragondrive_buff.get(): self.d_dragondrive_end("<turn off>") return True else: log( "cast", "dragondrive", self.name, f"base duration {self.dragon_gauge/self.drain:.4}s", ) self.dragondrive_timer.on(self.dragon_gauge / self.drain) self.dragondrive_buff.on() else: log("cast", "dshift", self.name) if len(self.act_list) == 0: self.parse_act(self.conf.act) self.dragon_gauge -= self.shift_cost if self.off_ele: self.adv.element = self.adv.slots.d.ele if self.shift_spd_mod is not None: self.shift_spd_mod.on() self.pause_auto_gauge() self.shift_count += 1 self.status = Action.STARTUP self._setdoing() g_logs.log_shift_dmg(True) self.shift_start_time = now() self.shift_end_timer.on(self.dtime()) self.reset_allow_end() self.shift_event() self.d_act_start("dshift") return True
class Buff(object): MAXHP_BUFF_CAP = 0.30 _static = Static({"all_buffs": [], "adv": None}) DB_DURATION = 15 # usual doublebuff effect duration for offensive buffs, note that regen lasts 20s def __init__( self, name="<buff_noname>", value=0, duration=0, mtype="att", morder=None, modifier=None, hidden=False, source=None, ): self.name = name self.__value = value self.duration = duration self.mod_type = mtype self.mod_order = morder or ("chance" if self.mod_type == "crit" else "buff") self.bufftype = "misc" if hidden else "self" self.source = source if self.source is not None and source[0] != "s" and source[0:2] != "ds": self.bufftime = self._ex_bufftime else: self.bufftime = self._bufftime self.buff_end_timer = Timer(self.buff_end_proc) if modifier: self.modifier = modifier self.get = self.modifier.get elif mtype != "effect": self.modifier = Modifier("mod_" + self.name, self.mod_type, self.mod_order, 0) self.modifier.get = self.get else: self.modifier = None self.dmg_test_event = Event("dmg_formula") self.dmg_test_event.dmg_coef = 1 self.dmg_test_event.dname = "test" self.hidden = hidden self.__stored = 0 self.__active = 0 self.buffevent = Event("buff") self.pause_time = -1 self.refresh_time = -1 # self.on() def logwrapper(self, *args): if not self.hidden: log("buff", *args) def _no_bufftime(self): return 1 def _ex_bufftime(self): return 1 + self._static.adv.sub_mod("buff", "ex") def _bufftime(self): return self._static.adv.mod("buff", operator=operator.add) def _debufftime(self): return self._static.adv.mod("debuff", operator=operator.add) def any_bufftime(self): self.bufftime = self._bufftime return self def no_bufftime(self): self.bufftime = self._no_bufftime return self def ex_bufftime(self): self.bufftime = self._ex_bufftime return self def value(self, newvalue=None): if newvalue is not None: self.logwrapper( self.name, f"{self.mod_type}({self.mod_order}): {newvalue:.02f}", "buff value change", ) return self.set(newvalue) else: return self.__value @allow_acl def get(self): if self.__active: return self.__value else: return 0 def set(self, v, d=None): self.__value = v if d != None: self.duration = d return self def stack(self): stack = 0 for i in self._static.all_buffs: if i.name == self.name: if i.__active != 0: stack += 1 return stack def valuestack(self): stack = 0 value = 0 for i in self._static.all_buffs: if i.name == self.name: if i.__active != 0: stack += 1 value += i.__value return value, stack def effect_on(self): value = self.get() if self.mod_type == "defense" and value > 0: db = Event("defchain") db.source = self.source db.on() if self.bufftype == "team": log("buff", "doublebuff", 15 * self.bufftime()) if self.bufftime == self._bufftime: self._static.adv.slots.c.set_need_bufftime() elif self.mod_type == "maxhp": if self._static.adv.sub_mod("maxhp", "buff") < Buff.MAXHP_BUFF_CAP: self.modifier.on() percent = value * 100 log("heal", self.name, self._static.adv.max_hp * value, "team" if self.bufftype == "team" else "self") self._static.adv.add_hp(percent) # FIXME: heal formula 1day twust elif self.mod_type == "regen" and value != 0: self.set_hp_event = Event("set_hp") self.set_hp_event.delta = value self.regen_timer = Timer(self.hp_regen, 3.9, True).on() elif self.mod_type == "heal" and value != 0: self.set_hp_event = Event("heal_make") self.set_hp_event.name = self.name self.set_hp_event.delta = self._static.adv.heal_formula( self.source, value) self.set_hp_event.target = "team" if self.bufftype == "team" else "self" self.regen_timer = Timer(self.hp_regen, 2.9, True).on() else: return self.modifier and self.modifier.on() def effect_off(self): if self.mod_type in ("regen", "heal"): self.regen_timer.off() else: return self.modifier and self.modifier.off() def buff_end_proc(self, e): self.logwrapper( self.name, f"{self.mod_type}({self.mod_order}): {self.value():.02f}", "buff end <timeout>", ) self.__active = 0 if self.__stored: self._static.all_buffs.remove(self) self.__stored = 0 value, stack = self.valuestack() if stack > 0: self.logwrapper( self.name, f"{self.mod_type}({self.mod_order}): {value:.02f}", f"buff stack <{stack}>", ) self.effect_off() def count_team_buff(self): if self.bufftype == "self": return base_mods = [ Modifier("base_cc", "crit", "chance", 0.12), Modifier("base_killer", "killer", "passive", 0.30), ] self.dmg_test_event.modifiers = ModifierDict() for mod in base_mods: self.dmg_test_event.modifiers.append(mod) for b in filter(lambda b: b.get() and b.bufftype == "simulated_def", self._static.all_buffs): self.dmg_test_event.modifiers.append(b.modifier) self.dmg_test_event() no_team_buff_dmg = self.dmg_test_event.dmg placeholders = [] for b in filter( lambda b: b.get() and b.bufftype in ("team", "debuff"), self._static.all_buffs, ): placehold = None if b.modifier.mod_type == "s": placehold = Modifier("placehold_sd", "att", "sd", b.modifier.get() / 2) elif b.modifier.mod_type == "spd": placehold = Modifier("placehold_spd", "att", "spd", b.modifier.get()) elif b.modifier.mod_type.endswith("_killer"): placehold = Modifier("placehold_k", "killer", "passive", b.modifier.get()) if placehold: self.dmg_test_event.modifiers.append(placehold) placeholders.append(placehold) else: self.dmg_test_event.modifiers.append(b.modifier) self.dmg_test_event() team_buff_dmg = self.dmg_test_event.dmg log("buff", "team", team_buff_dmg / no_team_buff_dmg - 1) for mod in chain(base_mods, placeholders): mod.off() def on(self, duration=None): d = max(-1, (duration or self.duration) * self.bufftime()) if d != -1 and self.bufftime == self._bufftime: self._static.adv.slots.c.set_need_bufftime() if self.__active == 0: self.__active = 1 if self.__stored == 0: self._static.all_buffs.append(self) self.__stored = 1 if d >= 0: self.buff_end_timer.on(d) proc_type = "start" else: if d >= 0: if self.buff_end_timer.online: self.buff_end_timer.on(d) else: self.refresh_time = d else: return self proc_type = "refresh" self.logwrapper( self.name, f"{self.mod_type}({self.mod_order}): {self.value():.02f}", f"buff {proc_type} <{d:.02f}s>", ) value, stack = self.valuestack() if stack > 1: log( "buff", self.name, f"{self.mod_type}({self.mod_order}): {value:.02f}", f"buff stack <{stack}>", ) self.effect_on() self.buffevent.buff = self self.buffevent.on() return self def hp_regen(self, t): self.set_hp_event.on() def off(self): if self.__active == 0: return self.logwrapper( self.name, f"{self.mod_type}({self.mod_order}): {self.value():.02f}", f"buff end <turn off>", ) self.__active = 0 self.buff_end_timer.off() self.effect_off() return self @property def adv(self): return self._static.adv @allow_acl def timeleft(self): return -1 if self.duration == -1 else (self.buff_end_timer.timing - now()) def add_time(self, delta): self.buff_end_timer.add(delta) def pause(self): self.pause_time = self.timeleft() if self.pause_time > 0: log("pause", self.name, self.pause_time) self.buff_end_timer.off() def resume(self): self.pause_time = max(self.pause_time, self.refresh_time) if self.pause_time > 0: log("resume", self.name, self.pause_time, now() + self.pause_time) self.buff_end_timer.on(self.pause_time) self.pause_time = -1