mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-10-31 05:24:13 +00:00 
			
		
		
		
	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…
	
	Add table
		
		Reference in a new issue