Skip to content

Commit 99a7e73

Browse files
committed
GameEvent
1 parent 68fd819 commit 99a7e73

3 files changed

Lines changed: 263 additions & 0 deletions

File tree

frontend/src/components/FieldVisualizerVision.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import SvgVision from '@/components/SvgVision.vue'
44
import SvgReferee from '@/components/SvgReferee.vue'
55
import SvgTracked from '@/components/SvgTracked.vue'
66
import SvgBallPlacement from '@/components/SvgBallPlacement.vue'
7+
import SvgGameEvents from '@/components/SvgGameEvents.vue'
78
import SettingsPanel from '@/components/SettingsPanel.vue'
89
import RefereeInfo from '@/components/RefereeInfo.vue'
910
import type { LayerVisibility } from '@/components/LayerControl.vue'
@@ -35,6 +36,7 @@ const layerVisibility = ref<LayerVisibility>({
3536
ball: true,
3637
referee: true,
3738
fieldLines: true,
39+
fouls: true,
3840
})
3941
</script>
4042

@@ -62,6 +64,10 @@ const layerVisibility = ref<LayerVisibility>({
6264
:referee="referee"
6365
:detection-frame="detectionFrame"
6466
/>
67+
<SvgGameEvents
68+
v-if="referee && layerVisibility.fouls"
69+
:game-events="referee.gameEvents"
70+
/>
6571
<SvgTracked v-if="trackedFrame" :tracked-frame="trackedFrame" />
6672
</FieldVisualizer>
6773
</div>

frontend/src/components/LayerControl.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export interface LayerVisibility {
33
ball: boolean
44
referee: boolean
55
fieldLines: boolean
6+
fouls: boolean
67
}
78
89
const layers = defineModel<LayerVisibility>({ required: true })
@@ -42,6 +43,15 @@ const toggleLayer = (layerName: keyof LayerVisibility) => {
4243
/>
4344
<span>フィールドライン</span>
4445
</label>
46+
<label class="layer-item">
47+
<input
48+
id="layer-fouls"
49+
type="checkbox"
50+
:checked="layers.fouls"
51+
@change="toggleLayer('fouls')"
52+
/>
53+
<span>ファール</span>
54+
</label>
4555
</div>
4656
</template>
4757

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
<script setup lang="ts">
2+
import type { GameEvent, GameEvent_Type } from '@/proto/gc/ssl_gc_game_event_pb.ts'
3+
import type { Team } from '@/proto/gc/ssl_gc_common_pb.ts'
4+
import { computed, inject, onMounted, onUnmounted, ref, type Ref } from 'vue'
5+
6+
const props = defineProps<{
7+
gameEvents: GameEvent[]
8+
}>()
9+
10+
// フィールド回転の状態を取得
11+
const rotateField = inject<Ref<boolean>>('rotate-field')!
12+
13+
// テキストの回転用transform関数
14+
function textTransform(x: number, y: number) {
15+
if (rotateField.value) {
16+
return `rotate(-90,${x},${y})`
17+
}
18+
return ''
19+
}
20+
21+
// 表示期間(ミリ秒)
22+
const DISPLAY_DURATION = 5000
23+
24+
// 現在時刻を定期的に更新
25+
const now = ref(Date.now())
26+
let timer: ReturnType<typeof setInterval> | null = null
27+
28+
onMounted(() => {
29+
timer = setInterval(() => {
30+
now.value = Date.now()
31+
}, 100)
32+
})
33+
34+
onUnmounted(() => {
35+
if (timer) {
36+
clearInterval(timer)
37+
}
38+
})
39+
40+
// 表示すべきイベントをフィルタリング
41+
const visibleEvents = computed(() => {
42+
return props.gameEvents.filter((event) => {
43+
if (!event.createdTimestamp) return false
44+
const eventTime = Number(event.createdTimestamp) / 1000 // マイクロ秒からミリ秒へ
45+
const elapsed = now.value - eventTime
46+
return elapsed >= 0 && elapsed < DISPLAY_DURATION
47+
})
48+
})
49+
50+
// ファールタイプごとの日本語名とアイコン
51+
const foulInfo: Record<number, { name: string; icon: string }> = {
52+
22: { name: 'CRASH', icon: '💥' }, // BOT_CRASH_UNIQUE
53+
21: { name: 'CRASH', icon: '💥' }, // BOT_CRASH_DRAWN
54+
24: { name: 'PUSH', icon: '👊' }, // BOT_PUSHED_BOT
55+
31: { name: 'エリア内', icon: '🚫' }, // DEFENDER_IN_DEFENSE_AREA
56+
19: { name: '接近', icon: '⚠️' }, // ATTACKER_TOO_CLOSE_TO_DEFENSE_AREA
57+
17: { name: 'ドリブル', icon: '🏃' }, // BOT_DRIBBLED_BALL_TOO_FAR
58+
18: { name: '速度超過', icon: '' }, // BOT_KICKED_BALL_TOO_FAST
59+
28: { name: 'STOP超過', icon: '🚦' }, // BOT_TOO_FAST_IN_STOP
60+
29: { name: 'キック点', icon: '📍' }, // DEFENDER_TOO_CLOSE_TO_KICK_POINT
61+
}
62+
63+
// チームカラーを取得
64+
function getTeamColor(team?: Team): string {
65+
if (team === undefined) return '#888888'
66+
// Team enum: UNKNOWN=0, YELLOW=1, BLUE=2
67+
return team === 1 ? '#ffcc00' : team === 2 ? '#0066cc' : '#888888'
68+
}
69+
70+
// イベントから位置情報を取得
71+
function getEventLocation(event: GameEvent): { x: number; y: number } | null {
72+
if (!event.event || !event.event.value) return null
73+
74+
const eventData = event.event.value
75+
if ('location' in eventData && eventData.location) {
76+
// GameEventのlocationはすでにメートル単位 [m] で来ている
77+
return {
78+
x: eventData.location.x ?? 0,
79+
y: -(eventData.location.y ?? 0), // Y軸を反転
80+
}
81+
}
82+
83+
return null
84+
}
85+
86+
// イベントからチームを取得
87+
function getEventTeam(event: GameEvent): Team | undefined {
88+
if (!event.event || !event.event.value) return undefined
89+
90+
const eventData = event.event.value
91+
if ('byTeam' in eventData) {
92+
return eventData.byTeam
93+
}
94+
95+
return undefined
96+
}
97+
98+
// イベントからボット番号を取得
99+
function getEventBot(event: GameEvent): number | undefined {
100+
if (!event.event || !event.event.value) return undefined
101+
102+
const eventData = event.event.value
103+
if ('byBot' in eventData) {
104+
return eventData.byBot
105+
}
106+
107+
return undefined
108+
}
109+
110+
// イベントから補足情報を取得
111+
function getEventDetails(event: GameEvent): string | undefined {
112+
if (!event.event || !event.event.value) return undefined
113+
114+
const eventData = event.event.value
115+
116+
// 速度超過系
117+
if ('initialBallSpeed' in eventData && eventData.initialBallSpeed !== undefined) {
118+
return `${eventData.initialBallSpeed.toFixed(1)}m/s`
119+
}
120+
if ('speed' in eventData && eventData.speed !== undefined) {
121+
return `${eventData.speed.toFixed(1)}m/s`
122+
}
123+
124+
// 距離系(既にメートル単位 [m])
125+
if ('distance' in eventData && eventData.distance !== undefined) {
126+
return `${eventData.distance.toFixed(2)}m`
127+
}
128+
129+
// ドリブル距離(start/endは既にメートル単位 [m])
130+
if ('start' in eventData && 'end' in eventData && eventData.start && eventData.end) {
131+
const dx = (eventData.end.x ?? 0) - (eventData.start.x ?? 0)
132+
const dy = (eventData.end.y ?? 0) - (eventData.start.y ?? 0)
133+
const dist = Math.sqrt(dx * dx + dy * dy)
134+
return `${dist.toFixed(2)}m`
135+
}
136+
137+
// 押し出し距離(既にメートル単位 [m])
138+
if ('pushedDistance' in eventData && eventData.pushedDistance !== undefined) {
139+
return `${eventData.pushedDistance.toFixed(2)}m`
140+
}
141+
142+
// 衝突速度
143+
if ('crashSpeed' in eventData && eventData.crashSpeed !== undefined) {
144+
return `${eventData.crashSpeed.toFixed(1)}m/s`
145+
}
146+
147+
return undefined
148+
}
149+
150+
// フェードアウト用の透明度を計算
151+
function getOpacity(event: GameEvent): number {
152+
if (!event.createdTimestamp) return 1
153+
const eventTime = Number(event.createdTimestamp) / 1000
154+
const elapsed = now.value - eventTime
155+
const remaining = DISPLAY_DURATION - elapsed
156+
157+
// 最後の1秒でフェードアウト
158+
if (remaining < 1000) {
159+
return remaining / 1000
160+
}
161+
162+
return 1
163+
}
164+
</script>
165+
166+
<template>
167+
<g v-for="event in visibleEvents" :key="event.id">
168+
<g v-if="getEventLocation(event)" :opacity="getOpacity(event)">
169+
<!-- 背景円 -->
170+
<circle
171+
:cx="getEventLocation(event)!.x"
172+
:cy="getEventLocation(event)!.y"
173+
r="0.04"
174+
:fill="getTeamColor(getEventTeam(event))"
175+
stroke="black"
176+
stroke-width="0.002"
177+
opacity="0.8"
178+
/>
179+
180+
<!-- アイコン(絵文字) -->
181+
<text
182+
:x="getEventLocation(event)!.x"
183+
:y="getEventLocation(event)!.y"
184+
:transform="textTransform(getEventLocation(event)!.x, getEventLocation(event)!.y)"
185+
text-anchor="middle"
186+
dominant-baseline="central"
187+
font-size="0.12"
188+
fill="white"
189+
style="pointer-events: none; user-select: none"
190+
>
191+
{{ foulInfo[event.type]?.icon || '⚠️' }}
192+
</text>
193+
194+
<!-- ファール名 -->
195+
<text
196+
:x="getEventLocation(event)!.x + (rotateField ? 0.14 : 0)"
197+
:y="getEventLocation(event)!.y + (rotateField ? 0 : 0.15)"
198+
:transform="textTransform(getEventLocation(event)!.x + (rotateField ? 0.14 : 0), getEventLocation(event)!.y + (rotateField ? 0 : 0.15))"
199+
text-anchor="middle"
200+
dominant-baseline="central"
201+
font-size="0.10"
202+
font-weight="bold"
203+
fill="white"
204+
stroke="black"
205+
stroke-width="0.004"
206+
style="paint-order: stroke; pointer-events: none; user-select: none"
207+
>
208+
{{ foulInfo[event.type]?.name || 'ファール' }}
209+
</text>
210+
211+
<!-- ボット番号(存在する場合) -->
212+
<text
213+
v-if="getEventBot(event) !== undefined"
214+
:x="getEventLocation(event)!.x + (rotateField ? 0.24 : 0)"
215+
:y="getEventLocation(event)!.y + (rotateField ? 0 : 0.28)"
216+
:transform="textTransform(getEventLocation(event)!.x + (rotateField ? 0.24 : 0), getEventLocation(event)!.y + (rotateField ? 0 : 0.28))"
217+
text-anchor="middle"
218+
dominant-baseline="central"
219+
font-size="0.08"
220+
fill="white"
221+
stroke="black"
222+
stroke-width="0.003"
223+
style="paint-order: stroke; pointer-events: none; user-select: none"
224+
>
225+
#{{ getEventBot(event) }}
226+
</text>
227+
228+
<!-- 補足情報(速度、距離など) -->
229+
<text
230+
v-if="getEventDetails(event)"
231+
:x="getEventLocation(event)!.x + (rotateField ? (getEventBot(event) !== undefined ? 0.34 : 0.24) : 0)"
232+
:y="getEventLocation(event)!.y + (rotateField ? 0 : (getEventBot(event) !== undefined ? 0.40 : 0.28))"
233+
:transform="textTransform(getEventLocation(event)!.x + (rotateField ? (getEventBot(event) !== undefined ? 0.34 : 0.24) : 0), getEventLocation(event)!.y + (rotateField ? 0 : (getEventBot(event) !== undefined ? 0.40 : 0.28)))"
234+
text-anchor="middle"
235+
dominant-baseline="central"
236+
font-size="0.08"
237+
font-weight="bold"
238+
fill="#ffff00"
239+
stroke="black"
240+
stroke-width="0.003"
241+
style="paint-order: stroke; pointer-events: none; user-select: none"
242+
>
243+
{{ getEventDetails(event) }}
244+
</text>
245+
</g>
246+
</g>
247+
</template>

0 commit comments

Comments
 (0)