Commit 2912c668 authored by 陈冲's avatar 陈冲

commit

parent 8a5923a5
DROP TABLE IF EXISTS tb_game;
CREATE TABLE tb_game
(
wechat_id VARCHAR(50),
wechat_id text,
nickname VARCHAR(50),
avatar VARCHAR(200),
avatar text,
game_rank INT NOT NULL,
item_num INT NOT NULL,
score INT NOT NULL DEFAULT 0,
......
......@@ -5,8 +5,8 @@ use async_trait::async_trait;
use serde_json::Value;
use tokio::sync::OnceCell;
pub mod mysql;
pub mod models;
pub mod mysql;
pub mod pg;
#[async_trait]
......
......@@ -3,7 +3,7 @@ use crate::db::DbDriver;
use async_trait::async_trait;
use serde_json::{Value, json};
use sqlx::mysql::{MySqlArguments, MySqlPoolOptions, MySqlRow};
use sqlx::{Column, MySql, MySqlPool, Row, TypeInfo};
use sqlx::{Column, MySql, MySqlConnection, MySqlPool, Row, TypeInfo};
pub struct MysqlDb {
pool: MySqlPool,
......@@ -12,11 +12,18 @@ pub struct MysqlDb {
impl MysqlDb {
pub async fn new() -> Result<Self, String> {
let pool = MySqlPoolOptions::new()
.after_connect(|conn: &mut MySqlConnection, _| {
Box::pin(async move {
sqlx::query("SET time_zone = '+08:00'")
.execute(conn)
.await?;
Ok(())
})
})
.max_connections(16)
.connect(&config.db.uri)
.await
.map_err(|err| err.to_string())?;
Ok(Self { pool })
}
}
......
......@@ -9,12 +9,12 @@ use serde_json::{Value, json};
use socketioxide::SocketIo;
use socketioxide::extract::{Data, SocketRef};
use socketioxide::socket::DisconnectReason;
use sqlx::encode::IsNull::No;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
lazy_static::lazy_static! {
/// 可以有多个管理员,但不要用同一个账户登录,否则后面的没效果
static ref CLIENT_ADMIN_MAP: DashMap<String, SocketRef> = DashMap::new();
static ref CLIENT_PLAYER_MAP: DashMap<String, (i32,SocketRef)> = DashMap::new(); // 1: 正在游戏中
static ref ROOM_STATE: std::sync::Mutex<RoomState> = std::sync::Mutex::new(RoomState::default());
......@@ -297,7 +297,7 @@ fn spawn_game_timeout(round_id: u64, secs: u64) {
if let Ok(mut state) = ROOM_STATE.lock() {
state.status = RoomStatus::Submit;
dbg!("submit");
// dbg!("submit");
drop(state);
}
});
......@@ -322,25 +322,30 @@ async fn handle_room_command(
) {
let evt = &format!("{cmd}_result");
match cmd {
"room_create" => {
"room_create" | "room_back" => {
//room_back返回当前游戏的Loading状态
// 只有管理员可以创建房间
if !is_admin {
return;
}
if let Some(index) = data.as_i64() {
if let Ok(mut state) = ROOM_STATE.lock() {
if state.status != RoomStatus::Closed {
drop(state);
emit_msgpack(&socket, evt, &err_resp("不能重复创建房间", None));
return;
if cmd == "room_create" {
if state.status != RoomStatus::Closed {
drop(state);
emit_msgpack(&socket, evt, &err_resp("不能重复创建房间", None));
return;
}
}
state.reset();
state.status = RoomStatus::Start;
// let snapshot = state.snapshot();
drop(state);
emit_msgpack(&socket, evt, &ok_resp(Some(json!(index))));
if cmd == "room_back" {
broadcast_msgpack_all_player(evt, &ok_resp(Some(json!(index))));
}
}
}
}
......@@ -504,7 +509,8 @@ async fn handle_room_command(
return;
}
if let Ok(state) = ROOM_STATE.lock() {
if !matches!(state.status, RoomStatus::Submit) {
//防止客户端提交的状态还在Running中
if !matches!(state.status, RoomStatus::Submit | RoomStatus::Running) {
drop(state);
return;
}
......@@ -616,6 +622,11 @@ pub async fn on_connect(_io: SocketIo, socket: SocketRef, Data(data): Data<Value
if is_admin {
match CLIENT_ADMIN_MAP.entry(userid.clone()) {
Entry::Occupied(_) => {
emit_msgpack(
&socket,
"login_result",
&err_resp("当前管理员已登录,不能重复登录", None),
);
_ = socket.disconnect();
return;
}
......
/// <reference types="vite/client" />
declare module 'crypto-js';
declare module 'crypto-js/sha256';
declare const __APP_DOMAIN__: string;
......
......@@ -19,6 +19,7 @@ type GameSocketOptions = {
onRescoreSubmitted?: (recount: number) => void
onRoomClosed?: (data: any) => void
onEvent?: (evt: string, payload: SocketPayload) => void
onRoomBack?: (data: any) => void
}
function getGameIndex(data: any) {
......@@ -122,11 +123,15 @@ export function useGameSocket(options: GameSocketOptions) {
options.onScoreSubmitted?.(true, data)
break
}
case 'room_close': {
case 'room_close_result': {
userJoinStatus.value = false
options.onRoomClosed?.(data)
break
}
case 'room_back_result': {
options.onRoomBack?.(data);
break;
}
}
options.onEvent?.(evt, payload)
......
......@@ -107,6 +107,22 @@ function showLoadingView() {
setDivDescVisible(false)
}
function resetToLoadingView() {
stopGameCountdown()
if (guFrameTimer) {
window.clearTimeout(guFrameTimer)
guFrameTimer = undefined
}
tick.value = 0
rank.value = 0
countdownInterval.value = 60
currentGu.value = guFrames[0]
isGuPlaying.value = false
userJoinStatus.value = false
currentView.value = 'loading'
setDivDescVisible(false)
}
function startGameView() {
stopGameCountdown()
tick.value = 0
......@@ -126,10 +142,17 @@ function replayGame() {
}
function backToWaiting() {
showLoadingView()
resetToLoadingView()
joinSharedRoom(nickname.value, token.value, avatar.value)
}
function handleRoomBack() {
resetToLoadingView()
window.setTimeout(() => {
joinSharedRoom(nickname.value, token.value, avatar.value)
}, 200)
}
const touchHandler = () => {
if (isGuPlaying.value) {
return
......@@ -186,6 +209,7 @@ if (wechat) {
submitScore(true);
},
onRoomClosed: showLoadingView,
onRoomBack: handleRoomBack,
})
_offSocketMessage.value = offSocketMessage
}
......
......@@ -120,6 +120,8 @@ defineEmits<{
.play-content {
margin-top: 240px;
margin-left: 40px;
margin-right: 40px;
padding: 30px 0;
border-radius: 25px;
background: rgba(40, 12, 8, 0.52);
......
<script setup lang="ts">
import { $getWechat } from '@/commons/utils';
import { onMounted, ref } from 'vue';
defineProps<{
imageUrls: Record<string, string>
tick: number
......@@ -9,6 +12,13 @@ defineEmits<{
replay: []
back: []
}>()
const nickname = ref('');
const avatar = ref('');
onMounted(() => {
const wechat = $getWechat();
nickname.value = wechat.nickname;
avatar.value = wechat.avatar;
});
</script>
<template>
......@@ -16,9 +26,10 @@ defineEmits<{
<div class="img-logo"></div>
<div class="bg-bottom"></div>
<div class="img-score">
<div class="score-avatar"></div>
<div class="score-avatar"
:style="`background: url(${avatar});border-radius:66px;width:132px;height:132px;`"></div>
<div class="score-content">
<div>张小明</div>
<div>{{ nickname }}</div>
<div>您的成绩</div>
<div>击鼓次数(次)</div>
<div>{{ tick }}</div>
......@@ -73,12 +84,12 @@ defineEmits<{
.score-avatar {
position: absolute;
top: -20px;
top: -50px;
left: 50%;
width: 60px;
height: 60px;
background: v-bind('imageUrls.avatar') center/cover;
transform: translateX(-50%) scale(2);
transform: translateX(-50%);
}
.score-content {
......@@ -91,41 +102,41 @@ defineEmits<{
text-align: center;
}
.score-content > div:nth-child(1) {
.score-content>div:nth-child(1) {
color: #E00000;
font-size: 32pt;
font-weight: bold;
}
.score-content > div:nth-child(2) {
.score-content>div:nth-child(2) {
margin-top: 20px;
font-size: 32pt;
}
.score-content > div:nth-child(3) {
.score-content>div:nth-child(3) {
margin-top: 40px;
}
.score-content > div:nth-child(4) {
.score-content>div:nth-child(4) {
margin-top: 30px;
color: #E00000;
font-size: 32pt;
font-weight: bold;
transform: scaleY(2.4);
/* font-weight: bold; */
transform: scale(1.5);
}
.score-content > div:nth-child(5) {
.score-content>div:nth-child(5) {
margin-top: 40px;
}
.score-content > div:nth-child(6) {
.score-content>div:nth-child(6) {
margin-top: 20px;
color: #E00000;
font-size: 32pt;
font-weight: bold;
}
.score-content > div:nth-child(6) label {
.score-content>div:nth-child(6) label {
color: #E00000;
font-size: 42pt;
margin: 0 10px;
......
......@@ -53,14 +53,6 @@ export function $read<T>(key: string | StorageRecord, defaultValue?: T): T | Sto
return defaultValue ?? null;
}
}
export function $goto_login(evt: string, router: any, path: string): boolean {
if (evt == "login_result") {
$remove_socket_storage();
router.replace(path).then(() => { });
return true;
}
return false;
}
export function $remove_socket_storage(): void {
localStorage.removeItem('passphrase');
......
import CryptoJS from 'crypto-js'
import { onBeforeUnmount, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { $goto_login, $read_socket_storge } from '@/commons/utils'
import { $read_socket_storge } from '@/commons/utils'
import { initSocket, onSocketMessage, sendSocketMessage } from '@/commons/ws'
type SocketPayload = {
......@@ -22,7 +21,6 @@ type AdminGameSocketOptions = {
}
export function useAdminGameSocket(options: AdminGameSocketOptions) {
const router = useRouter()
let offSocketMessage: (() => void) | undefined
function requestRoomState() {
......@@ -32,6 +30,9 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
function createRoom(index: number) {
return sendSocketMessage('room_create', index)
}
function backRoom(index: number) {
return sendSocketMessage('room_back', index)
}
function startGame() {
return sendSocketMessage('game_start', options.gameId)
......@@ -45,7 +46,7 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
const { passphrase, token, socket_path, client_ns, domain } = $read_socket_storge()
initSocket({
socket_path: `${domain}${socket_path}`,
client_ns:"/ws",
client_ns: "/ws",
auth: {
userid: token,
validuser: CryptoJS.AES.encrypt(token, passphrase).toString(),
......@@ -58,16 +59,11 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
offSocketMessage = onSocketMessage(async (evt, payload: SocketPayload) => {
const { msg, state, data } = payload
// if ($goto_login(evt, router, options.loginRedirectPath ?? '/game1')) {
// return
// }
if (state === 0) {
if (evt === 'game_start_result' && data === 1) {
options.onGameStartRejected?.(data)
return
}
alert(msg)
return
}
......@@ -104,6 +100,7 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
return {
requestRoomState,
createRoom,
backRoom,
startGame,
sendAdminMessage,
}
......
......@@ -59,7 +59,43 @@ const updateRankPlayers = (data: any) => {
)
}
const gotoLoading = () => {
const playerToRankPlayer = (player: any, index: number): Player => ({
active: player?.active ?? false,
telephone: player?.telephone ?? '',
userid: player?.userid ?? '',
wechat_id: player?.wechat_id || `empty-${index}`,
nickname: player?.nickname || '铏氫綅浠ュ緟',
avatar: player?.avatar || '',
score: Number(player?.score ?? 0),
})
const seedRankPlayersFromLoading = () => {
rankPlayers.value = Array.from(
{ length: RANK_LIMIT },
(_, index) => {
const player = loadingPlayers.value[index]
if (!player?.userid && !player?.wechat_id) {
return emptyRankPlayer(index)
}
return playerToRankPlayer(player, index)
},
)
}
const resetRoundState = () => {
playerCount.value = 0
loadingPlayers.value = Array.from({ length: PLAYER_LIMIT }, emptyLoadingPlayer)
rankPlayers.value = Array.from({ length: RANK_LIMIT }, (_, index) => emptyRankPlayer(index))
isPlayingStartAnimation = false
}
const gotoLoading = (reset = false) => {
if (reset) {
resetRoundState()
}
screen.value = 'loading'
}
......@@ -67,6 +103,7 @@ const gotoRank1WithIntro = async () => {
if (isPlayingStartAnimation) return
isPlayingStartAnimation = true
seedRankPlayersFromLoading()
screen.value = 'rank1'
await nextTick()
await rank1Ref.value?.startIntro()
......@@ -81,13 +118,14 @@ function handleRoomState(data: any) {
switch (data?.status) {
case 0:
gotoLoading()
gotoLoading(true)
createRoom(1)
break
case 1:
gotoLoading()
break
case 2:
seedRankPlayersFromLoading()
screen.value = 'rank1'
break
case 3:
......@@ -96,7 +134,7 @@ function handleRoomState(data: any) {
}
}
const { startGame, createRoom, requestRoomState } = useAdminGameSocket({
const { startGame, createRoom, backRoom } = useAdminGameSocket({
gameId: 'game1',
// loginRedirectPath: '/game1',
onGameStartRejected: gotoLoading,
......@@ -115,11 +153,9 @@ const startGameWithMusic = () => {
}
const nextRound = () => {
gotoLoading()
createRoom(1)
setTimeout(() => {
requestRoomState()
}, 200)
gotoLoading(true)
//重开房间
backRoom(1)
}
const nextRoundWithMusic = () => {
......@@ -138,7 +174,8 @@ onUnmounted(() => {
</script>
<template>
<Loading v-if="screen === 'loading'" :players="loadingPlayers" :player-count="playerCount" @start="startGameWithMusic" />
<Loading v-if="screen === 'loading'" :players="loadingPlayers" :player-count="playerCount"
@start="startGameWithMusic" />
<Rank1 v-else-if="screen === 'rank1'" ref="rank1Ref" :rank-list="rankPlayers" @start="startGameWithMusic" />
<Rank2 v-else :rank-list="rankPlayers" @next="nextRoundWithMusic" />
</template>
......@@ -50,13 +50,13 @@ const imageUrls = {
rankNo1Icon: cssAssetUrl('game1/no1-icon.png'),
rankNo2Icon: cssAssetUrl('game1/no2-icon.png'),
rankNo3Icon: cssAssetUrl('game1/no3-icon.png'),
avatar: cssAssetUrl('avatar.png'),
avatar: assetUrl('avatar.png'),
};
const emptyPlayer = (index: number): Player => ({
wechat_id: `empty-${index}`,
nickname: '-',
avatar: "",
avatar: '',
score: 0,
});
......@@ -232,6 +232,15 @@ const startIntro = async () => {
defineExpose({
startIntro,
});
const avatarStyle = (item: Player) => {
const avatar = item.avatar || imageUrls.avatar
return {
backgroundImage: `url("${avatar}")`,
}
}
</script>
<template>
......@@ -259,7 +268,7 @@ defineExpose({
<div v-for="(item, index) in list" :key="playerKey(item, index)" class="rank-item"
:class="[rankEffects[playerKey(item, index)], index < 3 ? `rank-top-${index + 1}` : '']">
<div class="rank-icon" :key="`rank-icon-${index}`">{{ index > 2 ? index + 1 : '' }}</div>
<div class="rank-avatar"></div>
<div class="rank-avatar" :style="avatarStyle(item)"></div>
<div class="rank-nickname">{{ item.nickname }}</div>
<div class="rank-score">{{ item.score ?? 0 }}</div>
</div>
......@@ -431,7 +440,9 @@ defineExpose({
}
.rank-avatar {
background: v-bind('imageUrls.avatar') center / cover no-repeat;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
width: 48px;
height: 48px;
border-radius: 24px;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment