Skip to content

Commit 3c54052

Browse files
authored
Merge pull request #203 from EndoHizumi:copilot/introduce-event-driven-architecture
イベント駆動アーキテクチャの導入(EventBus + DefaultUIHandler)
2 parents 064d2c5 + 4ef8290 commit 3c54052

14 files changed

Lines changed: 944 additions & 215 deletions

example-vue/src/App.vue

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,28 @@
2121
@select="onChoiceSelected"
2222
/>
2323

24+
<DialogPanel
25+
:visible="dialog.visible"
26+
:prompt="dialog.prompt"
27+
:actions="dialog.actions"
28+
@action="onDialogAction"
29+
/>
30+
2431
<WaitCursor :visible="waitCursor.visible" />
2532

2633
<!-- EventBus フロービジュアライザー -->
2734
<EventBusMonitor
2835
:pulse-phase="pulsePhase"
2936
:current-event="currentEvent"
3037
:recent-events="recentEvents"
38+
@clear="clearEvents"
3139
/>
3240

3341
<!-- Vue State Inspector (DevTools 風) -->
3442
<VueStateInspector
3543
:message="message"
3644
:choices="choices"
45+
:dialog="dialog"
3746
:wait-cursor="waitCursor"
3847
:pulse-phase="pulsePhase"
3948
:current-event="currentEvent"
@@ -44,6 +53,7 @@
4453
import { onMounted } from 'vue'
4554
import MessageWindow from './components/MessageWindow.vue'
4655
import ChoicePanel from './components/ChoicePanel.vue'
56+
import DialogPanel from './components/DialogPanel.vue'
4757
import WaitCursor from './components/WaitCursor.vue'
4858
import EventBusMonitor from './components/EventBusMonitor.vue'
4959
import VueStateInspector from './components/VueStateInspector.vue'
@@ -55,14 +65,16 @@ const props = defineProps({
5565
engineConfig: { type: Object, required: true },
5666
})
5767
58-
const { pulsePhase, currentEvent, recentEvents, recordEvent } = useEventBusMonitor()
68+
const { pulsePhase, currentEvent, recentEvents, recordEvent, clearEvents } = useEventBusMonitor()
5969
6070
const {
6171
message,
6272
choices,
73+
dialog,
6374
waitCursor,
6475
onTextDisplayed,
6576
onChoiceSelected,
77+
onDialogAction,
6678
onNext,
6779
onSetSkip,
6880
} = useWebTaleKit(props.game, props.engineConfig.resolution, { onEvent: recordEvent })
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<template>
2+
<!--
3+
DialogPanel
4+
EventBus の dialog:show イベントデータを受け取り、
5+
モーダルダイアログを表示するコンポーネント。
6+
7+
Core は選択結果が返るまで await して待機する。
8+
ユーザーがアクションボタンを押したら $emit('action', action) で親に通知し、
9+
親 (App.vue) が onDialogAction() を介して Core を再開させる。
10+
-->
11+
<Teleport to="body">
12+
<div v-if="visible" class="dialog-overlay">
13+
<div class="dialog-container">
14+
<p v-if="prompt" class="dialog-prompt">{{ prompt }}</p>
15+
<div class="dialog-buttons">
16+
<button
17+
v-for="action in actions"
18+
:key="action.id"
19+
class="dialog-button"
20+
@click.stop="select(action)"
21+
>
22+
{{ action.label }}
23+
</button>
24+
</div>
25+
</div>
26+
</div>
27+
</Teleport>
28+
</template>
29+
30+
<script setup>
31+
const props = defineProps({
32+
visible: { type: Boolean, default: false },
33+
prompt: { type: String, default: '' },
34+
actions: { type: Array, default: () => [] },
35+
})
36+
37+
const emit = defineEmits(['action'])
38+
39+
function select(action) {
40+
emit('action', action)
41+
}
42+
</script>
43+
44+
<style scoped>
45+
.dialog-overlay {
46+
position: fixed;
47+
inset: 0;
48+
display: flex;
49+
justify-content: center;
50+
align-items: center;
51+
background: rgba(0, 0, 0, 0.6);
52+
z-index: 100;
53+
}
54+
55+
.dialog-container {
56+
background: rgba(10, 15, 50, 0.95);
57+
border: 1px solid rgba(100, 160, 255, 0.4);
58+
border-radius: 8px;
59+
padding: 28px 36px 24px;
60+
min-width: 320px;
61+
max-width: 500px;
62+
backdrop-filter: blur(6px);
63+
animation: dialogIn 0.2s ease;
64+
}
65+
66+
@keyframes dialogIn {
67+
from {
68+
opacity: 0;
69+
transform: scale(0.95) translateY(8px);
70+
}
71+
to {
72+
opacity: 1;
73+
transform: scale(1) translateY(0);
74+
}
75+
}
76+
77+
.dialog-prompt {
78+
color: #dde;
79+
font-size: 16px;
80+
line-height: 1.7;
81+
margin: 0 0 20px;
82+
text-align: center;
83+
white-space: pre-wrap;
84+
}
85+
86+
.dialog-buttons {
87+
display: flex;
88+
flex-direction: column;
89+
gap: 10px;
90+
}
91+
92+
.dialog-button {
93+
width: 100%;
94+
padding: 12px 24px;
95+
background: rgba(20, 30, 90, 0.8);
96+
border: 1px solid rgba(100, 160, 255, 0.4);
97+
border-radius: 4px;
98+
color: #ddf;
99+
font-size: 15px;
100+
font-family: inherit;
101+
letter-spacing: 0.05em;
102+
cursor: pointer;
103+
transition: background 0.15s ease, border-color 0.15s ease;
104+
}
105+
106+
.dialog-button:hover {
107+
background: rgba(40, 60, 160, 0.9);
108+
border-color: rgba(150, 200, 255, 0.7);
109+
}
110+
111+
.dialog-button:active {
112+
background: rgba(60, 90, 200, 0.9);
113+
}
114+
</style>

