Files
shoot-miniprograms/src/pages/battle-room.vue
2025-07-17 09:35:30 +08:00

760 lines
22 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 } 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 } from "@/apis";
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 timerSeq = 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(false);
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);
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,
}
);
onLoad(async (options) => {
if (options.battleId) {
const battleInfo = uni.getStorageSync("current-battle");
console.log("----battleInfo", battleInfo);
if (battleInfo) {
battleId.value = battleInfo.id;
battleType.value = battleInfo.config.mode;
start.value = true;
step.value = 3;
if (battleInfo.config.mode === 1) {
redTeam.value = battleInfo.redTeam;
blueTeam.value = battleInfo.blueTeam;
currentRound.value = battleInfo.currentRound;
bluePoints.value = battleInfo.blueScore;
redPoints.value = battleInfo.redScore;
totalRounds.value = battleInfo.maxRound;
roundResults.value = battleInfo.roundResults;
currentShooterId.value = battleInfo.firePlayerIndex;
setTimeout(() => {
if (battleInfo.roundResults[battleInfo.roundResults.length - 1]) {
scores.value =
battleInfo.roundResults[
battleInfo.roundResults.length - 1
].redArrows;
blueScores.value =
battleInfo.roundResults[
battleInfo.roundResults.length - 1
].blueArrows;
}
}, 300);
if (
battleInfo.redTeam[0].shotHistory[battleInfo.currentRound] ||
battleInfo.blueTeam[0].shotHistory[battleInfo.currentRound]
) {
roundResults.value.push({
redArrows:
battleInfo.redTeam[0].shotHistory[battleInfo.currentRound],
blueArrows:
battleInfo.blueTeam[0].shotHistory[battleInfo.currentRound],
});
} else if (battleInfo.currentRound < 5) {
roundResults.value.push({
redArrows: [],
blueArrows: [],
});
}
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]}`;
}
}
const remain = Date.now() / 1000 - battleInfo.fireTime;
if (remain > 0 && remain < 15) {
// 等渲染好再通知
setTimeout(() => {
uni.$emit("update-ramain", remain);
}, 300);
}
} else if (battleInfo.config.mode === 2) {
tips.value = "请连续射出6支箭";
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.startTime) {
const remain = Date.now() / 1000 - battleInfo.startTime;
if (remain <= 90) {
setTimeout(() => {
uni.$emit("update-ramain", remain);
}, 300);
} else if (remain > 90 && remain <= 110) {
halfTimeTip.value = true;
total.value = 20;
tips.value = "准备下半场";
setTimeout(() => {
uni.$emit("update-ramain", 110 - remain);
}, 300);
} else if (remain > 110) {
setTimeout(() => {
uni.$emit("update-ramain", remain - 110);
}, 300);
}
}
}
}
}
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);
timerSeq.value += 1;
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) {
// 这里会掉多次;
timerSeq.value += 1;
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;
timerSeq.value = 0;
scores.value = [];
totalRounds.value = msg.groupUserStatus.config.maxRounds;
step.value = 3;
if (battleType.value === 2) {
tips.value = "请在90秒内射完12支箭";
seq.value += 1;
}
if (!roundResults.value.length)
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;
timerSeq.value = 0;
tips.value = "请连续射出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) {
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) {
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;
// roundResults.value = result.roundResults;
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) {
playersScores.value = {};
[
...msg.groupUserStatus.redTeam,
...msg.groupUserStatus.blueTeam,
].forEach((player) => {
playersScores.value[player.id] = player.arrows;
});
startCount.value = false;
halfTimeTip.value = true;
tips.value = "准备下半场";
}
if (msg.constructor === MESSAGETYPES.MatchOver) {
uni.setStorageSync("last-battle", msg.endStatus);
if (msg.endStatus.nosaved) {
uni.showToast({
title: "游戏结束",
icon: "none",
});
} else {
uni.redirectTo({
url: `/pages/battle-result?battleId=${msg.id}`,
});
}
}
}
});
}
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 (owner.value.id !== user.value.id && !battleId.value) {
exitRoomAPI(roomNumber.value);
}
});
</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" />
</view>
<view v-if="step === 3" :style="{ width: '100%' }">
<ShootProgress
:tips="tips"
:seq="seq"
:start="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 ? 'team' : 'solo'"
:power="start ? power : 0"
:currentRound="currentRound"
:totalRound="totalRounds"
:scores="scores"
:blueScores="blueScores"
:stop="halfTimeTip && !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>
<Timer :seq="timerSeq" />
<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>