feat(client): 🍪👈
This commit is contained in:
		| @@ -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: improve overall performance of client @syuilo | ||||
| - Client: ui tweaks @syuilo | ||||
| - Client: clicker game @syuilo | ||||
|  | ||||
| ### Bugfixes | ||||
| - Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468 | ||||
|   | ||||
| @@ -1361,6 +1361,7 @@ _widgets: | ||||
|   userList: "ユーザーリスト" | ||||
|   _userList: | ||||
|     chooseList: "リストを選択" | ||||
|   clicker: "クリッカー" | ||||
|  | ||||
| _cw: | ||||
|   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'); | ||||
|  | ||||
| 		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_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', | ||||
| 	component: page(() => import('./pages/antenna-timeline.vue')), | ||||
| 	loginRequired: true, | ||||
| }, { | ||||
| 	path: '/clicker', | ||||
| 	component: page(() => import('./pages/clicker.vue')), | ||||
| 	loginRequired: true, | ||||
| }, { | ||||
| 	name: 'index', | ||||
| 	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', | ||||
| 			text: 'API Console', | ||||
| 			icon: 'ti ti-terminal-2', | ||||
| 		}, { | ||||
| 			type: 'link', | ||||
| 			to: '/clicker', | ||||
| 			text: '🍪👈', | ||||
| 			icon: 'ti ti-cookie', | ||||
| 		}], | ||||
| 	}, null, { | ||||
| 		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('MkwAichan', defineAsyncComponent(() => import('./aichan.vue'))); | ||||
| 	app.component('MkwUserList', defineAsyncComponent(() => import('./user-list.vue'))); | ||||
| 	app.component('MkwClicker', defineAsyncComponent(() => import('./clicker.vue'))); | ||||
| } | ||||
|  | ||||
| export const widgets = [ | ||||
| @@ -52,4 +53,5 @@ export const widgets = [ | ||||
| 	'aiscriptApp', | ||||
| 	'aichan', | ||||
| 	'userList', | ||||
| 	'clicker', | ||||
| ]; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 syuilo
					syuilo