Skip to content

Commit 9d9a2ad

Browse files
authored
画像効果を追加した (#357)
- ダメージ発生時の表示 - 敵を倒したときの表示
2 parents 1e2c447 + 14e101f commit 9d9a2ad

30 files changed

+819
-50
lines changed

assets/file/shaders/retro.kage

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const ChromaticAberration = 0.6
99
const VignetteStrength = 0.2
1010
const ScanlineStrength = 0.1
1111

12+
// レトロ効果を適用するフィルター
1213
func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
1314
// ソース画像の原点とサイズを取得
1415
origin := imageSrc0Origin()
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//kage:unit pixels
2+
package main
3+
4+
// 白いシルエットを描画するシェーダー
5+
// アルファチャンネルを保持しながら色を白に変更する
6+
func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
7+
c := imageSrc0At(srcPos)
8+
if c.a > 0 {
9+
// 元のアルファ値を保持しつつ、色を白にする
10+
// color.a でフェードアウトの透明度を制御
11+
return vec4(color.a, color.a, color.a, c.a * color.a)
12+
}
13+
return vec4(0)
14+
}

internal/actions/attack.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ func (aa *AttackActivity) performAttack(act *Activity, world w.World) error {
141141
hit, criticalHit := aa.rollHitCheck(attacker, target, world)
142142
if !hit {
143143
aa.logAttackResult(attacker, target, world, false, false, 0, attackMethodName)
144+
worldhelper.SpawnVisualEffect(target, gc.NewMissEffect(), world)
144145
return nil
145146
}
146147

@@ -160,6 +161,9 @@ func (aa *AttackActivity) performAttack(act *Activity, world w.World) error {
160161
// 攻撃とダメージを1行でログ出力
161162
aa.logAttackResult(attacker, target, world, true, criticalHit, damage, attackMethodName)
162163

164+
// ダメージエフェクトを生成
165+
worldhelper.SpawnVisualEffect(target, gc.NewDamageEffect(damage), world)
166+
163167
// 死亡チェックと死亡ログ
164168
if pools.HP.Current <= 0 && beforeHP > 0 {
165169
target.AddComponent(world.Components.Dead, &gc.Dead{})

internal/components/components.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type EntitySpec struct {
4646
LightSource *LightSource
4747
Door *Door
4848
Interactable *Interactable
49+
VisualEffect *VisualEffects
4950

5051
// member ================
5152
Player *Player
@@ -105,6 +106,7 @@ type Components struct {
105106
Prop *ecs.NullComponent
106107
LightSource *ecs.SliceComponent `save:"true"`
107108
Interactable *ecs.SliceComponent
109+
VisualEffect *ecs.SliceComponent
108110

109111
// member ================
110112
Player *ecs.NullComponent `save:"true"`
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package components
2+
3+
import (
4+
"fmt"
5+
"image/color"
6+
)
7+
8+
// VisualEffect はビジュアルエフェクトのインターフェース
9+
type VisualEffect interface {
10+
// Update はエフェクトを更新し、継続中ならtrueを返す
11+
Update(deltaMs float64) bool
12+
}
13+
14+
// VisualEffects はエンティティに紐づくビジュアルエフェクトを管理する
15+
type VisualEffects struct {
16+
Effects []VisualEffect
17+
}
18+
19+
// ScreenTextEffect は画面座標でテキストを表示するエフェクト
20+
type ScreenTextEffect struct {
21+
OffsetX float64 // X座標(画面座標)
22+
OffsetY float64 // Y座標(画面座標)
23+
Text string // 表示テキスト
24+
Color color.RGBA // 表示色
25+
FadeInMs float64 // フェードイン時間(ミリ秒)
26+
HoldMs float64 // 表示維持時間(ミリ秒)
27+
FadeOutMs float64 // フェードアウト時間(ミリ秒)
28+
TotalMs float64 // 合計時間
29+
RemainingMs float64 // 残り時間(ミリ秒)
30+
Alpha float64 // 現在の透明度(0.0-1.0)
31+
}
32+
33+
// Update はエフェクトを更新し、継続中ならtrueを返す
34+
func (e *ScreenTextEffect) Update(deltaMs float64) bool {
35+
e.RemainingMs -= deltaMs
36+
elapsed := e.TotalMs - e.RemainingMs
37+
e.Alpha = calculateFadeAlpha(elapsed, e.FadeInMs, e.HoldMs, e.FadeOutMs)
38+
return e.RemainingMs > 0
39+
}
40+
41+
// DamageTextEffect はエンティティ座標で浮遊テキストを表示するエフェクト
42+
// ダメージ、回復、ミス、ヒットなどに使用する
43+
type DamageTextEffect struct {
44+
OffsetX float64 // X座標オフセット(ピクセル)
45+
OffsetY float64 // Y座標オフセット(ピクセル)
46+
Text string // 表示テキスト
47+
Color color.RGBA // 表示色
48+
VelocityY float64 // Y方向の速度(上に浮かぶ効果用)
49+
FadeInMs float64 // フェードイン時間(ミリ秒)
50+
HoldMs float64 // 表示維持時間(ミリ秒)
51+
FadeOutMs float64 // フェードアウト時間(ミリ秒)
52+
TotalMs float64 // 合計時間
53+
RemainingMs float64 // 残り時間(ミリ秒)
54+
Alpha float64 // 現在の透明度(0.0-1.0)
55+
}
56+
57+
// Update はエフェクトを更新し、継続中ならtrueを返す
58+
func (e *DamageTextEffect) Update(deltaMs float64) bool {
59+
e.RemainingMs -= deltaMs
60+
e.OffsetY += e.VelocityY
61+
elapsed := e.TotalMs - e.RemainingMs
62+
e.Alpha = calculateFadeAlpha(elapsed, e.FadeInMs, e.HoldMs, e.FadeOutMs)
63+
return e.RemainingMs > 0
64+
}
65+
66+
// SpriteFadeoutEffect はスプライトをフェードアウト表示するエフェクト
67+
// 敵撃破時などに使用する
68+
type SpriteFadeoutEffect struct {
69+
SpriteSheetName string // スプライトシート名
70+
SpriteKey string // スプライトキー
71+
FadeInMs float64 // フェードイン時間(ミリ秒)
72+
HoldMs float64 // 表示維持時間(ミリ秒)
73+
FadeOutMs float64 // フェードアウト時間(ミリ秒)
74+
TotalMs float64 // 合計時間
75+
RemainingMs float64 // 残り時間(ミリ秒)
76+
Alpha float64 // 現在の透明度(0.0-1.0)
77+
}
78+
79+
// Update はエフェクトを更新し、継続中ならtrueを返す
80+
func (e *SpriteFadeoutEffect) Update(deltaMs float64) bool {
81+
e.RemainingMs -= deltaMs
82+
elapsed := e.TotalMs - e.RemainingMs
83+
e.Alpha = calculateFadeAlpha(elapsed, e.FadeInMs, e.HoldMs, e.FadeOutMs)
84+
return e.RemainingMs > 0
85+
}
86+
87+
// calculateFadeAlpha はフェードシーケンスのAlpha値を計算する
88+
func calculateFadeAlpha(elapsed, fadeInMs, holdMs, fadeOutMs float64) float64 {
89+
if elapsed < fadeInMs {
90+
// フェードイン中
91+
if fadeInMs == 0 {
92+
return 1.0
93+
}
94+
return elapsed / fadeInMs
95+
}
96+
if elapsed < fadeInMs+holdMs {
97+
// ホールド中
98+
return 1.0
99+
}
100+
// フェードアウト中
101+
if fadeOutMs == 0 {
102+
return 0.0
103+
}
104+
fadeOutElapsed := elapsed - fadeInMs - holdMs
105+
return 1.0 - (fadeOutElapsed / fadeOutMs)
106+
}
107+
108+
// NewScreenTextEffect は画面中央にフェード表示するテキストエフェクトを作成する
109+
func NewScreenTextEffect(text string, screenW, screenH int) *ScreenTextEffect {
110+
fadeInMs := 500.0
111+
holdMs := 2000.0
112+
fadeOutMs := 500.0
113+
totalMs := fadeInMs + holdMs + fadeOutMs
114+
115+
return &ScreenTextEffect{
116+
OffsetX: float64(screenW) / 2,
117+
OffsetY: float64(screenH) / 3,
118+
Text: text,
119+
Color: color.RGBA{255, 255, 255, 255},
120+
FadeInMs: fadeInMs,
121+
HoldMs: holdMs,
122+
FadeOutMs: fadeOutMs,
123+
TotalMs: totalMs,
124+
RemainingMs: totalMs,
125+
Alpha: 0.0,
126+
}
127+
}
128+
129+
// NewDungeonTitleEffect はダンジョンタイトル表示エフェクトを作成する
130+
func NewDungeonTitleEffect(dungeonName string, depth int, screenW, screenH int) *ScreenTextEffect {
131+
text := fmt.Sprintf("%s %dF", dungeonName, depth)
132+
return NewScreenTextEffect(text, screenW, screenH)
133+
}
134+
135+
// NewDamageEffect はダメージ数値表示エフェクトを作成する
136+
func NewDamageEffect(damage int) *DamageTextEffect {
137+
totalMs := 800.0
138+
return &DamageTextEffect{
139+
OffsetX: 0,
140+
OffsetY: -8,
141+
Text: fmt.Sprintf("%d", damage),
142+
Color: color.RGBA{255, 80, 80, 255}, // 赤色
143+
VelocityY: -0.5, // 上に浮かぶ
144+
FadeInMs: 100,
145+
HoldMs: 500,
146+
FadeOutMs: 200,
147+
TotalMs: totalMs,
148+
RemainingMs: totalMs,
149+
Alpha: 0.0,
150+
}
151+
}
152+
153+
// NewMissEffect はミス表示エフェクトを作成する
154+
func NewMissEffect() *DamageTextEffect {
155+
totalMs := 600.0
156+
return &DamageTextEffect{
157+
OffsetX: 0,
158+
OffsetY: -8,
159+
Text: "MISS",
160+
Color: color.RGBA{180, 180, 180, 255}, // グレー
161+
VelocityY: -0.3,
162+
FadeInMs: 50,
163+
HoldMs: 400,
164+
FadeOutMs: 150,
165+
TotalMs: totalMs,
166+
RemainingMs: totalMs,
167+
Alpha: 0.0,
168+
}
169+
}
170+
171+
// NewHealEffect は回復数値表示エフェクトを作成する
172+
func NewHealEffect(amount int) *DamageTextEffect {
173+
totalMs := 800.0
174+
return &DamageTextEffect{
175+
OffsetX: 0,
176+
OffsetY: -8,
177+
Text: fmt.Sprintf("+%d", amount),
178+
Color: color.RGBA{80, 255, 80, 255}, // 緑色
179+
VelocityY: -0.5,
180+
FadeInMs: 100,
181+
HoldMs: 500,
182+
FadeOutMs: 200,
183+
TotalMs: totalMs,
184+
RemainingMs: totalMs,
185+
Alpha: 0.0,
186+
}
187+
}
188+
189+
// NewSpriteFadeoutEffect はスプライトフェードアウトエフェクトを作成する
190+
// 敵撃破時などにスプライトを表示してフェードアウトする
191+
func NewSpriteFadeoutEffect(spriteSheetName, spriteKey string) *SpriteFadeoutEffect {
192+
totalMs := 400.0
193+
return &SpriteFadeoutEffect{
194+
SpriteSheetName: spriteSheetName,
195+
SpriteKey: spriteKey,
196+
FadeInMs: 0,
197+
HoldMs: 100,
198+
FadeOutMs: 300,
199+
TotalMs: totalMs,
200+
RemainingMs: totalMs,
201+
Alpha: 1.0,
202+
}
203+
}

internal/resources/fonts.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ type fonts struct {
1313
func loadFonts(tfs *text.GoTextFaceSource) *fonts {
1414
smallFace := loadFont(tfs, 16)
1515
bodyFace := loadFont(tfs, 20)
16-
titleFontFace := loadFont(tfs, 28)
16+
titleFontFace := loadFont(tfs, 32)
1717

1818
return &fonts{
1919
smallFace: smallFace,

internal/states/dungeon.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,17 @@ func (st *DungeonState) OnStart(world w.World) error {
145145
// フロア移動時に探索済みマップをリセット
146146
world.Resources.Dungeon.ExploredTiles = make(map[gc.GridElement]bool)
147147

148-
// 視界キャッシュをクリア(新しい階のために)
148+
// 新しい階のために視界キャッシュをクリアする
149149
gs.ClearVisionCaches()
150150

151+
// ダンジョンタイトルエフェクト用エンティティを作成する
152+
screenW, screenH := world.Resources.GetScreenDimensions()
153+
titleEffect := gc.NewDungeonTitleEffect(def.Name, st.Depth, screenW, screenH)
154+
titleEntity := world.Manager.NewEntity()
155+
titleEntity.AddComponent(world.Components.VisualEffect, &gc.VisualEffects{
156+
Effects: []gc.VisualEffect{titleEffect},
157+
})
158+
151159
return nil
152160
}
153161

@@ -200,6 +208,7 @@ func (st *DungeonState) Update(world w.World) (es.Transition[w.World], error) {
200208
&gs.HUDRenderingSystem{},
201209
&gs.EquipmentChangedSystem{},
202210
&gs.InventoryChangedSystem{},
211+
&gs.VisualEffectSystem{},
203212
} {
204213
if sys, ok := world.Updaters[updater.String()]; ok {
205214
if err := sys.Update(world); err != nil {
@@ -234,6 +243,7 @@ func (st *DungeonState) Draw(world w.World, screen *ebiten.Image) error {
234243
&gs.RenderSpriteSystem{},
235244
&gs.VisionSystem{},
236245
&gs.HUDRenderingSystem{},
246+
&gs.VisualEffectSystem{},
237247
} {
238248
if sys, ok := world.Renderers[renderer.String()]; ok {
239249
if err := sys.Draw(world, screen); err != nil {
@@ -495,10 +505,14 @@ func (st *DungeonState) handleStateEvent(world w.World) (es.Transition[w.World],
495505
case resources.WarpNextEvent:
496506
// 次のフロアへ遷移する
497507
nextDepth := world.Resources.Dungeon.Depth + 1
498-
return es.Transition[w.World]{Type: es.TransSwitch, NewStateFuncs: []es.StateFactory[w.World]{NewDungeonState(nextDepth)}}, nil
508+
return es.Transition[w.World]{Type: es.TransPush, NewStateFuncs: []es.StateFactory[w.World]{
509+
NewFadeoutAnimationState(NewDungeonState(nextDepth)),
510+
}}, nil
499511
case resources.WarpEscapeEvent:
500512
// 街へ帰還
501-
return es.Transition[w.World]{Type: es.TransSwitch, NewStateFuncs: []es.StateFactory[w.World]{NewTownState()}}, nil
513+
return es.Transition[w.World]{Type: es.TransPush, NewStateFuncs: []es.StateFactory[w.World]{
514+
NewFadeoutAnimationState(NewTownState()),
515+
}}, nil
502516
case resources.GameClearEvent:
503517
return es.Transition[w.World]{Type: es.TransSwitch, NewStateFuncs: []es.StateFactory[w.World]{NewDungeonCompleteEndingState}}, nil
504518
}

0 commit comments

Comments
 (0)