Deploy main to deploy - #426
Merged
Merged
Conversation
* feat(sql): split magic seed data * feat(sql): extract component params * feat(sql): split prefab params Move shared shot/slime/drop/explode/build/field values to prefab-specific keys and seed them through a new SQL file. * feat(sql): split parameter skill * feat(chain-lightning): parameterize min damage * feat(wind-blade): decay damage on pierce * refactor(tide-call): use prefab parameters * chore: remove rain_cloud * feat(rallying-torch): add inspired buff * fix(leafair): refresh timed lifetimes * feat(frenzy-totem): parameterize buff * refactor: tower에서 tower, dummy mob을 분리하도록 수정 * feature: tower back에 tower 기능을 추가하고 attack mob으로 근접 공격도 가능하도록 구현 * refactor: fire spirit의 burn 효과 제공 범위를 sub attack range로 분리 * feature magma spirit의 범위 burn provider 추가 * fix: tornado 안에 있는 mod들에게 데미지를 주는 로직 추가 * fix: item 기능을 고침
Keep production migrations in WordOnlineDatabase and retain only test-specific SQL fixtures in the game server. Closes #274
* feature(botPresona) - 봇 페르소나 추가 * feature(scheduling) - 봇 스케쥴링 구현 * feature(botmagics) - 봇 마법 시전시 봇의 magic 파싱하게 수정 * Update WordOnlineLoop.java - 시간 초과시 승패 체크
* refactor(effect): support multiple effects * feat(card): add aura feedback
-태그 시스템에 폴백 추가
* refactor(coordinates): use y-up 3d axes * refactor(coordinates): remove Vector2 ground plane type * feature(magic): clamp cast position to map bounds before range * refactor(field): introduce AbstractFieldPrefabInitializer
* feat(bot): link personas to bot users * feat(debug): match practice with random bot
…to use random Z coordinate
* feat(wall) - 벽에 콜리젼을 추가함 * feature(newMagic) - Vine World 마법 추가 - Seed Nest 마법 추가 * feature(evolve) - 덩굴류 마법에 맞은 풀씨 정령이 덩굴 정령으로 진화하는 매커니즘 추가
- 소환하는 몹의 소환물 위치를 대상의 중심에서 대상이 최대 사거리 안이라면 대상 위치, 밖이라면 대상 방향의 최대사거리로 변경했습니다.
- 집결의 횃불 리워크 - 집결 매커니즘 변경 (대상 위치로 이동 -> 대상 위치로 이동 및 대상 주위 범위의 가능한 적 공격)
- 광란 상태이상 리워크 - 나 이외의 모든 대상을 공격합니다.
* 260403 deploy (#230) * Update Docker CI to support multiple platforms * Fix/228 (#229) * fix(bug) - pve boss 2스테이지 이후부터 소환 시 거리 제한이 있던 것 수정 - 공격 프리팹을 만들 때 시전자가 맞는 현상 수정 * fix(shot) - 덩굴, 바람 칼날 자해 안함 * adjust(pve balance) - pve 보스들의 소환 주기와 소환 수를 대폭 하향 - pve 보스 안나오고 사라지는 버그 수정 * feature(debugPVE) - 디버그용 pve 엔드포인트 생성 * adjust(pve balance) - pve 보스들의 소환 주기와 소환 수를 대폭 하향 * Update PveVineWitchPrefabInitializer.java --------- Co-authored-by: Yunseong Jeong <88422717+dev-yunseong@users.noreply.github.com> * Feature/249 (#250) (#251) * feautre(newmagic) - 전기 폭발 추가 - 칼바람 회오리 추가 * Update razor_gale.sql - 범위 수정 * Deploy main to deploy (#285) * Feature/271 (#272) * feat(sql): split magic seed data * feat(sql): extract component params * feat(sql): split prefab params Move shared shot/slime/drop/explode/build/field values to prefab-specific keys and seed them through a new SQL file. * feat(sql): split parameter skill * feat(chain-lightning): parameterize min damage * feat(wind-blade): decay damage on pierce * refactor(tide-call): use prefab parameters * chore: remove rain_cloud * feat(rallying-torch): add inspired buff * fix(leafair): refresh timed lifetimes * feat(frenzy-totem): parameterize buff * refactor: tower에서 tower, dummy mob을 분리하도록 수정 * feature: tower back에 tower 기능을 추가하고 attack mob으로 근접 공격도 가능하도록 구현 * refactor: fire spirit의 burn 효과 제공 범위를 sub attack range로 분리 * feature magma spirit의 범위 burn provider 추가 * fix: tornado 안에 있는 mod들에게 데미지를 주는 로직 추가 * fix: item 기능을 고침 * refactor(db): move SQL ownership (#275) Keep production migrations in WordOnlineDatabase and retain only test-specific SQL fixtures in the game server. Closes #274 * feat: expose debug session list (#279) * fix(deck): seed initial shuffle (#277) * Feature/273 (#280) * feature(botPresona) - 봇 페르소나 추가 * feature(scheduling) - 봇 스케쥴링 구현 * feature(botmagics) - 봇 마법 시전시 봇의 magic 파싱하게 수정 * Update WordOnlineLoop.java - 시간 초과시 승패 체크 * fix(gameplay): repair magic behavior regressions (#282) * 카드 선택 아우라 피드백 추가 (#284) * refactor(effect): support multiple effects * feat(card): add aura feedback * fix(error) -태그 시스템에 폴백 추가 --------- Co-authored-by: Monolong <83206119+Monolong@users.noreply.github.com> * fix: reconcile deploy with main (#297) * Feature/271 (#272) * feat(sql): split magic seed data * feat(sql): extract component params * feat(sql): split prefab params Move shared shot/slime/drop/explode/build/field values to prefab-specific keys and seed them through a new SQL file. * feat(sql): split parameter skill * feat(chain-lightning): parameterize min damage * feat(wind-blade): decay damage on pierce * refactor(tide-call): use prefab parameters * chore: remove rain_cloud * feat(rallying-torch): add inspired buff * fix(leafair): refresh timed lifetimes * feat(frenzy-totem): parameterize buff * refactor: tower에서 tower, dummy mob을 분리하도록 수정 * feature: tower back에 tower 기능을 추가하고 attack mob으로 근접 공격도 가능하도록 구현 * refactor: fire spirit의 burn 효과 제공 범위를 sub attack range로 분리 * feature magma spirit의 범위 burn provider 추가 * fix: tornado 안에 있는 mod들에게 데미지를 주는 로직 추가 * fix: item 기능을 고침 * refactor(db): move SQL ownership (#275) Keep production migrations in WordOnlineDatabase and retain only test-specific SQL fixtures in the game server. Closes #274 * feat: expose debug session list (#279) * fix(deck): seed initial shuffle (#277) * Feature/273 (#280) * feature(botPresona) - 봇 페르소나 추가 * feature(scheduling) - 봇 스케쥴링 구현 * feature(botmagics) - 봇 마법 시전시 봇의 magic 파싱하게 수정 * Update WordOnlineLoop.java - 시간 초과시 승패 체크 * fix(gameplay): repair magic behavior regressions (#282) * 카드 선택 아우라 피드백 추가 (#284) * refactor(effect): support multiple effects * feat(card): add aura feedback * fix(error) -태그 시스템에 폴백 추가 * 서버 좌표계를 Unity 3D 기준으로 전환 (#287) * refactor(coordinates): use y-up 3d axes * refactor(coordinates): remove Vector2 ground plane type * feature(magic): clamp cast position to map bounds before range * refactor(field): introduce AbstractFieldPrefabInitializer * feat(bot): 봇 사용자 연동 및 Debug 랜덤 연습 대전 (#289) * feat(bot): link personas to bot users * feat(debug): match practice with random bot --------- Co-authored-by: Monolong <83206119+Monolong@users.noreply.github.com> * Deploy main to deploy (#295) * Feature/271 (#272) * feat(sql): split magic seed data * feat(sql): extract component params * feat(sql): split prefab params Move shared shot/slime/drop/explode/build/field values to prefab-specific keys and seed them through a new SQL file. * feat(sql): split parameter skill * feat(chain-lightning): parameterize min damage * feat(wind-blade): decay damage on pierce * refactor(tide-call): use prefab parameters * chore: remove rain_cloud * feat(rallying-torch): add inspired buff * fix(leafair): refresh timed lifetimes * feat(frenzy-totem): parameterize buff * refactor: tower에서 tower, dummy mob을 분리하도록 수정 * feature: tower back에 tower 기능을 추가하고 attack mob으로 근접 공격도 가능하도록 구현 * refactor: fire spirit의 burn 효과 제공 범위를 sub attack range로 분리 * feature magma spirit의 범위 burn provider 추가 * fix: tornado 안에 있는 mod들에게 데미지를 주는 로직 추가 * fix: item 기능을 고침 * refactor(db): move SQL ownership (#275) Keep production migrations in WordOnlineDatabase and retain only test-specific SQL fixtures in the game server. Closes #274 * feat: expose debug session list (#279) * fix(deck): seed initial shuffle (#277) * Feature/273 (#280) * feature(botPresona) - 봇 페르소나 추가 * feature(scheduling) - 봇 스케쥴링 구현 * feature(botmagics) - 봇 마법 시전시 봇의 magic 파싱하게 수정 * Update WordOnlineLoop.java - 시간 초과시 승패 체크 * fix(gameplay): repair magic behavior regressions (#282) * 카드 선택 아우라 피드백 추가 (#284) * refactor(effect): support multiple effects * feat(card): add aura feedback * fix(error) -태그 시스템에 폴백 추가 * 서버 좌표계를 Unity 3D 기준으로 전환 (#287) * refactor(coordinates): use y-up 3d axes * refactor(coordinates): remove Vector2 ground plane type * feature(magic): clamp cast position to map bounds before range * refactor(field): introduce AbstractFieldPrefabInitializer * feat(bot): 봇 사용자 연동 및 Debug 랜덤 연습 대전 (#289) * feat(bot): link personas to bot users * feat(debug): match practice with random bot --------- Co-authored-by: Monolong <83206119+Monolong@users.noreply.github.com> --------- Co-authored-by: Monolong <83206119+Monolong@users.noreply.github.com>
* feat: block paths with dedicated rock remnants Rock creatures and rock turrets that die in combat now leave a RockRemnant instead of a live MiniRock: no movement, no attack, no hp, just a solid collider that expires after 20 seconds. StaticObstacle is needed because PhysicSystem only pairs objects that own a Collidable component, so a bare collider would never block. Closes #308 * feat: leave rock remnants for structures and rock units Extends the remnant allowlist to every cannon, tower, turret and towerback plus MiniRock and RockDrop, so any of them destroyed in combat blocks the ground it stood on. TTL expiry still leaves nothing, which is what most towers do when nobody kills them.
* fix: let aerial mobs engage targets they hover over Engagement was measured in 3D while mobs path on the ground plane, so a mob holding a hover height of three units could never satisfy its attack range against a ground target and parked above it instead. Range is now measured horizontally and checked before the path arrival bookkeeping, which used to drop a mob back to idle on the frame it reached the last path point. Also stops ThunderBird from diving one more frame at a destroyed target, and replaces the dive interpolation with a shared helper that no longer divides by a zero altitude gap. Closes #314 * feat: 바람 정령 지상 유닛 충돌 확장 (DEV-41) (#313) * feat: let wind spirits dive at ground units too WindSpirit now detects with TargetMask.ANY. Diving into ground crowds made the untargeted parts of the dive visible, so target selection and the explosion now skip self, allies and objects that are destroyed or still initializing. Closes #310 * fix: let wind spirits commit their dive on ground targets BehaviorMob measures target distance in 3D, so a mob hovering three units up never satisfied a 0.6 trigger distance and parked above ground targets instead of diving. The trigger distance now includes the altitude the dive has to cover. * Revert "fix: let wind spirits commit their dive on ground targets" This reverts commit 2608df0.
Magic execution ran on the STOMP inbound thread (and, for bots, on the bot executor thread) while the loop thread structurally modified the same unsynchronized lists (gameObjects, gameObjectsToAdd, createdObjectDtos, updatedObjectDtos), causing ConcurrentModificationException, dropped summons and half-paid casts. The loop now holds the GameContext monitor for the duration of a frame and both off-loop input entry points take the same monitor, so payment and effect stay atomic against a frame. Closes #317 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Collider cached the RigidBody in its constructor, but prefab initializers that register the RigidBody through the deferred addComponent queue build the collider before that queue is flushed. Those colliders permanently reported invMass 0 and zero velocity, so PhysicSystem produced no separation impulse and the units stacked or acted as immovable walls. Resolving the RigidBody on each read fixes every collider subclass at once. Closes #318 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
GameObject.start() called flushComponents() and then looped over components, so anything registered through addComponent() was started twice — OnStartAttacker dealt its AoE damage twice and BehaviorMob pushed duplicate gizmos. Merge the pending queue into components before the single start loop instead, which also keeps the start order the same as the prefab's registration order. Closes #319 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
PlayerData.mana was a plain int read-modify-written by the game loop thread in ManaCharger.chargeMana and by STOMP inbound threads in useCards and handleBotMagicInput, with useCards doing an unguarded check-then-act. Two concurrent casts could both pass validation against the same balance, and a concurrent charge could overwrite a spend, letting a player gain mana by casting or overdraw into negative mana. All mutators now synchronize on the owning PlayerData and mana is volatile for the plain reads elsewhere. Closes #321 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The WindSpirit only targets air mobs, which all hover at the same height, so the dive interpolation always divided by a zero altitude delta. That produced a NaN position that the out-of-bounds guard does not catch, leaving the mob stuck and unable to ever reach or damage its target. Same-altitude intercepts now home straight at the target. Closes #320 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat: block paths with dedicated rock remnants Rock creatures and rock turrets that die in combat now leave a RockRemnant instead of a live MiniRock: no movement, no attack, no hp, just a solid collider that expires after 20 seconds. StaticObstacle is needed because PhysicSystem only pairs objects that own a Collidable component, so a bare collider would never block. Closes #308 * feat: let aerial units fall before they die Aerial mobs vanished mid-air the instant their HP hit zero, so the client faked the fall with a throwaway sprite clone: its position was not authoritative and the fall distance was a fixed guess that missed the ground on anything but a standard cruise height. Death is now a server-side state. An aerial mob at zero HP enters Status.Dying, releases its hover so PhysicSystem's gravity takes over, and is only removed once it reaches the ground. While dying it takes no further damage, is dropped by every target detector, ticks no components and is skipped by collision, so the fall is purely cosmetic in gameplay terms while the position stays real. Aerial units are identified through ZPhysics.isAerial() -- the hover constructor was already the de facto flag -- so this needs no new unit data. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: explode self-destruct mobs where they die, not where they land An aerial self-destruct mob now entered the dying fall like any other, so its blast went off at the landing point instead of the point of impact -- a real balance change for WindSpirit, which is the whole reason it is aerial. Death that happens at a point opts out of the fall through Mob.fallsOnDeath(), which SelfDestructMob overrides. Keeping the opt-out on Mob rather than type-checking inside AerialDeathFall leaves the fall unaware of any particular mob class, the same way isActiveWhileDying() works. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A frame that threw only logged and broke out of the loop, so handleGameEnd() never ran: no ResultDto reached either client, MMR was not updated, and markOnline() was never called, leaving both users stuck flagged in-game. The catch block now forces a terminal result and finalizes the match, guarded by _running so a frame that throws after the match already ended does not broadcast or update ratings twice. Closes #323 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
findById always wrapped a scenario record, so an out-of-range scenarioId produced an empty-but-present scenario instead of failing fast: no boss or objective spawned, objectiveIds stayed empty, and PveResultChecker could never mark the match cleared. Treating "no installer rows" as absent makes PveScenarioRegistry's orElseThrow reachable again. Closes #331 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
saveGameResult writes statistic_games plus card, magic and update-time child rows through four separate JdbcClient statements. It runs on the game loop thread via SessionService.onLoopTerminated, so no transaction was bound and every statement autocommitted on its own: a mid-way failure left a statistic_games row with truncated children and no retry or cleanup, silently under-counting downstream aggregates. Closes #330 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Spring's proxy transaction advice only applies to public methods, so the package-private updateMatchResult ran with no transaction and committed each rating UPDATE in its own autocommit, letting a failure on the second write leave the ladder inflated. Making it public lets the class-level @transactional wrap both writes; the nested resolveRatingRef call is self-invocation and simply joins the same transaction. Closes #328 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
KnockbackStatusEffect never sets a speed modifier in start(), but its expire() zeroed the shared Stat.modifierPercent that Snared/Slow own, so a knockback landing during a snare cancelled the remaining slow while the Snared visual stayed on. Drop the reset and keep only the ZPhysics impulse revert, which now matches start()'s unconditional impulse. Closes #326 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A finished or unknown session made isSessionActive dereference null, so the lobby poll that observes the transition returned 500 instead of false. Session teardown also leaked the GameResultBuilder on draws and crash exits, and let a statistics write failure skip scenario completion and win counting. Closes #322 Closes #327 Closes #329 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Actuator serves only health and prometheus, bound to MANAGEMENT_PORT (default 8081), so the public app port never serves /actuator. Metrics carry an application tag so Prometheus can tell the servers apart. The management context is not covered by the app SecurityFilterChain, so 8081 must be firewalled to the Prometheus host only. Refs #363 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(server): publish heartbeat and session capacity * feat(session): expose ready session contract (#362) * refactor(server): bind server identity via ConfigurationProperties AGENTS.md now forbids injecting individual settings with @value. This branch added server.max-sessions, so ServerStatusService moves its whole server.* group to a constructor-injected ServerIdentityProperties record and its test drops ReflectionTestUtils. Defaults now live only in application.yml. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
game과 lobby가 각자의 DTO로만 계약을 검증하고 있어서, 필드명이나 중첩이 어긋나도 양쪽 스위트가 모두 통과했다. 계약이 깨져도 예외가 나지 않는다. lobby는 attemptId가 안 맞는 응답을 거절로 판정하고 다음 후보로 넘어가므로, 증상이 "모든 서버가 거절"과 503으로만 나타나고 로그에 원인이 남지 않는다. WordOnlineMatching과 같은 경로·같은 내용의 JSON 픽스처를 두고 양쪽이 각자 검증한다. 한쪽만 필드를 바꾸면 반대쪽 테스트가 깨진다. 이 저장소의 첫 ObjectMapper 기반 테스트다.
* feat(frame): add event list with hit events Frame info now carries a per-frame event list so single moments -- not just state -- reach the client. First entry: hit, so the client knows who struck whom and can place the hit effect on the struck side. Attacker id rides on AttackInfo and the event is emitted where damage actually lands, so any attack path opts in by setting the id. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feature(frame): widen hit event coverage The event list only fired for melee AttackMob and was dropped on sync frames, so most damage reached the client without an attacker. - AttackInfo.withAttacker(GameObject) reads better at the call sites, which are mostly inline expressions inside forEach/onDamaged - attach the attacker at the 27 damage sources that have one: melee, ranged, towers, self-destruct and magic. Magic is its own actor; visual-only projectiles report the unit that fired them - fall damage, damage-over-time, snare removal, overcharge, the summoning circle's self damage and heals stay without an attacker, so they emit nothing - SyncInfoDto carries the events of the frame it replaces; sync goes out every tenth frame and was silently swallowing them Refs #356 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feature(parameter) - 마나 1초당 5 -> 4 회복 - 소환 마법 패러미터 수정 * feature(golem) - 골렘 공격에 넉백 추가 * feature(clouddragon) - 운룡 변경 - 1초당 1번 공격, 데미지 5 - 15초 쿨타임 가지는 체인 라이트닝 공격 - 벽 수정 * feature(lightningdrop) - 낙뢰 마법 변경 - 낙뢰를 3회 소환하는 구름 소환 - 시전 즉시, 2초후, 4초후 낙뢰를 소환 - 낙뢰는 전기 데미지를 주고, 아군 전기 속성 유닛이 맞을 시 과충전 상태로 진입. 전기 필드를 생성. - * feature(windblade) - 바람 칼날 넉백 제거 * feature(knockback) - 넉백류 마법 개선 - 파도 소환과 바람 토템의 매커니즘을 갱신형 push로 변경 - 모든 강제 이동의 거리를 mass에 반비례해서 설정 * fix(wall)
Sessions live only in SessionService's in-memory map, so a restart drops every one of them silently while the lobby's match tickets stay stuck in MATCHED. The restarted process answers on the same domain and port and passes health checks, so the lobby cannot tell a brief outage from a restart that lost everything. Publish a per-process UUID: returned on the session readiness response so the lobby can pin it to the ticket, and written to the servers row on boot and on every heartbeat so the row always names the live process. One comparison then settles whether the session still exists. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The lobby keeps match tickets in Redis, so a session that finished here left the ticket in MATCHED and silently swallowed the player's next queue attempt. DB state was not enough: only an HTTP call closes the ticket. Adds this repository's first outbound lobby call. Credentials mirror the lobby's own convention because this server holds only JWT_PUBLIC_KEY and cannot mint a token: a pre-issued token is read from a configured path, cached on success so a file mounted after boot still gets picked up. Notification failure never breaks teardown. Attempts are bounded and short, 4xx is not retried, and an unset base URL or token path degrades to a log line, since the lobby's reconciler is the safety net for a lost notification while a stuck teardown has none. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
lobby and admin already read the same AccountServer-issued server token from JWT_FILE_PATH. A second variable for the same file would have implied the game host needs its own token minted, when the deployed one already fits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Refs #379 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(bot): make the self-play game count configurable The auto-match scheduler created a single bot game only while the server was completely idle, which is not enough to put the session layer under load. Replace the "skip if any session is active" check with a target count: every check interval the scheduler tops the server back up to `bot.auto-match. target-games` sessions. The default of 1 reproduces the previous behaviour exactly (skip when active >= 1, otherwise create one game), so nothing changes unless the knob is raised. Both the target and the check interval are now bound to environment variables so a synthetic load can be dialled in without a rebuild, which is what the upcoming game-loop scheduling work needs in order to be measured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018bCNcTUnk6eeaAY98M1CRY * refactor(bot): bind auto-match settings with properties object AGENTS.md requires grouped @ConfigurationProperties over per-field @value, so tests can inject settings without reflection. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
UpdateTimeAspect measures how long each GameSystem takes to run. That is CPU work: a frame that starts late still reports the same per-system durations, so a loop dropping from 20 FPS to 10 FPS leaves every existing metric unchanged. Nothing recorded the interval between frames. Add FrameIntervalAspect, which advises FrameDataSystem.earlyUpdate - the first statement of WordOnlineLoop.update(), so it runs exactly once per frame - and records the gap since the previous frame under the name "Frame". The loop's own update() is package-private and cannot be advised by Spring AOP, and routing this through an aspect keeps the game package free of any dependency on statistic. The previous timestamp lives on GameResultBuilder rather than in a map owned by the aspect, so it is discarded together with the builder when saveGameResult removes it instead of leaking an entry per game played. Supporting changes: - Widen UpdateTimeStatistic min/max and addInterval to long. The old int cast capped intervals at ~2.147s, which never mattered for microsecond per-system durations but would corrupt exactly the stalled frames worth measuring. - Key the statistic map by String instead of Class<? extends GameSystem>, since a frame-interval entry is not a GameSystem. StatisticService keeps its Class parameter and resolves the simple name internally, so rows already written for each system keep an identical name value and no schema change is needed. - Add the statistic_update_time definition to the test schema. It existed nowhere in the repository except the INSERT statement, so the persistence path could not be tested at all. Claude-Session: https://claude.ai/code/session_018bCNcTUnk6eeaAY98M1CRY Co-authored-by: Claude <noreply@anthropic.com>
is_running() backed a field initialised to true at declaration, and the session was registered before Thread.start(), so a session counted as active before its loop ran. An Error escaping update() also skipped onTerminated, leaving the session in the registry advertising itself as alive forever. The loop now flips to RUNNING from inside its own thread and back in a finally block, and creation waits for the first tick before reporting the session ready. The list order was ConcurrentHashMap bucket order over random UUID keys, so "the room near the top" meant nothing reproducible. Sessions now carry their creation instant, the list is ordered oldest first, and RoomInfoDto exposes the instant so the admin page can tell a fresh session from a long-running one. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The scheduler resolves its target on every tick: servers.target_bot_sessions first, bot.auto-match.target-games when the override is NULL or the read fails. The entity maps the column read-only so the whole-row heartbeat save cannot write back a stale value and undo an admin change. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
A session existed only in the in-memory map and its statistics were written only from the loop's finally path, so a deadlocked loop left no trace while both players stayed stuck. Insert a lifecycle row at session start, sweep stalled loops from a scheduled watchdog that flushes abandoned statistics with the loop thread's stack trace, and record draws instead of dropping them. Closes #400 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
BotBrain costed a recipe by its magic card alone, but PlayerData.useCards charges the mana_cost of every card in it. A [Shoot, Fire, Rock] cast really costs 35 and the bot believed it cost 15, so it queued casts it could not pay for, the input handler rejected them with FAIL_LACK_OF_CARD, and the same judgement repeated every reaction cycle until mana caught up. Move the cost and range lookups into BotSpellStats, which sums the whole recipe and treats an unpriced card as unaffordable rather than free. Classify Drop as offensive alongside Shoot and Explode: it is a ranged area attack, so dropping it on a random point in the half-disc facing the enemy wasted the cast. Lower the per-tick and per-action bot logs from info to debug; they buried everything else in the log while a bot game was running. Closes #390 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…d lane (#393) * fix(bot): charge every card in the recipe when costing a cast BotBrain costed a recipe by its magic card alone, but PlayerData.useCards charges the mana_cost of every card in it. A [Shoot, Fire, Rock] cast really costs 35 and the bot believed it cost 15, so it queued casts it could not pay for, the input handler rejected them with FAIL_LACK_OF_CARD, and the same judgement repeated every reaction cycle until mana caught up. Move the cost and range lookups into BotSpellStats, which sums the whole recipe and treats an unpriced card as unaffordable rather than free. Classify Drop as offensive alongside Shoot and Explode: it is a ranged area attack, so dropping it on a random point in the half-disc facing the enemy wasted the cast. Lower the per-tick and per-action bot logs from info to debug; they buried everything else in the log while a bot game was running. Closes #390 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feature(bot): score casts by converted damage and defend the contested lane The bot ranked candidates by combo size and a flat per-tier bonus, aimed every offensive spell at the nearest enemy, and dropped every summon on a random point in the half-disc facing the enemy. It never assessed what was walking at it, never used the blast radius, never checked whether a target was already almost dead, and effectively never attacked the enemy player, which is the win condition. Introduce three focused pieces the brain composes: - ThreatAssessment snapshots targetable enemies with their remaining hit points, reports how hard the bot is being pushed, and reports which lane the pressure sits in. - BlastTargetSelector treats every reachable enemy position as a candidate blast centre and picks the one converting the most damage, capped at each target's remaining hit points and weighted up on the enemy player. - PlacementPlanner puts summons in the contested lane, intercepting when the bot is pushed and advancing when the field is clear. Scoring is now value per real mana cost, so offence and placement compare on one scale, and BotEye exposes the enemy player's hit points so the core can be valued honestly. Replace the per-tier score bonus with a per-tier exploration rate. The bonus was added equally to every candidate, so it could never change the pick and the tiers played identically; discarding the ranking part of the time is what actually separates an INTRO opponent from an ELITE one. Damage and radius are read per card type, so they approximate the spell that finally spawns. Resolving the concrete magic's own parameters needs a magic_id reverse index and is left out of this change. Closes #391 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
… queue (#402) Off-loop threads no longer mutate game state. The STOMP inbound threads, the bot executor and the ping timeout scheduler queue their work on a per-session GameActionQueue, and the loop thread drains it at the top of every frame. That makes the loop thread the only writer, so the session-wide monitor and the per-method locks it forced onto PlayerData come out. It also closes a hole the monitor never covered: selectCard, unselectCard and cancelCard mutated components on a live GameObject from the inbound thread without holding it at all. Bot reads stay as they are - BotEye and BotBrain still observe live objects off the loop thread, so PlayerData.mana stays volatile, cards stays a synchronized list, and gameObjects stays copy-on-write until #399. Closes #398 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* refactor(game): serialize input and bot actions through a loop action queue Off-loop threads no longer mutate game state. The STOMP inbound threads, the bot executor and the ping timeout scheduler queue their work on a per-session GameActionQueue, and the loop thread drains it at the top of every frame. That makes the loop thread the only writer, so the session-wide monitor and the per-method locks it forced onto PlayerData come out. It also closes a hole the monitor never covered: selectCard, unselectCard and cancelCard mutated components on a live GameObject from the inbound thread without holding it at all. Bot reads stay as they are - BotEye and BotBrain still observe live objects off the loop thread, so PlayerData.mana stays volatile, cards stays a synchronized list, and gameObjects stays copy-on-write until #399. Closes #398 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(bot): give bot threads an immutable frame snapshot BotEye copied the game object list but not the objects in it, so the bot executor thread walked live GameObjects while the loop thread moved them. The copy-on-write list, BotEye's synchronized block and PlayerData's volatile mana never closed that - they guarded the container, not the state. BotEye is now an immutable record taken by the loop thread in BotAgentSystem and handed to the executor, carrying its own Vector3 copies. BotBrain reads it instead of GameObject and takes Parameters directly rather than the GameLoop. With no off-loop reader left, gameObjects goes back to a plain ArrayList, PlayerData holds plain fields, BotAgent drops its synchronized in favour of a volatile pendingDecision, and the debug endpoint's deck reset goes through the action queue like every other off-loop write. No synchronized remains anywhere under the game package. Closes #399 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A loop that is too slow to play is invisible to everything the server already watches. The reaping watchdog only fires once a loop stops completing frames entirely, and the frame rate itself was not measured anywhere, so an overloaded box degraded silently until someone looked. The rate is taken in the watchdog's existing sweep rather than in a second schedule, and it counts the frame currently in flight once that one has outrun the last completed frame - otherwise a loop falling behind keeps reporting the comfortable rate it managed before. Alerting is per episode, not per session per sweep: a server past its capacity has dozens of slow loops every few seconds, which would be several hundred webhook posts a minute and unreadable. An episode opens with a summary naming the three slowest sessions, repeats at most once per cooldown, and closes with a recovery line. Delivery is off the sweep thread through a bounded queue, so a slow or dead webhook drops alerts instead of stalling the watchdog. The webhook is the same DISCORD_WEBHOOK_URL the deployments already set; leaving it unset disables alerting. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
- y 위치 10 -> 2
…#425) The interval was a hardcoded 10s constant, and the toad spawned nothing for a full interval after being summoned. Read spawn_interval from the game object parameters and emit the first tadpole on the summon tick. LimitedSequenceSpawner keeps its previous behavior for existing callers; the immediate first spawn is opt-in through a new constructor flag. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promotes the current main branch to deploy.