Scale meteor impacts by size

This commit is contained in:
Horoli 2026-05-26 18:26:34 +09:00
parent 9df4f3dcde
commit 2c013247a9
6 changed files with 81 additions and 11 deletions

View File

@ -1,3 +1,10 @@
# Update: Variable Meteor Scale
- Fire and frost world-effect meteors now randomize their size on each drop.
- `WORLD_EFFECT.SIZE_SCALE_VARIANCE` controls the per-drop random range around the base size.
- The same random multiplier is applied to both the damage/frost zone bounds and the falling/impact sprite scale.
- Meteor screen shake scales from the same size multiplier, with base values in `WORLD_EFFECT.METEOR_SHAKE_DURATION_MS` and `WORLD_EFFECT.METEOR_SHAKE_INTENSITY`.
# Update: Team Size Constants # Update: Team Size Constants
- The battle setup team-size limit is centralized in `SPAWN.MAX_TEAM_SIZE` inside `src/constants.js`. - The battle setup team-size limit is centralized in `SPAWN.MAX_TEAM_SIZE` inside `src/constants.js`.

View File

@ -1,3 +1,10 @@
# Update: Variable Meteor Scale
- `worldEffects.js` resolves a fresh size multiplier for every fire/frost meteor drop.
- Tune the base damage/frost zone with `WORLD_EFFECT.AREA_TILES`, the base sprite size with `WORLD_EFFECT.VISUAL_SCALE`, and the shared random spread with `WORLD_EFFECT.SIZE_SCALE_VARIANCE`.
- The same multiplier changes both the damage/frost zone bounds and the falling/impact sprite scale.
- Meteor impact shake uses the same size multiplier, scaling from `WORLD_EFFECT.METEOR_SHAKE_DURATION_MS` and `WORLD_EFFECT.METEOR_SHAKE_INTENSITY`.
# Update: Large Battle Targeting # Update: Large Battle Targeting
- `combat.js` now prepares a per-frame target spatial index through `prepareCombatFrame(scene)`. - `combat.js` now prepares a per-frame target spatial index through `prepareCombatFrame(scene)`.
@ -43,8 +50,8 @@
- **효과**: 메테오 투하 주기가 `SUDDEN_DEATH.INTERVAL_MS`로 단축되며, `FORCE_FROST` 설정 시 빙결 효과를 가진 냉기 메테오가 집중적으로 생성됩니다. - **효과**: 메테오 투하 주기가 `SUDDEN_DEATH.INTERVAL_MS`로 단축되며, `FORCE_FROST` 설정 시 빙결 효과를 가진 냉기 메테오가 집중적으로 생성됩니다.
- **목적**: 장기전을 방지하고 전장에 무작위 변수를 극대화하여 물량 중심 팀에게 리스크를 부여합니다. - **목적**: 장기전을 방지하고 전장에 무작위 변수를 극대화하여 물량 중심 팀에게 리스크를 부여합니다.
- **낙하 방향과 크기**: 대상이 전장 좌측 반면(2, 3사분면)이면 화살표가 좌상단에서 우하단으로, 우측 반면(1, 4사분면)이면 좌우 반전되어 우상단에서 좌하단으로 이동합니다. 스프라이트를 45도로 기울이고 전용 시각 배율을 사용해 전역 마법 규모로 표현합니다. - **낙하 방향과 크기**: 대상이 전장 좌측 반면(2, 3사분면)이면 화살표가 좌상단에서 우하단으로, 우측 반면(1, 4사분면)이면 좌우 반전되어 우상단에서 좌하단으로 이동합니다. 스프라이트를 45도로 기울이고 전용 시각 배율을 사용해 전역 마법 규모로 표현합니다.
- **화염 메테오**: `world_Effect.png`의 7프레임 애니메이션이 낙하하면 화면 흔들림을 적용하고, 5x5 타일 영역의 생존자에게 고정 피해를 줍니다. 자동 관전 진입 전에는 `CAMERA.METEOR_FOCUS_ENABLED`가 켜져 있을 때 착탄 위치를 임시 포커싱합니다. 환경 피해로 인한 사망은 킬 보상을 지급하지 않지만 사망 통계와 승패 판정에는 반영됩니다. - **화염 메테오**: `world_Effect.png`의 7프레임 애니메이션이 낙하하면 크기에 따른 화면 흔들림을 적용하고, 5x5 타일 영역의 생존자에게 고정 피해를 줍니다. 자동 관전 진입 전에는 `CAMERA.METEOR_FOCUS_ENABLED`가 켜져 있을 때 착탄 위치를 임시 포커싱합니다. 환경 피해로 인한 사망은 킬 보상을 지급하지 않지만 사망 통계와 승패 판정에는 반영됩니다.
- **냉기 메테오**: `world_Effect_2.png`의 7프레임 애니메이션으로 착탄을 표시하고, 자동 관전 진입 전에는 같은 설정에 따라 착탄 위치를 임시 포커싱합니다. 착탄 시 별도 조정 가능한 피해를 주며, 생존한 피격 대상은 캐릭터 본체와 실루엣이 얼음색으로 바뀐 채 2초 동안 기절합니다. 이후 남은 5x5 냉각지대 안에서는 공격속도와 이동속도 감속 배율을 적용하며, 영역을 벗어나거나 지속시간이 끝나면 배율을 복구합니다. - **냉기 메테오**: `world_Effect_2.png`의 7프레임 애니메이션으로 착탄을 표시하고 크기에 따른 화면 흔들림을 적용하며, 자동 관전 진입 전에는 같은 설정에 따라 착탄 위치를 임시 포커싱합니다. 착탄 시 별도 조정 가능한 피해를 주며, 생존한 피격 대상은 캐릭터 본체와 실루엣이 얼음색으로 바뀐 채 2초 동안 기절합니다. 이후 남은 5x5 냉각지대 안에서는 공격속도와 이동속도 감속 배율을 적용하며, 영역을 벗어나거나 지속시간이 끝나면 배율을 복구합니다.
### 최종교전 슬로우모션 ### 최종교전 슬로우모션
`COMBAT.FINAL_SLOW_MOTION_ENABLED`가 활성화된 경우: `COMBAT.FINAL_SLOW_MOTION_ENABLED`가 활성화된 경우:

