Add out of zone handicap

This commit is contained in:
Sebastien Riviere
2025-09-14 16:27:35 +02:00
parent 7e4d9f910a
commit 0f64fc59f9
6 changed files with 70 additions and 28 deletions

View File

@@ -7,6 +7,7 @@ http {
server { server {
listen 80; listen 80;
client_max_body_size 15M; client_max_body_size 15M;
access_log off;
location / { location / {
proxy_pass http://front:3000/; proxy_pass http://front:3000/;
proxy_set_header Host $host; proxy_set_header Host $host;

View File

@@ -1,5 +1,5 @@
// React // React
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef, Fragment } from 'react';
import { ScrollView, View, Text, Image, Alert, StyleSheet, TouchableOpacity, TouchableHighlight } from 'react-native'; import { ScrollView, View, Text, Image, Alert, StyleSheet, TouchableOpacity, TouchableHighlight } from 'react-native';
import MapView, { Marker, Circle, Polygon } from 'react-native-maps'; import MapView, { Marker, Circle, Polygon } from 'react-native-maps';
// Expo // Expo
@@ -34,7 +34,7 @@ export default function Display() {
const {SERVER_URL} = useSocket(); const {SERVER_URL} = useSocket();
const {messages, zoneType, zoneExtremities, nextZoneDate, isShrinking, location, startLocationTracking, stopLocationTracking, gameState, startDate} = useTeamContext(); const {messages, zoneType, zoneExtremities, nextZoneDate, isShrinking, location, startLocationTracking, stopLocationTracking, gameState, startDate} = useTeamContext();
const {loggedIn, logout, loading} = useTeamConnexion(); const {loggedIn, logout, loading} = useTeamConnexion();
const {sendCurrentPosition, capture, enemyLocation, enemyName, startingArea, captureCode, name, ready, captured, lastSentLocation, locationSendDeadline, teamId, outOfZone, outOfZoneDeadline, distance, finishDate, nCaptures, nSentLocation} = useGame(); const {sendCurrentPosition, capture, enemyLocation, enemyName, startingArea, captureCode, name, ready, captured, lastSentLocation, locationSendDeadline, teamId, outOfZone, outOfZoneDeadline, distance, finishDate, nCaptures, nSentLocation, hasHandicap, enemyHasHandicap} = useGame();
const [enemyCaptureCode, setEnemyCaptureCode] = useState(""); const [enemyCaptureCode, setEnemyCaptureCode] = useState("");
const [timeLeftSendLocation] = useTimeDifference(locationSendDeadline, 1000); const [timeLeftSendLocation] = useTimeDifference(locationSendDeadline, 1000);
const [timeLeftNextZone] = useTimeDifference(nextZoneDate, 1000); const [timeLeftNextZone] = useTimeDifference(nextZoneDate, 1000);
@@ -175,7 +175,8 @@ export default function Display() {
{ gameState == GameState.SETUP && <Text style={styles.gameState}>Préparation de la partie</Text>} { gameState == GameState.SETUP && <Text style={styles.gameState}>Préparation de la partie</Text>}
{ gameState == GameState.PLACEMENT && <Text style={styles.gameState}>Phase de placement</Text>} { gameState == GameState.PLACEMENT && <Text style={styles.gameState}>Phase de placement</Text>}
{ gameState == GameState.PLAYING && !outOfZone && <Text style={styles.gameState}>La partie est en cours</Text>} { gameState == GameState.PLAYING && !outOfZone && <Text style={styles.gameState}>La partie est en cours</Text>}
{ gameState == GameState.PLAYING && outOfZone && <Text style={styles.gameStateOutOfZone}>Hors zone (pénalité dans {formatTimeMinutes(-timeLeftOutOfZone)})</Text>} { gameState == GameState.PLAYING && outOfZone && !hasHandicap && <Text style={styles.gameStateOutOfZone}>Hors zone (handicap dans {formatTimeMinutes(-timeLeftOutOfZone)})</Text>}
{ gameState == GameState.PLAYING && hasHandicap && <Text style={styles.gameStateOutOfZone}>Hors zone (position révélée en continue)</Text>}
{ gameState == GameState.FINISHED && <Text style={styles.gameState}>La partie est terminée</Text>} { gameState == GameState.FINISHED && <Text style={styles.gameState}>La partie est terminée</Text>}
</TouchableOpacity> </TouchableOpacity>
); );
@@ -194,7 +195,7 @@ export default function Display() {
return ( return (
<View style={{width: "100%", alignItems: "center", justifyContent: "center"}}> <View style={{width: "100%", alignItems: "center", justifyContent: "center"}}>
<Text style={{fontSize: 15}}>Position envoyée dans</Text> <Text style={{fontSize: 15}}>Position envoyée dans</Text>
<Text style={{fontSize: 30, fontWeight: "bold"}}>{formatTimeMinutes(-timeLeftSendLocation)}</Text> <Text style={{fontSize: 30, fontWeight: "bold"}}>{ !hasHandicap ? formatTimeMinutes(-timeLeftSendLocation) : "00:00"}</Text>
</View> </View>
); );
} }
@@ -245,17 +246,17 @@ export default function Display() {
switch (zoneType) { switch (zoneType) {
case zoneTypes.circle: case zoneTypes.circle:
return ( return (
<View> <Fragment>
{ zoneExtremities.begin && <Circle center={latToLatitude(zoneExtremities.begin.center)} radius={zoneExtremities.begin.radius} strokeColor="red" fillColor="rgba(255,0,0,0.1)" strokeWidth={2} />} { zoneExtremities.begin && <Circle center={latToLatitude(zoneExtremities.begin.center)} radius={zoneExtremities.begin.radius} strokeColor="red" fillColor="rgba(255,0,0,0.1)" strokeWidth={2} />}
{ zoneExtremities.end && <Circle center={latToLatitude(zoneExtremities.end.center)} radius={zoneExtremities.end.radius} strokeColor="green" fillColor="rgba(0,255,0,0.1)" strokeWidth={2} />} { zoneExtremities.end && <Circle center={latToLatitude(zoneExtremities.end.center)} radius={zoneExtremities.end.radius} strokeColor="green" fillColor="rgba(0,255,0,0.1)" strokeWidth={2} />}
</View> </Fragment>
); );
case zoneTypes.polygon: case zoneTypes.polygon:
return ( return (
<View> <Fragment>
{ zoneExtremities.begin && <Polygon coordinates={zoneExtremities.begin.polygon.map(pos => latToLatitude(pos))} strokeColor="red" fillColor="rgba(255,0,0,0.1)" strokeWidth={2} /> } { zoneExtremities.begin && <Polygon coordinates={zoneExtremities.begin.polygon.map(pos => latToLatitude(pos))} strokeColor="red" fillColor="rgba(255,0,0,0.1)" strokeWidth={2} /> }
{ zoneExtremities.end && <Polygon coordinates={zoneExtremities.end.polygon.map(pos => latToLatitude(pos))} strokeColor="green" fillColor="rgba(0,255,0,0.1)" strokeWidth={2} /> } { zoneExtremities.end && <Polygon coordinates={zoneExtremities.end.polygon.map(pos => latToLatitude(pos))} strokeColor="green" fillColor="rgba(0,255,0,0.1)" strokeWidth={2} /> }
</View> </Fragment>
); );
default: default:
return null; return null;
@@ -272,12 +273,12 @@ export default function Display() {
<Image source={require("../assets/images/marker/blue.png")} style={{width: 24, height: 24}} resizeMode="contain"/> <Image source={require("../assets/images/marker/blue.png")} style={{width: 24, height: 24}} resizeMode="contain"/>
</Marker> </Marker>
} }
{ gameState == GameState.PLAYING && lastSentLocation && { gameState == GameState.PLAYING && lastSentLocation && !hasHandicap &&
<Marker coordinate={{ latitude: lastSentLocation[0], longitude: lastSentLocation[1] }} anchor={{ x: 0.33, y: 0.33 }}> <Marker coordinate={{ latitude: lastSentLocation[0], longitude: lastSentLocation[1] }} anchor={{ x: 0.33, y: 0.33 }}>
<Image source={require("../assets/images/marker/grey.png")} style={{width: 24, height: 24}} resizeMode="contain"/> <Image source={require("../assets/images/marker/grey.png")} style={{width: 24, height: 24}} resizeMode="contain"/>
</Marker> </Marker>
} }
{ gameState == GameState.PLAYING && enemyLocation && { gameState == GameState.PLAYING && enemyLocation && !hasHandicap &&
<Marker coordinate={{ latitude: enemyLocation[0], longitude: enemyLocation[1] }} anchor={{ x: 0.33, y: 0.33 }}> <Marker coordinate={{ latitude: enemyLocation[0], longitude: enemyLocation[1] }} anchor={{ x: 0.33, y: 0.33 }}>
<Image source={require("../assets/images/marker/red.png")} style={{width: 24, height: 24}} resizeMode="contain"/> <Image source={require("../assets/images/marker/red.png")} style={{width: 24, height: 24}} resizeMode="contain"/>
</Marker> </Marker>
@@ -287,7 +288,7 @@ export default function Display() {
} }
const UpdatePositionButton = () => { const UpdatePositionButton = () => {
return ( return ( !hasHandicap &&
<TouchableOpacity style={styles.updatePositionContainer} onPress={sendCurrentPosition}> <TouchableOpacity style={styles.updatePositionContainer} onPress={sendCurrentPosition}>
<Image source={require("../assets/images/update_position.png")} style={{width: 40, height: 40}} resizeMode="contain"></Image> <Image source={require("../assets/images/update_position.png")} style={{width: 40, height: 40}} resizeMode="contain"></Image>
</TouchableOpacity> </TouchableOpacity>
@@ -327,8 +328,9 @@ export default function Display() {
const ChasedTeamImage = () => { const ChasedTeamImage = () => {
return ( return (
<View style={styles.imageContainer}> <View style={styles.imageContainer}>
<Text style={{fontSize: 15, margin: 5}}>{"Cible (" + (enemyName ?? "Indisponible") + ")"}</Text> {<Text style={{fontSize: 15, margin: 5}}>{"Cible (" + (enemyName ?? "Indisponible") + ")"}</Text>}
<CustomImage source={{ uri : enemyImageURI }} canZoom/> {enemyHasHandicap && <Text style={{fontSize: 15, margin: 5}}>Position ennemie révélée en continue</Text>}
{<CustomImage source={{ uri : enemyImageURI }} canZoom/>}
</View> </View>
); );
} }
@@ -417,11 +419,13 @@ export default function Display() {
<Collapsible style={[styles.collapsibleWindow, {height: bottomContainerHeight - 44}]} title="Collapse" collapsed={collapsibleState}> <Collapsible style={[styles.collapsibleWindow, {height: bottomContainerHeight - 44}]} title="Collapse" collapsed={collapsibleState}>
<ScrollView contentContainerStyle={styles.collapsibleContent}> <ScrollView contentContainerStyle={styles.collapsibleContent}>
{ TeamCaptureCode() } { TeamCaptureCode() }
{ ChasedTeamImage() } { !hasHandicap && <Fragment>
<View style={styles.actionsContainer}> { ChasedTeamImage() }
{ CaptureCode() } <View style={styles.actionsContainer}>
{ CaptureButton() } { CaptureCode() }
</View> { CaptureButton() }
</View>
</Fragment>}
{ Stats() } { Stats() }
</ScrollView> </ScrollView>
</Collapsible> </Collapsible>
@@ -504,7 +508,6 @@ const styles = StyleSheet.create({
}, },
bottomContainer: { bottomContainer: {
flex: 1, flex: 1,
alignItems: 'center'
}, },
mapContainer: { mapContainer: {
flex: 1, flex: 1,
@@ -518,11 +521,9 @@ const styles = StyleSheet.create({
bottom: 0, bottom: 0,
left: 0, left: 0,
right: 0, right: 0,
alignItems: 'center'
}, },
innerDrawerContainer: { innerDrawerContainer: {
width: "100%", width: "100%",
alignItems: 'center',
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
borderTopLeftRadius: 30, borderTopLeftRadius: 30,
borderTopRightRadius: 30, borderTopRightRadius: 30,
@@ -536,13 +537,11 @@ const styles = StyleSheet.create({
}, },
collapsibleWindow: { collapsibleWindow: {
width: "100%", width: "100%",
alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
}, },
collapsibleContent: { collapsibleContent: {
paddingHorizontal: 15, paddingHorizontal: 15,
alignItems: 'center'
}, },
centerMapContainer: { centerMapContainer: {
position: 'absolute', position: 'absolute',

View File

@@ -106,6 +106,7 @@ export default {
// Zone // Zone
team.outOfZone = false; team.outOfZone = false;
team.outOfZoneDeadline = null; team.outOfZoneDeadline = null;
team.hasHandicap = false;
// Stats // Stats
team.distance = 0; team.distance = 0;
team.nCaptures = 0; team.nCaptures = 0;
@@ -274,6 +275,7 @@ export default {
// Zone // Zone
outOfZone: false, outOfZone: false,
outOfZoneDeadline: null, outOfZoneDeadline: null,
hasHandicap: false,
// Stats // Stats
distance: 0, distance: 0,
nCaptures: 0, nCaptures: 0,
@@ -362,7 +364,17 @@ export default {
}, },
handicapTeam(teamId) { handicapTeam(teamId) {
// TODO // Test of parameters
if (!this.hasTeam(teamId)) return false;
// Variables
const team = this.getTeam(teamId);
// Make the capture
team.hasHandicap = true;
sendPositionTimeouts.clear(team.id);
// Broadcast new infos
secureAdminBroadcast("teams", this.teams);
sendUpdatedTeamInformations(team.id);
return true;
}, },
@@ -375,12 +387,20 @@ export default {
if (!location) return false; if (!location) return false;
// Variables // Variables
const team = this.getTeam(teamId); const team = this.getTeam(teamId);
const enemyTeam = this.getTeam(team.chasing);
const dateNow = Date.now(); const dateNow = Date.now();
// Update distance // Update distance
if (team.currentLocation) team.distance += Math.floor(getDistanceFromLatLon({lat: location[0], lng: location[1]}, {lat: team.currentLocation[0], lng: team.currentLocation[1]})); if (team.currentLocation) team.distance += Math.floor(getDistanceFromLatLon({lat: location[0], lng: location[1]}, {lat: team.currentLocation[0], lng: team.currentLocation[1]}));
// Update of currentLocation // Update of currentLocation
team.currentLocation = location; team.currentLocation = location;
team.lastCurrentLocationDate = dateNow; team.lastCurrentLocationDate = dateNow;
if (team.hasHandicap) {
team.lastSentLocation = team.currentLocation;
}
// Update of enemyLocation
if (enemyTeam.hasHandicap) {
team.enemyLocation = enemyTeam.currentLocation;
}
// Update of ready // Update of ready
if (this.state == GameState.PLACEMENT && team.startingArea) { if (this.state == GameState.PLACEMENT && team.startingArea) {
team.ready = isInCircle({ lat: location[0], lng: location[1] }, team.startingArea.center, team.startingArea.radius); team.ready = isInCircle({ lat: location[0], lng: location[1] }, team.startingArea.center, team.startingArea.radius);
@@ -394,6 +414,11 @@ export default {
} else if (!teamCurrentlyOutOfZone && team.outOfZone) { } else if (!teamCurrentlyOutOfZone && team.outOfZone) {
team.outOfZone = false; team.outOfZone = false;
team.outOfZoneDeadline = null; team.outOfZoneDeadline = null;
team.hasHandicap = false;
if (!sendPositionTimeouts.has(team.id)) {
team.locationSendDeadline = dateNow + sendPositionTimeouts.delay * 60 * 1000;
sendPositionTimeouts.set(team.id);
}
outOfZoneTimeouts.clear(teamId); outOfZoneTimeouts.clear(teamId);
} }
// Broadcast new infos // Broadcast new infos

View File

@@ -31,15 +31,18 @@ export function playersBroadcast(event, data) {
* @param {String} teamId The team that will receive the message * @param {String} teamId The team that will receive the message
*/ */
export function sendUpdatedTeamInformations(teamId) { export function sendUpdatedTeamInformations(teamId) {
// Test of parameters
if (!game.hasTeam(teamId)) return false;
// Variables
const team = game.getTeam(teamId); const team = game.getTeam(teamId);
if (!team) return; const enemyTeam = game.getTeam(team.chasing);
teamBroadcast(teamId, "update_team", { teamBroadcast(teamId, "update_team", {
// Identification // Identification
name: team.name, name: team.name,
captureCode: team.captureCode, captureCode: team.captureCode,
// Chasing // Chasing
captured: team.captured, captured: team.captured,
enemyName: game.getTeam(team.chasing)?.name ?? null, enemyName: enemyTeam?.name,
// Locations // Locations
lastSentLocation: team.lastSentLocation, lastSentLocation: team.lastSentLocation,
enemyLocation: team.enemyLocation, enemyLocation: team.enemyLocation,
@@ -50,6 +53,8 @@ export function sendUpdatedTeamInformations(teamId) {
outOfZone: team.outOfZone, outOfZone: team.outOfZone,
outOfZoneDeadline: team.outOfZoneDeadline, outOfZoneDeadline: team.outOfZoneDeadline,
locationSendDeadline: team.locationSendDeadline, locationSendDeadline: team.locationSendDeadline,
hasHandicap: team.hasHandicap,
enemyHasHandicap: enemyTeam?.hasHandicap,
// Stats // Stats
distance: team.distance, distance: team.distance,
nCaptures: team.nCaptures, nCaptures: team.nCaptures,

View File

@@ -5,6 +5,10 @@ class TimeoutManager {
this.timeouts = new Map(); this.timeouts = new Map();
} }
has(key) {
return this.timeouts.has(key);
}
set(key, callback, delay) { set(key, callback, delay) {
const newCallback = () => { const newCallback = () => {
this.timeouts.delete(key); this.timeouts.delete(key);
@@ -32,6 +36,10 @@ export const sendPositionTimeouts = {
timeoutManager: new TimeoutManager(), timeoutManager: new TimeoutManager(),
delay: 10, // Minutes delay: 10, // Minutes
has(teamID) {
return this.timeoutManager.has(teamID);
},
set(teamID) { set(teamID) {
const callback = () => { const callback = () => {
game.sendLocation(teamID); game.sendLocation(teamID);
@@ -58,6 +66,10 @@ export const outOfZoneTimeouts = {
timeoutManager: new TimeoutManager(), timeoutManager: new TimeoutManager(),
delay: 10, // Minutes delay: 10, // Minutes
has(teamID) {
return this.timeoutManager.has(teamID);
},
set(teamID) { set(teamID) {
const callback = () => { const callback = () => {
game.handicapTeam(teamID); game.handicapTeam(teamID);

View File

@@ -9,7 +9,7 @@ export function getStatus(team, gamestate) {
case GameState.PLACEMENT: case GameState.PLACEMENT:
return team.ready ? teamStatus.ready : teamStatus.notready; return team.ready ? teamStatus.ready : teamStatus.notready;
case GameState.PLAYING: case GameState.PLAYING:
return team.captured ? teamStatus.captured : team.outofzone ? teamStatus.outofzone : teamStatus.playing; return team.captured ? teamStatus.captured : team.outOfZone ? teamStatus.outofzone : teamStatus.playing;
case GameState.FINISHED: case GameState.FINISHED:
return team.captured ? teamStatus.captured : teamStatus.playing; return team.captured ? teamStatus.captured : teamStatus.playing;
default: default: