Repository navigation
Releases: DanielOjeda25/MoodEngine
Release list
v3.9.0-fase4-hito9
feat(F4H9): ataques que dañan al player — melee + ranged + wind-up procedural
Tercer hito Sub-fase 4.2. F4H7 dejó la state machine; F4H8 hizo que el
enemy te persiga; F4H9 hace que te golpee. El enemy en estado Attack
aplica spec.damage HP cada spec.attackCooldown segundos via
Health::applyDamage, con wind-up telegraph procedural de spec.windUpSec
(default 0.3s) antes del primer golpe — ventana de esquive Doom/Quake.
Schema .moodenemy extendido con 3 fields F4H9 (sin schema bump — defaults
sanos back-compat F4H7/F4H8): attackKind "melee" o "projectile", windUpSec
con clamp >= 0, projectileWeapon path al .moodweapon. EnemyComponent
reemplaza lastAttackTime (reservado F4H7 nunca usado) por
attackCooldownTimer + windUpTimer; ambos se resetean al re-entrar Attack
(no se "ahorra" tiempo corriendo).
applyEnemyAttack helper dispatch por attackKind: branch melee usa
Health::applyDamage (engine-generic — si player tiene ArmorComponent F4H4
la absorb ratio HL2 se aplica automatic); branch projectile auto-add
WeaponComponent al enemy on-demand (D7 mismo patron NavAgent F4H8) y
llama Weapon::fire(scene, enemy, FireParams{origin=enemyPos+up_0.5,
dir=normalize(player-enemy), randomSeed=...}, physics, audio, assets).
El projectile se mueve via F4H5 Projectile::tickSystem que ya corre en
el bridge — engine-generic puro, cero código nuevo de proyectil.
Constante k_timerEps = 1e-5f en switch Attack (R2 reactive): 7 ticks de
0.05s dejaban windUpTimer en residuo float ~5e-8 en lugar de exactamente
0; chequeo > 0 quedaba true infinito y el strike nunca ejecutaba.
Convención durable: todo timer float comparado contra 0 en el motor
usa epsilon tolerance.
Enemy::tickSystem signature extendida con PhysicsWorld* physics =
nullptr (D6 — tests headless funcionan; melee sin physics OK; projectile
sin physics → no-op silent). AssetManager& pasa a non-const para
loadWeapon on-demand. transitionTo con optional EnemySpec* (D7 reset
windUpTimer = spec->windUpSec al entrar Attack).
Bridge EditorApplication_Run::tickSystems pasa m_physicsWorld.get() al
Enemy::tickSystem. InspectorPanel_Enemy muestra los 3 nuevos fields en
categoria "Ataque" + runtime debug attackCooldownTimer + windUpTimer
decrementando en vivo. Lua enemy.spec(tag) retorna table con
attack_kind + wind_up_sec + projectile_weapon.
Demo data (D8): grunt.moodenemy extendido con attackKind="melee" +
windUpSec=0.3 + projectileWeapon="" (explicit aunque sean defaults).
imp.moodenemy NEW (ranged demo: HP=30 fragil, aggro=18m, attackRange=10m
largo, moveSpeed=3.5, windUpSec=0.5 telegraph mas largo,
attackCooldown=1.5s, attackKind=projectile,
projectileWeapon="weapons/imp_fireball.moodweapon"). imp_fireball.moodweapon
NEW (projectile category, speed=12 m/s slow visible — el player puede ver
venir el fireball y esquivar, splashRadius=1.5m, directDamage=18 +
splashDamage=12 slow but punchy, ignoreOwner=true para evitar self-damage
del Imp por su propio splash, lifetimeSec=4.0).
2 decisiones AskUserQuestion + 6 convenciones agente (D3-D8). 2 ajustes
reactivos R1-R2 descubiertos via tests: R1 SIGABRT crash al primer
applyEnemyAttack porque makePlayer en tests no agregaba HealthComponent
— fix: makePlayer agrega HealthComponent default current=100/max=100;
R2 k_timerEps tolerance + helper test advanceTicks(scene, dt, ticks,
...) para fast-forward granular preciso.
11 tests nuevos verdes: 4 en test_enemy_spec.cpp (defaults attack fields
F4H9 / roundtrip JSON melee / roundtrip JSON projectile / clamps
attackKind unknown→melee + windUpSec negativo→0); 7 en
test_enemy_system.cpp (wind-up no damage primeros 0.3s / damage al
player tras windUpSec / cooldown anti-spam / re-arm Pain→Chase→Attack
no instant strike post-pain / projectile sin physics no-op silent /
multiples enemies Attack independent / Attack reset al transition out +
re-entrada).
Suite full 1461/12384 verde (+11 cases / +31 asserts vs F4H8:
1450 → 1461). 0 regresión. Build MoodEditor verde.
Strategic deferral preservado: enemy sigue cubo placeholder; visual pass
unificado Sub-fase 4.3 con NPC Mixamo + ragdoll. Imp sin mesh distintivo
todavía. Hit react animation del player queda backlog F4H9.2 si emerge.
Plan F4H9 en docs/PLAN_HITO_F4H9.md. Próximo F4H10 (animaciones impacto +
ragdoll on death — posiblemente primer hito con NPC Mixamo o cubos
articulados, D1 a tomar al abrir).
Chequear:
- Spawn un grunt (Crear Entidad > Gameplay > Enemigo); spawn un player
cerca; entrar Play; dejarse atrapar; ver wind-up 0.3s antes del primer
golpe (HP baja en cada hit cada ~1s); damage flash + pain reaction
del player triggean automatic via polling F4H4/F4H6. - Spawn imp.moodenemy (cambiar el .moodenemy en el Inspector del enemy
o crear otro con combo): debe quedar a distancia (attackRange=10m
vs 2m del grunt) y disparar fireball hacia el player tras
windUpSec=0.5; el fireball vuela a 12 m/s visible, explota con splash
1.5m al impactar o al timeout 4s. - Matar al imp antes del primer golpe: el wind-up se cancela (transition
Dead via HP=0), no hay fireball spawned. - Recibir damage tipo Pain del player (dispararle al enemy en mid-Attack):
el enemy entra Pain stagger 0.3s; al recuperarse vuelve Chase y el
wind-up rearmarse — no instant strike post-pain. - Inspector del enemy en Play: ver attackCooldownTimer + windUpTimer
decrementando en vivo cuando esta en Attack. - Validar que el imp no se autodaña con su propio splash (ignoreOwner=true
en imp_fireball.moodweapon).
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
v3.8.0-fase4-hito8
feat(F4H8): chase con A* + attack tracking continuo (Doom Eternal-style)
Segundo hito Sub-fase 4.2. F4H7 dejo la state machine armada con Chase
y Attack como branches no-op. F4H8 activa el movimiento real conectando
EnemySystem con NavAgentComponent + NavSystem (Hito 23 A*). El enemy
persigue al player con A* puro grid y sigue rastreando mientras lo
golpea — Doom Eternal-style.
Helpers activateNavAgent + deactivateNavAgent inline en EnemySystem.cpp
(D7 auto-add on demand, no remove en transitions — solo desactivar flag).
State machine integrada:
- Alert (D5): dist > attackRange → Chase inmediato. Alert deja de ser
estable; apenas te ve, persigue. - Chase + Attack (D1): activate NavAgent target=playerPos +
speed=spec.moveSpeed. Tracking continuo en ambos — el enemy te marca
siempre mientras pelea. - Idle/Alert/Pain: deactivate NavAgent (no se mueven).
- Dead: deactivate ANTES de auto-add Dynamic RB (D8 — evita pelea de
autoridad sobre Transform que el comment NavAgentComponent flagea
explicito desde Hito 23). - Pain → Chase (no Alert) — A* retoma directo al recuperarse del stagger
con target valido.
R1 reactive — Post-switch NavAgent side-effects block.
Version inicial llamaba activateNavAgent adentro del case del switch
que evaluaba state PRE-transition. Tests reportaron fail
REQUIRE(hasComponent()) tras 2 ticks Idle→Alert→Chase:
el case Chase NO se ejecutaba en mismo tick de la transition. Fix via
SEGUNDO switch que evalua state POST-transition al final del lambda,
garantizando que Alert→Chase activa NavAgent en el MISMO tick. El switch
original quedo solo para evaluar transitions. Patron reusable para F4H9.
Bridge EditorApplication_Run (D4): forEach que
seteaba target=playerPos para TODOS los NavAgents ahora skipea entities
con EnemyComponent (if (e.hasComponent()) return).
EnemySystem es responsable de sus NavAgents enemy; el forEach queda
para back-compat con futuros NavAgents standalone (companions,
patrol NPCs Lua, vehiculos AI).
2 decisiones AskUserQuestion D1+D2 + 6 convenciones agente D3-D8 +
1 ajuste reactivo R1.
Decision out-of-scope capturada (verbatim dev):
"no quiero un mapa como una mesa de ajedrez, la idea no es que sea un
mapa plano, sino que tambien esten en zonas alejadas en puntos
diferentes, ni siquiera estoy hablando de un generador de mapas como
el de unreal imaginate". Es level design (F4H13 encounter), NO entra
a F4H8. Memoria project_pandemonium_map_design capturada — navmesh 3D
queda backlog post-F4H13 si emerge friccion con mapas multi-altura
(rampas/plataformas).
9 tests nuevos verdes + 1 update (F4H7 "Pain → Alert" → F4H8
"Pain → Chase" por semantic change). Tests cubren: Alert→Chase dist >
attackRange / Chase agrega NavAgent + target/speed/active / Attack
mantiene NavAgent active D1 tracking / Chase actualiza target cada tick
player movement / Pain desactiva NavAgent / Dead desactiva NavAgent
evita pelea Dynamic RB / Idle desactiva NavAgent player lejos / NO
double-add NavAgent en Chase↔Idle↔Chase / Attack→Chase si player se
aleja > attackRange*1.5.
Suite full 1450/12353 verde (+9 cases / +33 asserts vs F4H7: 1441→1450).
0 regresion.
Chequear:
- Crear player + enemy a 5m de distancia, Play mode.
- El enemy entra Alert (1 frame) y de inmediato Chase: empieza a
perseguirte usando A* (Transform.position se desplaza hacia ti). - Alejate > 18m (aggroRange*1.5): vuelve a Idle, deja de moverse.
- Volve cerca: re-engaga + Chase de nuevo. El componente NavAgent NO
se duplica (test NO double-add lo cubre, validar tambien runtime). - Acercate hasta < 2m (attackRange): transition a Attack, pero el
enemy SIGUE moviendose para mantenerte cerca (D1 Doom Eternal
tracking). Sin retroceso ni stop estilo Doom clasico. - Dispara al enemy: state → Pain + NavAgent active=false (se detiene
~0.3s) + post-painDuration vuelve a Chase + retoma A*. - Matalo (mas disparos): state Dead + NavAgent active=false +
Dynamic RB auto-added → cae con fisica (sin pelea sobre Transform). - Crear OTRO entity con NavAgentComponent manual (ej. un script Lua
agregando NavAgent a un companion) y sin EnemyComponent: el bridge
generico le sigue seteando target=playerPos automatico (back-compat
con el patron pre-F4H8).
Plan en docs/PLAN_HITO_F4H8.md. Memoria PANDEMONIUM map design
capturada para F4H13+.
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
v3.7.0-fase4-hito7
feat(F4H7): enemigo basico con maquina de estados + .moodenemy data-driven (apertura Sub-fase 4.2)
Primer hito Sub-fase 4.2 "¿es divertido pelear?". F4H1-F4H6 cubrieron el
combate desde el lado del player (salud + armas + HUD + game feel); F4H7
arranca el lado del enemigo: state machine + asset data-driven +
integracion con Health. El enemy sigue cubo placeholder per strategic
deferral del usuario en F4H6 — toda Sub-fase 4.2 cierra con cubos,
visual pass unificado en Sub-fase 4.3 con NPC Mixamo + ragdoll.
EnemyComponent engine-generic + EnemyState enum 6 estados
(Idle/Alert/Chase/Attack/Pain/Dead, D1 esqueleto completo desde dia 1 —
F4H8/F4H9 activan branches movimiento/ataque sin refactor) +
k_noTarget=0xFFFFFFFFu sentinela (R1 — EnTT handle 0 es valido).
.moodenemy asset type paralelo .moodweapon (D3): EnemySpec con
{displayName, health, aggroRange, attackRange, moveSpeed, damage,
attackCooldown, painThreshold, painDuration, viewmodelMesh, hitSound,
deathSound} + toJson/fromJson roundtrip + clamps (HP≥1, ranges≥0.01,
moveSpeed[0,50], cooldown≥0.05, painDuration≥0.05).
AssetManager::loadEnemy/getEnemy/enemyPathOf/missingEnemyId/
enumerateEnemies mismo patron Weapon + slot 0 fallback.
EnemySystem::tickSystem (~210 LOC) state machine: Idle→Alert si
dist≤aggroRange (D2 esfera de aggro, sin LoS raycast — convencion
Doom/Serious Sam); Alert hysteresis salir si dist>aggroRange*1.5;
Alert→Attack si dist≤attackRange; Pain trigger via polling
hc->hitFlashTimer > prevHitFlashTimer (D5, mismo patron R4 F4H4 sin
callbacks); Pain→Alert/Idle despues de spec.painDuration; Dead
transition + auto-add RB Dynamic en MISMO tick (D6+R2, descubierto via
tests).
Serialization SavedEnemy {enemyPath, state}; timers transients.
InspectorPanel_Enemy combo + Spec read-only + runtime debug.
handleAddEnemy via Crear Entidad → Gameplay → "Enemigo (cubo)" con
ICON_FA_SKULL. grunt.moodenemy demo (Grunt 50 HP, 12m aggro, 2m attack,
4 m/s, 15 dmg, 1.0s cooldown, painThreshold=10 painDuration=0.3s).
Lua bindings enemy.* (mirror weapon/health): get_state / set_state
(debug) / kill (apply 99999 dmg via Health) / spec.
Wireup EditorApplication_Run::tickSystems: Enemy::tickSystem despues
de Pickup, antes de Projectile + MOOD_PROFILE_SCOPE.
2 decisiones AskUserQuestion D1+D2 + 8 convenciones agente D3-D10 +
3 ajustes reactivos R1-R3 + 1 bug build-time B1 (StubTexture API drift).
23 tests nuevos verdes: 9 en test_enemy_spec.cpp (defaults / roundtrip /
missing-field / schema mismatch / clamps / saveFile-loadFile /
inexistente / JSON malformado / extension); 14 en test_enemy_system.cpp
(default Idle / Idle→Alert aggro / Idle fuera aggro / Pain polling /
Pain→Alert con target / Dead HP=0 / Dead auto-add RB / Dead terminal /
Dead no double-add / sin player Idle / Alert→Idle salida aggro*1.5 /
Alert→Attack attackRange / stateTime / multiples enemies independent).
Suite full 1441/12320 verde (+23 cases / +80 asserts vs F4H6: 1418→1441).
0 regresion.
Chequear:
- Crear Entidad → Gameplay → Enemigo: spawn cubo + HealthComponent +
EnemyComponent con grunt.moodenemy asignado. - Inspector del enemy: combo .moodenemy + spec read-only + dropdown
debug de state (idle/alert/chase/attack/pain/dead). - Crear "player" (otro entity tag="player") y acercarte al enemy en
Play mode: state Idle → Alert al entrar 12m. - Alejarte > 18m: state vuelve a Idle (hysteresis).
- Disparar al enemy: log "[enemy] 'Enemy_1' state Alert -> Pain";
~0.3s despues "state Pain -> Alert". - Matar al enemy (varios disparos): log "[enemy] state -> Dead" +
cae con fisica (RigidBody Dynamic auto-added). - Lua console: enemy.get_state('Enemy_1') / enemy.kill('Enemy_1') /
enemy.spec('Enemy_1'). - Save/Load: el state Idle persiste al recargar el .moodmap;
un enemy muerto sigue muerto al reabrir.
Plan en docs/PLAN_HITO_F4H7.md.
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
v3.6.0-fase4-hito6
feat(F4H6): game feel pass — muzzle flash + hit marker + screen shake + crosshair dinámico + pain reaction + tracer (cierre Sub-fase 4.1)
Cierre de Sub-fase 4.1 "¿se siente bien disparar?". F4H1-F4H5 dieron combate
funcional; F4H6 convierte combate funcional → combate que SE SIENTE.
6 feedback layers procedurales (sin assets art, intensidad HL/COD sutil):
- Muzzle flash — ParticleBurst naranja-amarillo en Weapon::fire (hitscan + projectile).
- Hit marker — cyan flash en crosshair via triggerHitMarker() al targetsHit > 0.
- Screen shake — HudState{shake_amp,t,max_t} + triggerCameraShake(amp, dur) con
anti-spam (>50ms) + FpsCamera::setShakeOffset() con eje aleatorio xorshift32 +
decay sin(t*80) * amp * (t/max_t). - Crosshair dinámico — crosshair_spread_deg synceado en bridge desde
WeaponSpec.spread + recoil acumulado, gap = 3 + min(16, spread*1.5). - Pain reaction — HudState{pain_pitch_amp,t,max_t, pain_roll_offset} +
triggerPainReaction() con anti-spam (>100ms) xorshift32 roll +
FpsCamera::setPainOffset(pitch, roll) aplica al forward() + viewMatrix(). - Tracer trail proyectil — ParticleEmitter adjunto al projectile entity con
preset heuristic por displayName (rocket→gris humo / plasma→cian aditivo /
grenade→naranja gravity sparks).
Triggers shake: dispara (0.015/0.08s), recibe daño (0.05/0.2s), explosion
projectile (0.2/0.4s con falloff distancia hasta 10m).
Polling pattern R4 F4H4 preservado: bridge polla state del frame anterior para
transitions sin acoplar engine→game. Camera offsets aplicados al final del
frame, reset a 0 si timers expiran.
Projectile::tickSystem retorno void → TickStats{explosionCount, damageTargetsHit,
lastExplosionCenter} para bridge polling post-explosion. applySplashDamage
void → int (count entities dañadas).
2 decisiones AskUserQuestion: D1 todos 6 efectos en hito atómico; D2 intensidad
HL/COD sutil (vs Doom-Eternal / Quake hyper-arcade).
4 convenciones agente D3-D6: procedural-only sin assets (defer visual pass a
Sub-fase 4.3); polling vs callbacks decoupled; eje shake xorshift32 deterministic;
tracer preset heuristic displayName (vs field schema — defer a F4H6.1).
3 ajustes reactivos R1-R3: R1 triggerCameraShake anti-spam >50ms (granadas
multi-bounce stutter); R2 Projectile::tickSystem retorno void → TickStats;
R3 tracer NO se agregó al schema (heuristic cubre F4H5 demo).
Strategic deferral del usuario (verbatim): "antes de lo visual falta algo mas en
el sistema? cuando tengamos toda la logica implementada ahi podemos ver lo
visual, ademas no quiero usar el cesium man, ese eliminalo, tenemos ya un npc
de mixamo con algunas animaciones y podemos bajar mas".
Implicación: Sub-fase 4.2 (F4H7-F4H12 enemies) cierra con cubos placeholder.
Visual pass unificado en Sub-fase 4.3 después (texturas + decals + viewmodel
meshes + ragdoll NPC Mixamo + animaciones impacto).
Asset cleanup: assets/meshes/CesiumMan.glb eliminado (Fox.glb preservado:
test_scene_loader.cpp:56 lo referencia).
14 tests nuevos verdes en test_game_feel_f4h6.cpp (40 asserts):
- 3 triggerCameraShake (setea amp+timers / anti-spam más débil / más fuerte reemplaza).
- 2 triggerPainReaction (setea pitch+roll+timer / anti-spam timer > 0.1s).
- 5 FpsCamera offsets (shake desplaza view / shake(0) restaura / pain modifica
forward pitch / pain(0,0) restaura / defaults sin offsets). - 2 applySplashDamage (retorna count / radius=0 retorna 0).
- 2 crosshair spread (HudState default 0 / reset limpia timers F4H6).
Suite full 1418/12240 verde (+14 cases / +40 asserts vs F4H5: 1404 → 1418).
0 regresión.
Chequear:
- Disparar arma hitscan (escopeta/pistola): muzzle flash naranja-amarillo +
micro screen shake + crosshair se abre brevemente + cierra suave. - Disparar arma projectile (rocket/plasma): tracer trail visible al volar
(gris humo / cian aditivo / naranja con gravity), explosion → camera shake
proporcional a distancia (cerca = fuerte, >10m = 0), hit marker cyan si pegó. - Recibir daño del dummy: pain reaction pitch up 2° + roll aleatorio +
damage flash vignette rojo + camera shake medio. - Granada multi-bounce con 3-4 explosiones rápidas: anti-spam debería evitar
stutter visual (shake no se reinicia salvo más fuerte). - HudState.reset() limpia timers (validable con tab→Play→tab→Play).
Plan en docs/PLAN_HITO_F4H6.md.
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
v3.5.0-fase4-hito5
feat(F4H5): armas de proyectil (rocket + plasma + granada) con splash radial
Quinto hito de Sub-fase 4.1. Cierra el item "armas de proyectil" del
PLAN_FASE4 §5 (era F4H4 textual original, renumerado al cerrar F4H4 =
HUD + pickups). Hasta F4H4 todas las armas eran hitscan via raycast —
F4H5 introduce el proyectil fisico con velocidad finita visible,
colision continua via raycast prevPos->newPos, y splash damage radial.
Sub-tareas (6/6):
- Schema .moodweapon extendido con bloque
projectile {meshPath, materialPath, speed, gravity, bounceCount, bounceFactor, lifetimeSec, directDamage, splashRadius, splashDamage}. Defaults razonables.
Clamps anti-tunneling (speed cap 200 m/s) + sanidad. - ProjectileComponent engine-generic + Projectile::tickSystem
(~250 LOC) + applySplashDamage funcion libre con falloff lineal
dmg = baseDmg * max(0, 1 - dist/radius). ignoreOwner u32 raw
skipea shooter (opt-out rocket-jump). - Weapon::fire branch dual hitscan/projectile. Projectile spawnea
entity efimera con Transform + MeshRenderer cubo placeholder +
ProjectileComponent + offset 0.4m forward. - Wireup EditorApplication_Run: Projectile::tickSystem despues de
Pickup::tickSystem con MOOD_PROFILE_SCOPE. - 3 .moodweapon demo en assets/weapons/:
- rocket: 25 m/s, splash 4m, dmg 80+60, mag 4, Quake risk/reward.
- plasma: 50 m/s, splash 1.5m, dmg 40+15, mag 30, cadencia 3.5/s.
- grenade: 12 m/s, gravity 9.8, bounce x3 factor 0.55, lifetime
2.5s, splash 4.5m, dmg 30+120 alpha kill por skill-shot.
2 decisiones AskUserQuestion (D1 las 3 armas data-driven dia 1 / D2
splash esfera + falloff lineal Doom/Quake — descartado ForceField
one-shot por overkill + raycast visibility check por costo 2-3x).
5 convenciones agente D3-D7 (self-damage on default, mesh placeholder
cubo, bounce manual sin Jolt Dynamic, raycast continuo, ParticleBurst
on explode). 3 ajustes reactivos R1-R3 (owner u32 raw vs entt::entity,
applySplashDamage u32 ignoreOwnerRaw, bounceFactor [0,1] inclusivo).
1 bug build-time fix B1 (entt::null requiere include, fix via u32 raw).
13 tests nuevos / suite full 1404/12200 verde (+13 cases / +36 asserts
vs F4H4).
Renumeracion roadmap: F4H5 textual (game feel) -> F4H6.
Chequear:
- Crear nuevo proyecto -> spawneo Player -> Inspector "Arma": 4 slots,
slot 0 con primera arma alfa (rocket / pistola / plasma / shotgun
segun orden alfabetico del catalogo). - AssetBrowser tab Armas muestra las 6 .moodweapon (shotgun + pistola +
rocket + plasma + grenade + cualquier otra existente). - Play -> swap a rocket (scroll/1-4) -> click -> ves cubo rojo volando
a 25 m/s desde el crosshair -> impacta pared o maniqui -> explosion
particle burst naranja + maniqui cercano (dentro de 4m) recibe damage
scaled (centro 60 dmg, borde 0). - Swap a plasma -> click -> proyectil cyan rapido + splash chico.
- Swap a granada -> click -> proyectil verde con arco gravitacional ->
rebota 3 veces en superficies -> explota tras 2.5s o al pegar a un
enemy directo. - Damage al maniqui: con HP=100, rocket en centro = 100 - 60 = 40 HP
restantes; en borde del splash = 100 HP. - Rocket-jump: pararse cerca del impacto del propio rocket -> el player
recibe splash (convencion Quake). Para desactivarlo, editar
rocket.moodweapon: ignoreOwner: false (default true para hitscan,
se aplica al splash tambien — el player NO se daña al disparar).
Wait: re-leer — por default ignoreOwner=true entonces el player NO
se daña a si mismo. Para que el rocket-jump funcione, cambiar
ignoreOwner:false en el .moodweapon.
Plan en docs/PLAN_HITO_F4H5.md.
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
v3.4.0-fase4-hito4
feat(F4H4): HUD de combate + Pickups en el mapa
Cierra F4H4 — los 2 faltantes del F4H3 textual del roadmap (HUD + pickups)
sobre el cimiento data-driven de F4H1-F4H3. Sub-fase 4.1 queda con HP/
armor/ammo/arsenal visibles en pantalla + pickups recogibles.
Sub-tareas (9/9):
- ArmorComponent {current, max=100, absorbRatio=0.66}. Health::applyDamage
consume armor primero (HL2 style). SavedArmor roundtrip. Lua bindings
armor.give/set/get. - PickupComponent + PickupSystem engine-generic. Enum {Weapon, Ammo,
Health, Armor} + spin/bob/radius. tickSystem detecta overlap por
distancia + aplica payload + destroy. Refill ammo en arma duplicada.
applyHealth/Armor retornan bool (no consume si full — Doom-style). - HUD widgets reusando framework GameOverlay (R1 vs plan):
- drawArmorNumber bottom-left arriba del HP (azul HL).
- drawArsenalOverlay top-center 4 boxes con activo highlighted + fade 3s.
- HudState extendido (armor/max_armor/arsenal_*).
- GameState::triggerArsenalOverlay() gemelo de triggerDamageFlash.
- drawDamageVignette + drawCrosshair ya existian (F2H39).
- handleAddPlayer revert: solo slot 0 default + HP=100 + armor=0. Sandbox
F4H3 (4 slots auto) eliminado. - InspectorPanel_Armor.cpp + InspectorPanel_Pickup.cpp (categoria
Gameplay). Pickup tiene dropdown enum + campos condicionales. - Bridge EditorApplication_Run::tickSystems: Pickup::tickSystem despues
de Weapon::tickViewmodel + sync HudState cada frame + transition
detection (m_f4h4_prevHitFlashTimer / m_f4h4_prevActiveSlot) para
trigger damage flash + arsenal overlay. Polling vs callbacks (R4)
mantiene engine/gameplay desacoplado de engine/game. - 18 i18n keys (es+en) + 3 entradas Add Component menu (Health, Armor,
Pickup en categoria Logic). - Tests: 9 ArmorComponent + 11 PickupSystem = 39 cases / 94 asserts
nuevos. Suite full 1391/12164 verde (+39 vs F4H3).
4 decisiones D1-D4 (bundle / HUD HL-style sutil / kit completo 4 tipos /
proyectiles van a F4H5). 5 ajustes reactivos R1-R5 (framework reuse,
trigger en GameState, retorno bool no-consume Doom-style, polling vs
callbacks, Add Component menu vs cards modal). 2 bugs build-time fixed
(WeaponSpec.h include en PickupSystem.cpp, LuaBindings_Armor.cpp en
tests CMakeLists).
Renumeracion roadmap PLAN_FASE4: F4H4 textual (proyectiles) -> F4H5;
F4H5 (game feel) -> F4H6. Cascada documentada.
Chequear:
- Crear nuevo proyecto -> spawneo Player -> Play.
- HUD muestra:
- Bottom-left: HP=100/100 (rojo) + ARMOR=0/100 (azul, arriba del HP).
- Bottom-right: ammo 6/6 (escopeta default).
- Crosshair centrado.
- En Inspector: tab Gameplay muestra "Salud" + "Armadura" + "Arma" (4
slots, slot 0 con escopeta, 1-3 vacios). - En Inspector: Add Component -> Logic ahora tiene Health/Armor/Pickup.
- Crear Empty -> Add Component -> Pickup. En el Inspector: dropdown type
(Weapon/Ammo/Health/Armor) + campos condicionales segun tipo. - En Play: caminar hacia el pickup Health/Armor -> HP/armor sube + pickup
desaparece. Si player full HP/armor, pickup NO se consume (queda en
suelo, Doom-style). - En Play: swap de arma (scroll/Q/1-4) -> arsenal overlay aparece
top-center 3s con los 4 slots (activo highlighted naranja) + fade. - Consola Lua: health.damage("player", 25) -> HP baja 25 + vignette
rojo full-screen. - Consola Lua: armor.give("player", 50) -> armor sube 50. Volver a
damage: armor come 66% primero, sobrante al HP. - Save proyecto + close + reopen: HP/armor/arsenal/pickups persisten.
Plan en docs/PLAN_HITO_F4H4.md.
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
v3.3.0-fase4-hito3
feat(F4H3): arsenal multi-slot + swap (scroll/Q/1-4) + viewmodel en mano
Cierra F4H3 — segunda arma + swap + viewmodel sobre el cimiento data-driven
de F4H2. Loop "tengo varias armas, las cambio, las veo en mano".
Sub-tareas (9/9):
- Multi-slot WeaponComponent: WeaponSlot{weaponAssetId, currentAmmo} x4
- activeSlot + lastActiveSlot. Refactor fire/reload/canFire/tickSystem
con helper activeSlotOf() (clamp defensivo).
- activeSlot + lastActiveSlot. Refactor fire/reload/canFire/tickSystem
- Swap API: swapToSlot/swapNext/swapPrev/swapLast. Cycle skip vacios.
applySwap helper resetea fireTimer/reloadTimer (interrumpe reload). - InputActions extendido: BindingType::MouseWheel (code +/-1),
wasActionTriggered one-shot (prevPressed map + scroll delta sign),
notifyScrollEvent/pollScrollDelta/endFrame/resetState lifecycle. - Bridge swap en EditorApplication_Run: 7 dispatches wasActionTriggered
-> Weapon::swap*. processEvents notifica SDL_MOUSEWHEEL. - Viewmodel render: ViewmodelComponent + Weapon::tickViewmodel free
function. Sync transform camera-space + mesh on slot change (fallback
missingMeshId cubo si !spec.viewmodelMesh). - assets/weapons/pistola.moodweapon (25 dmg / 80m / 1 pellet / 5 rate).
- Inspector Weapon: 4 tabs (uno por slot) + marker * activo + boton
"Activar este slot". 2 i18n keys nuevas. - handleAddPlayer rellena los 4 slots con enumerateWeapons() + crea
entity hija "__viewmodel" con cubo placeholder.
Persistencia: SavedWeaponSlot x4 + activeSlot. Parser detecta F4H3
(slots[]) vs F4H2 (path/currentAmmo plano) -> mapea F4H2 a slots[0]
automatic. Back-compat exhaustiva con maps F4H2.
Decisiones (D1 scroll+Q NOT Doom numerico / D2 4 slots Quake style /
D3 viewmodel con near-plane bajo, render pass dedicado F4H3.1 si emerge
clip-thru / D4 sandbox con todos los slots desde inicio). 5 ajustes
reactivos R1-R5 (clamp activeSlot / equipWeapon back-compat / sandbox
rellena TODOS / componente+free func / mesh fallback cubo).
Tests: 19 nuevos / suite full 1352/12070 verde (+10 cases / +34 asserts
vs F4H2 Bloque B).
Bug build-time: campo MeshRendererComponent.mesh (no .meshId); 2 sitios
corregidos.
Chequear:
- Crear nuevo proyecto -> Play -> spawneo Player desde Create Entity.
- El Player tiene 4 slots de armas equipados (Inspector > Arma muestra
4 tabs con shotgun + pistola + posibles otros si hay catalogo). - Tab activo marcado con '*'. Boton "Activar este slot" en no-activos
hace swap inmediato + reset de timers. - En Play mode: scroll wheel up/down cicla armas (skip slots vacios).
- Tecla Q vuelve a la arma anterior usada.
- Teclas 1-4 swap directo al slot N.
- Viewmodel visible flotando en la camara (hoy es cubo placeholder
porque ninguna .moodweapon tiene viewmodelMesh art real). Cambia
al hacer swap (con missing-mesh el cubo se mantiene visible). - Click izquierdo dispara el arma activa con su damage/range/spread
propios (pistola precisa 1 pellet, shotgun 8 pellets con spread). - R recarga el arma activa; el ammo del resto de slots NO se pierde.
- Save proyecto + close + reopen: los 4 slots persisten con su ammo +
activeSlot restaurado. - Cargar map F4H2 viejo: WeaponComponent plano (path/currentAmmo)
mapea automatic a slots[0]; activeSlot=0; el resto vacios.
Plan en docs/PLAN_HITO_F4H3.md.
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
v3.2.0-fase4-hito2-B
docs(F4H2 Bloque B cierre): PLAN CERRADO + ESTADO/HITOS/DECISIONS
Cierre formal del bloque tras validacion e2e del dev ("todo ok"):
- PLAN_HITO_F4H2_B.md: estado CERRADO + bloque "Entregado" con Sub-1..4 + 5 fixes UX + 3 bugs adicionales detectados en validacion.
- ESTADO_ACTUAL.md: entry F4H2 Bloque B agregado a Sub-fase 4.1 (con tag v3.2.0-fase4-hito2-B). Proximo hito = F4H3 (segunda arma + swap + viewmodel).
- HITOS.md: bullet F4H2 Bloque B en Fase 4 / Sub-fase 4.1.
- DECISIONS.md: nueva entrada 2026-05-29 F4H2 Bloque B con D1..D5 (input bridge via tag "player", Weapon empty card eliminada, auto-asignar primera arma, drag-drop -> combo, brushes con RigidBody auto convencion Hammer) + B1..B4 (bugs detectados durante validacion).
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
v3.2.0-fase4-hito2-A
feat(F4H2 Bloque A): primera arma hitscan data-driven (.moodweapon asset + WeaponSystem + bindings + inspector)
Cierra el bloque A de F4H2: infraestructura engine-generic completa
para armas hitscan. Decisión arquitectónica del dev al cerrar F4H1.5:
"no quiero nada hardcodeado, que pueda ser dinamico para reutilizar a
futuro en otro juego". Fase 4 entera engine-generic — el motor provee
sistemas, el juego provee data. PANDEMONIUM en assets/, no en src/.
Asset type .moodweapon:
- WeaponSpec engine/gameplay/weapon/WeaponSpec.{h,cpp} ~180 LOC
Schema JSON: damage/range/pellets/spread/fireRate/magSize/reload- asset refs viewmodel/sound/vfx + clamps de sanidad + roundtrip.
- AssetManager::loadWeapon/getWeapon/weaponPathOf/missingWeaponId
mismo patrón Vehicle/Item/Quest; slot 0 fallback con defaults. - rename helper para .moodweapon.
WeaponComponent (plain data):
{ weaponAssetId, currentAmmo, fireTimer, reloadTimer, firing }
- serialización roundtrip (timers transients).
WeaponSystem ~310 LOC engine-generic:
- fire(scene, shooter, FireParams, physics, audio, assets) → FireResult
Loop pellets con dispersión cónica (perturbConeDirection muestra
direcciones uniformes en cono sólido), raycast Jolt con ignoreOwner,
Health::applyDamage al hit, sonido 3D positional + particle burst
one-shot (ParticleBurstComponent transient con TTL auto-cleanup). - reload / equipWeapon / canFire / ammoLeft / tickSystem.
4 decisiones del dev:
- D1 escopeta primero (8 pellets + 6° spread; schema soporta pistola
via pellets=1). - D2 sonido + particle siempre al impactar (decal NO entra — agendizado
F4H2.1, no existe DecalComponent). - D3 slot vacío + drag-drop (Crear Entidad UX queda para Bloque B).
- D4 data-driven keybindings desde día 1 (UserSettings.input.keybindings
agendizado a Bloque B).
Lua bindings weapon.*:
LuaBindings_Weapon.cpp ~140 LOC mirror Health.
weapon.equip(tag, path) / fire(tag, opts) / reload / can_fire / ammo /
spec(tag) → table read-only.
Wireado con audio=nullptr placeholder (cascada AudioDevice via
ScriptSystem agendizada a Bloque B).
Inspector "Arma" en categoría Gameplay:
- Drag-drop .moodweapon al slot del WeaponComponent.
- Display read-only del Spec equipado (damage/range/pellets/etc).
- Slider Ammo runtime (debug).
Demo data: assets/weapons/shotgun.moodweapon
(15 dmg, 25 m, 8 pellets, 6° spread, 1.5 rate, 6 mag, 1.8 s reload)
Wireado:
- WeaponSystem::tickSystem en EditorApplication_Run Play loop.
- setupWeaponBindings en setupLuaBindings.
32 tests verdes / 105 asserts:
- 18 en test_weapon_asset.cpp (defaults, roundtrip shotgun+pistola,
missing-field defaults, version mismatch, clamps damage/pellets/
fireRate/spread/mag/reload, saveToFile-loadFromFile, archivo
inexistente, JSON malformado). - 14 en test_weapon_system.cpp (equipWeapon basic+empty path+missing
asset, canFire false sin component/sin arma/cooldown/reload/ammo=0,
canFire true mag full, reload arranca timer + no-op si full/sin arma,
tickSystem decae fireTimer + completa reload + auto-clears firing flag).
Suite full 1342/12036 verde (+33 vs F4H1.5: 1309 → 1342). Cero regresión.
Scope split decisión: al cerrar Sub-tareas 1-4 + Inspector + demo data,
las restantes Sub-tareas 5 (input bridge data-driven UserSettings
keybindings), 6.b (AssetBrowser tab "Weapons"), 7 (Crear Entidad UX) y
la cascada AudioDevice → ScriptSystem requieren wiring transversal
sustancial. Shipping Bloque A con tag dedicado para preservar la
infraestructura como cimiento limpio; Bloque B cerrará F4H2 completo
con tag v3.2.0-fase4-hito2. Bloque A es testeable HOY vía Lua manual.
Cómo probarlo HOY (Lua manual en la consola del editor):
weapon.equip("player", "weapons/shotgun.moodweapon")
weapon.fire("player", {origin_x=0, origin_y=2, origin_z=0,
dir_x=0, dir_y=0, dir_z=-1})
print(weapon.ammo("player")) -- 5
Backlog F4H2 Bloque B (PLAN_HITO_F4H2_B.md):
- Input bridge UserSettings.input.keybindings.fire → mouse-left dispara
sin Lua manual. - AudioDevice cascada ScriptSystem::update → setupLuaBindings →
setupWeaponBindings → sonido al disparar. - AssetBrowser tab "Weapons" filtrada por .moodweapon.
- Crear Entidad → Gameplay → "Arma (vacía)".
Backlog post-F4H2 completo:
- F4H2.1 decals; F4H2.2 .moodvfx asset type; F4H2.3 reload animation;
F4H2.4 recoil + camera shake; F4H2.5 bullet tracer. - F4H3 segunda arma + swap.
Chequear:
- Abrir editor → cargar proyecto cualquiera.
- Agregar entidad con TagComponent "player" + WeaponComponent.
- Inspector debería mostrar sección "Arma" con slot vacío.
- Drag assets/weapons/shotgun.moodweapon al slot → spec se muestra
read-only (15 dmg / 25 m / 8 pellets / 6° / 1.5 rate / 6 mag). - Slider Ammo runtime debería arrancar en 6.
- En Play mode → consola Lua → weapon.fire("player", {...}) debería
loggear "[weapon] 'player' disparo 'Escopeta' (8 pellets, N hits,
ammo 5/6)". - Verificar que entidad con HealthComponent recibe damage al ser
impactada por raycast. - Suite mood_tests verde.
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
v3.1.1-fase4-hito1-5
refactor(F4H1.5): code audit pre-F4H2 + 3 splits + extract findEntityByTag
Sub-hito reactivo tras pedido del dev al cerrar F4H1: "antes de escribir
mucho codigo en F4H2-F4H11, audit. Sin spaghetti, sin deadcode, sin
archivos enormes."
Quick audit estilo F3H3 sobre 3 buckets (LOC + duplicacion + deadcode).
Output: docs/CODE_AUDIT_F4_PRE.md.
Veredicto top-line: codebase mas limpio que esperado.
- 0 TODOs reales
- Helpers ya extraidos donde importan (inspectorResetButton,
pushEditIfDone, beginComponentSection) - Guards "if (!m_scene)" 38 callsites son idiomatic (no spaghetti)
- 4 archivos sobre hard cap 800 LOC: 3 fixeados, 1 agendizado
Fixes aplicados:
- InspectorPanel_Internal.h 885 -> 412 + 506
Multi-edit helpers (allMatch + 5 templates) extraidos a
InspectorPanel_Internal_MultiEdit.h. Include desde _Internal.h
para back-compat de callsites. - SceneRenderer.cpp 894 -> 554 + 298
loadSkyboxAndIblFromBase + applyEnvironmentFromScene movidos a
SceneRenderer_Environment.cpp. Includes simetricos con el core. - findEntityByTag centralizado en BindingsCommon.h
3 copias inline en LuaBindings_{Health,Ragdoll,Vehicle}.cpp
reemplazadas por "using bindings::findEntityByTag". F4H2+ va a
sumar bindings de weapon/enemy que reusan el helper.
Fix agendizado:
4. InspectorPanel_Environment.cpp 1149 LOC -> F4H1.6
Requiere extraer ~180 LOC de infraestructura comun (kEnvDefaults,
EditEnvironmentSubsetCommand class, drawSectionResetButton template,
drawSectionDivider) a header dedicado. Riesgo > beneficio
inmediato; F4H2-F4H11 no tocan este archivo (Inspector Environment,
no combate). Sub-hito propio con plan dedicado.
Hallazgo reactivo:
- DemoSpawners_Drop.cpp 759 LOC NO es deadcode. Es el handler de
drops del viewport (textura/mesh/material/script al area 3D del
editor, F2H24 + F3H17). Nombre "Demo" es historico.
LOC residual sobre cap (2 archivos, ambos diferidos con razon):
- InspectorPanel_Environment.cpp 1149 -> F4H1.6 (plan dedicado).
- SceneLoader.cpp 819 (1 LOC sobre cap, marginal) -> F4H6 cuando
enemy serialization lo empuje.
Decisiones (4):
- D1 Quick audit vs full (eleccion AskUserQuestion).
- D2 Hito separado con fixes vs solo escribir el audit (AUQ).
- D3 (reactivo) DemoSpawners no es deadcode.
- D4 (reactivo) _Environment split aplazado a F4H1.6.
Tests: suite full 1309/11931 verde. Sin tests nuevos (refactor puro,
los tests existentes garantizan equivalencia).
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com