master
Denis Ranneft 1 month ago
parent 7f3b04b424
commit dc5fc9b82e

@ -1238,6 +1238,18 @@ func (e *Engine) processHeroAttack(cs *model.CombatState, now time.Time) {
EnemyHP: combatEvt.EnemyHP,
DebuffApplied: combatEvt.DebuffApplied,
})
if combatEvt.DebuffApplied != "" {
if dt, ok := model.ValidDebuffType(combatEvt.DebuffApplied); ok {
if def, ok := model.DebuffDefinition(dt); ok {
e.sender.SendToHero(cs.HeroID, "debuff_applied", model.DebuffAppliedPayload{
DebuffType: string(dt),
DurationMs: def.Duration.Milliseconds(),
Magnitude: def.Magnitude,
ExpiresAt: now.Add(def.Duration),
})
}
}
}
}
if !cs.Enemy.IsAlive() {

@ -1,6 +1,9 @@
package model
import "encoding/json"
import (
"encoding/json"
"time"
)
// WSEnvelope is the wire format for all WebSocket messages (both directions).
type WSEnvelope struct {
@ -200,6 +203,14 @@ type BuffAppliedPayload struct {
Magnitude float64 `json:"magnitude"`
}
// DebuffAppliedPayload is sent when a debuff is applied to the hero.
type DebuffAppliedPayload struct {
DebuffType string `json:"debuffType"`
DurationMs int64 `json:"durationMs"`
Magnitude float64 `json:"magnitude,omitempty"`
ExpiresAt time.Time `json:"expiresAt"`
}
// ErrorPayload is sent when a client command fails validation.
type ErrorPayload struct {
Code string `json:"code"`

@ -9,6 +9,8 @@ import type {
NearbyHeroData,
NPCData,
BuildingData,
ActiveDebuff,
DebuffType,
} from './types';
import { GamePhase } from './types';
import { GameRenderer, worldToScreen } from './renderer';
@ -620,6 +622,38 @@ export class GameEngine {
this._notifyStateChange();
}
/** Apply or refresh a single debuff from WS. */
applyDebuffApplied(type: DebuffType, durationMs: number, expiresAtMs?: number): void {
const hero = this._gameState.hero;
if (!hero) return;
const nowMs = Date.now();
const expMs = Number.isFinite(expiresAtMs)
? (expiresAtMs as number)
: nowMs + Math.max(0, durationMs);
const remainingMs = Math.max(0, expMs - nowMs);
const next: ActiveDebuff = {
type,
remainingMs,
durationMs: Math.max(0, durationMs),
expiresAtMs: expMs,
};
const debuffs = hero.debuffs
.filter((d) => d.type !== type)
.filter((d) => {
const exp = d.expiresAtMs ?? nowMs + d.remainingMs;
return exp > nowMs && d.remainingMs > 0;
});
debuffs.push(next);
this._gameState = {
...this._gameState,
hero: { ...hero, debuffs },
};
this._notifyStateChange();
}
/** Apply a full server state override (used for backward compat). */
applyServerState(state: GameState): void {
this._gameState = {

@ -30,6 +30,7 @@ export enum DebuffType {
Stun = 'stun', // no attacks (2 sec)
Slow = 'slow', // -40% movement
Weaken = 'weaken', // -30% outgoing damage
IceSlow = 'ice_slow', // -20% attack speed (Ice Guardian)
}
export interface BuffChargeState {
@ -494,6 +495,13 @@ export interface BuffAppliedPayload {
magnitude?: number;
}
export interface DebuffAppliedPayload {
debuffType: string;
durationMs?: number;
magnitude?: number;
expiresAt?: string;
}
export interface TownEnterPayload {
townId: number;
townName: string;

@ -25,8 +25,10 @@ import type {
EnemyState,
LootDrop,
MerchantLootPayload,
DebuffAppliedPayload,
} from './types';
import { EnemyType, Rarity } from './types';
import { DebuffType, EnemyType, Rarity } from './types';
import { DEBUFF_DURATION_DEFAULTS } from '../shared/constants';
// ---- Callback types for UI layer (App.tsx) ----
@ -64,6 +66,9 @@ export function wireWSHandler(
engine: GameEngine,
callbacks: WSHandlerCallbacks,
): void {
const isDebuffType = (value: string): value is DebuffType => (
Object.values(DebuffType).includes(value as DebuffType)
);
// ---- Server -> Client: Movement ----
ws.on('hero_move', (msg: ServerMessage) => {
@ -111,6 +116,16 @@ export function wireWSHandler(
engine.applyAttack(p.source, p.damage, p.isCrit, p.heroHp, p.enemyHp);
});
ws.on('debuff_applied', (msg: ServerMessage) => {
const p = msg.payload as DebuffAppliedPayload;
if (!p?.debuffType || !isDebuffType(p.debuffType)) return;
const nowMs = Date.now();
const fallbackMs = DEBUFF_DURATION_DEFAULTS[p.debuffType] ?? 0;
const durationMs = Number.isFinite(p.durationMs) ? Math.max(0, p.durationMs as number) : fallbackMs;
const expiresAtMs = p.expiresAt ? Date.parse(p.expiresAt) : nowMs + durationMs;
engine.applyDebuffApplied(p.debuffType, durationMs, expiresAtMs);
});
ws.on('combat_end', (msg: ServerMessage) => {
const p = msg.payload as CombatEndPayload;
engine.applyCombatEnd();

@ -87,6 +87,7 @@ export const en = {
debuffStun: 'Stun',
debuffSlow: 'Slow',
debuffWeaken: 'Weaken',
debuffIceSlow: 'Ice Slow',
// Quest system
questLog: 'Quest Log',

@ -89,6 +89,7 @@ export const ru: Translations = {
debuffStun: '\u041e\u0433\u043b\u0443\u0448\u0435\u043d\u0438\u0435',
debuffSlow: '\u0417\u0430\u043c\u0435\u0434\u043b\u0435\u043d\u0438\u0435',
debuffWeaken: '\u041e\u0441\u043b\u0430\u0431\u043b\u0435\u043d\u0438\u0435',
debuffIceSlow: '\u041b\u0435\u0434\u044f\u043d\u043e\u0435 \u0437\u0430\u043c\u0435\u0434\u043b\u0435\u043d\u0438\u0435',
// Quest system
questLog: '\u0416\u0443\u0440\u043d\u0430\u043b \u0437\u0430\u0434\u0430\u043d\u0438\u0439',

@ -96,6 +96,7 @@ export const DEBUFF_COLORS: Record<string, string> = {
stun: '#ffdd44',
slow: '#4488ff',
weaken: '#aa44dd',
ice_slow: '#66aaff',
};
// ---- Debuff Default Durations (ms) ----
@ -107,6 +108,7 @@ export const DEBUFF_DURATION_DEFAULTS: Record<string, number> = {
stun: 2000,
slow: 4000,
weaken: 5000,
ice_slow: 4000,
};
/** Loot popup display duration in milliseconds */

@ -11,6 +11,7 @@ const DEBUFF_META: Record<DebuffType, { icon: string; label: string }> = {
[DebuffType.Stun]: { icon: '\uD83D\uDCAB', label: 'Stun' },
[DebuffType.Slow]: { icon: '\uD83D\uDC22', label: 'Slow' },
[DebuffType.Weaken]: { icon: '\uD83D\uDCC9', label: 'Weaken' },
[DebuffType.IceSlow]: { icon: '\u2744\uFE0F', label: 'Ice Slow' },
};
// ---- Types ----

@ -27,6 +27,7 @@ function debuffLabel(tr: Translations, type: DebuffType): string {
[DebuffType.Stun]: tr.debuffStun,
[DebuffType.Slow]: tr.debuffSlow,
[DebuffType.Weaken]: tr.debuffWeaken,
[DebuffType.IceSlow]: tr.debuffIceSlow,
};
return map[type] ?? type;
}
@ -130,7 +131,8 @@ export function HeroStatsContent({ hero, nowMs }: HeroPanelProps) {
|| hasActiveBuff(hero.activeBuffs, BuffType.PowerPotion, nowMs);
const atkNerfed = hasActiveDebuff(hero.debuffs, DebuffType.Weaken, nowMs);
const spdBuffed = hasActiveBuff(hero.activeBuffs, BuffType.WarCry, nowMs);
const spdNerfed = hasActiveDebuff(hero.debuffs, DebuffType.Freeze, nowMs);
const spdNerfed = hasActiveDebuff(hero.debuffs, DebuffType.Freeze, nowMs)
|| hasActiveDebuff(hero.debuffs, DebuffType.IceSlow, nowMs);
const defBuffed = hasActiveBuff(hero.activeBuffs, BuffType.Shield, nowMs);
return (

Loading…
Cancel
Save