feat(client): 🍪👈
This commit is contained in:
parent
fceeb1b108
commit
27c2ca5048
12 changed files with 269 additions and 0 deletions
|
@ -88,6 +88,7 @@ You should also include the user name that made the change.
|
||||||
- Client: show bot warning on screen when logged in as bot account @syuilo
|
- Client: show bot warning on screen when logged in as bot account @syuilo
|
||||||
- Client: improve overall performance of client @syuilo
|
- Client: improve overall performance of client @syuilo
|
||||||
- Client: ui tweaks @syuilo
|
- Client: ui tweaks @syuilo
|
||||||
|
- Client: clicker game @syuilo
|
||||||
|
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
- Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468
|
- Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468
|
||||||
|
|
|
@ -1361,6 +1361,7 @@ _widgets:
|
||||||
userList: "ユーザーリスト"
|
userList: "ユーザーリスト"
|
||||||
_userList:
|
_userList:
|
||||||
chooseList: "リストを選択"
|
chooseList: "リストを選択"
|
||||||
|
clicker: "クリッカー"
|
||||||
|
|
||||||
_cw:
|
_cw:
|
||||||
hide: "隠す"
|
hide: "隠す"
|
||||||
|
|
BIN
packages/frontend/assets/cookie.png
Normal file
BIN
packages/frontend/assets/cookie.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
70
packages/frontend/src/components/MkClickerGame.vue
Normal file
70
packages/frontend/src/components/MkClickerGame.vue
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="game.ready" :class="$style.game">
|
||||||
|
<div :class="$style.count" class=""><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div>
|
||||||
|
<button v-click-anime class="_button" :class="$style.button" @click="onClick">
|
||||||
|
<img src="/client-assets/cookie.png" :class="$style.img">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<MkLoading/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, defineAsyncComponent, onMounted, onUnmounted } from 'vue';
|
||||||
|
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { useInterval } from '@/scripts/use-interval';
|
||||||
|
import * as game from '@/scripts/clicker-game';
|
||||||
|
import number from '@/filters/number';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const saveData = game.saveData;
|
||||||
|
const cookies = computed(() => saveData.value?.cookies);
|
||||||
|
|
||||||
|
function onClick(ev: MouseEvent) {
|
||||||
|
saveData.value!.cookies++;
|
||||||
|
saveData.value!.clicked++;
|
||||||
|
|
||||||
|
const x = ev.clientX;
|
||||||
|
const y = ev.clientY;
|
||||||
|
os.popup(MkPlusOneEffect, { x, y }, {}, 'end');
|
||||||
|
}
|
||||||
|
|
||||||
|
useInterval(game.save, 1000 * 5, {
|
||||||
|
immediate: false,
|
||||||
|
afterMounted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await game.load();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
game.save();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.game {
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
font-size: 1.3em;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.img {
|
||||||
|
max-width: 90px;
|
||||||
|
}
|
||||||
|
</style>
|
69
packages/frontend/src/components/MkPlusOneEffect.vue
Normal file
69
packages/frontend/src/components/MkPlusOneEffect.vue
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
<template>
|
||||||
|
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
|
||||||
|
<span class="text" :class="{ up }">+1</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}>(), {
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'end'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let up = $ref(false);
|
||||||
|
const zIndex = os.claimZIndex('middle');
|
||||||
|
const angle = (45 - (Math.random() * 90)) + 'deg';
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
up = true;
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
emit('end');
|
||||||
|
}, 1100);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
pointer-events: none;
|
||||||
|
position: fixed;
|
||||||
|
width: 128px;
|
||||||
|
height: 128px;
|
||||||
|
|
||||||
|
&:global {
|
||||||
|
> .text {
|
||||||
|
display: block;
|
||||||
|
height: 1em;
|
||||||
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 0 6px #000;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
transform: translateY(0px);
|
||||||
|
transition: transform 1s cubic-bezier(0,.5,0,1), opacity 1s cubic-bezier(.5,0,1,.5);
|
||||||
|
will-change: opacity, transform;
|
||||||
|
|
||||||
|
&.up {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-50px) rotateZ(v-bind(angle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -12,6 +12,9 @@ export default {
|
||||||
target.classList.add('_anime_bounce_standBy');
|
target.classList.add('_anime_bounce_standBy');
|
||||||
|
|
||||||
el.addEventListener('mousedown', () => {
|
el.addEventListener('mousedown', () => {
|
||||||
|
target.classList.remove('_anime_bounce_ready');
|
||||||
|
target.classList.remove('_anime_bounce');
|
||||||
|
|
||||||
target.classList.add('_anime_bounce_standBy');
|
target.classList.add('_anime_bounce_standBy');
|
||||||
target.classList.add('_anime_bounce_ready');
|
target.classList.add('_anime_bounce_ready');
|
||||||
|
|
||||||
|
|
24
packages/frontend/src/pages/clicker.vue
Normal file
24
packages/frontend/src/pages/clicker.vue
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<template>
|
||||||
|
<MkStickyContainer>
|
||||||
|
<template #header><MkPageHeader/></template>
|
||||||
|
<MkSpacer :content-max="800">
|
||||||
|
<MkClickerGame/>
|
||||||
|
</MkSpacer>
|
||||||
|
</MkStickyContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import MkClickerGame from '@/components/MkClickerGame.vue';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
|
||||||
|
definePageMetadata({
|
||||||
|
title: '🍪👈',
|
||||||
|
icon: 'ti ti-cookie',
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
|
||||||
|
</style>
|
|
@ -460,6 +460,10 @@ export const routes = [{
|
||||||
path: '/timeline/antenna/:antennaId',
|
path: '/timeline/antenna/:antennaId',
|
||||||
component: page(() => import('./pages/antenna-timeline.vue')),
|
component: page(() => import('./pages/antenna-timeline.vue')),
|
||||||
loginRequired: true,
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/clicker',
|
||||||
|
component: page(() => import('./pages/clicker.vue')),
|
||||||
|
loginRequired: true,
|
||||||
}, {
|
}, {
|
||||||
name: 'index',
|
name: 'index',
|
||||||
path: '/',
|
path: '/',
|
||||||
|
|
46
packages/frontend/src/scripts/clicker-game.ts
Normal file
46
packages/frontend/src/scripts/clicker-game.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
|
||||||
|
type SaveData = {
|
||||||
|
gameVersion: number;
|
||||||
|
cookies: number;
|
||||||
|
clicked: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveData = ref<SaveData>();
|
||||||
|
export const ready = computed(() => saveData.value != null);
|
||||||
|
|
||||||
|
let prev = '';
|
||||||
|
|
||||||
|
export async function load() {
|
||||||
|
try {
|
||||||
|
saveData.value = await os.api('i/registry/get', {
|
||||||
|
scope: ['clickerGame'],
|
||||||
|
key: 'saveData',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'NO_SUCH_KEY') {
|
||||||
|
saveData.value = {
|
||||||
|
gameVersion: 1,
|
||||||
|
cookies: 0,
|
||||||
|
clicked: 0,
|
||||||
|
};
|
||||||
|
save();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function save() {
|
||||||
|
const current = JSON.stringify(saveData.value);
|
||||||
|
if (current === prev) return;
|
||||||
|
|
||||||
|
await os.api('i/registry/set', {
|
||||||
|
scope: ['clickerGame'],
|
||||||
|
key: 'saveData',
|
||||||
|
value: saveData.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
prev = current;
|
||||||
|
}
|
|
@ -41,6 +41,11 @@ export function openInstanceMenu(ev: MouseEvent) {
|
||||||
to: '/api-console',
|
to: '/api-console',
|
||||||
text: 'API Console',
|
text: 'API Console',
|
||||||
icon: 'ti ti-terminal-2',
|
icon: 'ti ti-terminal-2',
|
||||||
|
}, {
|
||||||
|
type: 'link',
|
||||||
|
to: '/clicker',
|
||||||
|
text: '🍪👈',
|
||||||
|
icon: 'ti ti-cookie',
|
||||||
}],
|
}],
|
||||||
}, null, {
|
}, null, {
|
||||||
type: 'parent',
|
type: 'parent',
|
||||||
|
|
44
packages/frontend/src/widgets/clicker.vue
Normal file
44
packages/frontend/src/widgets/clicker.vue
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<template>
|
||||||
|
<MkContainer :show-header="widgetProps.showHeader" class="mkw-clicker">
|
||||||
|
<template #header><i class="ti ti-cookie"></i>Clicker</template>
|
||||||
|
<MkClickerGame/>
|
||||||
|
</MkContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, onUnmounted, Ref, ref, watch } from 'vue';
|
||||||
|
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
|
||||||
|
import { GetFormResultType } from '@/scripts/form';
|
||||||
|
import { $i } from '@/account';
|
||||||
|
import MkContainer from '@/components/MkContainer.vue';
|
||||||
|
import MkClickerGame from '@/components/MkClickerGame.vue';
|
||||||
|
|
||||||
|
const name = 'clicker';
|
||||||
|
|
||||||
|
const widgetPropsDef = {
|
||||||
|
showHeader: {
|
||||||
|
type: 'boolean' as const,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
|
||||||
|
|
||||||
|
// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
|
||||||
|
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
|
||||||
|
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
|
||||||
|
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
|
||||||
|
const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>();
|
||||||
|
|
||||||
|
const { widgetProps, configure } = useWidgetPropsManager(name,
|
||||||
|
widgetPropsDef,
|
||||||
|
props,
|
||||||
|
emit,
|
||||||
|
);
|
||||||
|
|
||||||
|
defineExpose<WidgetComponentExpose>({
|
||||||
|
name,
|
||||||
|
configure,
|
||||||
|
id: props.widget ? props.widget.id : null,
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -25,6 +25,7 @@ export default function(app: App) {
|
||||||
app.component('MkwAiscriptApp', defineAsyncComponent(() => import('./aiscript-app.vue')));
|
app.component('MkwAiscriptApp', defineAsyncComponent(() => import('./aiscript-app.vue')));
|
||||||
app.component('MkwAichan', defineAsyncComponent(() => import('./aichan.vue')));
|
app.component('MkwAichan', defineAsyncComponent(() => import('./aichan.vue')));
|
||||||
app.component('MkwUserList', defineAsyncComponent(() => import('./user-list.vue')));
|
app.component('MkwUserList', defineAsyncComponent(() => import('./user-list.vue')));
|
||||||
|
app.component('MkwClicker', defineAsyncComponent(() => import('./clicker.vue')));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const widgets = [
|
export const widgets = [
|
||||||
|
@ -52,4 +53,5 @@ export const widgets = [
|
||||||
'aiscriptApp',
|
'aiscriptApp',
|
||||||
'aichan',
|
'aichan',
|
||||||
'userList',
|
'userList',
|
||||||
|
'clicker',
|
||||||
];
|
];
|
||||||
|
|
Loading…
Reference in a new issue