Commit 740e86b2 authored by 陈冲's avatar 陈冲

feat: 后端接口优化

parent aebc5dc7
......@@ -34,8 +34,6 @@ const GAME_RUNNING_SECS: u64 = 60;
struct RoomPlayer {
/// 成绩分数,具体含义由前端和游戏逻辑自行定义,后端只负责存储和排序。
score: i64,
/// 客户端上一次提交的累计分数,用于重连后继续累计而不是覆盖旧分数。
last_client_score: i64,
// /// wechat原始token用于记录
// wechat: String,
/// 是否在线,离线的玩家不允许提交分数,但保留在排行榜里。
......@@ -474,6 +472,7 @@ async fn handle_room_command(
}
}
state.reset();
state.current_game_id = Some(format!("game{index}"));
state.status = RoomStatus::Start;
// let snapshot = state.snapshot();
drop(state);
......@@ -493,8 +492,14 @@ async fn handle_room_command(
//超过300人的情况
if get_player_count() > 300 {
emit_msgpack(&socket, evt, &err_resp("本场游戏人员已满", Some(json!(3))));
_ = socket.clone().disconnect();
return;
}
let gameId = data
.get("gameId")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let nickname = data
.get("nickname")
.and_then(|value| value.as_str())
......@@ -507,6 +512,22 @@ async fn handle_room_command(
.to_string();
if let Ok(mut state) = ROOM_STATE.lock() {
let Some(current_game_id) = state.current_game_id.clone() else {
drop(state);
return;
};
// 当前h5进入的不是大屏开启的游戏房间
if current_game_id != gameId {
drop(state);
emit_msgpack(
&socket,
evt,
&err_resp("当前游戏房间还未开启", Some(json!(2))),
);
_ = socket.clone().disconnect();
return;
}
let already_joined = state.players.contains_key(userid);
if already_joined {
if let Some(player) = state.players.get_mut(userid) {
......@@ -532,7 +553,11 @@ async fn handle_room_command(
.and_then(|payload| payload.get("gameId"))
.and_then(|value| value.as_str())
{
emit_msgpack(&socket, "game_start_result", &ok_resp(Some(json!(game_id))));
emit_msgpack(
&socket,
"game_start_result",
&ok_resp(Some(json!(game_id))),
);
}
}
return;
......@@ -547,6 +572,7 @@ async fn handle_room_command(
Some(json!(2)),
),
);
_ = socket.clone().disconnect();
return;
}
let entry_order = state.next_entry_order;
......@@ -562,7 +588,6 @@ async fn handle_room_command(
})
.or_insert(RoomPlayer {
score: 0,
last_client_score: 0,
online: true,
entry_order,
nickname: nickname.to_string(),
......@@ -576,7 +601,7 @@ async fn handle_room_command(
if let Some(mut player) = CLIENT_PLAYER_MAP.get_mut(userid) {
player.0 = 1;
}
emit_msgpack(&socket, evt, &ok_resp(None));
emit_msgpack(&socket, evt, &ok_resp(Some(json!(current_game_id))));
// 给所有管理员广播房间状态更新
broadcast_msgpack_all_admin(
evt,
......@@ -630,9 +655,12 @@ async fn handle_room_command(
if !state.players.contains_key(userid) {
drop(state);
emit_msgpack(&socket, evt, &err_resp("您不在房间中", None));
_ = socket.clone().disconnect();
return;
} else if !matches!(state.status, RoomStatus::Running) {
drop(state);
emit_msgpack(&socket, evt, &err_resp("游戏还未开始", None));
return;
}
/* else if !state
.players
......@@ -645,16 +673,11 @@ async fn handle_room_command(
}*/
else if state.players.contains_key(userid) {
let player = state.players.get_mut(userid).unwrap();
let score_delta = if score >= player.last_client_score {
score - player.last_client_score
} else {
score
};
player.score += score_delta.max(0);
player.last_client_score = score;
player.score = score;
// player.wechat = wechat.to_string();
player.online = true;
drop(state);
return;
// 方案1:每有一名玩家提交时发送给所有玩家跟管理员,玩家过多时发送太频繁,
// 这里会选第2种方案, 游戏中时每秒发给所有玩家跟管理员 (push_room_score_ranking)
......@@ -667,6 +690,7 @@ async fn handle_room_command(
} else {
drop(state);
emit_msgpack(&socket, evt, &err_resp("提交异常", None));
return;
}
}
}
......@@ -683,25 +707,34 @@ async fn handle_room_command(
if wechat.is_empty() {
return;
}
let (score, rank) = if let Ok(state) = ROOM_STATE.lock() {
let (score, rank, ranking) = if let Ok(state) = ROOM_STATE.lock() {
//防止客户端提交的状态还在Running中
if !matches!(state.status, RoomStatus::Submit | RoomStatus::Running) {
drop(state);
return;
}
let full_ranking = state.full_score_ranking();
let ranking = RoomState::ranking_payload(
&full_ranking
.iter()
.take(ROOM_SCORE_RANK_LIMIT)
.cloned()
.collect::<Vec<_>>(),
);
let server_score = state
.players
.get(userid)
.map(|player| player.score)
.unwrap_or(score);
let server_rank = state
.player_rank(userid)
.map(|rank| rank as i64)
let server_rank = full_ranking
.iter()
.position(|player| player.wechat == userid)
.map(|index| (index + 1) as i64)
.unwrap_or(rank);
drop(state);
(server_score, server_rank)
(server_score, server_rank, ranking)
} else {
(score, rank)
(score, rank, Vec::new())
};
let game = Game {
wechat_id: wechat.to_string(),
......@@ -713,7 +746,14 @@ async fn handle_room_command(
create_date: None,
};
if game.insert().await {
emit_msgpack(&socket, evt, &ok_resp(None));
emit_msgpack(
&socket,
evt,
&ok_resp(Some(json!({
"list": ranking,
"rank": rank
}))),
);
} else {
emit_msgpack(&socket, evt, &err_resp("提交异常", None));
}
......@@ -926,7 +966,6 @@ pub async fn login(req: &mut Request, res: &mut Response) {
let params = match req.parse_body::<HashMap<String, String>>().await {
Ok(params) => params,
Err(_) => {
res.status_code(StatusCode::BAD_REQUEST);
res.render(Json(err_resp("参数格式错误", None)));
return;
}
......@@ -942,7 +981,6 @@ pub async fn login(req: &mut Request, res: &mut Response) {
.unwrap_or_default();
if username.is_empty() || password.is_empty() {
res.status_code(StatusCode::BAD_REQUEST);
res.render(Json(err_resp("username 和 password 不能为空", None)));
return;
}
......@@ -963,9 +1001,15 @@ pub async fn login(req: &mut Request, res: &mut Response) {
let passphrase = utils::get_aes_passphrase();
if passphrase.is_none() {
res.status_code(StatusCode::BAD_REQUEST);
res.render(Json(err_resp("username 和 password 不能为空", None)));
return;
}
//当前管理员已登录,不能重复登录
if (CLIENT_ADMIN_MAP.contains_key(&manager.token)) {
res.render(Json(err_resp("当前管理员已登录,不能重复登录", None)));
return;
}
res.render(Json(ok_resp(Some(json!({
"passphrase": passphrase,
"token": manager.token,
......
......@@ -12,12 +12,14 @@
"crypto-js": "^4.2.0",
"msgpack": "^1.0.3",
"socket.io-client": "^4.8.3",
"vconsole": "^3.15.1",
"vue": "^3.5.32",
"vue-router": "^4.6.4"
},
"devDependencies": {
"@tsconfig/node24": "^24.0.4",
"@types/node": "^24.12.2",
"@vitejs/plugin-basic-ssl": "^2.3.0",
"@vitejs/plugin-vue": "^6.0.6",
"@vue/tsconfig": "^0.9.1",
"npm-run-all2": "^8.0.4",
......@@ -438,6 +440,15 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
......@@ -929,6 +940,19 @@
"undici-types": "~7.16.0"
}
},
"node_modules/@vitejs/plugin-basic-ssl": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.3.0.tgz",
"integrity": "sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"peerDependencies": {
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/@vitejs/plugin-vue": {
"version": "6.0.7",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz",
......@@ -1334,6 +1358,29 @@
"dev": true,
"license": "MIT"
},
"node_modules/copy-text-to-clipboard": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.2.tgz",
"integrity": "sha512-T6SqyLd1iLuqPA90J5N4cTalrtovCySh58iiZDGJ6FGznbclKh4UI+FGacQSgFzwKG77W7XT5gwbVEbd9cIH1A==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/core-js": {
"version": "3.49.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
"integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
......@@ -2035,6 +2082,11 @@
"dev": true,
"license": "MIT"
},
"node_modules/mutation-observer": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/mutation-observer/-/mutation-observer-1.0.3.tgz",
"integrity": "sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA=="
},
"node_modules/nan": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz",
......@@ -2512,6 +2564,18 @@
"browserslist": ">= 4.21.0"
}
},
"node_modules/vconsole": {
"version": "3.15.1",
"resolved": "https://registry.npmjs.org/vconsole/-/vconsole-3.15.1.tgz",
"integrity": "sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.17.2",
"copy-text-to-clipboard": "^3.0.1",
"core-js": "^3.11.0",
"mutation-observer": "^1.0.3"
}
},
"node_modules/vite": {
"version": "8.0.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz",
......
This diff is collapsed.
......@@ -8,13 +8,15 @@ const props = withDefaults(defineProps<{
mode?: 'contain' | 'cover'
backgroundMode?: 'contain' | 'cover',
showGameRule?: boolean,
showGameRank?: boolean,
}>(), {
background: 'linear-gradient(180deg, #fbf7ef 0%, #ffe7dc 100%)',
width: 750,
height: 1624,
mode: 'contain',
backgroundMode: 'cover',
showGameRule: false
showGameRule: false,
showGameRank: false
})
const emit = defineEmits<{
......@@ -156,6 +158,11 @@ defineExpose({
<slot name="gameRule"></slot>
</div>
</Transition>
<Transition name="game-rule-fade">
<div v-if="!countdownVisible && showGameRank" class="modal-container">
<slot name="gameRank"></slot>
</div>
</Transition>
</main>
</template>
......
......@@ -22,6 +22,8 @@ type GameSocketOptions = {
onRoomBack?: (data: any) => void
}
let gameId = '';
function getGameIndex(data: any) {
if (typeof data === 'object' && data !== null) {
return Number(data.game ?? data.game_id ?? data.gameId ?? data.id)
......@@ -41,7 +43,7 @@ function getGameHomePath(data: any) {
}
export function joinSharedRoom(nickname: string, token: string, avatar: string) {
return sendSocketMessage('room_join', { nickname: nickname, token, avatar })
return sendSocketMessage('room_join', { nickname: nickname, token, avatar, gameId: gameId })
}
export function sendGameMessage(cmd: string, data: any = {}) {
......@@ -50,6 +52,7 @@ export function sendGameMessage(cmd: string, data: any = {}) {
export const userJoinStatus = ref(false)
export function useGameSocket(options: GameSocketOptions) {
gameId = options.gameId;
const router = useRouter()
let socket: Socket | null = null
let offSocketMessage: (() => void) | undefined
......@@ -76,7 +79,10 @@ export function useGameSocket(options: GameSocketOptions) {
if (evt === 'room_join_result') {
switch (data) {
case 1: userJoinStatus.value = true; break;
case 3: alert(msg); break;
case 2:
case 3:
alert(msg);
break;
}
} else if (evt == 'submit_score_save') {
//提交分数的情况下有问题的情况下,只重试3次
......@@ -106,6 +112,10 @@ export function useGameSocket(options: GameSocketOptions) {
break
}
case 'room_join_result': {
if (data != options.gameId) {
alert('当前游戏房间还未开启');
return;
}
userJoinStatus.value = true
break
}
......
This diff is collapsed.
......@@ -28,6 +28,7 @@ type TargetItem = {
}
const list = ref<TargetItem[]>([]);
const moleContainerRef = ref<HTMLElement | null>(null)
for (let i = 0; i < 9; i++) {
list.value.push({
id: i,
......@@ -129,6 +130,45 @@ function handleTargetTouch(index: number) {
}, item.hitMs)
}
function handleBoardTouch(event: MouseEvent) {
const container = moleContainerRef.value
if (!container) {
return
}
const rect = container.getBoundingClientRect()
const x = ((event.clientX - rect.left) / rect.width) * 750
const y = ((event.clientY - rect.top) / rect.height) * 600
const colWidth = 250
const rowGap = 70
const rowHeight = (600 - rowGap * 2) / 3
const hitRadius = 225
let targetIndex = -1
let targetDistance = Number.POSITIVE_INFINITY
list.value.forEach((item, index) => {
if (!item.kind || item.state === 'idle' || item.state === 'hit') {
return
}
const col = index % 3
const row = Math.floor(index / 3)
const centerX = col * colWidth + colWidth / 2
const centerY = row * (rowHeight + rowGap) + rowHeight / 2
const distance = Math.hypot(x - centerX, y - centerY)
if (distance <= hitRadius && distance < targetDistance) {
targetIndex = index
targetDistance = distance
}
})
if (targetIndex >= 0) {
handleTargetTouch(targetIndex)
}
}
onMounted(() => {
scheduleRandomTargets()
})
......@@ -165,16 +205,14 @@ onBeforeUnmount(() => {
</div>
</div>
<div class="mole-container">
<div ref="moleContainerRef" class="mole-container" @click="handleBoardTouch">
<div v-for="(item, index) in list" :key="item.id">
<div class="img-land"></div>
<button v-if="item.kind" class="mole-hit-area" type="button" @click="handleTargetTouch(index)">
<span class="img-mole" :class="[`is-${item.kind}`, `is-${item.state}`]" :style="{
<span v-if="item.kind" class="img-mole" :class="[`is-${item.kind}`, `is-${item.state}`]" :style="{
'--show-duration': `${item.showMs}ms`,
'--hide-duration': `${item.hideMs}ms`,
'--hit-duration': `${item.hitMs}ms`,
}"></span>
</button>
</div>
</div>
<div class="img-hammer"></div>
......@@ -266,18 +304,6 @@ onBeforeUnmount(() => {
}
}
.mole-hit-area {
position: absolute;
inset: 0;
}
.mole-hit-area {
z-index: 2;
padding: 0;
border: 0;
background: transparent;
}
.img-land,
.img-mole {
position: absolute;
......
......@@ -9,6 +9,7 @@ defineProps<{
}>()
defineEmits<{
showRank: []
replay: []
back: []
}>()
......@@ -24,23 +25,6 @@ onMounted(() => {
<template>
<div class="h5-page game-stage">
<div class="img-logo"></div>
<div class="bg-bottom"></div>
<!-- <div class="img-score">
<div class="score-avatar"
:style="`background: url(${avatar});border-radius:66px;width:132px;height:132px;`"></div>
<div class="score-content">
<div>{{ nickname }}</div>
<div>您的成绩</div>
<div>击鼓次数(次)</div>
<div>{{ tick }}</div>
<div>最终排名</div>
<div><label>{{ rank }}</label></div>
</div>
</div>
<div class="btn-group">
<div class="btn-replay" @click="$emit('replay')"></div>
<div class="btn-back" @click="$emit('back')"></div>
</div> -->
</div>
</template>
......@@ -52,16 +36,6 @@ onMounted(() => {
padding: 0;
}
.bg-bottom {
position: absolute;
bottom: 0;
left: var(--stage-viewport-left, 0);
width: var(--stage-viewport-width, 750px);
height: 479px;
background: v-bind('imageUrls.bg') center/cover;
opacity: 0.6;
}
.img-logo {
position: absolute;
top: 66px;
......
This diff is collapsed.
......@@ -13,7 +13,6 @@
"dependencies": {
"@msgpack/msgpack": "^3.1.3",
"crypto-js": "^4.2.0",
"msgpack": "^1.0.3",
"socket.io-client": "^4.8.3",
"vue": "^3.5.32",
"vue-router": "^4.6.4"
......
......@@ -117,7 +117,8 @@ onBeforeUnmount(() => {
<template>
<div class="design-stage-viewport" :style="{ background }">
<div class="design-stage" :style="stageStyle">
<div v-if="gameCountdownVisible" class="game-countdown">{{ gameCountdown }}</div>
<div v-if="gameCountdownVisible" class="game-countdown">
倒计时: &nbsp;{{ gameCountdown }}</div>
<div v-if="countdownVisible" class="countdown-modal">
<div :key="countdownKey" class="countdown-number">{{ countdown }}</div>
</div>
......@@ -159,16 +160,14 @@ onBeforeUnmount(() => {
top: 64px;
right: 80px;
z-index: 10;
min-width: 220px;
height: 132px;
width: 300px;
height: 84px;
padding: 0 40px;
border: 6px solid rgba(255, 255, 255, 0.85);
border-radius: 66px;
background: rgba(0, 0, 0, 0.42);
border: 1px dashed #ccc;
color: #fff;
font-size: 92px;
font-weight: 800;
line-height: 120px;
font-size: 42px;
line-height: 84px;
text-align: center;
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.25);
text-shadow: 0 8px 18px rgba(0, 0, 0, 0.4);
......
......@@ -9,7 +9,22 @@ const imageUrls = {
bg1: cssAssetUrl('main-bg1.png'),
logo: cssAssetUrl('game1/logo.png'),
title1: cssAssetUrl('main-title.png'),
icon1: cssAssetUrl('icon1.png'),
icon2: cssAssetUrl('icon2.png'),
icon3: cssAssetUrl('icon3.png'),
icon4: cssAssetUrl('icon4.png'),
icon5: cssAssetUrl('icon5.png'),
icon6: cssAssetUrl('icon6.png'),
}
const iconUrls = [
imageUrls.icon1,
imageUrls.icon2,
imageUrls.icon3,
imageUrls.icon4,
imageUrls.icon5,
imageUrls.icon6,
]
const list = ref(['击鼓齐福', '聚宝接福',
'马上有福', '策马迎福', '圈彩纳福', '福运当头'
......@@ -49,7 +64,7 @@ const loginSuccessHandler = () => {
<div v-for="item, index in list" @click="gameHandler(index)">
<div class="card">
<div class="img">
<div></div>
<div :style="{ background: `${iconUrls[index]} center / cover no-repeat` }"></div>
</div>
<div class="btn">{{ item }}</div>
</div>
......@@ -153,13 +168,10 @@ const loginSuccessHandler = () => {
transition: transform 0.2s ease 0.1s;
>div {
width: calc(100% - 20px);
height: calc(100% - 20px);
border: 1px solid red;
width: 100%;
height: 100%;
border-radius: 20px;
position: absolute;
left: 10px;
top: 10px;
}
}
......
......@@ -148,7 +148,6 @@ const { startGame, createRoom, backRoom } = useAdminGameSocket({
onRelogin: async () => {
$remove_socket_storage();
await router.replace('/')
debugger
}
})
......
......@@ -141,7 +141,7 @@ function handleRoomState(data: any) {
switch (data?.status) {
case 0:
gotoLoading(true);
createRoom(1);
createRoom(4);
break;
case 1:
gotoLoading();
......
......@@ -121,7 +121,7 @@ function handleRoomState(data: any) {
switch (data?.status) {
case 0:
gotoLoading(true)
createRoom(1)
createRoom(6)
break
case 1:
gotoLoading()
......
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