@vylos/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/package.json +34 -0
  2. package/src/components/app/EngineView.vue +40 -0
  3. package/src/components/app/GameShell.vue +265 -0
  4. package/src/components/app/LoadingScreen.vue +10 -0
  5. package/src/components/app/MainMenu.vue +243 -0
  6. package/src/components/core/BackgroundLayer.vue +27 -0
  7. package/src/components/core/ChoicePanel.vue +115 -0
  8. package/src/components/core/CustomOverlay.vue +27 -0
  9. package/src/components/core/DialogueBox.vue +144 -0
  10. package/src/components/core/DrawableOverlay.vue +109 -0
  11. package/src/components/core/ForegroundLayer.vue +31 -0
  12. package/src/components/menu/ActionOverlay.vue +126 -0
  13. package/src/components/menu/LocationOverlay.vue +136 -0
  14. package/src/components/menu/PauseMenu.vue +196 -0
  15. package/src/components/menu/SaveLoadMenu.vue +377 -0
  16. package/src/components/menu/SettingsMenu.vue +111 -0
  17. package/src/components/menu/TopBar.vue +65 -0
  18. package/src/composables/useConfig.ts +4 -0
  19. package/src/composables/useEngine.ts +16 -0
  20. package/src/composables/useGameState.ts +9 -0
  21. package/src/composables/useLanguage.ts +31 -0
  22. package/src/engine/core/CheckpointManager.ts +122 -0
  23. package/src/engine/core/Engine.ts +272 -0
  24. package/src/engine/core/EngineFactory.ts +102 -0
  25. package/src/engine/core/EventRunner.ts +488 -0
  26. package/src/engine/errors/EventEndError.ts +7 -0
  27. package/src/engine/errors/InterruptSignal.ts +10 -0
  28. package/src/engine/errors/JumpSignal.ts +10 -0
  29. package/src/engine/errors/StateValidationError.ts +13 -0
  30. package/src/engine/managers/ActionManager.ts +62 -0
  31. package/src/engine/managers/EventManager.ts +166 -0
  32. package/src/engine/managers/HistoryManager.ts +84 -0
  33. package/src/engine/managers/InputManager.ts +117 -0
  34. package/src/engine/managers/LanguageManager.ts +51 -0
  35. package/src/engine/managers/LocationManager.ts +76 -0
  36. package/src/engine/managers/NavigationManager.ts +75 -0
  37. package/src/engine/managers/SaveManager.ts +86 -0
  38. package/src/engine/managers/SettingsManager.ts +70 -0
  39. package/src/engine/managers/WaitManager.ts +47 -0
  40. package/src/engine/schemas/baseGameState.schema.ts +19 -0
  41. package/src/engine/schemas/checkpoint.schema.ts +11 -0
  42. package/src/engine/schemas/engineState.schema.ts +59 -0
  43. package/src/engine/schemas/location.schema.ts +21 -0
  44. package/src/engine/storage/VylosStorage.ts +131 -0
  45. package/src/engine/types/actions.ts +20 -0
  46. package/src/engine/types/checkpoint.ts +31 -0
  47. package/src/engine/types/config.ts +9 -0
  48. package/src/engine/types/dialogue.ts +15 -0
  49. package/src/engine/types/engine.ts +85 -0
  50. package/src/engine/types/events.ts +117 -0
  51. package/src/engine/types/game-state.ts +15 -0
  52. package/src/engine/types/index.ts +10 -0
  53. package/src/engine/types/locations.ts +32 -0
  54. package/src/engine/types/plugin.ts +11 -0
  55. package/src/engine/types/save.ts +40 -0
  56. package/src/engine/utils/TimeHelper.ts +39 -0
  57. package/src/engine/utils/logger.ts +43 -0
  58. package/src/env.d.ts +17 -0
  59. package/src/index.ts +74 -0
  60. package/src/stores/engineState.ts +127 -0
  61. package/src/stores/gameState.ts +49 -0
  62. package/tests/engine/ActionManager.test.ts +94 -0
  63. package/tests/engine/CheckpointManager.test.ts +136 -0
  64. package/tests/engine/EventManager.test.ts +145 -0
  65. package/tests/engine/EventRunner.test.ts +318 -0
  66. package/tests/engine/HistoryManager.test.ts +113 -0
  67. package/tests/engine/LocationManager.test.ts +128 -0
  68. package/tests/engine/schemas.test.ts +250 -0
  69. package/tests/engine/utils.test.ts +75 -0
  70. package/tests/integration/game-loop.test.ts +201 -0
  71. package/tests/safety/event-validation.test.ts +102 -0
  72. package/tests/safety/state-schema.test.ts +96 -0
  73. package/tests/setup.ts +2 -0
  74. package/tsconfig.json +14 -0
  75. package/vitest.config.ts +16 -0
@@ -0,0 +1,115 @@
1
+ <template>
2
+ <Transition name="choice-fade">
3
+ <div v-if="engineState.choices" class="choice-overlay">
4
+ <p v-if="engineState.choices.prompt" class="choice-prompt">
5
+ {{ engineState.choices.prompt }}
6
+ </p>
7
+
8
+ <div class="choice-list">
9
+ <button
10
+ v-for="option in engineState.choices.options"
11
+ :key="option.value"
12
+ :disabled="option.disabled"
13
+ :class="[
14
+ 'choice-btn',
15
+ { 'choice-btn--selected': isRedoMode && option.value === engineState.choices!.historySelectedValue },
16
+ ]"
17
+ @click="handleChoice(option.value)"
18
+ >
19
+ {{ option.text }}
20
+ </button>
21
+ </div>
22
+ </div>
23
+ </Transition>
24
+ </template>
25
+
26
+ <script setup lang="ts">
27
+ import { computed, inject } from 'vue';
28
+ import { useEngineStateStore } from '../../stores/engineState';
29
+ import { ENGINE_INJECT_KEY } from '../../composables/useEngine';
30
+ import type { Engine } from '../../engine/core/Engine';
31
+
32
+ const engineState = useEngineStateStore();
33
+ const engine = inject<Engine>(ENGINE_INJECT_KEY);
34
+
35
+ const isRedoMode = computed(() => engineState.choices?.historyStepIndex != null);
36
+
37
+ function handleChoice(value: string): void {
38
+ if (!engine) return;
39
+
40
+ if (isRedoMode.value) {
41
+ const stepIndex = engineState.choices!.historyStepIndex!;
42
+ engineState.historyBrowsing = false;
43
+ engineState.setChoices(null);
44
+ engine.eventRunner.requestRedoChoice(stepIndex, value);
45
+ } else {
46
+ engine.eventRunner.resolveWait(value);
47
+ }
48
+ }
49
+ </script>
50
+
51
+ <style scoped>
52
+ .choice-fade-enter-active,
53
+ .choice-fade-leave-active {
54
+ transition: opacity 0.25s ease;
55
+ }
56
+ .choice-fade-enter-from,
57
+ .choice-fade-leave-to {
58
+ opacity: 0;
59
+ }
60
+
61
+ .choice-overlay {
62
+ position: absolute;
63
+ inset: 0;
64
+ z-index: 35;
65
+ display: flex;
66
+ flex-direction: column;
67
+ align-items: center;
68
+ justify-content: center;
69
+ background: rgba(0, 0, 0, 0.4);
70
+ padding: 0 4cqw;
71
+ }
72
+
73
+ .choice-prompt {
74
+ color: white;
75
+ font-size: 2cqw;
76
+ font-weight: 600;
77
+ margin-bottom: 3cqh;
78
+ text-align: center;
79
+ }
80
+
81
+ .choice-list {
82
+ display: flex;
83
+ flex-direction: column;
84
+ gap: 1.5cqh;
85
+ width: 100%;
86
+ max-width: 50cqw;
87
+ }
88
+
89
+ .choice-btn {
90
+ padding: 1.5cqh 3cqw;
91
+ background: rgba(0, 0, 0, 0.7);
92
+ border: 1px solid rgba(255, 255, 255, 0.3);
93
+ color: white;
94
+ font-size: 1.8cqw;
95
+ text-align: left;
96
+ border-radius: 0.5cqw;
97
+ cursor: pointer;
98
+ transition: background-color 0.2s ease, border-color 0.2s ease;
99
+ }
100
+
101
+ .choice-btn:hover {
102
+ background: rgba(255, 255, 255, 0.1);
103
+ border-color: rgba(255, 255, 255, 0.6);
104
+ }
105
+
106
+ .choice-btn:disabled {
107
+ opacity: 0.4;
108
+ cursor: not-allowed;
109
+ }
110
+
111
+ .choice-btn--selected {
112
+ border-color: rgba(147, 197, 253, 0.6);
113
+ background: rgba(147, 197, 253, 0.15);
114
+ }
115
+ </style>
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <div v-if="engineState.overlayId" class="absolute inset-0 z-40 pointer-events-none">
3
+ <!-- Project-specific overlay components are rendered here by ID -->
4
+ <!-- Actual dynamic component resolution is handled by the project layer -->
5
+ <component
6
+ :is="resolvedComponent"
7
+ v-if="resolvedComponent"
8
+ v-bind="engineState.overlayProps ?? {}"
9
+ />
10
+ </div>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import { computed, inject } from 'vue';
15
+ import type { Component } from 'vue';
16
+ import { useEngineStateStore } from '../../stores/engineState';
17
+
18
+ const engineState = useEngineStateStore();
19
+
20
+ /** Optional registry injected by the project shell */
21
+ const overlayRegistry = inject<Record<string, Component>>('vylos-overlay-registry', {});
22
+
23
+ const resolvedComponent = computed<Component | undefined>(() => {
24
+ if (!engineState.overlayId) return undefined;
25
+ return overlayRegistry[engineState.overlayId];
26
+ });
27
+ </script>
@@ -0,0 +1,144 @@
1
+ <template>
2
+ <Transition name="dlg-slide">
3
+ <div
4
+ v-if="engineState.dialogue"
5
+ class="dlg-wrapper"
6
+ @click="handleClick"
7
+ >
8
+ <div class="dlg-box" :class="{ 'dlg-box--history': engineState.historyBrowsing }">
9
+ <!-- Speaker name -->
10
+ <div v-if="engineState.dialogue.speaker" class="dlg-speaker">
11
+ {{ engineState.dialogue.speaker }}
12
+ </div>
13
+
14
+ <!-- Dialogue text -->
15
+ <p class="dlg-text" :class="{ 'dlg-text--narration': engineState.dialogue.isNarration }">
16
+ {{ engineState.dialogue.text }}
17
+ </p>
18
+
19
+ <!-- Continue / History indicator -->
20
+ <div class="dlg-continue">
21
+ <template v-if="engineState.historyBrowsing">
22
+ &#9664; &#9654; history
23
+ </template>
24
+ <template v-else>
25
+ &#9660; continue
26
+ </template>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ </Transition>
31
+ </template>
32
+
33
+ <script setup lang="ts">
34
+ import { inject } from 'vue';
35
+ import { useEngineStateStore } from '../../stores/engineState';
36
+ import { ENGINE_INJECT_KEY } from '../../composables/useEngine';
37
+ import type { Engine } from '../../engine/core/Engine';
38
+
39
+ const engineState = useEngineStateStore();
40
+ const engine = inject<Engine>(ENGINE_INJECT_KEY);
41
+
42
+ function handleClick(): void {
43
+ if (!engine) return;
44
+
45
+ if (engine.eventRunner.isBrowsingHistory) {
46
+ // In history mode — advance through history (click = forward)
47
+ const step = engine.eventRunner.historyForward();
48
+ if (step) {
49
+ if (step.type === 'say' && step.dialogue) {
50
+ engineState.setDialogue(step.dialogue);
51
+ engineState.setChoices(null);
52
+ } else if (step.type === 'choice' && step.choiceOptions) {
53
+ engineState.setDialogue(null);
54
+ engineState.setChoices({
55
+ prompt: null,
56
+ options: step.choiceOptions,
57
+ historyStepIndex: step.stepIndex,
58
+ historySelectedValue: step.choiceResult,
59
+ });
60
+ }
61
+ }
62
+ if (!engine.eventRunner.isBrowsingHistory) {
63
+ engineState.historyBrowsing = false;
64
+ const live = engine.eventRunner.getLiveDialogue();
65
+ if (live) {
66
+ engineState.setDialogue({
67
+ text: live.text,
68
+ speaker: live.speaker,
69
+ isNarration: !live.speaker,
70
+ });
71
+ }
72
+ engineState.setChoices(null);
73
+ }
74
+ } else {
75
+ // Normal mode — resolve the wait to advance dialogue
76
+ engine.eventRunner.resolveWait();
77
+ }
78
+ }
79
+ </script>
80
+
81
+ <style scoped>
82
+ .dlg-slide-enter-active,
83
+ .dlg-slide-leave-active {
84
+ transition: transform 0.3s ease, opacity 0.3s ease;
85
+ }
86
+ .dlg-slide-enter-from,
87
+ .dlg-slide-leave-to {
88
+ transform: translateY(100%);
89
+ opacity: 0;
90
+ }
91
+
92
+ .dlg-wrapper {
93
+ position: absolute;
94
+ bottom: 0;
95
+ left: 0;
96
+ right: 0;
97
+ z-index: 30;
98
+ padding: 2cqh 2cqw;
99
+ cursor: pointer;
100
+ user-select: none;
101
+ }
102
+
103
+ .dlg-box {
104
+ background: rgba(0, 0, 0, 0.8);
105
+ border: 1px solid rgba(255, 255, 255, 0.2);
106
+ border-radius: 1cqw;
107
+ padding: 2cqh 2.5cqw;
108
+ max-width: 85cqw;
109
+ margin: 0 auto;
110
+ transition: border-color 0.2s ease;
111
+ }
112
+
113
+ .dlg-box--history {
114
+ border-color: rgba(147, 197, 253, 0.4);
115
+ }
116
+
117
+ .dlg-speaker {
118
+ color: #fde047;
119
+ font-weight: 700;
120
+ font-size: 1.8cqw;
121
+ margin-bottom: 1cqh;
122
+ text-transform: uppercase;
123
+ letter-spacing: 0.05em;
124
+ }
125
+
126
+ .dlg-text {
127
+ color: white;
128
+ font-size: 2cqw;
129
+ line-height: 1.6;
130
+ margin: 0;
131
+ }
132
+
133
+ .dlg-text--narration {
134
+ font-style: italic;
135
+ color: rgba(255, 255, 255, 0.8);
136
+ }
137
+
138
+ .dlg-continue {
139
+ text-align: right;
140
+ color: rgba(255, 255, 255, 0.4);
141
+ font-size: 1.2cqw;
142
+ margin-top: 1cqh;
143
+ }
144
+ </style>
@@ -0,0 +1,109 @@
1
+ <template>
2
+ <div
3
+ v-if="engineState.drawableEvents.length > 0 && !engineState.dialogue && !engineState.choices"
4
+ class="draw-overlay"
5
+ >
6
+ <button
7
+ v-for="entry in engineState.drawableEvents"
8
+ :key="entry.id"
9
+ :title="entry.label"
10
+ :class="['draw-btn', `draw-btn--${entry.position}`]"
11
+ @click.stop="engine?.navigationManager.selectDrawableEvent(entry.id)"
12
+ >
13
+ <div class="draw-btn__bg"></div>
14
+ <div class="draw-btn__content">
15
+ <span v-if="entry.icon" class="draw-btn__icon">{{ entry.icon }}</span>
16
+ <span class="draw-btn__label">{{ entry.label }}</span>
17
+ </div>
18
+ </button>
19
+ </div>
20
+ </template>
21
+
22
+ <script setup lang="ts">
23
+ import { inject } from 'vue';
24
+ import { useEngineStateStore } from '../../stores/engineState';
25
+ import { ENGINE_INJECT_KEY } from '../../composables/useEngine';
26
+ import type { Engine } from '../../engine/core/Engine';
27
+
28
+ const engineState = useEngineStateStore();
29
+ const engine = inject<Engine>(ENGINE_INJECT_KEY);
30
+ </script>
31
+
32
+ <style scoped>
33
+ .draw-overlay {
34
+ position: absolute;
35
+ inset: 0;
36
+ z-index: 15;
37
+ pointer-events: none;
38
+ display: flex;
39
+ align-items: center;
40
+ justify-content: center;
41
+ gap: 3cqw;
42
+ padding: 5cqh 3cqw;
43
+ }
44
+
45
+ .draw-btn {
46
+ pointer-events: auto;
47
+ position: relative;
48
+ border: none;
49
+ border-radius: 1.5cqw;
50
+ padding: 2cqh 2.5cqw;
51
+ cursor: pointer;
52
+ transition: transform 0.2s ease-out;
53
+ background: none;
54
+ }
55
+
56
+ .draw-btn--left {
57
+ align-self: center;
58
+ margin-right: auto;
59
+ }
60
+
61
+ .draw-btn--center {
62
+ align-self: center;
63
+ }
64
+
65
+ .draw-btn--right {
66
+ align-self: center;
67
+ margin-left: auto;
68
+ }
69
+
70
+ .draw-btn:hover {
71
+ transform: scale(1.08) translateY(-0.5cqh);
72
+ }
73
+
74
+ .draw-btn__bg {
75
+ position: absolute;
76
+ inset: 0;
77
+ background: rgba(0, 0, 0, 0.4);
78
+ backdrop-filter: blur(4px);
79
+ border: 1px solid rgba(255, 255, 255, 0.2);
80
+ border-radius: 1.5cqw;
81
+ transition: all 0.2s;
82
+ }
83
+
84
+ .draw-btn:hover .draw-btn__bg {
85
+ background: rgba(168, 85, 247, 0.5);
86
+ border-color: rgba(255, 255, 255, 0.4);
87
+ }
88
+
89
+ .draw-btn__content {
90
+ position: relative;
91
+ display: flex;
92
+ flex-direction: column;
93
+ align-items: center;
94
+ gap: 0.5cqh;
95
+ }
96
+
97
+ .draw-btn__icon {
98
+ font-size: 4cqw;
99
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5));
100
+ }
101
+
102
+ .draw-btn__label {
103
+ color: white;
104
+ font-weight: 500;
105
+ font-size: 2cqw;
106
+ text-align: center;
107
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5));
108
+ }
109
+ </style>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <Transition name="fg-fade">
3
+ <div
4
+ v-if="engineState.foreground"
5
+ class="absolute inset-0 flex items-end justify-center z-10 pointer-events-none"
6
+ >
7
+ <img
8
+ :src="engineState.foreground"
9
+ class="max-h-full object-contain"
10
+ alt=""
11
+ />
12
+ </div>
13
+ </Transition>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ import { useEngineStateStore } from '../../stores/engineState';
18
+
19
+ const engineState = useEngineStateStore();
20
+ </script>
21
+
22
+ <style scoped>
23
+ .fg-fade-enter-active,
24
+ .fg-fade-leave-active {
25
+ transition: opacity 0.3s ease;
26
+ }
27
+ .fg-fade-enter-from,
28
+ .fg-fade-leave-to {
29
+ opacity: 0;
30
+ }
31
+ </style>
@@ -0,0 +1,126 @@
1
+ <template>
2
+ <div
3
+ v-if="engineState.availableActions.length > 0 && !engineState.dialogue && !engineState.choices"
4
+ class="act-overlay"
5
+ >
6
+ <div class="act-overlay__row">
7
+ <button
8
+ v-for="action in engineState.availableActions"
9
+ :key="action.id"
10
+ :title="action.label"
11
+ class="act-btn group"
12
+ @click="engine?.navigationManager.selectAction(action.id)"
13
+ >
14
+ <div class="act-btn__bg"></div>
15
+ <div class="act-btn__ring"></div>
16
+ <div class="act-btn__label">
17
+ <span class="act-btn__text">{{ action.label }}</span>
18
+ </div>
19
+ <div class="act-btn__gradient"></div>
20
+ </button>
21
+ </div>
22
+ </div>
23
+ </template>
24
+
25
+ <script setup lang="ts">
26
+ import { inject } from 'vue';
27
+ import { useEngineStateStore } from '../../stores/engineState';
28
+ import { ENGINE_INJECT_KEY } from '../../composables/useEngine';
29
+ import type { Engine } from '../../engine/core/Engine';
30
+
31
+ const engineState = useEngineStateStore();
32
+ const engine = inject<Engine>(ENGINE_INJECT_KEY);
33
+ </script>
34
+
35
+ <style scoped>
36
+ .act-overlay {
37
+ position: absolute;
38
+ inset: 0;
39
+ z-index: 20;
40
+ pointer-events: none;
41
+ display: flex;
42
+ align-items: flex-end;
43
+ justify-content: flex-start;
44
+ padding: 1.5cqh 1.5cqw;
45
+ }
46
+
47
+ .act-overlay__row {
48
+ display: flex;
49
+ gap: 1cqw;
50
+ pointer-events: auto;
51
+ height: 18cqh;
52
+ align-items: center;
53
+ }
54
+
55
+ .act-btn {
56
+ position: relative;
57
+ overflow: hidden;
58
+ border: none;
59
+ border-radius: 9999px;
60
+ height: 100%;
61
+ aspect-ratio: 1;
62
+ cursor: pointer;
63
+ transition: transform 0.2s ease-out;
64
+ background: none;
65
+ }
66
+
67
+ .act-btn:hover {
68
+ transform: scale(1.1) translateY(-0.8cqh);
69
+ }
70
+
71
+ .act-btn__bg {
72
+ position: absolute;
73
+ inset: 0;
74
+ background: rgba(0, 0, 0, 0.3);
75
+ backdrop-filter: blur(4px);
76
+ border: 1px solid rgba(255, 255, 255, 0.2);
77
+ border-radius: 9999px;
78
+ transition: all 0.2s;
79
+ }
80
+
81
+ .act-btn:hover .act-btn__bg {
82
+ background: rgba(22, 163, 74, 0.6);
83
+ border-color: rgba(255, 255, 255, 0.4);
84
+ }
85
+
86
+ .act-btn__ring {
87
+ position: absolute;
88
+ inset: 0.6cqw;
89
+ border-radius: 9999px;
90
+ border: 1px solid rgba(255, 255, 255, 0.1);
91
+ transition: border-color 0.2s;
92
+ }
93
+
94
+ .act-btn:hover .act-btn__ring {
95
+ border-color: rgba(255, 255, 255, 0.3);
96
+ }
97
+
98
+ .act-btn__label {
99
+ position: relative;
100
+ width: 100%;
101
+ height: 100%;
102
+ display: flex;
103
+ align-items: center;
104
+ justify-content: center;
105
+ }
106
+
107
+ .act-btn__text {
108
+ color: white;
109
+ font-weight: 500;
110
+ font-size: 2.2cqw;
111
+ text-align: center;
112
+ line-height: 1.1;
113
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5));
114
+ overflow: hidden;
115
+ text-overflow: ellipsis;
116
+ max-width: 90%;
117
+ }
118
+
119
+ .act-btn__gradient {
120
+ position: absolute;
121
+ inset: 0;
122
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), transparent);
123
+ border-radius: 9999px;
124
+ pointer-events: none;
125
+ }
126
+ </style>
@@ -0,0 +1,136 @@
1
+ <template>
2
+ <div
3
+ v-if="engineState.availableLocations.length > 0 && !engineState.dialogue && !engineState.choices"
4
+ class="loc-overlay"
5
+ >
6
+ <div class="loc-overlay__row">
7
+ <button
8
+ v-for="location in engineState.availableLocations"
9
+ :key="location.id"
10
+ :disabled="!location.accessible"
11
+ :title="location.name"
12
+ class="loc-btn group"
13
+ @click="engine?.navigationManager.selectLocation(location.id)"
14
+ >
15
+ <div class="loc-btn__bg"></div>
16
+ <div class="loc-btn__ring"></div>
17
+ <div class="loc-btn__label">
18
+ <span class="loc-btn__text">{{ location.name }}</span>
19
+ </div>
20
+ <div class="loc-btn__gradient"></div>
21
+ </button>
22
+ </div>
23
+ </div>
24
+ </template>
25
+
26
+ <script setup lang="ts">
27
+ import { inject } from 'vue';
28
+ import { useEngineStateStore } from '../../stores/engineState';
29
+ import { ENGINE_INJECT_KEY } from '../../composables/useEngine';
30
+ import type { Engine } from '../../engine/core/Engine';
31
+
32
+ const engineState = useEngineStateStore();
33
+ const engine = inject<Engine>(ENGINE_INJECT_KEY);
34
+ </script>
35
+
36
+ <style scoped>
37
+ .loc-overlay {
38
+ position: absolute;
39
+ inset: 0;
40
+ z-index: 20;
41
+ pointer-events: none;
42
+ display: flex;
43
+ align-items: flex-end;
44
+ justify-content: flex-end;
45
+ padding: 1.5cqh 1.5cqw;
46
+ }
47
+
48
+ .loc-overlay__row {
49
+ display: flex;
50
+ gap: 1cqw;
51
+ pointer-events: auto;
52
+ height: 18cqh;
53
+ align-items: center;
54
+ }
55
+
56
+ .loc-btn {
57
+ position: relative;
58
+ overflow: hidden;
59
+ border: none;
60
+ border-radius: 9999px;
61
+ height: 100%;
62
+ aspect-ratio: 1;
63
+ cursor: pointer;
64
+ transition: transform 0.2s ease-out;
65
+ background: none;
66
+ }
67
+
68
+ .loc-btn:hover {
69
+ transform: scale(1.1) translateY(-0.8cqh);
70
+ }
71
+
72
+ .loc-btn:disabled {
73
+ opacity: 0.4;
74
+ cursor: not-allowed;
75
+ }
76
+
77
+ .loc-btn:disabled:hover {
78
+ transform: none;
79
+ }
80
+
81
+ .loc-btn__bg {
82
+ position: absolute;
83
+ inset: 0;
84
+ background: rgba(0, 0, 0, 0.3);
85
+ backdrop-filter: blur(4px);
86
+ border: 1px solid rgba(255, 255, 255, 0.2);
87
+ border-radius: 9999px;
88
+ transition: all 0.2s;
89
+ }
90
+
91
+ .loc-btn:hover .loc-btn__bg {
92
+ background: rgba(37, 99, 235, 0.6);
93
+ border-color: rgba(255, 255, 255, 0.4);
94
+ }
95
+
96
+ .loc-btn__ring {
97
+ position: absolute;
98
+ inset: 0.6cqw;
99
+ border-radius: 9999px;
100
+ border: 1px solid rgba(255, 255, 255, 0.1);
101
+ transition: border-color 0.2s;
102
+ }
103
+
104
+ .loc-btn:hover .loc-btn__ring {
105
+ border-color: rgba(255, 255, 255, 0.3);
106
+ }
107
+
108
+ .loc-btn__label {
109
+ position: relative;
110
+ width: 100%;
111
+ height: 100%;
112
+ display: flex;
113
+ align-items: center;
114
+ justify-content: center;
115
+ }
116
+
117
+ .loc-btn__text {
118
+ color: white;
119
+ font-weight: 500;
120
+ font-size: 2.2cqw;
121
+ text-align: center;
122
+ line-height: 1.1;
123
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5));
124
+ overflow: hidden;
125
+ text-overflow: ellipsis;
126
+ max-width: 90%;
127
+ }
128
+
129
+ .loc-btn__gradient {
130
+ position: absolute;
131
+ inset: 0;
132
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), transparent);
133
+ border-radius: 9999px;
134
+ pointer-events: none;
135
+ }
136
+ </style>