View File

@ -1,5 +1,11 @@
# Context: Core & Infrastructure # Context: Core & Infrastructure
# Update: Variable Meteor Scale
- `WORLD_EFFECT.SIZE_SCALE_VARIANCE` randomizes each fire/frost meteor drop around the base size.
- The randomized size applies to both the world-effect damage/frost zone bounds from `WORLD_EFFECT.AREA_TILES` and the sprite scale from `WORLD_EFFECT.VISUAL_SCALE`.
- Meteor impact shake strength follows the same size multiplier, using `WORLD_EFFECT.METEOR_SHAKE_DURATION_MS` and `WORLD_EFFECT.METEOR_SHAKE_INTENSITY` as base values.
# Update: Team Size Constants # Update: Team Size Constants
- `SPAWN.MAX_TEAM_SIZE` is the single source of truth for the battle setup team-size maximum. - `SPAWN.MAX_TEAM_SIZE` is the single source of truth for the battle setup team-size maximum.
@ -42,6 +48,6 @@
- **물리 수치 조정**: 역할별 기본 체력/속도/사거리/공격 수치는 `src/constants.js``FIGHTER_TYPE_STATS`에서 변경하고, 특정 스킨만 다르게 할 때는 `fighterManifest.js``stats` 또는 `combat` 설정을 사용하십시오. - **물리 수치 조정**: 역할별 기본 체력/속도/사거리/공격 수치는 `src/constants.js``FIGHTER_TYPE_STATS`에서 변경하고, 특정 스킨만 다르게 할 때는 `fighterManifest.js``stats` 또는 `combat` 설정을 사용하십시오.
- **처치 성장 상한 조정**: 처치 보상으로 캐릭터가 커지는 최대치와 공격/이동 배율 상한은 `src/constants.js``KILL_GROWTH_MAX_MULTIPLIER`를 수정합니다. - **처치 성장 상한 조정**: 처치 보상으로 캐릭터가 커지는 최대치와 공격/이동 배율 상한은 `src/constants.js``KILL_GROWTH_MAX_MULTIPLIER`를 수정합니다.
- **공격력 조정**: 역할별 기본 피해량은 `src/constants.js``FIGHTER_TYPE_STATS.<type>.damageMin/damageMax`를 수정합니다. 캐릭터별 특수 공격 방식은 `fighterManifest.js``combat` 설정을 우선 확인합니다. - **공격력 조정**: 역할별 기본 피해량은 `src/constants.js``FIGHTER_TYPE_STATS.<type>.damageMin/damageMax`를 수정합니다. 캐릭터별 특수 공격 방식은 `fighterManifest.js``combat` 설정을 우선 확인합니다.
- **월드 이펙트 조정**: `src/constants.js``WORLD_EFFECT.INTERVAL`, `WORLD_EFFECT.FALL_TRAVEL_TILES`, `WORLD_EFFECT.VISUAL_SCALE`, `WORLD_EFFECT.DOMINANCE_TARGETING_MULTIPLIER`, `WORLD_EFFECT.METEOR_DAMAGE`, `WORLD_EFFECT.FROST_DAMAGE`, `WORLD_EFFECT.FROST_STUN_DURATION`, `WORLD_EFFECT.FROST_STUN_TINT`, `WORLD_EFFECT.FROST_DURATION`, `WORLD_EFFECT.FROST_SPEED_MULTIPLIER`를 수정합니다. 독주 표적 배율은 `0`이면 기존 생존 유닛 비례 추첨이며, `1`이면 구매 배수 지분보다 높은 생존 지분의 초과분을 표적 가중치로 반영합니다. 임시 메테오 카메라는 `CAMERA.METEOR_FOCUS_ENABLED`로 끌 수 있습니다. - **월드 이펙트 조정**: `src/constants.js``WORLD_EFFECT.INTERVAL`, `WORLD_EFFECT.AREA_TILES`, `WORLD_EFFECT.SIZE_SCALE_VARIANCE`, `WORLD_EFFECT.FALL_TRAVEL_TILES`, `WORLD_EFFECT.VISUAL_SCALE`, `WORLD_EFFECT.METEOR_SHAKE_DURATION_MS`, `WORLD_EFFECT.METEOR_SHAKE_INTENSITY`, `WORLD_EFFECT.DOMINANCE_TARGETING_MULTIPLIER`, `WORLD_EFFECT.METEOR_DAMAGE`, `WORLD_EFFECT.FROST_DAMAGE`, `WORLD_EFFECT.FROST_STUN_DURATION`, `WORLD_EFFECT.FROST_STUN_TINT`, `WORLD_EFFECT.FROST_DURATION`, `WORLD_EFFECT.FROST_SPEED_MULTIPLIER`를 수정합니다. 독주 표적 배율은 `0`이면 기존 생존 유닛 비례 추첨이며, `1`이면 구매 배수 지분보다 높은 생존 지분의 초과분을 표적 가중치로 반영합니다. 임시 메테오 카메라는 `CAMERA.METEOR_FOCUS_ENABLED`로 끌 수 있습니다.
- **DOM 접근**: 성능을 위해 `ArenaScene`은 좌측 HUD badge 등 필요한 시점에만 최소한으로 DOM에 접근합니다. - **DOM 접근**: 성능을 위해 `ArenaScene`은 좌측 HUD badge 등 필요한 시점에만 최소한으로 DOM에 접근합니다.
- **패키지 락 파일**: 이 프로젝트는 `package-lock.json`을 저장소에서 제외합니다. 의존성 변경 시 `package.json`을 기준으로 관리합니다. - **패키지 락 파일**: 이 프로젝트는 `package-lock.json`을 저장소에서 제외합니다. 의존성 변경 시 `package.json`을 기준으로 관리합니다.

