diff --git a/pom.xml b/pom.xml index 557a4346..141ac643 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ spring Intro project for Spring Boot - 21 + 17 3.1.3.RELEASE 3.1.3.RELEASE @@ -300,7 +300,7 @@ maven-compiler-plugin 3.11.0 - 21 + 17 org.projectlombok diff --git a/src/main/java/com/open/spring/mvc/PauseMenu/GamerScoreController.java b/src/main/java/com/open/spring/mvc/PauseMenu/GamerScoreController.java new file mode 100644 index 00000000..9df579a4 --- /dev/null +++ b/src/main/java/com/open/spring/mvc/PauseMenu/GamerScoreController.java @@ -0,0 +1,68 @@ +package com.open.spring.mvc.PauseMenu; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import java.util.HashMap; +import java.util.Map; + +/** + * Lightweight controller to handle gamer score submissions from the frontend. + * Exposed at /api/gamer/score (public, no auth required). + */ +@RestController +@RequestMapping("/api/pausemenu") +public class GamerScoreController { + + @Autowired + private ScorePauseMenuRepo scoreRepository; + + public static class GamerScoreRequest { + public String user; + public Integer score; + public String gameName; + public String variableName; + } + + @PostMapping("/score") + public ResponseEntity> saveGamerScore(@RequestBody GamerScoreRequest payload) { + try { + int score = payload != null && payload.score != null ? payload.score : 0; + String user = payload != null ? payload.user : null; + if (user == null || user.trim().isEmpty()) { + user = "guest"; + } + String gameName = payload != null ? payload.gameName : null; + if (gameName == null || gameName.trim().isEmpty()) { + gameName = "unknown"; + } + String variableName = payload != null ? payload.variableName : null; + if (variableName == null || variableName.trim().isEmpty()) { + variableName = "unknown"; + } + + ScoreCounter newScore = new ScoreCounter(); + newScore.setUser(user); + newScore.setScore(score); + newScore.setGameName(gameName); + newScore.setVariableName(variableName); + + ScoreCounter saved = scoreRepository.save(newScore); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("id", saved.getId()); + response.put("message", "Score saved successfully"); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("message", "Error saving score: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + } + } +} diff --git a/src/main/java/com/open/spring/mvc/PauseMenu/PauseMenuApiController.java b/src/main/java/com/open/spring/mvc/PauseMenu/PauseMenuApiController.java new file mode 100644 index 00000000..33f35d2c --- /dev/null +++ b/src/main/java/com/open/spring/mvc/PauseMenu/PauseMenuApiController.java @@ -0,0 +1,147 @@ +package com.open.spring.mvc.PauseMenu; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import lombok.Data; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +/** + * API Controller for Pause Menu Score Management + */ +@RestController +@RequestMapping("/api/pausemenu/score") +public class PauseMenuApiController { + + @Autowired + private ScorePauseMenuRepo scoreRepository; + + /** + * DTO for receiving score data from the frontend + */ + @Data + public static class ScorePauseMenuRequest { + private String user; + private int score; + } + + /** + * Save a new score + * POST /api/pausemenu/score/save + */ + @PostMapping("/save") + public ResponseEntity> saveScore(@RequestBody ScorePauseMenuRequest request) { + try { + ScoreCounter newScore = new ScoreCounter(); + // default to "guest" when user is missing or blank + String user = request.getUser(); + if (user == null || user.trim().isEmpty()) { + user = "guest"; + } + newScore.setUser(user); + newScore.setScore(request.getScore()); + + ScoreCounter saved = scoreRepository.save(newScore); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("id", saved.getId()); + response.put("message", "Score saved successfully"); + + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("message", "Error saving score: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + } + } + + /** + * Get all scores + * GET /api/pausemenu/score/all + */ + @GetMapping("/all") + public ResponseEntity> getAllScores() { + List scores = scoreRepository.findAll(); + return ResponseEntity.ok(scores); + } + + /** + * Get scores for a specific user + * GET /api/pausemenu/score/user/{user} + */ + @GetMapping("/user/{user}") + public ResponseEntity> getScoresByUser(@PathVariable String user) { + List scores = scoreRepository.findByUser(user); + return ResponseEntity.ok(scores); + } + + /** + * Get a specific score by ID + * GET /api/pausemenu/score/{id} + */ + @GetMapping("/{id}") + public ResponseEntity getScoreById(@PathVariable Long id) { + return scoreRepository.findById(id) + .map(score -> ResponseEntity.ok((Object) score)) + .orElse(ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(Map.of("error", "Score not found"))); + } + + /** + * Delete a score + * DELETE /api/pausemenu/score/{id} + */ + @DeleteMapping("/{id}") + public ResponseEntity> deleteScore(@PathVariable Long id) { + if (scoreRepository.existsById(id)) { + scoreRepository.deleteById(id); + return ResponseEntity.ok(Map.of("message", "Score deleted")); + } + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(Map.of("error", "Score not found")); + } + + /** + * Alternate endpoint used by frontend + * POST /api/gamer/score + * Accepts JSON: { "score": number, "user": string? } + * If user is missing, defaults to "guest" + */ + @PostMapping(path = "/api/gamer/score", consumes = "application/json") + public ResponseEntity> saveGamerScore(@RequestBody Map payload) { + try { + int score = 0; + Object scoreObj = payload.get("score"); + if (scoreObj instanceof Number) { + score = ((Number) scoreObj).intValue(); + } + + String user = (payload.get("user") instanceof String) ? (String) payload.get("user") : null; + if (user == null || user.trim().isEmpty()) { + user = "guest"; + } + + ScoreCounter newScore = new ScoreCounter(); + newScore.setUser(user); + newScore.setScore(score); + + ScoreCounter saved = scoreRepository.save(newScore); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("id", saved.getId()); + response.put("message", "Score saved successfully"); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("message", "Error saving score: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + } + } +} diff --git a/src/main/java/com/open/spring/mvc/PauseMenu/PauseMenuController.java b/src/main/java/com/open/spring/mvc/PauseMenu/PauseMenuController.java new file mode 100644 index 00000000..0e6b2840 --- /dev/null +++ b/src/main/java/com/open/spring/mvc/PauseMenu/PauseMenuController.java @@ -0,0 +1,44 @@ +package com.open.spring.mvc.PauseMenu; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import java.util.List; + +/** + * MVC Controller for displaying pause menu scores + */ +@Controller +@RequestMapping("/pausemenu") +public class PauseMenuController { + + @Autowired + private ScorePauseMenuRepo scoreRepository; + + /** + * Display all scores + * GET /pausemenu/scores + */ + @GetMapping("/scores") + public String showAllScores(Model model) { + List scores = scoreRepository.findAll(); + model.addAttribute("scores", scores); + model.addAttribute("pageTitle", "All Scores"); + return "pausemenu/scores-table"; + } + + /** + * Display scores for a specific user + * GET /pausemenu/scores?user=Username + */ + @GetMapping("/scores/user") + public String showUserScores(String user, Model model) { + List scores = scoreRepository.findByUser(user); + model.addAttribute("scores", scores); + model.addAttribute("pageTitle", "Scores for " + user); + model.addAttribute("userName", user); + return "pausemenu/scores-table"; + } +} diff --git a/src/main/java/com/open/spring/mvc/PauseMenu/ScoreCounter.java b/src/main/java/com/open/spring/mvc/PauseMenu/ScoreCounter.java new file mode 100644 index 00000000..5ba5c166 --- /dev/null +++ b/src/main/java/com/open/spring/mvc/PauseMenu/ScoreCounter.java @@ -0,0 +1,33 @@ +package com.open.spring.mvc.PauseMenu; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * ScoreCounter Entity - Stores game scores + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "score_counter") +public class ScoreCounter { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = true) + private String user; + + @Column(nullable = false) + private int score; + + @Column(nullable = true) + private String gameName; + + @Column(nullable = true) + private String variableName; +} diff --git a/src/main/java/com/open/spring/mvc/PauseMenu/ScorePauseMenuRepo.java b/src/main/java/com/open/spring/mvc/PauseMenu/ScorePauseMenuRepo.java new file mode 100644 index 00000000..e23f1f75 --- /dev/null +++ b/src/main/java/com/open/spring/mvc/PauseMenu/ScorePauseMenuRepo.java @@ -0,0 +1,17 @@ +package com.open.spring.mvc.PauseMenu; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.List; + +/** + * Repository for ScoreCounter entity + */ +@Repository +public interface ScorePauseMenuRepo extends JpaRepository { + + /** + * Find all scores for a specific user + */ + List findByUser(String user); +} diff --git a/src/main/java/com/open/spring/mvc/grades/DataLoader.java b/src/main/java/com/open/spring/mvc/grades/DataLoader.java index a24b546b..0a729d26 100644 --- a/src/main/java/com/open/spring/mvc/grades/DataLoader.java +++ b/src/main/java/com/open/spring/mvc/grades/DataLoader.java @@ -19,20 +19,23 @@ public class DataLoader implements CommandLineRunner { @Override public void run(String... args) throws Exception { - // Sample Grades - gradeRepository.save(new Grade("STU001", "Mathematics", 85.5, "A", "submission link")); - gradeRepository.save(new Grade("STU001", "Science", 92.0, "A+", "submission link")); - gradeRepository.save(new Grade("STU002", "Mathematics", 78.0, "B+", "submission link")); - gradeRepository.save(new Grade("STU002", "English", 88.5, "A-", "submission link")); - gradeRepository.save(new Grade("STU003", "Science", 95.0, "A+", "submission link")); + try { + // Sample Grades + gradeRepository.save(new Grade("STU001", "Mathematics", 85.5, "A", "submission link")); + gradeRepository.save(new Grade("STU001", "Science", 92.0, "A+", "submission link")); + gradeRepository.save(new Grade("STU002", "Mathematics", 78.0, "B+", "submission link")); + gradeRepository.save(new Grade("STU002", "English", 88.5, "A-", "submission link")); + gradeRepository.save(new Grade("STU003", "Science", 95.0, "A+", "submission link")); + // Sample Progress + progressRepository.save(new Progress("STU001", "Mathematics", 75.0, "In Progress")); + progressRepository.save(new Progress("STU001", "Science", 100.0, "Completed")); + progressRepository.save(new Progress("STU002", "Mathematics", 60.0, "In Progress")); + progressRepository.save(new Progress("STU002", "English", 90.0, "Completed")); + progressRepository.save(new Progress("STU003", "Science", 45.0, "In Progress")); - // Sample Progress - progressRepository.save(new Progress("STU001", "Mathematics", 75.0, "In Progress")); - progressRepository.save(new Progress("STU001", "Science", 100.0, "Completed")); - progressRepository.save(new Progress("STU002", "Mathematics", 60.0, "In Progress")); - progressRepository.save(new Progress("STU002", "English", 90.0, "Completed")); - progressRepository.save(new Progress("STU003", "Science", 45.0, "In Progress")); - - System.out.println("Sample data loaded successfully!"); + System.out.println("Sample data loaded successfully!"); + } catch (Exception e) { + System.err.println("Grades DataLoader skipped due to database schema mismatch: " + e.getMessage()); + } } -} +} \ No newline at end of file diff --git a/src/main/java/com/open/spring/mvc/leaderboard/LeaderboardController.java b/src/main/java/com/open/spring/mvc/leaderboard/LeaderboardController.java new file mode 100644 index 00000000..208c1ac8 --- /dev/null +++ b/src/main/java/com/open/spring/mvc/leaderboard/LeaderboardController.java @@ -0,0 +1,102 @@ +package com.open.spring.mvc.leaderboard; + +import com.open.spring.mvc.PauseMenu.ScoreCounter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import java.util.List; + +/** + * API Controller for Leaderboard (reads from score_counter table) + * CORS configured for public access without authentication + * + * Endpoints: + * - GET /api/leaderboard - Main leaderboard endpoint (all scores) + * - GET /api/pausemenu/score/leaderboard - Alias for compatibility with PauseMenu + * - GET /api/leaderboard/top/{limit} - Top N scores + * - GET /api/leaderboard/game/{gameName} - Scores for specific game + * - GET /api/leaderboard/user/{user} - Scores for specific user + */ +@RestController +@CrossOrigin( + origins = "*", + allowedHeaders = "*", + methods = { + org.springframework.web.bind.annotation.RequestMethod.GET, + org.springframework.web.bind.annotation.RequestMethod.OPTIONS + }, + allowCredentials = "false" +) +public class LeaderboardController { + + @Autowired + private LeaderboardService leaderboardService; + + /** + * READ - Get all leaderboard entries ordered by score + * This pulls directly from the score_counter table + * GET /api/leaderboard (primary endpoint - used by frontend) + * GET /api/pausemenu/score/leaderboard (alias to match PauseMenu path structure) + */ + @GetMapping({"/api/leaderboard", "/api/pausemenu/score/leaderboard"}) + public ResponseEntity> getAllEntries() { + try { + List entries = leaderboardService.getAllEntriesByScore(); + // Always return a valid JSON array, even if empty + if (entries == null) { + entries = List.of(); + } + System.out.println("Leaderboard: Returning " + entries.size() + " entries"); + return ResponseEntity.ok(entries); + } catch (Exception e) { + System.err.println("Error fetching leaderboard: " + e.getMessage()); + e.printStackTrace(); + return ResponseEntity.ok(List.of()); // Return empty array on error + } + } + + /** + * READ - Get top N scores + * GET /api/leaderboard/top/{limit} + */ + @GetMapping("/api/leaderboard/top/{limit}") + public ResponseEntity> getTopScores(@PathVariable int limit) { + List entries = leaderboardService.getTopScores(limit); + return ResponseEntity.ok(entries != null ? entries : List.of()); + } + + /** + * READ - Get leaderboard entries for a specific game + * GET /api/leaderboard/game/{gameName} + */ + @GetMapping("/api/leaderboard/game/{gameName}") + public ResponseEntity> getEntriesByGame(@PathVariable String gameName) { + List entries = leaderboardService.getEntriesByGame(gameName); + return ResponseEntity.ok(entries != null ? entries : List.of()); + } + + /** + * READ - Get leaderboard entries for a specific user + * GET /api/leaderboard/user/{user} + */ + @GetMapping("/api/leaderboard/user/{user}") + public ResponseEntity> getUserEntries(@PathVariable String user) { + List entries = leaderboardService.getUserEntries(user); + return ResponseEntity.ok(entries != null ? entries : List.of()); + } + + /** + * READ - Get entries for a specific user and game + * GET /api/leaderboard/user/{user}/game/{gameName} + */ + @GetMapping("/api/leaderboard/user/{user}/game/{gameName}") + public ResponseEntity> getUserGameEntries( + @PathVariable String user, + @PathVariable String gameName) { + List entries = leaderboardService.getUserGameEntries(user, gameName); + return ResponseEntity.ok(entries != null ? entries : List.of()); + } +} \ No newline at end of file diff --git a/src/main/java/com/open/spring/mvc/leaderboard/LeaderboardRepository.java b/src/main/java/com/open/spring/mvc/leaderboard/LeaderboardRepository.java new file mode 100644 index 00000000..29d1a830 --- /dev/null +++ b/src/main/java/com/open/spring/mvc/leaderboard/LeaderboardRepository.java @@ -0,0 +1,35 @@ +package com.open.spring.mvc.leaderboard; + +import com.open.spring.mvc.PauseMenu.ScoreCounter; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import java.util.List; + +// NOTE: This repository queries the existing score_counter table from PauseMenu +// No separate leaderboard table needed! + +@Repository +public interface LeaderboardRepository extends JpaRepository { + + // Get all scores ordered by score descending + @Query("SELECT s FROM ScoreCounter s ORDER BY s.score DESC") + List findAllByOrderByScoreDesc(); + + // Get top N scores + @Query("SELECT s FROM ScoreCounter s ORDER BY s.score DESC") + List findTopScores(); + + // Get scores for a specific game + @Query("SELECT s FROM ScoreCounter s WHERE s.gameName = :gameName ORDER BY s.score DESC") + List findByGameNameOrderByScoreDesc(@Param("gameName") String gameName); + + // Get scores for a specific user + @Query("SELECT s FROM ScoreCounter s WHERE s.user = :user ORDER BY s.score DESC") + List findByUserOrderByScoreDesc(@Param("user") String user); + + // Get scores for a specific user and game + @Query("SELECT s FROM ScoreCounter s WHERE s.user = :user AND s.gameName = :gameName ORDER BY s.score DESC") + List findByUserAndGameNameOrderByScoreDesc(@Param("user") String user, @Param("gameName") String gameName); +} \ No newline at end of file diff --git a/src/main/java/com/open/spring/mvc/leaderboard/LeaderboardService.java b/src/main/java/com/open/spring/mvc/leaderboard/LeaderboardService.java new file mode 100644 index 00000000..b7d9434d --- /dev/null +++ b/src/main/java/com/open/spring/mvc/leaderboard/LeaderboardService.java @@ -0,0 +1,55 @@ +package com.open.spring.mvc.leaderboard; + +import com.open.spring.mvc.PauseMenu.ScoreCounter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import java.util.List; +import java.util.stream.Collectors; + +// This service reads directly from the score_counter table (PauseMenu) +// No separate leaderboard table needed! + +@Service +public class LeaderboardService { + + @Autowired + private LeaderboardRepository leaderboardRepository; + + /** + * Get top N entries from pausemenu table + */ + public List getTopScores(int limit) { + return leaderboardRepository.findTopScores() + .stream() + .limit(limit) + .collect(Collectors.toList()); + } + + /** + * Get all entries ordered by score (for the leaderboard widget) + */ + public List getAllEntriesByScore() { + return leaderboardRepository.findAllByOrderByScoreDesc(); + } + + /** + * Get entries for a specific game + */ + public List getEntriesByGame(String gameName) { + return leaderboardRepository.findByGameNameOrderByScoreDesc(gameName); + } + + /** + * Get entries for a specific user + */ + public List getUserEntries(String user) { + return leaderboardRepository.findByUserOrderByScoreDesc(user); + } + + /** + * Get entries for a specific user and game + */ + public List getUserGameEntries(String user, String gameName) { + return leaderboardRepository.findByUserAndGameNameOrderByScoreDesc(user, gameName); + } +} \ No newline at end of file diff --git a/src/main/java/com/open/spring/mvc/multiplayer/Player.java b/src/main/java/com/open/spring/mvc/multiplayer/Player.java new file mode 100644 index 00000000..30811aca --- /dev/null +++ b/src/main/java/com/open/spring/mvc/multiplayer/Player.java @@ -0,0 +1,44 @@ +package com.open.spring.mvc.multiplayer; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + +@Entity +@Data +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "players") +public class Player { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + @Column(nullable = false, unique = true) + private String uid; // username + + @Column(nullable = false) + private String status; // "online" or "offline" + + @Column(name = "last_active") + private LocalDateTime lastActive; + + @Column(name = "connected_at") + private LocalDateTime connectedAt; + + private double x; + private double y; + private int level; + + // Constructor + public Player(String uid, String status) { + this.uid = uid; + this.status = status; + this.lastActive = LocalDateTime.now(); + this.connectedAt = LocalDateTime.now(); + } +} + diff --git a/src/main/java/com/open/spring/mvc/multiplayer/PlayerController.java b/src/main/java/com/open/spring/mvc/multiplayer/PlayerController.java new file mode 100644 index 00000000..efdd8b5d --- /dev/null +++ b/src/main/java/com/open/spring/mvc/multiplayer/PlayerController.java @@ -0,0 +1,145 @@ +package com.open.spring.mvc.multiplayer; + +import com.open.spring.mvc.person.Person; +import com.open.spring.mvc.person.PersonJpaRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/players") +@CrossOrigin +public class PlayerController { + + @Autowired + private PlayerRepository playerRepository; + + @Autowired + private PersonJpaRepository personRepository; + + // Get current logged-in user and add to player table + @GetMapping("/current") + public Player getCurrentUser() { + String currentUsername = SecurityContextHolder.getContext() + .getAuthentication().getName(); + Person person = personRepository.findByUid(currentUsername); + + if (person == null) { + throw new RuntimeException("User not found"); + } + + // Check if player already exists + Player player = playerRepository.findByUid(person.getUid()) + .orElse(new Player()); + + // Update or create player with just username + player.setUid(person.getUid()); + player.setStatus("online"); + player.setLastActive(LocalDateTime.now()); + if (player.getConnectedAt() == null) { + player.setConnectedAt(LocalDateTime.now()); + } + + return playerRepository.save(player); + } + + @PostMapping("/connect") + public Player connect(@RequestBody Map request) { + String uid = request.get("uid"); + + Person person = personRepository.findByUid(uid); + if (person == null) { + throw new RuntimeException("User not found"); + } + + Player player = playerRepository.findByUid(uid) + .orElse(new Player()); + + player.setUid(uid); + player.setStatus("online"); + player.setLastActive(LocalDateTime.now()); + if (player.getConnectedAt() == null) { + player.setConnectedAt(LocalDateTime.now()); + } + + return playerRepository.save(player); + } + + @GetMapping("/online") + public Map getOnlinePlayers() { + List onlinePlayers = playerRepository.findByStatus("online"); + return Map.of("players", onlinePlayers); + } + + @PutMapping("/status") + public Player updateStatus(@RequestBody Map request) { + String uid = request.get("uid"); + String status = request.get("status"); + + Player player = playerRepository.findByUid(uid) + .orElseThrow(() -> new RuntimeException("Player not found")); + + player.setStatus(status); + player.setLastActive(LocalDateTime.now()); + + return playerRepository.save(player); + } + + @PostMapping("/disconnect") + public void disconnect(@RequestBody Map request) { + String uid = request.get("uid"); + + Player player = playerRepository.findByUid(uid) + .orElseThrow(() -> new RuntimeException("Player not found")); + + player.setStatus("offline"); + playerRepository.save(player); + } + + @PutMapping("/location") + public Player updateLocation(@RequestBody Map request) { + String uid = (String) request.get("uid"); + double x = (double) request.get("x"); + double y = (double) request.get("y"); + + Player player = playerRepository.findByUid(uid) + .orElseThrow(() -> new RuntimeException("Player not found")); + + player.setX(x); + player.setY(y); + player.setLastActive(LocalDateTime.now()); + + return playerRepository.save(player); + } + + @PutMapping("/level") + public Player updateLevel(@RequestBody Map request) { + String uid = (String) request.get("uid"); + int level = (int) request.get("level"); + + Player player = playerRepository.findByUid(uid) + .orElseThrow(() -> new RuntimeException("Player not found")); + + player.setLevel(level); + player.setLastActive(LocalDateTime.now()); + + return playerRepository.save(player); + } + + @GetMapping("/locations") + public Map getPlayerLocations() { + List players = playerRepository.findByStatus("online"); + + return Map.of("players", players.stream().map(p -> Map.of( + "uid", p.getUid(), + "x", p.getX(), + "y", p.getY(), + "level", p.getLevel() + )).toList()); + } +} + + diff --git a/src/main/java/com/open/spring/mvc/multiplayer/PlayerInit.java b/src/main/java/com/open/spring/mvc/multiplayer/PlayerInit.java new file mode 100644 index 00000000..9af6fc57 --- /dev/null +++ b/src/main/java/com/open/spring/mvc/multiplayer/PlayerInit.java @@ -0,0 +1,45 @@ +package com.open.spring.mvc.multiplayer; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.LocalDateTime; +import java.util.Random; + +@Configuration +public class PlayerInit { + + @Bean + CommandLineRunner initGamers(PlayerRepository repository) { + return args -> { + + if (repository.count() == 0) { + + int count = 50; + String[] uids = new String[count]; + for (int i = 0; i < count; i++) { + uids[i] = "player" + (i + 1); + } + + Random random = new Random(); + + for (String uid : uids) { + Player player = new Player(); + player.setUid(uid); + player.setStatus("offline"); + player.setLevel(random.nextInt(10) + 1); + player.setX(random.nextDouble() * 1000.0); + player.setY(random.nextDouble() * 1000.0); + player.setLastActive(LocalDateTime.now()); + player.setConnectedAt(LocalDateTime.now()); + + repository.save(player); + } + + System.out.println("Gamer database initialized with " + + uids.length + " players"); + } + }; + } +} \ No newline at end of file diff --git a/src/main/java/com/open/spring/mvc/multiplayer/PlayerRepository.java b/src/main/java/com/open/spring/mvc/multiplayer/PlayerRepository.java new file mode 100644 index 00000000..a316cb95 --- /dev/null +++ b/src/main/java/com/open/spring/mvc/multiplayer/PlayerRepository.java @@ -0,0 +1,14 @@ +package com.open.spring.mvc.multiplayer; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Optional; + +@Repository +public interface PlayerRepository extends JpaRepository { + Optional findByUid(String uid); + List findByStatus(String status); + List findByLevel(int level); + List findByXBetweenAndYBetween(double x1, double x2, double y1, double y2); +} \ No newline at end of file diff --git a/src/main/java/com/open/spring/security/SecurityConfig.java b/src/main/java/com/open/spring/security/SecurityConfig.java index bae7d849..5e58e77e 100644 --- a/src/main/java/com/open/spring/security/SecurityConfig.java +++ b/src/main/java/com/open/spring/security/SecurityConfig.java @@ -83,6 +83,12 @@ public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exce // ========== PUBLIC API ENDPOINTS ========== // Intentionally public - used for polling and public features .requestMatchers("/api/jokes/**").permitAll() + // Pause Menu APIs should be public + .requestMatchers("/api/pausemenu/**").permitAll() + // Leaderboard should be public - displays scores without authentication + .requestMatchers("/api/leaderboard/**").permitAll() + // Frontend calls gamer score endpoint; make it public + .requestMatchers("/api/gamer/**").permitAll() // ========================================== .requestMatchers("/api/exports/**").hasAuthority("ROLE_ADMIN") .requestMatchers("/api/imports/**").hasAuthority("ROLE_ADMIN") diff --git a/src/main/resources/static/js/pauseMenu/score.js b/src/main/resources/static/js/pauseMenu/score.js new file mode 100644 index 00000000..ae40e3e7 --- /dev/null +++ b/src/main/resources/static/js/pauseMenu/score.js @@ -0,0 +1,183 @@ +/** + * Score Management Module + * Handles saving, retrieving, and managing game scores + * Communicates with /api/score backend endpoints + */ + +class ScoreManager { + constructor(backendUrl = "http://localhost:8585") { + this.backendUrl = backendUrl; + this.apiEndpoint = `${this.backendUrl}/api/score`; + } + + /** + * Save a score to the database + * @param {string} personName - The player's name + * @param {number} score - The score to save + * @returns {Promise} - Response from the server + */ + async saveScore(personName, score) { + try { + const response = await fetch(`${this.apiEndpoint}/save`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + personName: personName, + score: parseInt(score) + }) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log("Score saved successfully:", data); + return data; + } catch (error) { + console.error("Error saving score:", error); + throw error; + } + } + + /** + * Get all scores for a specific player + * @param {string} personName - The player's name + * @returns {Promise} - Array of scores for that player + */ + async getPlayerScores(personName) { + try { + const response = await fetch(`${this.apiEndpoint}/person/${personName}`); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log(`Scores for ${personName}:`, data); + return data; + } catch (error) { + console.error("Error fetching player scores:", error); + throw error; + } + } + + /** + * Get all scores in the database + * @returns {Promise} - Array of all scores + */ + async getAllScores() { + try { + const response = await fetch(`${this.apiEndpoint}/all`); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log("All scores:", data); + return data; + } catch (error) { + console.error("Error fetching all scores:", error); + throw error; + } + } + + /** + * Get a score by ID + * @param {number} scoreId - The score ID + * @returns {Promise} - Score object + */ + async getScoreById(scoreId) { + try { + const response = await fetch(`${this.apiEndpoint}/${scoreId}`); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log("Score found:", data); + return data; + } catch (error) { + console.error("Error fetching score:", error); + throw error; + } + } + + /** + * Delete a score by ID + * @param {number} scoreId - The score ID to delete + * @returns {Promise} - Response message + */ + async deleteScore(scoreId) { + try { + const response = await fetch(`${this.apiEndpoint}/${scoreId}`, { + method: "DELETE" + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const message = await response.text(); + console.log("Score deleted:", message); + return message; + } catch (error) { + console.error("Error deleting score:", error); + throw error; + } + } + + /** + * Display scores on the page (helper function) + * @param {Array} scores - Array of score objects + * @param {string} containerId - ID of the HTML element to display scores in + */ + displayScores(scores, containerId = "scores-container") { + const container = document.getElementById(containerId); + if (!container) { + console.error(`Container with ID '${containerId}' not found`); + return; + } + + if (scores.length === 0) { + container.innerHTML = "

No scores found.

"; + return; + } + + let html = ""; + scores.forEach(score => { + html += ``; + }); + html += "
IDPlayerScore
${score.id}${score.personName}${score.score}
"; + + container.innerHTML = html; + } + + /** + * Get the highest score from an array of scores + * @param {Array} scores - Array of score objects + * @returns {Object|null} - Highest score object or null if empty + */ + getHighestScore(scores) { + if (!scores || scores.length === 0) return null; + return scores.reduce((max, score) => (score.score > max.score) ? score : max); + } + + /** + * Get the average score from an array of scores + * @param {Array} scores - Array of score objects + * @returns {number} - Average score + */ + getAverageScore(scores) { + if (!scores || scores.length === 0) return 0; + const sum = scores.reduce((acc, score) => acc + score.score, 0); + return (sum / scores.length).toFixed(2); + } +} + +// Create a global instance for easy access +const scoreManager = new ScoreManager();