example-vue/src/components/EventBusMonitor.vue

Lines changed: 106 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,79 @@
11
<template>
2-
<div class="ebm-panel">
3-
<!-- ヘッダー -->
4-
<div class="ebm-header">
5-
<span class="ebm-dot" :class="{ pulse: pulsePhase > 0 }" />
2+
<div class="ebm-panel" :class="{ collapsed }">
3+
<!-- ヘッダー(常時表示・クリック可能) -->
4+
<div class="ebm-header" @click="toggle">
5+
<span class="ebm-dot" :class="{ pulse: isPulsing, retained: isRetained }" />
66
EventBus Monitor
7+
<span class="ebm-toggle">{{ collapsed ? '▸' : '▾' }}</span>
78
</div>
89

9-
<!-- フロー図: Core → EventBus → Vue -->
10-
<div class="ebm-flow">
11-
<div class="ebm-node" :class="{ active: pulsePhase >= 1 }">
12-
<span class="ebm-icon">◈</span> Core
13-
</div>
10+
<!-- 折りたたみ可能な本体 -->
11+
<template v-if="!collapsed">
12+
<!-- フロー図: Core → EventBus → Vue -->
13+
<div class="ebm-flow">
14+
<div class="ebm-node" :class="{ active: pulsePhase >= 1 && pulsePhase <= 3, retained: isRetained }">
15+
<span class="ebm-icon">◈</span> Core
16+
</div>
1417

15-
<div class="ebm-connector" :class="{ active: pulsePhase >= 2 }">
16-
<span class="ebm-arrow-line" />
17-
<span class="ebm-emit-label" :class="{ visible: pulsePhase >= 1 }">
18-
emit( <em>{{ currentEvent || '…' }}</em> )
19-
</span>
20-
</div>
18+
<div class="ebm-connector" :class="{ active: pulsePhase >= 2 && pulsePhase <= 3, retained: isRetained }">
19+
<span class="ebm-arrow-line" />
20+
<span class="ebm-emit-label" :class="{ visible: pulsePhase >= 1 || isRetained, retained: isRetained }">
21+
emit( <em>{{ currentEvent || '…' }}</em> )
22+
</span>
23+
</div>
2124

22-
<div class="ebm-node accent" :class="{ active: pulsePhase >= 2 }">
23-
<span class="ebm-icon">⚡</span> EventBus
24-
</div>
25+
<div class="ebm-node accent" :class="{ active: pulsePhase >= 2 && pulsePhase <= 3, retained: isRetained }">
26+
<span class="ebm-icon">⚡</span> EventBus
27+
</div>
2528

26-
<div class="ebm-connector" :class="{ active: pulsePhase >= 3 }">
27-
<span class="ebm-arrow-line" />
28-
<span class="ebm-emit-label" :class="{ visible: pulsePhase >= 2 }">
29-
receive
30-
</span>
31-
</div>
29+
<div class="ebm-connector" :class="{ active: pulsePhase >= 3 && pulsePhase <= 3, retained: isRetained }">
30+
<span class="ebm-arrow-line" />
31+
<span class="ebm-emit-label" :class="{ visible: pulsePhase >= 2 || isRetained, retained: isRetained }">
32+
receive
33+
</span>
34+
</div>
3235

33-
<div class="ebm-node vue" :class="{ active: pulsePhase >= 3 }">
34-
<span class="ebm-icon">◆</span> Vue UI
36+
<div class="ebm-node vue" :class="{ active: pulsePhase === 3, retained: isRetained }">
37+
<span class="ebm-icon">◆</span> Vue UI
38+
</div>
3539
</div>
36-
</div>
3740

38-
<!-- イベントログ -->
39-
<div class="ebm-log-header">recent events</div>
40-
<TransitionGroup name="log" tag="ul" class="ebm-log">
41-
<li v-for="ev in recentEvents" :key="ev.id" class="ebm-log-item">
42-
<span class="ebm-tag" :style="{ borderColor: ev.color, color: ev.color }">
43-
{{ ev.name }}
44-
</span>
45-
<span v-if="ev.summary" class="ebm-summary">{{ ev.summary }}</span>
46-
</li>
47-
</TransitionGroup>
41+
<!-- イベントログ -->
42+
<div class="ebm-log-header">recent events</div>
43+
<TransitionGroup name="log" tag="ul" class="ebm-log">
44+
<li v-for="ev in recentEvents" :key="ev.id" class="ebm-log-item">
45+
<span class="ebm-tag" :style="{ borderColor: ev.color, color: ev.color }">
46+
{{ ev.name }}
47+
</span>
48+
<span v-if="ev.summary" class="ebm-summary">{{ ev.summary }}</span>
49+
</li>
50+
</TransitionGroup>
51+
</template>
4852
</div>
4953
</template>
5054

