small update

boss prototype, auto planter, visual reworks, bug fixes
This commit is contained in:
2026-08-29 10:45:34 +02:00
parent 6840b715de
commit feb4500c6c
31 changed files with 1293 additions and 213 deletions
+57 -7
View File
@@ -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):