Files
shoot-miniprograms/src/pages/battle-room.vue
2025-07-25 09:59:54 +08:00

829 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { ref, watch, onMounted, onUnmounted } from "vue";
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import PlayerSeats from "@/components/PlayerSeats.vue";
import Guide from "@/components/Guide.vue";
import Timer from "@/components/Timer.vue";
import SButton from "@/components/SButton.vue";
import BowTarget from "@/components/BowTarget.vue";
import BattleHeader from "@/components/BattleHeader.vue";
import BattleFooter from "@/components/BattleFooter.vue";
import BowPower from "@/components/BowPower.vue";
import ShootProgress from "@/components/ShootProgress.vue";
import PlayersRow from "@/components/PlayersRow.vue";
import SModal from "@/components/SModal.vue";
import ScreenHint from "@/components/ScreenHint.vue";
import RoundEndTip from "@/components/RoundEndTip.vue";
import TestDistance from "@/components/TestDistance.vue";
import PlayerScore from "@/components/PlayerScore.vue";
import {
getRoomAPI,
destroyRoomAPI,
exitRoomAPI,
startRoomAPI,
getCurrentGameAPI,
} from "@/apis";
import { isGameEnded } from "@/util";
import { MESSAGETYPES, roundsName, getMessageTypeName } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const step = ref(1);
const seq = ref(0);
const battleId = ref("");
const room = ref({});
const roomNumber = ref("");
const owner = ref({});
const opponent = ref({});
const redTeam = ref([]);
const blueTeam = ref([]);
const currentShooterId = ref(0);
const players = ref([]);
const playersSorted = ref([]);
const currentRound = ref(1);
const totalRounds = ref(0);
const start = ref(false);
const startCount = ref(true);
const power = ref(0);
const scores = ref([]);
const blueScores = ref([]);
const tips = ref("即将开始...");
const roundResults = ref([]);
const redPoints = ref(0);
const bluePoints = ref(0);
const currentRedPoint = ref(0);
const currentBluePoint = ref(0);
const showRoundTip = ref(false);
const playersScores = ref({});
const showModal = ref(false);
const halfTimeTip = ref(false);
const isFinalShoot = ref(false);
const total = ref(15);
const battleType = ref(0);
const isEnded = ref(false);
watch(
() => [players.value, playersScores.value],
([n_players, n_scores]) => {
if (n_players.length) {
playersSorted.value = Object.keys(n_scores)
.sort((a, b) => n_scores[b].length - n_scores[a].length)
.map((pid) => n_players.find((p) => p.id == pid));
}
},
{
deep: true, // 添加深度监听
immediate: true,
}
);
function recoverData(battleInfo) {
uni.removeStorageSync("last-awake-time");
battleId.value = battleInfo.id;
battleType.value = battleInfo.config.mode;
step.value = 2;
if (battleInfo.status === 0) {
const readyRemain = Date.now() / 1000 - battleInfo.startTime;
console.log(`当前局已进行${readyRemain}`);
if (readyRemain > 0) {
setTimeout(() => {
uni.$emit("update-timer", 15 - readyRemain);
}, 200);
}
return;
} else {
step.value = 3;
start.value = true;
}
if (battleInfo.config.mode === 1) {
redTeam.value = battleInfo.redTeam;
blueTeam.value = battleInfo.blueTeam;
if (battleInfo.status !== 0) {
bluePoints.value = 0;
redPoints.value = 0;
currentRound.value = battleInfo.currentRound;
totalRounds.value = battleInfo.maxRound;
roundResults.value = battleInfo.roundResults;
battleInfo.roundResults.forEach((round) => {
const blueTotal = round.blueArrows.reduce(
(last, next) => last + next.ring,
0
);
const redTotal = round.redArrows.reduce(
(last, next) => last + next.ring,
0
);
if (blueTotal === redTotal) {
bluePoints.value += 1;
redPoints.value += 1;
} else if (blueTotal > redTotal) {
bluePoints.value += 2;
} else {
redPoints.value += 2;
}
});
setTimeout(() => {
if (battleInfo.roundResults[battleInfo.roundResults.length - 1]) {
scores.value = battleInfo.roundResults[
battleInfo.roundResults.length - 1
].redArrows.filter((item) => !!item.playerId);
blueScores.value = battleInfo.roundResults[
battleInfo.roundResults.length - 1
].blueArrows.filter((item) => !!item.playerId);
}
}, 300);
if (
battleInfo.redTeam[0].shotHistory[battleInfo.currentRound] ||
battleInfo.blueTeam[0].shotHistory[battleInfo.currentRound]
) {
roundResults.value.push({
redArrows: battleInfo.redTeam[0].shotHistory[
battleInfo.currentRound
].filter((item) => !!item.playerId),
blueArrows: battleInfo.blueTeam[0].shotHistory[
battleInfo.currentRound
].filter((item) => !!item.playerId),
});
} else if (battleInfo.currentRound < 5) {
roundResults.value.push({
redArrows: [],
blueArrows: [],
});
}
}
// 这个状态不准
// if (battleInfo.status !== 11) return;
if (battleInfo.firePlayerIndex) {
currentShooterId.value = battleInfo.firePlayerIndex;
if (redTeam.value[0].id === currentShooterId.value) {
tips.value = `请红队射箭-第${roundsName[currentRound.value]}`;
} else {
tips.value = `请蓝队射箭-第${roundsName[currentRound.value]}`;
}
}
if (battleInfo.fireTime > 0) {
const remain = Date.now() / 1000 - battleInfo.fireTime;
console.log(`当前箭已过${remain}`);
if (remain > 0 && remain < 15) {
// 等渲染好再通知
setTimeout(() => {
uni.$emit("update-ramain", 15 - remain);
}, 300);
}
}
} else if (battleInfo.config.mode === 2) {
total.value = 90;
players.value = [...battleInfo.blueTeam, ...battleInfo.redTeam];
players.value.forEach((p) => {
playersScores.value[p.id] = [...p.arrows];
if (p.id === user.value.id) scores.value = [...p.arrows];
});
if (battleInfo.status === 2) {
startCount.value = true;
const remain = Date.now() / 1000 - battleInfo.startTime;
console.log(`当前局已进行${remain}`);
tips.value = battleInfo.halfGame
? "下半场请再射6箭"
: "上半场请先射6箭";
setTimeout(() => {
uni.$emit("update-ramain", 90 - remain);
}, 300);
} else if (battleInfo.status === 9) {
startCount.value = false;
tips.value = "准备下半场";
setTimeout(() => {
uni.$emit("update-ramain", 0);
}, 300);
}
}
}
onLoad(async (options) => {
if (options.battleId) {
const battleInfo = uni.getStorageSync("current-battle");
if (battleInfo) recoverData(battleInfo);
setTimeout(getCurrentGameAPI, 2000);
}
if (options.roomNumber) {
roomNumber.value = options.roomNumber;
const result = await getRoomAPI(options.roomNumber);
room.value = result;
battleType.value = result.battleType;
result.members.some((m) => {
if (m.userInfo.id === result.creator) {
owner.value = {
id: m.userInfo.id,
name: m.userInfo.name,
avatar: m.userInfo.avatar,
};
return true;
}
return false;
});
if (result.battleType === 1) {
if (user.value.id !== owner.value.id) {
opponent.value = {
id: user.value.id,
name: user.value.nickName,
avatar: user.value.avatar,
};
} else if (result.members.length > 1) {
result.members.some((m) => {
if (m.userInfo.id !== owner.value.id) {
opponent.value = {
id: m.userInfo.id,
name: m.userInfo.name,
avatar: m.userInfo.avatar,
};
return true;
}
return false;
});
}
} else if (result.battleType === 2) {
const ownerIndex = result.members.findIndex(
(m) => m.userInfo.id === result.creator
);
if (ownerIndex !== -1) {
players.value.push(result.members[ownerIndex].userInfo);
} else {
players.value.push({});
}
result.members.forEach((m, index) => {
if (ownerIndex !== index) players.value.push(m.userInfo);
});
}
}
});
const startGame = async () => {
const result = await startRoomAPI(room.value.number);
step.value = 2;
};
async function onReceiveMessage(messages = []) {
messages.forEach((msg) => {
if (
msg.roomNumber === roomNumber.value ||
(battleId.value && msg.id === battleId.value) ||
msg.constructor === MESSAGETYPES.WaitForAllReady
) {
console.log("收到消息:", getMessageTypeName(msg.constructor), msg);
}
if (msg.constructor === MESSAGETYPES.WaitForAllReady) {
// 这里会掉多次;
battleId.value = msg.id;
step.value = 2;
if (battleType.value === 1) {
redTeam.value = msg.groupUserStatus.redTeam;
blueTeam.value = msg.groupUserStatus.blueTeam;
} else if (battleType.value === 2) {
players.value = [
...msg.groupUserStatus.redTeam,
...msg.groupUserStatus.blueTeam,
];
players.value.forEach((p) => {
playersScores.value[p.id] = [];
});
}
}
if (msg.roomNumber === roomNumber.value) {
if (msg.constructor === MESSAGETYPES.UserEnterRoom) {
if (battleType.value === 1) {
if (msg.userId === room.value.creator) {
owner.value = {
id: msg.userId,
name: msg.name,
avatar: msg.avatar,
};
} else {
opponent.value = {
id: msg.userId,
name: msg.name,
avatar: msg.avatar,
};
}
}
if (battleType.value === 2) {
if (room.value.creator === msg.userId) {
players.value[0] = {
id: msg.userId,
name: msg.name,
avatar: msg.avatar,
};
} else {
players.value.push({
id: msg.userId,
name: msg.name,
avatar: msg.avatar,
});
}
}
}
if (!start.value && msg.constructor === MESSAGETYPES.UserExitRoom) {
if (battleType.value === 1) {
if (msg.userId === room.value.creator) {
owner.value = {
id: "",
};
} else {
opponent.value = {
id: "",
};
}
}
if (battleType.value === 2) {
players.value = players.value.filter((p) => p.id !== msg.userId);
}
}
if (msg.constructor === MESSAGETYPES.RoomDestroy && !battleId.value) {
uni.showToast({
title: "房间已解散",
icon: "none",
});
roomNumber.value = "";
setTimeout(() => {
uni.navigateBack();
}, 1000);
}
}
if (msg.id === battleId.value) {
if (msg.constructor === MESSAGETYPES.AllReady) {
start.value = true;
totalRounds.value = msg.groupUserStatus.config.maxRounds;
step.value = 3;
roundResults.value.push({
redArrows: [],
blueArrows: [],
});
total.value = 15;
}
if (msg.constructor === MESSAGETYPES.MeleeAllReady) {
start.value = true;
startCount.value = true;
step.value = 3;
seq.value += 1;
tips.value = scores.value.length
? "下半场请再射6箭"
: "上半场请先射6箭";
total.value = 90;
halfTimeTip.value = false;
}
if (msg.constructor === MESSAGETYPES.ToSomeoneShoot) {
if (battleType.value === 1) {
if (currentShooterId.value !== msg.userId) {
seq.value += 1;
currentShooterId.value = msg.userId;
if (redTeam.value[0].id === currentShooterId.value) {
tips.value = "请红队射箭-";
} else {
tips.value = "请蓝队射箭-";
}
if (isFinalShoot.value) {
tips.value += "决金箭";
} else {
tips.value += `${roundsName[currentRound.value]}`;
}
}
}
}
if (msg.constructor === MESSAGETYPES.ShootResult) {
if (battleType.value === 1) {
// 会有蓝队射箭时间,红队射箭也有结果返回的情况
if (currentShooterId.value !== msg.userId) return;
const isRed = redTeam.value.find((item) => item.id === msg.userId);
if (isRed) scores.value.push(msg.target);
else blueScores.value.push(msg.target);
if (roundResults.value[currentRound.value - 1]) {
if (isRed && roundResults.value[currentRound.value - 1].redArrows) {
roundResults.value[currentRound.value - 1].redArrows.push(
msg.target
);
}
if (
!isRed &&
roundResults.value[currentRound.value - 1].blueArrows
) {
roundResults.value[currentRound.value - 1].blueArrows.push(
msg.target
);
}
} else {
roundResults.value.push({
redArrows: [],
blueArrows: [],
});
}
}
if (battleType.value === 2 && msg.userId === user.value.id) {
scores.value.push({ ...msg.target });
power.value = msg.target.battery;
}
if (playersScores.value[msg.userId])
playersScores.value[msg.userId].push({ ...msg.target });
}
if (msg.constructor === MESSAGETYPES.CurrentRoundEnded) {
if (battleType.value === 1) {
const result = msg.preRoundResult;
scores.value = [];
blueScores.value = [];
currentShooterId.value = 0;
currentBluePoint.value = result.blueScore;
currentRedPoint.value = result.redScore;
bluePoints.value += result.blueScore;
redPoints.value += result.redScore;
currentRound.value = result.currentRound + 1;
uni.$emit("update-ramain", 0);
if (result.currentRound < 5) {
roundResults.value.push({
redArrows: [],
blueArrows: [],
});
showRoundTip.value = true;
}
}
}
if (msg.constructor === MESSAGETYPES.FinalShoot) {
if (!isFinalShoot.value) {
isFinalShoot.value = true;
showRoundTip.value = true;
tips.value = "准备开始决金箭";
}
}
if (msg.constructor === MESSAGETYPES.HalfTimeOver) {
uni.$emit("update-ramain", 0);
[
...msg.groupUserStatus.redTeam,
...msg.groupUserStatus.blueTeam,
].forEach((player) => {
playersScores.value[player.id] = [...player.arrows];
if (player.id === user.value.id) scores.value = [...player.arrows];
});
startCount.value = false;
halfTimeTip.value = true;
tips.value = "准备下半场";
}
if (msg.constructor === MESSAGETYPES.MatchOver) {
if (msg.endStatus.noSaved) {
// uni.showToast({
// title: "本场比赛无人射箭,已取消",
// icon: "none",
// });
currentBluePoint.value = 0;
currentRedPoint.value = 0;
setTimeout(() => {
uni.navigateBack();
}, 1500);
} else {
isEnded.value = true;
uni.setStorageSync("last-battle", msg.endStatus);
setTimeout(() => {
uni.redirectTo({
url: "/pages/battle-result",
});
}, 1500);
}
}
if (msg.constructor === MESSAGETYPES.BackToGame) {
uni.$emit("update-header-loading", false);
if (msg.battleInfo) recoverData(msg.battleInfo);
}
}
});
}
const destroyRoom = async () => {
if (roomNumber.value) await destroyRoomAPI(roomNumber.value);
};
const exitRoom = async () => {
uni.navigateBack();
};
const setClipboardData = () => {
uni.setClipboardData({
data: roomNumber.value,
success() {
uni.showToast({ title: "复制成功" });
},
});
};
const onBack = () => {
if (battleId.value) {
uni.$showHint(2);
} else {
showModal.value = true;
}
};
onMounted(() => {
uni.setKeepScreenOn({
keepScreenOn: true,
});
uni.$on("socket-inbox", onReceiveMessage);
});
onUnmounted(() => {
uni.setKeepScreenOn({
keepScreenOn: false,
});
uni.$off("socket-inbox", onReceiveMessage);
if (roomNumber.value && owner.value.id !== user.value.id && !battleId.value) {
exitRoomAPI(roomNumber.value);
}
});
const refreshTimer = ref(null);
onShow(async () => {
if (battleId.value) {
if (!isEnded.value && (await isGameEnded(battleId.value))) return;
getCurrentGameAPI();
const refreshData = () => {
const lastAwakeTime = uni.getStorageSync("last-awake-time");
if (lastAwakeTime) {
getCurrentGameAPI();
} else {
clearInterval(refreshTimer.value);
}
};
refreshTimer.value = setInterval(refreshData, 2000);
}
});
onHide(() => {
if (refreshTimer.value) clearInterval(refreshTimer.value);
uni.setStorageSync("last-awake-time", Date.now());
});
</script>
<template>
<Container title="对战" :onBack="onBack" :bgType="battleId ? 1 : 0">
<view class="standby-phase" v-if="step === 1">
<Guide>
<view class="battle-guide">
<view :style="{ display: 'flex', flexDirection: 'column' }">
<text :style="{ color: '#fed847' }">弓箭手们人都到齐了吗?</text>
<text v-if="battleType === 1">1v1比赛即将开始! </text>
<text v-if="battleType === 2">大乱斗即将开始! </text>
</view>
<view @click="setClipboardData">邀请好友</view>
</view>
</Guide>
<view v-if="battleType === 1" class="team-mode">
<image src="../static/1v1-bg.png" mode="widthFix" />
<view>
<view class="player" :style="{ transform: 'translateY(-60px)' }">
<image
:src="owner.avatar || '../static/user-icon.png'"
mode="widthFix"
/>
<text>{{ owner.name }}</text>
</view>
<image src="../static/versus.png" mode="widthFix" />
<block v-if="opponent.id">
<view class="player" :style="{ transform: 'translateY(60px)' }">
<image
:src="opponent.avatar || '../static/user-icon.png'"
mode="widthFix"
/>
<text v-if="opponent.name">{{ opponent.name }}</text>
</view>
</block>
<block v-else>
<view class="no-player">
<image src="../static/question-mark.png" mode="widthFix" />
</view>
</block>
</view>
</view>
<PlayerSeats
v-if="battleType === 2"
:total="room.count || 10"
:players="players"
/>
<view>
<SButton
v-if="user.id === owner.id && battleType === 1"
:disabled="!opponent.id"
:onClick="startGame"
>进入对战</SButton
>
<SButton
v-if="user.id === owner.id && battleType === 2"
:disabled="players.length < 3"
:onClick="startGame"
>进入大乱斗</SButton
>
<SButton v-if="user.id !== owner.id" disabled>等待房主开启对战</SButton>
<text class="tips">创建者点击下一步所有人即可进入游戏</text>
</view>
</view>
<view v-if="step === 2" :style="{ width: '100%' }">
<BattleHeader
:blueTeam="blueTeam"
:redTeam="redTeam"
:players="players"
/>
<TestDistance :guide="false" />
<Timer />
</view>
<view v-if="step === 3" :style="{ width: '100%' }">
<ShootProgress
:tips="tips"
:seq="seq"
:start="players.length > 0 && start && startCount"
:total="total"
:currentRound="currentRound"
:battleId="battleId"
:melee="players.length > 0"
/>
<PlayersRow
v-if="blueTeam.length && redTeam.length"
:blueTeam="blueTeam"
:redTeam="redTeam"
:currentShooterId="currentShooterId"
/>
<BowTarget
:mode="battleType === 1 ? 'team' : 'solo'"
:power="start ? power : 0"
:currentRound="scores.length"
:totalRound="battleType === 1 ? 3 : totalRounds"
:scores="scores"
:blueScores="blueScores"
:stop="!startCount"
/>
<BattleFooter
v-if="roundResults.length"
:roundResults="roundResults"
:redPoints="redPoints"
:bluePoints="bluePoints"
/>
<PlayerScore
v-if="playersSorted.length"
v-for="(player, index) in playersSorted"
:key="index"
:name="player.name"
:avatar="player.avatar"
:scores="playersScores[player.id] || []"
/>
</view>
<ScreenHint
:show="showRoundTip"
:onClose="() => (showRoundTip = false)"
:mode="isFinalShoot ? 'tall' : 'normal'"
>
<RoundEndTip
:isFinal="isFinalShoot"
:round="currentRound - 1"
:bluePoint="currentBluePoint"
:redPoint="currentRedPoint"
:roundData="
roundResults[roundResults.length - 2]
? roundResults[roundResults.length - 2]
: []
"
:onAutoClose="() => (showRoundTip = false)"
/>
</ScreenHint>
<ScreenHint
:show="halfTimeTip"
mode="small"
:onClose="() => (halfTimeTip = false)"
>
<view class="half-time-tip">
<text>上半场结束休息一下吧:</text>
<text>20秒后开始下半场</text>
</view>
</ScreenHint>
<SModal :show="showModal" :onClose="() => (showModal = false)">
<view class="btns">
<block v-if="!battleId">
<SButton :onClick="exitRoom" width="200px" :rounded="20">
暂时离开
</SButton>
<block v-if="owner.id === user.id">
<view :style="{ height: '20px' }"></view>
<SButton :onClick="destroyRoom" width="200px" :rounded="20">
解散房间
</SButton>
</block>
</block>
<block v-else>
<SButton :onClick="exitRoom" width="200px" :rounded="20">
退出比赛
</SButton>
</block>
</view>
</SModal>
</Container>
</template>
<style scoped>
.standby-phase {
width: 100%;
height: calc(100% - 40px);
overflow-x: hidden;
}
.tips {
color: #fff9;
width: 100%;
text-align: center;
display: block;
margin-top: 10px;
font-size: 12px;
}
.player-unknow {
width: 40px;
height: 40px;
margin: 0 10px;
border: 1px solid #fff3;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: #69686866;
}
.player-unknow > image {
width: 40%;
}
.team-mode {
width: calc(100vw - 30px);
height: 125vw;
margin: 15px;
}
.team-mode > image:first-child {
position: absolute;
width: calc(100vw - 30px);
z-index: -1;
}
.team-mode > view {
display: flex;
justify-content: center;
align-items: center;
height: 95%;
}
.player {
width: 70px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transform: translateY(-60px);
color: #fff9;
font-size: 14px;
}
.player > image {
width: 70px;
height: 70px;
border-radius: 50%;
background-color: #ccc;
margin-bottom: 5px;
}
.player > text {
width: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
.team-mode > view > image:nth-child(2) {
width: 120px;
}
.no-player {
width: 70px;
height: 70px;
border-radius: 50%;
background-color: #ccc;
display: flex;
justify-content: center;
align-items: center;
transform: translateY(60px);
}
.no-player > image {
width: 20px;
margin-right: 2px;
}
.btns {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.battle-guide {
display: flex;
align-items: center;
justify-content: space-between;
}
.battle-guide > view:last-child {
color: #fed847;
border: 1px solid #fed847;
margin-right: 10px;
padding: 5px 12px;
border-radius: 20px;
position: relative;
}
</style>