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, EnemyHP: combatEvt.EnemyHP,
DebuffApplied: combatEvt.DebuffApplied, 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() { if !cs.Enemy.IsAlive() {

@ -1,6 +1,9 @@
package model package model
import "encoding/json" import (
"encoding/json"
"time"
)
// WSEnvelope is the wire format for all WebSocket messages (both directions). // WSEnvelope is the wire format for all WebSocket messages (both directions).
type WSEnvelope struct { type WSEnvelope struct {
@ -200,6 +203,14 @@ type BuffAppliedPayload struct {
Magnitude float64 `json:"magnitude"` 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. // ErrorPayload is sent when a client command fails validation.
type ErrorPayload struct { type ErrorPayload struct {
Code string `json:"code"` Code string `json:"code"`

@ -9,6 +9,8 @@ import type {
NearbyHeroData, NearbyHeroData,
NPCData, NPCData,
BuildingData, BuildingData,
ActiveDebuff,
DebuffType,
} from './types'; } from './types';
import { GamePhase } from './types'; import { GamePhase } from './types';
import { GameRenderer, worldToScreen } from './renderer'; import { GameRenderer, worldToScreen } from './renderer';
@ -620,6 +622,38 @@ export class GameEngine {
this._notifyStateChange(); 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). */ /** Apply a full server state override (used for backward compat). */
applyServerState(state: GameState): void { applyServerState(state: GameState): void {
this._gameState = { this._gameState = {

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

@ -25,8 +25,10 @@ import type {
EnemyState, EnemyState,
LootDrop, LootDrop,
MerchantLootPayload, MerchantLootPayload,
DebuffAppliedPayload,
} from './types'; } 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) ---- // ---- Callback types for UI layer (App.tsx) ----
@ -64,6 +66,9 @@ export function wireWSHandler(
engine: GameEngine, engine: GameEngine,
callbacks: WSHandlerCallbacks, callbacks: WSHandlerCallbacks,
): void { ): void {
const isDebuffType = (value: string): value is DebuffType => (
Object.values(DebuffType).includes(value as DebuffType)
);
// ---- Server -> Client: Movement ---- // ---- Server -> Client: Movement ----
ws.on('hero_move', (msg: ServerMessage) => { 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); 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) => { ws.on('combat_end', (msg: ServerMessage) => {
const p = msg.payload as CombatEndPayload; const p = msg.payload as CombatEndPayload;
engine.applyCombatEnd(); engine.applyCombatEnd();

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

@ -89,6 +89,7 @@ export const ru: Translations = {
debuffStun: '\u041e\u0433\u043b\u0443\u0448\u0435\u043d\u0438\u0435', debuffStun: '\u041e\u0433\u043b\u0443\u0448\u0435\u043d\u0438\u0435',
debuffSlow: '\u0417\u0430\u043c\u0435\u0434\u043b\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', 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 // Quest system
questLog: '\u0416\u0443\u0440\u043d\u0430\u043b \u0437\u0430\u0434\u0430\u043d\u0438\u0439', 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', stun: '#ffdd44',
slow: '#4488ff', slow: '#4488ff',
weaken: '#aa44dd', weaken: '#aa44dd',
ice_slow: '#66aaff',
}; };
// ---- Debuff Default Durations (ms) ---- // ---- Debuff Default Durations (ms) ----
@ -107,6 +108,7 @@ export const DEBUFF_DURATION_DEFAULTS: Record<string, number> = {
stun: 2000, stun: 2000,
slow: 4000, slow: 4000,
weaken: 5000, weaken: 5000,
ice_slow: 4000,
}; };
/** Loot popup display duration in milliseconds */ /** 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.Stun]: { icon: '\uD83D\uDCAB', label: 'Stun' },
[DebuffType.Slow]: { icon: '\uD83D\uDC22', label: 'Slow' }, [DebuffType.Slow]: { icon: '\uD83D\uDC22', label: 'Slow' },
[DebuffType.Weaken]: { icon: '\uD83D\uDCC9', label: 'Weaken' }, [DebuffType.Weaken]: { icon: '\uD83D\uDCC9', label: 'Weaken' },
[DebuffType.IceSlow]: { icon: '\u2744\uFE0F', label: 'Ice Slow' },
}; };
// ---- Types ---- // ---- Types ----

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

Loading…
Cancel
Save