5155
<script setup>
52-
defineProps({
56+
import { computed, ref } from 'vue'
57+
58+
const props = defineProps({
5359
pulsePhase: { type: Number, default: 0 },
5460
currentEvent: { type: String, default: '' },
5561
recentEvents: { type: Array, default: () => [] },
5662
})
63+
64+
const emit = defineEmits(['clear'])
65+
66+
const collapsed = ref(false)
67+
const isRetained = computed(() => props.pulsePhase === 4)
68+
const isPulsing = computed(() => props.pulsePhase > 0 && props.pulsePhase < 4)
69+
70+
function toggle() {
71+
collapsed.value = !collapsed.value
72+
// 再表示時にログをクリアする
73+
if (!collapsed.value) {
74+
emit('clear')
75+
}
76+
}
5777
</script>
5878

5979
<style scoped>
@@ -67,14 +87,17 @@ defineProps({
6787
border: 1px solid rgba(100, 140, 255, 0.2);
6888
border-radius: 8px;
6989
padding: 10px 12px 8px;
70-
pointer-events: none;
7190
z-index: 90;
7291
font-family: 'Courier New', monospace;
7392
font-size: 11px;
7493
color: rgba(200, 210, 255, 0.75);
7594
backdrop-filter: blur(4px);
7695
}
7796
97+
.ebm-panel.collapsed {
98+
padding-bottom: 10px;
99+
}
100+
78101
/* ── ヘッダー ────────────────────────────────── */
79102
.ebm-header {
80103
display: flex;
@@ -85,6 +108,21 @@ defineProps({
85108
color: rgba(150, 170, 255, 0.6);
86109
text-transform: uppercase;
87110
margin-bottom: 10px;
111+
cursor: pointer;
112+
user-select: none;
113+
}
114+
115+
.ebm-panel.collapsed .ebm-header {
116+
margin-bottom: 0;
117+
}
118+
119+
.ebm-header:hover {
120+
color: rgba(180, 200, 255, 0.9);
121+
}
122+
123+
.ebm-toggle {
124+
margin-left: auto;
125+
font-size: 9px;
88126
}
89127
90128
.ebm-dot {
@@ -98,6 +136,10 @@ defineProps({
98136
background: #7af;
99137
box-shadow: 0 0 6px #7af;
100138
}
139+
.ebm-dot.retained {
140+
background: rgba(122, 170, 255, 0.65);
141+
box-shadow: 0 0 3px rgba(122, 170, 255, 0.35);
142+
}
101143
102144
/* ── フロー図 ────────────────────────────────── */
103145
.ebm-flow {
@@ -131,18 +173,36 @@ defineProps({
131173
background: rgba(40, 70, 160, 0.5);
132174
box-shadow: 0 0 10px rgba(100, 160, 255, 0.3);
133175
}
176+
.ebm-node.retained {
177+
color: rgba(200, 230, 255, 0.78);
178+
border-color: rgba(120, 180, 255, 0.35);
179+
background: rgba(34, 52, 110, 0.3);
180+
box-shadow: 0 0 4px rgba(100, 160, 255, 0.14);
181+
}
134182
.ebm-node.accent.active {
135183
color: #ffe08a;
136184
border-color: rgba(250, 200, 80, 0.6);
137185
background: rgba(80, 60, 20, 0.5);
138186
box-shadow: 0 0 10px rgba(250, 200, 80, 0.3);
139187
}
188+
.ebm-node.accent.retained {
189+
color: rgba(255, 224, 138, 0.78);
190+
border-color: rgba(250, 200, 80, 0.35);
191+
background: rgba(80, 60, 20, 0.28);
192+
box-shadow: 0 0 4px rgba(250, 200, 80, 0.16);
193+
}
140194
.ebm-node.vue.active {
141195
color: #6ee7b7;
142196
border-color: rgba(52, 211, 153, 0.6);
143197
background: rgba(20, 70, 50, 0.5);
144198
box-shadow: 0 0 10px rgba(52, 211, 153, 0.3);
145199
}
200+
.ebm-node.vue.retained {
201+
color: rgba(110, 231, 183, 0.82);
202+
border-color: rgba(52, 211, 153, 0.35);
203+
background: rgba(20, 70, 50, 0.28);
204+
box-shadow: 0 0 4px rgba(52, 211, 153, 0.16);
205+
}
146206
147207
.ebm-icon {
148208
font-size: 9px;
@@ -170,6 +230,10 @@ defineProps({
170230
background: rgba(120, 180, 255, 0.7);
171231
box-shadow: 0 0 4px rgba(120, 180, 255, 0.5);
172232
}
233+
.ebm-connector.retained .ebm-arrow-line {
234+
background: rgba(120, 180, 255, 0.42);
235+
box-shadow: 0 0 2px rgba(120, 180, 255, 0.22);
236+
}
173237
174238
.ebm-emit-label {
175239
font-size: 9px;
@@ -183,6 +247,9 @@ defineProps({
183247
.ebm-emit-label.visible {
184248
color: rgba(150, 170, 255, 0.7);
185249
}
250+
.ebm-emit-label.retained {
251+
color: rgba(150, 170, 255, 0.48);
252+
}
186253
.ebm-emit-label em {
187254
font-style: normal;
188255
color: #fde68a;

0 commit comments

Comments
 (0)