View File

@ -132,11 +132,14 @@ export const PROJECTILE = {
export const WORLD_EFFECT = { export const WORLD_EFFECT = {
INTERVAL: 4000, INTERVAL: 4000,
AREA_TILES: 15, AREA_TILES: 15,
SIZE_SCALE_VARIANCE: 3,
FRAMES: 7, FRAMES: 7,
FRAME_RATE: 14, FRAME_RATE: 14,
FALL_DURATION: 920, FALL_DURATION: 920,
FALL_TRAVEL_TILES: 8, FALL_TRAVEL_TILES: 8,
VISUAL_SCALE: 50, VISUAL_SCALE: 50,
METEOR_SHAKE_DURATION_MS: 150,
METEOR_SHAKE_INTENSITY: 0.004,
// 0 keeps target selection proportional to living units. // 0 keeps target selection proportional to living units.
// 1 adds pressure when a team's living share exceeds its paid spawn share. // 1 adds pressure when a team's living share exceeds its paid spawn share.
DOMINANCE_TARGETING_MULTIPLIER: 0.5, DOMINANCE_TARGETING_MULTIPLIER: 0.5,
@ -144,7 +147,7 @@ export const WORLD_EFFECT = {
FROST_DAMAGE: 45, FROST_DAMAGE: 45,
FROST_STUN_DURATION: 2000, FROST_STUN_DURATION: 2000,
FROST_STUN_TINT: 0x82e9ff, FROST_STUN_TINT: 0x82e9ff,
FROST_DURATION: 20000, FROST_DURATION: 2000,
FROST_SPEED_MULTIPLIER: 0.55, FROST_SPEED_MULTIPLIER: 0.55,
SUDDEN_DEATH: { SUDDEN_DEATH: {
ENABLED: false, ENABLED: false,

View File

@ -125,7 +125,7 @@ function triggerWorldEffect(scene) {
} }
const target = chooseWorldEffectTarget(livingFighters); const target = chooseWorldEffectTarget(livingFighters);
const zone = createEffectZone(target); const zone = createEffectZone(target, resolveWorldEffectSizeScale());
// Sudden Death 상태이고 냉기 고정 설정이 되어있으면 무조건 냉기 메테오 // Sudden Death 상태이고 냉기 고정 설정이 되어있으면 무조건 냉기 메테오
if ((scene.isSuddenDeath && WORLD_EFFECT.SUDDEN_DEATH.FORCE_FROST) || Phaser.Math.Between(0, 1) === 0) { if ((scene.isSuddenDeath && WORLD_EFFECT.SUDDEN_DEATH.FORCE_FROST) || Phaser.Math.Between(0, 1) === 0) {
@ -218,7 +218,7 @@ function spawnMeteor(scene, zone) {
onImpact: () => { onImpact: () => {
scene.tweens.killTweensOf(marker); scene.tweens.killTweensOf(marker);
marker.setAlpha(1); marker.setAlpha(1);
scene.cameras.main.shake(150, 0.004); applyMeteorImpactShake(scene, zone);
resolveImpactDamage(scene, zone, WORLD_EFFECT.METEOR_DAMAGE); resolveImpactDamage(scene, zone, WORLD_EFFECT.METEOR_DAMAGE);
}, },
onAnimationComplete: () => { onAnimationComplete: () => {
@ -239,6 +239,7 @@ function spawnFrostZone(scene, zone) {
disposeCombatObject(scene, marker); disposeCombatObject(scene, marker);
}, },
onImpact: () => { onImpact: () => {
applyMeteorImpactShake(scene, zone);
resolveImpactDamage(scene, zone, WORLD_EFFECT.FROST_DAMAGE, (fighter) => { resolveImpactDamage(scene, zone, WORLD_EFFECT.FROST_DAMAGE, (fighter) => {
applyFrostStun(scene, fighter); applyFrostStun(scene, fighter);
}); });
@ -261,7 +262,7 @@ function dropWorldEffectSprite(
const sprite = scene.add const sprite = scene.add
.sprite(trajectory.startX, trajectory.startY, effectKey, 0) .sprite(trajectory.startX, trajectory.startY, effectKey, 0)
.setDepth(3) .setDepth(3)
.setScale(WORLD_EFFECT.VISUAL_SCALE) .setScale(resolveWorldEffectVisualScale(zone))
.setFlipX(trajectory.flipX) .setFlipX(trajectory.flipX)
.setAngle(trajectory.angle) .setAngle(trajectory.angle)
.setAlpha(0.9); .setAlpha(0.9);
@ -300,6 +301,40 @@ function worldEffectAnimationKey(effectKey) {
return `${effectKey}-anim`; return `${effectKey}-anim`;
} }
function resolveWorldEffectSizeScale() {
const variance = Math.max(0, Number(WORLD_EFFECT.SIZE_SCALE_VARIANCE) || 0);
if (variance === 0) {
return 1;
}
const minScale = Math.max(0.1, 1 - variance);
const maxScale = 1 + variance;
return Phaser.Math.FloatBetween(minScale, maxScale);
}
function resolveWorldEffectVisualScale(zone) {
const baseScale = Math.max(0.01, Number(WORLD_EFFECT.VISUAL_SCALE) || 1);
return baseScale * Math.max(0.1, Number(zone?.sizeScale) || 1);
}
function applyMeteorImpactShake(scene, zone) {
const sizeScale = Math.max(0.1, Number(zone?.sizeScale) || 1);
const duration = Math.round(
Math.max(0, Number(WORLD_EFFECT.METEOR_SHAKE_DURATION_MS) || 0)
* Math.sqrt(sizeScale),
);
const intensity =
Math.max(0, Number(WORLD_EFFECT.METEOR_SHAKE_INTENSITY) || 0) * sizeScale;
if (duration <= 0 || intensity <= 0) {
return;
}
scene.cameras.main.shake(duration, intensity);
}
function createFallTrajectory(zone) { function createFallTrajectory(zone) {
const distance = ARENA.TILE_SIZE * WORLD_EFFECT.FALL_TRAVEL_TILES; const distance = ARENA.TILE_SIZE * WORLD_EFFECT.FALL_TRAVEL_TILES;
const isLeftHalf = zone.centerX < ARENA.SIZE / 2; const isLeftHalf = zone.centerX < ARENA.SIZE / 2;
@ -313,16 +348,22 @@ function createFallTrajectory(zone) {
}; };
} }
function createEffectZone(target) { function createEffectZone(target, sizeScale = 1) {
const size = ARENA.TILE_SIZE * WORLD_EFFECT.AREA_TILES; const areaTiles = Math.max(
1,
(Number(WORLD_EFFECT.AREA_TILES) || 1) * Math.max(0.1, Number(sizeScale) || 1),
);
const size = ARENA.TILE_SIZE * areaTiles;
const centerX = target.body?.center.x ?? target.x; const centerX = target.body?.center.x ?? target.x;
const centerY = target.body?.center.y ?? target.y; const centerY = target.body?.center.y ?? target.y;
return { return {
areaTiles,
bounds: new Phaser.Geom.Rectangle(centerX - size / 2, centerY - size / 2, size, size), bounds: new Phaser.Geom.Rectangle(centerX - size / 2, centerY - size / 2, size, size),
centerX, centerX,
centerY, centerY,
marker: null, marker: null,
sizeScale,
}; };
} }
@ -336,7 +377,7 @@ function createZoneMarker(scene, zone, color) {
marker.strokeRect(x, y, width, height); marker.strokeRect(x, y, width, height);
marker.lineStyle(1, color, 0.34); marker.lineStyle(1, color, 0.34);
for (let index = 1; index < WORLD_EFFECT.AREA_TILES; index += 1) { for (let index = 1; index < zone.areaTiles; index += 1) {
const offset = index * ARENA.TILE_SIZE; const offset = index * ARENA.TILE_SIZE;
marker.lineBetween(x + offset, y, x + offset, y + height); marker.lineBetween(x + offset, y, x + offset, y + height);
marker.lineBetween(x, y + offset, x + width, y + offset); marker.lineBetween(x, y + offset, x + width, y + offset);

View File

@ -229,7 +229,7 @@
- **조치 사항**: - **조치 사항**:
- 생존 캐릭터가 30명 미만이거나 최종 2팀만 남으면 후반 자동 줌과 교전 중심 포커싱이 시작되도록 관전 조건을 확장. - 생존 캐릭터가 30명 미만이거나 최종 2팀만 남으면 후반 자동 줌과 교전 중심 포커싱이 시작되도록 관전 조건을 확장.
- 치명타의 `Critical!` 표기와 즉시 처치는 유지하면서 카메라 흔들림을 제거. - 치명타의 `Critical!` 표기와 즉시 처치는 유지하면서 카메라 흔들림을 제거.
- 화염 메테오가 착탄할 때 화면 흔들림을 적용하고, 냉각지대 착탄과 피해 계산에서는 흔들림을 분리. - 화염 메테오 착탄 화면 흔들림을 먼저 적용했으며, 이후 모든 메테오 착탄이 크기 기반 흔들림을 공유하도록 확장.
37. 자동 관전 이전 메테오 임시 포커싱 추가 (완료) 37. 자동 관전 이전 메테오 임시 포커싱 추가 (완료)
- **조치 사항**: - **조치 사항**:
@ -285,3 +285,9 @@
- Added `FIGHTER.DEAD_DESPAWN_DELAY_MS` in `src/constants.js` so corpse lifetime is easy to tune. - Added `FIGHTER.DEAD_DESPAWN_DELAY_MS` in `src/constants.js` so corpse lifetime is easy to tune.
- Updated `combat.js` to keep a dead fighter at initial opacity, fade it toward `FIGHTER.DEAD_DESPAWN_ALPHA`, then remove it from `scene.fighters` and destroy the sprite after the configured delay. - Updated `combat.js` to keep a dead fighter at initial opacity, fade it toward `FIGHTER.DEAD_DESPAWN_ALPHA`, then remove it from `scene.fighters` and destroy the sprite after the configured delay.
- Kept death bookkeeping, kill rewards, split-on-death, and winner checks ahead of the despawn schedule. - Kept death bookkeeping, kill rewards, split-on-death, and winner checks ahead of the despawn schedule.
46. Variable meteor impact and visual scale (completed)
- **Changes**:
- Added `WORLD_EFFECT.SIZE_SCALE_VARIANCE` so each fire/frost meteor drop can pick a different size multiplier.
- Updated `worldEffects.js` to apply the same per-drop multiplier to both the damage/frost zone bounds and the falling/impact sprite scale.
- Added `WORLD_EFFECT.METEOR_SHAKE_DURATION_MS` and `WORLD_EFFECT.METEOR_SHAKE_INTENSITY`, then scaled fire/frost meteor camera shake from the meteor size multiplier.