small update
boss prototype, auto planter, visual reworks, bug fixes
This commit is contained in:
+57
-7
@@ -9,6 +9,7 @@ from .skills import (
|
||||
S_HEAL_REDUCTION, S_HOT, S_ICICLES, S_INTOXICATE, S_LYING_WAIT, S_MADNESS, S_OBLIVION,
|
||||
S_PREVENT_FIRST_DAMAGE, S_SEED, S_SHIELD, S_SLEEP, S_SNOWFLAKES, S_SPEED_DOWN,
|
||||
S_SPEED_UP, S_TETHER, S_VULNERABLE, S_WILT, S_ZINNIA_WEAKEN,
|
||||
SHIELD_DURATION,
|
||||
EFF_DOT, EFF_HEAL, EFF_HOT, EFF_SHIELD, EFF_STATUS, EFF_WILT,
|
||||
TARGET_ALL_ALLIES, TARGET_ALL_ENEMIES, TARGET_ALLY, TARGET_ANY, TARGET_ENEMY,
|
||||
TARGET_SELF, TARGET_TWO_ENEMIES, Skill, roll_crit,
|
||||
@@ -135,6 +136,7 @@ class Battle:
|
||||
|
||||
def _crit(self, unit: Unit) -> float:
|
||||
pct = unit.crit * 100 - self._status_value(unit, S_CRIT_DOWN)
|
||||
pct += self._rune_pct(unit, "crit") * 100
|
||||
return max(0.0, pct / 100)
|
||||
|
||||
# --- tank protection ----------------------------------------------
|
||||
@@ -278,6 +280,15 @@ class Battle:
|
||||
del unit.status[key]
|
||||
else:
|
||||
unit.status[key] = (value, dur)
|
||||
# Shields decay like any buff: each application counts down SHIELD_DURATION
|
||||
# turns from when it was applied, independently of newer shields.
|
||||
shields = unit.status.get(S_SHIELD)
|
||||
if shields:
|
||||
remaining = [(amount, turns - 1) for amount, turns in shields if turns > 1]
|
||||
if remaining:
|
||||
unit.status[S_SHIELD] = remaining
|
||||
else:
|
||||
unit.status.pop(S_SHIELD, None)
|
||||
|
||||
# --- status ticking ------------------------------------------------
|
||||
def _tick_dot(self, unit: Unit) -> None:
|
||||
@@ -371,13 +382,20 @@ class Battle:
|
||||
blue = self._rune_pct(target, "shield")
|
||||
if blue:
|
||||
amount = int(amount * (1 + blue))
|
||||
target.status[S_SHIELD] = target.status.get(S_SHIELD, 0) + amount
|
||||
# Each application has its own decay timer; stacking does NOT refresh
|
||||
# shields already up (they still decay SHIELD_DURATION turns after being
|
||||
# applied).
|
||||
target.status.setdefault(S_SHIELD, []).append((amount, SHIELD_DURATION))
|
||||
if self._action is None:
|
||||
src = f" from {source.name}" if source else ""
|
||||
self.log.append(f"{target.name} gains {amount} shield{src}")
|
||||
if self._action is not None:
|
||||
self._action["shield"] += amount
|
||||
|
||||
def _shield_total(self, unit: Unit) -> int:
|
||||
"""Total shield remaining across all applications."""
|
||||
return sum(amount for amount, _ in unit.status.get(S_SHIELD, []))
|
||||
|
||||
def _shield_power(self, user: Unit, base: int) -> int:
|
||||
"""Shield amounts scale with the caster's Attack so they keep up with
|
||||
growing health pools (like the Sunflower passive)."""
|
||||
@@ -396,7 +414,7 @@ class Battle:
|
||||
if weaken:
|
||||
mult *= 1 - weaken / 100.0
|
||||
# tulip_equinox: any ally of Tulip with a shield deals +10%
|
||||
if attacker.status.get(S_SHIELD, 0) > 0:
|
||||
if self._shield_total(attacker) > 0:
|
||||
team = self.players if attacker.team == TEAM_PLAYER else self.enemies
|
||||
if any(u.passive == "tulip_equinox" for u in team):
|
||||
mult *= 1.1
|
||||
@@ -507,10 +525,25 @@ class Battle:
|
||||
def _absorb(self, attacker: Optional[Unit], target: Unit, dmg: int) -> int:
|
||||
self._consume_hits_statuses(target)
|
||||
reflect = isinstance(target.status.get("bulb_reflect"), tuple) and target.status["bulb_reflect"][0] > 0
|
||||
shield = target.status.get(S_SHIELD, 0)
|
||||
shield_list = target.status.get(S_SHIELD, [])
|
||||
shield = sum(amount for amount, _ in shield_list)
|
||||
if shield > 0:
|
||||
if shield >= dmg:
|
||||
target.status[S_SHIELD] = shield - dmg
|
||||
# Consume the damage across applications (oldest first).
|
||||
remaining = dmg
|
||||
new_shields = []
|
||||
for amount, turns in shield_list:
|
||||
if remaining <= 0:
|
||||
new_shields.append((amount, turns))
|
||||
elif amount > remaining:
|
||||
new_shields.append((amount - remaining, turns))
|
||||
remaining = 0
|
||||
else:
|
||||
remaining -= amount
|
||||
if new_shields:
|
||||
target.status[S_SHIELD] = new_shields
|
||||
else:
|
||||
target.status.pop(S_SHIELD, None)
|
||||
self.log.append(f" {target.name}'s shield absorbs {dmg}")
|
||||
self.events.append({"type": "shield", "target": target, "amount": dmg})
|
||||
if target.passive == "camelia_frigid":
|
||||
@@ -674,6 +707,8 @@ class Battle:
|
||||
if skill.requires_form and skill.requires_form != user.status.get("form"):
|
||||
return []
|
||||
if skill.target == TARGET_ENEMY:
|
||||
if skill.mechanic == "boss_landslide" and user.status.get("boss_stacks", 0) < 3:
|
||||
return [] # Landslide only becomes available at 3 stacks
|
||||
return self._targetable(self.foes_of(user), self._bypass_tanks(user, skill))
|
||||
if skill.target in (TARGET_ALL_ENEMIES, TARGET_TWO_ENEMIES):
|
||||
return self.foes_of(user)
|
||||
@@ -755,6 +790,10 @@ class Battle:
|
||||
return False
|
||||
if skill.cooldown_left > 0:
|
||||
return False
|
||||
if skill.mechanic == "boss_landslide" and user.status.get("boss_stacks", 0) < 3:
|
||||
return False
|
||||
if user.acted and not skill.extra_action:
|
||||
return False # a unit acts once per turn (extra-action skills still allowed)
|
||||
if skill.requires_form and skill.requires_form != user.status.get("form"):
|
||||
return False
|
||||
if isinstance(user.status.get(S_OBLIVION), tuple) and user.status[S_OBLIVION][0] > 0:
|
||||
@@ -891,6 +930,8 @@ class Battle:
|
||||
if skill.mechanic == "dissolve":
|
||||
skill.power += self._status_value(user, S_LYING_WAIT)
|
||||
power_bonus = int(self._defense(user) * 0.5) if skill.mechanic == "def_bonus" else 0
|
||||
if skill.mechanic == "boss_landslide":
|
||||
power_bonus += 55 * user.status.get("boss_stacks", 0)
|
||||
if skill.target in (TARGET_ALL_ENEMIES, TARGET_TWO_ENEMIES):
|
||||
targets = self.foes_of(user)
|
||||
if skill.target == TARGET_TWO_ENEMIES:
|
||||
@@ -931,9 +972,12 @@ class Battle:
|
||||
else:
|
||||
self._apply_wilt_damage(target, int(dealt * 0.3))
|
||||
if skill.mechanic == "heal_random_ally":
|
||||
allies = self.allies_of(user)
|
||||
if allies:
|
||||
self._heal(user, random.choice(allies), 40)
|
||||
# Only pick from allies that are missing HP, so the heal always
|
||||
# lands on someone it can actually help (a random full-HP ally
|
||||
# would silently heal for 0).
|
||||
injured = [a for a in self.allies_of(user) if a.hp < a.max_hp]
|
||||
if injured:
|
||||
self._heal(user, random.choice(injured), skill.power)
|
||||
if skill.mechanic == "strip_buffs":
|
||||
self._strip_buffs(target)
|
||||
if skill.mechanic == "tether_lowest":
|
||||
@@ -942,6 +986,12 @@ class Battle:
|
||||
low = min(allies, key=lambda a: a.hp)
|
||||
low.status[S_TETHER] = (1, 1)
|
||||
self.log.append(f" {low.name} is tethered to {user.name}")
|
||||
if skill.mechanic == "boss_stack":
|
||||
user.status["boss_stacks"] = min(3, user.status.get("boss_stacks", 0) + 1)
|
||||
self.log.append(f" {user.name}'s power stacks ({user.status['boss_stacks']}/3).")
|
||||
if skill.mechanic == "boss_landslide":
|
||||
user.status["boss_stacks"] = 0
|
||||
self.log.append(f" {user.name} unleashes Landslide and releases all stacks!")
|
||||
|
||||
def _strip_buffs(self, unit: Unit) -> None:
|
||||
for k in (S_ATTACK_UP, S_DEFENSE_UP, S_SPEED_UP, S_SHIELD):
|
||||
|
||||
Reference in New Issue
Block a user