кровавые кролики жирнее

This commit is contained in:
azykov@mail.ru
2026-09-04 21:57:43 +03:00
parent dbc0094986
commit 2f57140899
3 changed files with 32 additions and 10 deletions
+2 -3
View File
@@ -1,7 +1,6 @@
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import type { GameStore } from '../game/GameStore'; import type { GameStore } from '../game/GameStore';
import type { RabbitState } from '../game/types'; import type { RabbitState } from '../game/types';
import { RABBIT_MAX_HP } from '../game/rabbits';
import { RABBIT_NIGHT_SPRITE, RABBIT_SPRITE } from '../game/sprites'; import { RABBIT_NIGHT_SPRITE, RABBIT_SPRITE } from '../game/sprites';
import HealthBar from './HealthBar'; import HealthBar from './HealthBar';
import PixelSprite from './PixelSprite'; import PixelSprite from './PixelSprite';
@@ -24,8 +23,8 @@ const RabbitEntity = observer(function RabbitEntity({ rabbit, store }: { rabbit:
> >
<PixelSprite pixels={store.isNight ? RABBIT_NIGHT_SPRITE : RABBIT_SPRITE} size={size} /> <PixelSprite pixels={store.isNight ? RABBIT_NIGHT_SPRITE : RABBIT_SPRITE} size={size} />
</div> </div>
{rabbit.hp < RABBIT_MAX_HP && ( {rabbit.hp < store.rabbitMaxHp && (
<HealthBar x={rabbit.x} y={rabbit.y - rabbit.z - size * 0.5 - 6} ratio={rabbit.hp / RABBIT_MAX_HP} /> <HealthBar x={rabbit.x} y={rabbit.y - rabbit.z - size * 0.5 - 6} ratio={rabbit.hp / store.rabbitMaxHp} />
)} )}
</div> </div>
); );
+26 -7
View File
@@ -20,6 +20,7 @@ import {
import { BLOOD_MOON_BANNER_SECONDS, DAY_HOURS, isBloodMoonNight, isNightHour, NIGHT_SPEED, nightBelongsToDay } from './daynight'; import { BLOOD_MOON_BANNER_SECONDS, DAY_HOURS, isBloodMoonNight, isNightHour, NIGHT_SPEED, nightBelongsToDay } from './daynight';
import { clearSave, loadSave, writeSave } from './persistence'; import { clearSave, loadSave, writeSave } from './persistence';
import { import {
BLOOD_MOON_COMBAT_MULTIPLIER,
BLOOD_MOON_SIZE_MULTIPLIER, BLOOD_MOON_SIZE_MULTIPLIER,
RABBIT_ACTION_DURATION, RABBIT_ACTION_DURATION,
RABBIT_HEALTH_REGEN_INTERVAL, RABBIT_HEALTH_REGEN_INTERVAL,
@@ -233,6 +234,17 @@ export class GameStore {
return RABBIT_HIT_RADIUS * (this.isBloodMoon ? BLOOD_MOON_SIZE_MULTIPLIER : 1); return RABBIT_HIT_RADIUS * (this.isBloodMoon ? BLOOD_MOON_SIZE_MULTIPLIER : 1);
} }
/** A rabbit's max hp — during a blood moon they're tougher, not just bigger. */
get rabbitMaxHp(): number {
return RABBIT_MAX_HP * (this.isBloodMoon ? BLOOD_MOON_COMBAT_MULTIPLIER : 1);
}
/** Damage a rabbit's strike deals to the character — during a blood moon they hit harder, not
* just bigger. */
get rabbitStrikeDamage(): number {
return RABBIT_STRIKE_DAMAGE * (this.isBloodMoon ? BLOOD_MOON_COMBAT_MULTIPLIER : 1);
}
/** Buys the next level of the given upgrade if there are enough coins. Returns whether it bought. */ /** Buys the next level of the given upgrade if there are enough coins. Returns whether it bought. */
buyUpgrade(id: UpgradeId): boolean { buyUpgrade(id: UpgradeId): boolean {
const level = this.upgradeLevels[id]; const level = this.upgradeLevels[id];
@@ -289,7 +301,7 @@ export class GameStore {
// Recomputed from actionTime on the very next tick if actually mid-jump, so 0 is a safe // Recomputed from actionTime on the very next tick if actually mid-jump, so 0 is a safe
// starting value regardless. // starting value regardless.
z: 0, z: 0,
hp: rabbit.hp ?? RABBIT_MAX_HP, hp: rabbit.hp ?? this.rabbitMaxHp,
// Purely a transient visual cue — always starts unflashed regardless of the save. // Purely a transient visual cue — always starts unflashed regardless of the save.
hitFlashTimer: 0, hitFlashTimer: 0,
// Transient accumulator — always starts fresh regardless of the save. // Transient accumulator — always starts fresh regardless of the save.
@@ -387,7 +399,7 @@ export class GameStore {
x: pos.x, x: pos.x,
y: pos.y, y: pos.y,
z: 0, z: 0,
hp: RABBIT_MAX_HP, hp: this.rabbitMaxHp,
hitFlashTimer: 0, hitFlashTimer: 0,
healthRegenProgress: 0, healthRegenProgress: 0,
action: 'idle', action: 'idle',
@@ -525,7 +537,8 @@ export class GameStore {
this.updateAutosave(deltaTime); this.updateAutosave(deltaTime);
} }
/** Shows the "КРОВАВАЯ ЛУНА!" banner for a few seconds right as a blood moon night begins. */ /** Shows the "КРОВАВАЯ ЛУНА!" banner for a few seconds and heals every rabbit to full (their
* now-tripled max hp) right as a blood moon night begins. */
private updateBloodMoon(deltaTime: number) { private updateBloodMoon(deltaTime: number) {
if (this.bloodMoonBannerTimer > 0) if (this.bloodMoonBannerTimer > 0)
this.bloodMoonBannerTimer = Math.max(0, this.bloodMoonBannerTimer - deltaTime); this.bloodMoonBannerTimer = Math.max(0, this.bloodMoonBannerTimer - deltaTime);
@@ -537,6 +550,11 @@ export class GameStore {
return; return;
this.lastAnnouncedBloodMoonDay = nightDay; this.lastAnnouncedBloodMoonDay = nightDay;
this.bloodMoonBannerTimer = BLOOD_MOON_BANNER_SECONDS; this.bloodMoonBannerTimer = BLOOD_MOON_BANNER_SECONDS;
const maxHp = this.rabbitMaxHp;
for (const rabbit of this.rabbits) {
rabbit.hp = maxHp;
rabbit.healthRegenProgress = 0;
}
} }
/** Hp regenerates a whole point at a time (same accumulate-then-spend cadence as chopProgress, /** Hp regenerates a whole point at a time (same accumulate-then-spend cadence as chopProgress,
@@ -567,7 +585,7 @@ export class GameStore {
* out — the ~1s of "striking" afterward is just the rabbit committed to that action and unable * out — the ~1s of "striking" afterward is just the rabbit committed to that action and unable
* to do anything else, not a wind-up the character could still escape. */ * to do anything else, not a wind-up the character could still escape. */
private landRabbitStrike() { private landRabbitStrike() {
this.characterHp = Math.max(0, this.characterHp - RABBIT_STRIKE_DAMAGE); this.characterHp = Math.max(0, this.characterHp - this.rabbitStrikeDamage);
this.characterHitFlashTimer = HIT_FLASH_DURATION; this.characterHitFlashTimer = HIT_FLASH_DURATION;
if (this.characterHp <= 0) { if (this.characterHp <= 0) {
this.isDead = true; this.isDead = true;
@@ -606,14 +624,15 @@ export class GameStore {
* character's own regen — unlike the character's, this isn't paused by anything (no upgrade for * character's own regen — unlike the character's, this isn't paused by anything (no upgrade for
* it either), so it ticks along even mid-fight. */ * it either), so it ticks along even mid-fight. */
private updateRabbitHealthRegen(rabbit: RabbitState, deltaTime: number) { private updateRabbitHealthRegen(rabbit: RabbitState, deltaTime: number) {
if (rabbit.hp >= RABBIT_MAX_HP) { const maxHp = this.rabbitMaxHp;
if (rabbit.hp >= maxHp) {
rabbit.healthRegenProgress = 0; rabbit.healthRegenProgress = 0;
return; return;
} }
rabbit.healthRegenProgress += deltaTime; rabbit.healthRegenProgress += deltaTime;
while (rabbit.healthRegenProgress >= RABBIT_HEALTH_REGEN_INTERVAL && rabbit.hp < RABBIT_MAX_HP) { while (rabbit.healthRegenProgress >= RABBIT_HEALTH_REGEN_INTERVAL && rabbit.hp < maxHp) {
rabbit.healthRegenProgress -= RABBIT_HEALTH_REGEN_INTERVAL; rabbit.healthRegenProgress -= RABBIT_HEALTH_REGEN_INTERVAL;
rabbit.hp = Math.min(RABBIT_MAX_HP, rabbit.hp + 1); rabbit.hp = Math.min(maxHp, rabbit.hp + 1);
} }
} }
+4
View File
@@ -39,6 +39,10 @@ export const RABBIT_NIGHT_JUMP_SPREAD_DEGREES = 30;
/** Rabbits (and their strike/hit ranges) are this much bigger during a blood moon. */ /** Rabbits (and their strike/hit ranges) are this much bigger during a blood moon. */
export const BLOOD_MOON_SIZE_MULTIPLIER = 2; export const BLOOD_MOON_SIZE_MULTIPLIER = 2;
/** Rabbits deal this much more strike damage and have this much more max hp during a blood moon —
* a bigger jump than their (purely visual/reach) size multiplier above. */
export const BLOOD_MOON_COMBAT_MULTIPLIER = 3;
/** Seconds per hp of rabbit health regen (fixed — unlike the character's own regen, there's no /** Seconds per hp of rabbit health regen (fixed — unlike the character's own regen, there's no
* upgrade for it). */ * upgrade for it). */
export const RABBIT_HEALTH_REGEN_INTERVAL = 3; export const RABBIT_HEALTH_REGEN_INTERVAL = 3;