mirror of
https://github.com/HexCardGames/HexDeck.git
synced 2025-09-05 03:08:39 +02:00
Compare commits
7 Commits
37c47a7b72
...
bdc8a151f4
Author | SHA1 | Date | |
---|---|---|---|
bdc8a151f4 | |||
0d3b42193f | |||
6349d5055f | |||
338710d762 | |||
2cfcd0089f | |||
4104b01978 | |||
7bbe33725f |
@ -95,6 +95,36 @@ func onPlayerJoin(client *socketio.Socket, room *types.Room, player *types.Playe
|
|||||||
game.OnRoomUpdate(room)
|
game.OnRoomUpdate(room)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
client.On("SetCardDeck", func(datas ...any) {
|
||||||
|
setCardDeckRequest := types.C2S_SetCardDeck{}
|
||||||
|
unpackData(datas, &setCardDeckRequest)
|
||||||
|
|
||||||
|
if room.GameState != types.StateLobby {
|
||||||
|
client.Emit("Status", types.S2C_Status{
|
||||||
|
IsError: true,
|
||||||
|
StatusCode: "game_already_running",
|
||||||
|
Message: "You can't change the card deck while the game is running",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !player.HasPermissionBit(types.PermissionHost) {
|
||||||
|
client.Emit("Status", types.S2C_Status{
|
||||||
|
IsError: true,
|
||||||
|
StatusCode: "insufficient_permission",
|
||||||
|
Message: "You can't change the card deck unless you are host",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !game.SetCardDeck(room, setCardDeckRequest.CardDeckId) {
|
||||||
|
client.Emit("Status", types.S2C_Status{
|
||||||
|
IsError: true,
|
||||||
|
StatusCode: "invalid_card_deck",
|
||||||
|
Message: "No card deck exists with this ID",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
client.On("UpdatePlayer", func(datas ...any) {
|
client.On("UpdatePlayer", func(datas ...any) {
|
||||||
player.Mutex.Lock()
|
player.Mutex.Lock()
|
||||||
defer player.Mutex.Unlock()
|
defer player.Mutex.Unlock()
|
||||||
@ -248,7 +278,7 @@ func onPlayerJoin(client *socketio.Socket, room *types.Room, player *types.Playe
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
card := player.Cards[*updatePlayerRequest.CardIndex]
|
card := player.Cards[*updatePlayerRequest.CardIndex]
|
||||||
if !room.CardDeck.PlayCard(card) {
|
if !room.CardDeck.CanPlay(card) {
|
||||||
client.Emit("Status", types.S2C_Status{
|
client.Emit("Status", types.S2C_Status{
|
||||||
IsError: true,
|
IsError: true,
|
||||||
StatusCode: "card_not_playable",
|
StatusCode: "card_not_playable",
|
||||||
@ -257,6 +287,9 @@ func onPlayerJoin(client *socketio.Socket, room *types.Room, player *types.Playe
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
player.Cards = append(player.Cards[:*updatePlayerRequest.CardIndex], player.Cards[*updatePlayerRequest.CardIndex+1:]...)
|
player.Cards = append(player.Cards[:*updatePlayerRequest.CardIndex], player.Cards[*updatePlayerRequest.CardIndex+1:]...)
|
||||||
|
if !room.CardDeck.PlayCard(card) {
|
||||||
|
slog.Error("Cannot play card after checking", "roomId", room.RoomId.Hex(), "playerId", player.PlayerId.Hex())
|
||||||
|
}
|
||||||
game.OnPlayCard(room, player, *updatePlayerRequest.CardIndex, card)
|
game.OnPlayCard(room, player, *updatePlayerRequest.CardIndex, card)
|
||||||
|
|
||||||
if len(player.Cards) == 0 {
|
if len(player.Cards) == 0 {
|
||||||
|
@ -13,6 +13,10 @@ func DeckFromInterface(cardDeckId int, cardDeck bson.D) types.CardDeck {
|
|||||||
deck := Classic{}
|
deck := Classic{}
|
||||||
bson.Unmarshal(bsonBytes, &deck)
|
bson.Unmarshal(bsonBytes, &deck)
|
||||||
return &deck
|
return &deck
|
||||||
|
case 1:
|
||||||
|
deck := HexV1{}
|
||||||
|
bson.Unmarshal(bsonBytes, &deck)
|
||||||
|
return &deck
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -26,6 +30,10 @@ func CardFromInterface(cardDeckId int, card bson.D) types.Card {
|
|||||||
deck := ClassicCard{}
|
deck := ClassicCard{}
|
||||||
bson.Unmarshal(bsonBytes, &deck)
|
bson.Unmarshal(bsonBytes, &deck)
|
||||||
return &deck
|
return &deck
|
||||||
|
case 1:
|
||||||
|
deck := HexV1Card{}
|
||||||
|
bson.Unmarshal(bsonBytes, &deck)
|
||||||
|
return &deck
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
205
backend/decks/hexv1.go
Normal file
205
backend/decks/hexv1.go
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
package decks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand/v2"
|
||||||
|
|
||||||
|
"github.com/HexCardGames/HexDeck/types"
|
||||||
|
"github.com/HexCardGames/HexDeck/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HexV1 struct {
|
||||||
|
room *types.Room
|
||||||
|
CardsPlayed []*HexV1Card
|
||||||
|
PlayerOrder []int
|
||||||
|
ActiveIndex int
|
||||||
|
}
|
||||||
|
|
||||||
|
type HexV1Card struct {
|
||||||
|
Symbol string
|
||||||
|
Color string
|
||||||
|
NumericValue int
|
||||||
|
}
|
||||||
|
|
||||||
|
var HexV1Colors = []string{"blue", "green", "yellow", "purple"}
|
||||||
|
var HexV1ActionCards = []string{"shuffle", "skip", "draw", "swap"}
|
||||||
|
|
||||||
|
func (deck *HexV1) Init(room *types.Room) {
|
||||||
|
deck.room = room
|
||||||
|
deck.PlayerOrder = make([]int, len(room.Players))
|
||||||
|
deck.ActiveIndex = 0
|
||||||
|
|
||||||
|
deck.room.PlayersMutex.Lock()
|
||||||
|
defer deck.room.PlayersMutex.Unlock()
|
||||||
|
for i, player := range deck.room.Players {
|
||||||
|
deck.PlayerOrder[i] = i
|
||||||
|
player.Mutex.Lock()
|
||||||
|
defer player.Mutex.Unlock()
|
||||||
|
deck.drawMany(player, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deck *HexV1) SetRoom(room *types.Room) {
|
||||||
|
deck.room = room
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deck *HexV1) IsEmpty() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deck *HexV1) getTopCard() *HexV1Card {
|
||||||
|
if len(deck.CardsPlayed) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return deck.CardsPlayed[len(deck.CardsPlayed)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deck *HexV1) GetTopCard() types.Card {
|
||||||
|
return deck.getTopCard()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deck *HexV1) generateCard() *HexV1Card {
|
||||||
|
cardType := rand.IntN(16 + len(HexV1ActionCards))
|
||||||
|
cardColor := HexV1Colors[rand.IntN(len(HexV1Colors))]
|
||||||
|
if cardType < 16 {
|
||||||
|
return &HexV1Card{
|
||||||
|
Symbol: fmt.Sprintf("%x", cardType),
|
||||||
|
Color: cardColor,
|
||||||
|
NumericValue: cardType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cardSymbol := HexV1ActionCards[cardType-16]
|
||||||
|
if rand.IntN(100) <= 10 {
|
||||||
|
cardColor = "rainbow"
|
||||||
|
}
|
||||||
|
return &HexV1Card{
|
||||||
|
Symbol: "action:" + cardSymbol,
|
||||||
|
Color: cardColor,
|
||||||
|
NumericValue: 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deck *HexV1) drawCard(player *types.Player) types.Card {
|
||||||
|
card := deck.generateCard()
|
||||||
|
player.Cards = append(player.Cards, card)
|
||||||
|
return card
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deck *HexV1) drawMany(player *types.Player, cards int) {
|
||||||
|
for i := 0; i < cards; i++ {
|
||||||
|
deck.drawCard(player)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deck *HexV1) DrawCard() types.Card {
|
||||||
|
// Can't draw another card before wildcard color is selected
|
||||||
|
topCard := deck.getTopCard()
|
||||||
|
if topCard != nil && topCard.Color == "rainbow" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
card := deck.drawCard(deck.getPlayer(deck.ActiveIndex))
|
||||||
|
deck.nextPlayer()
|
||||||
|
return card
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deck *HexV1) getNextValidIndex(index int) int {
|
||||||
|
if len(deck.room.Players) == 0 || len(deck.PlayerOrder) == 0 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
checkIndex := utils.Mod(index, len(deck.PlayerOrder))
|
||||||
|
for deck.PlayerOrder[checkIndex] >= len(deck.room.Players) {
|
||||||
|
checkIndex = utils.Mod(checkIndex+1, len(deck.PlayerOrder))
|
||||||
|
}
|
||||||
|
return checkIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deck *HexV1) getPlayer(index int) *types.Player {
|
||||||
|
playerIndex := deck.getNextValidIndex(index)
|
||||||
|
if playerIndex == -1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return deck.room.Players[deck.PlayerOrder[playerIndex]]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deck *HexV1) getNextPlayerIndex() int {
|
||||||
|
return deck.getNextValidIndex(deck.ActiveIndex + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deck *HexV1) nextPlayer() {
|
||||||
|
deck.ActiveIndex = deck.getNextPlayerIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deck *HexV1) IsPlayerActive(target *types.Player) bool {
|
||||||
|
return deck.getPlayer(deck.ActiveIndex) == target
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deck *HexV1) CanPlay(card types.Card) bool {
|
||||||
|
topCard := deck.getTopCard()
|
||||||
|
checkCard := card.(*HexV1Card)
|
||||||
|
if topCard == nil || checkCard == nil {
|
||||||
|
return topCard == nil
|
||||||
|
}
|
||||||
|
return topCard.Color != "rainbow" && (checkCard.Color == "rainbow" || checkCard.Color == topCard.Color || checkCard.Symbol == topCard.Symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deck *HexV1) PlayCard(card types.Card) bool {
|
||||||
|
if !deck.CanPlay(card) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
deckCard := card.(*HexV1Card)
|
||||||
|
targetPlayer := deck.getPlayer(deck.ActiveIndex)
|
||||||
|
nextPlayer := deck.getPlayer(deck.getNextPlayerIndex())
|
||||||
|
|
||||||
|
if deckCard.Symbol == "action:skip" && deckCard.Color != "rainbow" {
|
||||||
|
deck.nextPlayer()
|
||||||
|
} else if deckCard.Symbol == "action:draw" {
|
||||||
|
amount := 3
|
||||||
|
topCard := deck.getTopCard()
|
||||||
|
if topCard != nil {
|
||||||
|
amount = topCard.NumericValue
|
||||||
|
}
|
||||||
|
deck.drawMany(nextPlayer, amount)
|
||||||
|
} else if deckCard.Symbol == "action:shuffle" {
|
||||||
|
utils.ShuffleSlice(&deck.PlayerOrder)
|
||||||
|
} else if deckCard.Symbol == "action:swap" {
|
||||||
|
p1Cards := targetPlayer.Cards
|
||||||
|
p2Cards := nextPlayer.Cards
|
||||||
|
targetPlayer.Cards = p2Cards
|
||||||
|
nextPlayer.Cards = p1Cards
|
||||||
|
}
|
||||||
|
|
||||||
|
if deckCard.Color != "rainbow" {
|
||||||
|
deck.nextPlayer()
|
||||||
|
}
|
||||||
|
|
||||||
|
deck.CardsPlayed = append(deck.CardsPlayed, deckCard)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (deck *HexV1) UpdatePlayedCard(cardData interface{}) types.Card {
|
||||||
|
topCard := deck.getTopCard()
|
||||||
|
if topCard.Color != "rainbow" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
updateData, ok := cardData.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
newColor, ok := updateData["Color"].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, color := range HexV1Colors {
|
||||||
|
if newColor == color {
|
||||||
|
deck.nextPlayer()
|
||||||
|
if topCard.Symbol == "action:skip" {
|
||||||
|
deck.nextPlayer()
|
||||||
|
}
|
||||||
|
topCard.Color = color
|
||||||
|
return topCard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -39,7 +39,7 @@ func CreateRoom() *types.Room {
|
|||||||
GameState: types.StateLobby,
|
GameState: types.StateLobby,
|
||||||
Players: make([]*types.Player, 0),
|
Players: make([]*types.Player, 0),
|
||||||
PlayersMutex: &sync.Mutex{},
|
PlayersMutex: &sync.Mutex{},
|
||||||
CardDeckId: 0,
|
CardDeckId: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
db.Conn.InsertRoom(newRoom)
|
db.Conn.InsertRoom(newRoom)
|
||||||
@ -123,6 +123,24 @@ func UpdateGameState(room *types.Room, newState types.GameState) {
|
|||||||
OnRoomUpdate(room)
|
OnRoomUpdate(room)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetCardDeck(room *types.Room, id int) bool {
|
||||||
|
if id < 0 || id > 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
room.CardDeckId = id
|
||||||
|
OnRoomUpdate(room)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateCardDeckObj(room *types.Room) {
|
||||||
|
switch room.CardDeckId {
|
||||||
|
case 0:
|
||||||
|
room.CardDeck = &decks.Classic{}
|
||||||
|
case 1:
|
||||||
|
room.CardDeck = &decks.HexV1{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func BroadcastInRoom(room *types.Room, topic string, data interface{}) {
|
func BroadcastInRoom(room *types.Room, topic string, data interface{}) {
|
||||||
for _, player := range room.Players {
|
for _, player := range room.Players {
|
||||||
if !player.Connection.IsConnected || player.Connection.Socket == nil {
|
if !player.Connection.IsConnected || player.Connection.Socket == nil {
|
||||||
@ -179,7 +197,7 @@ func StartGame(room *types.Room) {
|
|||||||
if room.GameState != types.StateLobby {
|
if room.GameState != types.StateLobby {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
room.CardDeck = &decks.Classic{}
|
CreateCardDeckObj(room)
|
||||||
room.CardDeck.Init(room)
|
room.CardDeck.Init(room)
|
||||||
UpdateGameState(room, types.StateRunning)
|
UpdateGameState(room, types.StateRunning)
|
||||||
UpdateAllPlayers(room)
|
UpdateAllPlayers(room)
|
||||||
|
@ -47,6 +47,9 @@ type S2C_PlayedCardUpdate struct {
|
|||||||
Card Card
|
Card Card
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type C2S_SetCardDeck struct {
|
||||||
|
CardDeckId int
|
||||||
|
}
|
||||||
type C2S_UpdatePlayer struct {
|
type C2S_UpdatePlayer struct {
|
||||||
PlayerId bson.ObjectID
|
PlayerId bson.ObjectID
|
||||||
Username *string
|
Username *string
|
||||||
|
@ -34,5 +34,8 @@ func ShuffleSlice[T any](slice *([]T)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Mod(a, b int) int {
|
func Mod(a, b int) int {
|
||||||
|
if b == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
return (a%b + b) % b
|
return (a%b + b) % b
|
||||||
}
|
}
|
||||||
|
29
frontend/src/components/CardDeckShowcase.svelte
Normal file
29
frontend/src/components/CardDeckShowcase.svelte
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import CardDisplay from "./Game/CardDisplay.svelte";
|
||||||
|
|
||||||
|
export let cardComponent;
|
||||||
|
export let cardDeckId: number;
|
||||||
|
export let cardWidth: number;
|
||||||
|
export let cardHeight: number;
|
||||||
|
export let centerDistancePx: number;
|
||||||
|
export let maxRotationDeg: number;
|
||||||
|
|
||||||
|
const SHOWCASED_CARDS: any = {
|
||||||
|
0: [
|
||||||
|
{ CanPlay: true, Card: { Symbol: "1", Color: "red" } },
|
||||||
|
{ CanPlay: true, Card: { Symbol: "9", Color: "green" } },
|
||||||
|
{ CanPlay: true, Card: { Symbol: "action:draw_2", Color: "blue" } },
|
||||||
|
{ CanPlay: true, Card: { Symbol: "action:skip", Color: "yellow" } },
|
||||||
|
{ CanPlay: true, Card: { Symbol: "action:draw_4", Color: "black" } },
|
||||||
|
],
|
||||||
|
1: [
|
||||||
|
{ CanPlay: true, Card: { Symbol: "1", Color: "blue" } },
|
||||||
|
{ CanPlay: true, Card: { Symbol: "F", Color: "green" } },
|
||||||
|
{ CanPlay: true, Card: { Symbol: "action:shuffle", Color: "yellow" } },
|
||||||
|
{ CanPlay: true, Card: { Symbol: "action:skip", Color: "purple" } },
|
||||||
|
{ CanPlay: true, Card: { Symbol: "action:draw", Color: "rainbow" } },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CardDisplay canPlayCards={true} canUpdateCards={false} {cardComponent} cards={SHOWCASED_CARDS[cardDeckId]} {cardWidth} {cardHeight} {centerDistancePx} {maxRotationDeg} />
|
110
frontend/src/components/Cards/HexV1Card.svelte
Normal file
110
frontend/src/components/Cards/HexV1Card.svelte
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Card } from "../../stores/sessionStore";
|
||||||
|
|
||||||
|
interface ClassicCard {
|
||||||
|
Symbol: string;
|
||||||
|
Color: string;
|
||||||
|
}
|
||||||
|
export let data: ClassicCard = { Color: "", Symbol: "" };
|
||||||
|
export let canUpdateCard: boolean = false;
|
||||||
|
export let updateCard: (newCard: Card) => void = () => {};
|
||||||
|
export let width: number;
|
||||||
|
export let height: number;
|
||||||
|
|
||||||
|
const COLOR_MAP: { [key: string]: string } = {
|
||||||
|
blue: "#009bff",
|
||||||
|
green: "#00c841",
|
||||||
|
yellow: "#ffa500",
|
||||||
|
purple: "#7300c8",
|
||||||
|
rainbow: "#ababab",
|
||||||
|
black: "#000000",
|
||||||
|
"": "#999",
|
||||||
|
};
|
||||||
|
|
||||||
|
function selectColor(color: string) {
|
||||||
|
let newCard: ClassicCard = {
|
||||||
|
Color: color,
|
||||||
|
Symbol: data.Symbol,
|
||||||
|
};
|
||||||
|
updateCard(newCard);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="card" style:background={COLOR_MAP[data.Color]} style:--width={`${width}px`} style:--height={`${height}px`}>
|
||||||
|
{#if data.Symbol.length <= 2}
|
||||||
|
<span class="symbol">{data.Symbol}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="symbol large">{data.Symbol.split(":")[1].replace("_", " ")}</span>
|
||||||
|
{/if}
|
||||||
|
{#if data.Color == "rainbow" && canUpdateCard}
|
||||||
|
<div class="select mt-5">
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
style:background={COLOR_MAP["blue"]}
|
||||||
|
aria-label="Blue"
|
||||||
|
on:click={() => {
|
||||||
|
selectColor("blue");
|
||||||
|
}}
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
style:background={COLOR_MAP["green"]}
|
||||||
|
aria-label="Green"
|
||||||
|
on:click={() => {
|
||||||
|
selectColor("green");
|
||||||
|
}}
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
style:background={COLOR_MAP["yellow"]}
|
||||||
|
aria-label="Yellow"
|
||||||
|
on:click={() => {
|
||||||
|
selectColor("yellow");
|
||||||
|
}}
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
style:background={COLOR_MAP["purple"]}
|
||||||
|
aria-label="Purple"
|
||||||
|
on:click={() => {
|
||||||
|
selectColor("purple");
|
||||||
|
}}
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: var(--width);
|
||||||
|
height: var(--height);
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select button {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card span {
|
||||||
|
text-align: left;
|
||||||
|
word-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .symbol {
|
||||||
|
font-size: 25px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .symbol.large {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
@ -69,7 +69,13 @@
|
|||||||
"player_name": "Spielername",
|
"player_name": "Spielername",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"host": "Host",
|
"host": "Host",
|
||||||
"you": "Du"
|
"you": "Du",
|
||||||
|
"player": "Spieler",
|
||||||
|
"return_to_game": "Zurück zum Spiel",
|
||||||
|
"selected_card_deck": "Ausgewähltes Kartendeck:",
|
||||||
|
"change_card_deck": "Ändern",
|
||||||
|
"choose_card_deck": "Auswählen",
|
||||||
|
"card_deck_modal": "Wähle ein Kartendeck aus"
|
||||||
},
|
},
|
||||||
"end_screen": {
|
"end_screen": {
|
||||||
"game_has_ended": "Das Spiel ist vorbei",
|
"game_has_ended": "Das Spiel ist vorbei",
|
||||||
@ -83,7 +89,7 @@
|
|||||||
"disconnected": "Getrennt"
|
"disconnected": "Getrennt"
|
||||||
},
|
},
|
||||||
"game_status": {
|
"game_status": {
|
||||||
"game_status": "Spielstatus: {game_status}",
|
"game_status": "Spielstatus:",
|
||||||
"lobby": "Lobby",
|
"lobby": "Lobby",
|
||||||
"running": "Läuft",
|
"running": "Läuft",
|
||||||
"ended": "Beendet"
|
"ended": "Beendet"
|
||||||
|
@ -71,7 +71,11 @@
|
|||||||
"host": "Host",
|
"host": "Host",
|
||||||
"you": "You",
|
"you": "You",
|
||||||
"player": "Player",
|
"player": "Player",
|
||||||
"return_to_game": "Return to game"
|
"return_to_game": "Return to game",
|
||||||
|
"selected_card_deck": "Selected card deck:",
|
||||||
|
"change_card_deck": "Change",
|
||||||
|
"choose_card_deck": "Choose",
|
||||||
|
"card_deck_modal": "Select a card deck"
|
||||||
},
|
},
|
||||||
"end_screen": {
|
"end_screen": {
|
||||||
"game_has_ended": "The game has ended",
|
"game_has_ended": "The game has ended",
|
||||||
|
@ -10,6 +10,11 @@
|
|||||||
import { requestJoinRoom } from "../stores/roomStore";
|
import { requestJoinRoom } from "../stores/roomStore";
|
||||||
import gameStore from "../stores/gameStore";
|
import gameStore from "../stores/gameStore";
|
||||||
|
|
||||||
|
let maxRotationDeg = 20;
|
||||||
|
let centerDistancePx = 200;
|
||||||
|
let cardWidth = 100;
|
||||||
|
let cardHeight = 150;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// TODO: check if already connected to room, currently its overwriting the session
|
// TODO: check if already connected to room, currently its overwriting the session
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
@ -51,7 +56,7 @@
|
|||||||
{#if $sessionStore.gameState == GameState.Lobby}
|
{#if $sessionStore.gameState == GameState.Lobby}
|
||||||
<div>
|
<div>
|
||||||
<!-- Lobby and player list -->
|
<!-- Lobby and player list -->
|
||||||
<Lobby />
|
<Lobby {cardWidth} {cardHeight} {centerDistancePx} {maxRotationDeg} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@ -59,11 +64,11 @@
|
|||||||
<div class="size-full">
|
<div class="size-full">
|
||||||
{#if $gameStore.isLobbyOverlayShown}
|
{#if $gameStore.isLobbyOverlayShown}
|
||||||
<div class="absolute inset-0 z-10 bg-white/30 dark:bg-black/30 backdrop-blur-sm mt-24">
|
<div class="absolute inset-0 z-10 bg-white/30 dark:bg-black/30 backdrop-blur-sm mt-24">
|
||||||
<Lobby />
|
<Lobby {cardWidth} {cardHeight} {centerDistancePx} {maxRotationDeg} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<!-- Running game -->
|
<!-- Running game -->
|
||||||
<Main />
|
<Main {cardWidth} {cardHeight} {centerDistancePx} {maxRotationDeg} />
|
||||||
</div>
|
</div>
|
||||||
{:else if $sessionStore.gameState == GameState.Ended}
|
{:else if $sessionStore.gameState == GameState.Ended}
|
||||||
<EndScreen />
|
<EndScreen />
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<div class="page-slot">
|
<div class="page-slot" style="overflow-y: auto;">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
13
frontend/src/stores/cardDeck.ts
Normal file
13
frontend/src/stores/cardDeck.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import ClassicCard from "../components/Cards/ClassicCard.svelte";
|
||||||
|
import HexV1Card from "../components/Cards/HexV1Card.svelte";
|
||||||
|
|
||||||
|
interface CardDeck {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
cardComponent: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CardDecks: CardDeck[] = [
|
||||||
|
{ id: 0, name: "Classic", cardComponent: ClassicCard },
|
||||||
|
{ id: 1, name: "HexV1", cardComponent: HexV1Card },
|
||||||
|
];
|
@ -80,6 +80,10 @@ interface PlayedCardUpdateObj {
|
|||||||
Card: Card;
|
Card: Card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SetCardDeckReq {
|
||||||
|
CardDeckId: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface PlayCardReq {
|
interface PlayCardReq {
|
||||||
CardIndex?: number;
|
CardIndex?: number;
|
||||||
CardData: any;
|
CardData: any;
|
||||||
@ -367,6 +371,13 @@ class SessionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCardDeck(id: number) {
|
||||||
|
let request: SetCardDeckReq = {
|
||||||
|
CardDeckId: id,
|
||||||
|
};
|
||||||
|
this.sendMessage("SetCardDeck", JSON.stringify(request));
|
||||||
|
}
|
||||||
|
|
||||||
drawCard() {
|
drawCard() {
|
||||||
this.sendMessage("DrawCard", "");
|
this.sendMessage("DrawCard", "");
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import RenamePlayer from "../../components/RenamePlayer.svelte";
|
import RenamePlayer from "../../components/RenamePlayer.svelte";
|
||||||
import { Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell, TableSearch, Badge, Button, Modal, Popover, Tooltip } from "flowbite-svelte";
|
import CardDeckShowcase from "../../components/CardDeckShowcase.svelte";
|
||||||
|
import { Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell, TableSearch, Badge, Button, Modal, Popover, Tooltip, Card } from "flowbite-svelte";
|
||||||
import { CircleArrowOutUpLeft, Copy, AlertCircle, UserX, Play, TextCursorInput, Gamepad2 } from "lucide-svelte";
|
import { CircleArrowOutUpLeft, Copy, AlertCircle, UserX, Play, TextCursorInput, Gamepad2 } from "lucide-svelte";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
import { GameState, sessionStore } from "../../stores/sessionStore";
|
import { GameState, sessionStore } from "../../stores/sessionStore";
|
||||||
import { toggleLobbyOverlay } from "../../stores/gameStore";
|
import { toggleLobbyOverlay } from "../../stores/gameStore";
|
||||||
|
import { CardDecks } from "../../stores/cardDeck";
|
||||||
|
|
||||||
let copied = false;
|
let copied = false;
|
||||||
let showLeaveModal = false;
|
let showLeaveModal = false;
|
||||||
@ -16,6 +18,12 @@
|
|||||||
let showRenameModal = false;
|
let showRenameModal = false;
|
||||||
let kick_player = "";
|
let kick_player = "";
|
||||||
let showKickModal = false;
|
let showKickModal = false;
|
||||||
|
let showCardDeckModal = false;
|
||||||
|
|
||||||
|
export let maxRotationDeg: number;
|
||||||
|
export let centerDistancePx: number;
|
||||||
|
export let cardWidth: number;
|
||||||
|
export let cardHeight: number;
|
||||||
|
|
||||||
function filteredPlayers() {
|
function filteredPlayers() {
|
||||||
return players.filter((player) => player.Username.toLowerCase().includes(searchQuery.toLowerCase()));
|
return players.filter((player) => player.Username.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||||
@ -85,34 +93,61 @@
|
|||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<!-- Leave Room Button -->
|
<!-- Modal: Select card deck -->
|
||||||
<Button
|
<Modal bind:open={showCardDeckModal} size="md" backdropClass="fixed inset-0 z-40 bg-gray-900 bg-black/50 dark:bg-black/80 backdrop-opacity-50" style="color: unset;" autoclose outsideclose>
|
||||||
color="none"
|
<div class="text-center">
|
||||||
class="sm:absolute m-2 border-2 border-gray-500 dark:border-gray-300 hover:bg-gray-500 dark:hover:bg-gray-300 hover:text-white dark:hover:text-black rounded-full text-gray-500 dark:text-gray-300"
|
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">
|
||||||
on:click={() => {
|
{$_("lobby.card_deck_modal")}
|
||||||
showLeaveModal = true;
|
</h3>
|
||||||
}}
|
<div class="grid justify-center gap-5">
|
||||||
>
|
{#each CardDecks as cardDeck}
|
||||||
<CircleArrowOutUpLeft class="mr-2" />
|
<Card style="color: unset;">
|
||||||
<span>{$_("lobby.leave_game")}</span>
|
<span class="mb-[-15px] text-xl">{cardDeck.name}</span>
|
||||||
</Button>
|
<div class="flex justify-center">
|
||||||
|
<CardDeckShowcase cardComponent={cardDeck.cardComponent} cardDeckId={cardDeck.id} {cardHeight} {cardWidth} centerDistancePx={centerDistancePx * 3} {maxRotationDeg} />
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
on:click={() => {
|
||||||
|
sessionStore.setCardDeck(cardDeck.id);
|
||||||
|
}}>{$_("lobby.choose_card_deck")}</Button
|
||||||
|
>
|
||||||
|
</Card>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<Button on:click={() => (showCardDeckModal = false)} color="alternative" class="mt-5 hover:text-dark hover:bg-gray-100">{$_("lobby.cancel")}</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<!-- Return to game Button -->
|
<div class="flex">
|
||||||
{#if sessionStore.getState().gameState !== GameState.Lobby}
|
<!-- Leave Room Button -->
|
||||||
<Button
|
<Button
|
||||||
color="none"
|
color="none"
|
||||||
class="sm:absolute m-2 right-0 border-2 border-gray-500 dark:border-gray-300 hover:bg-gray-500 dark:hover:bg-gray-300 hover:text-white dark:hover:text-black rounded-full text-gray-500 dark:text-gray-300"
|
class="m-2 border-2 border-gray-500 dark:border-gray-300 hover:bg-gray-500 dark:hover:bg-gray-300 hover:text-white dark:hover:text-black rounded-full text-gray-500 dark:text-gray-300"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
toggleLobbyOverlay();
|
showLeaveModal = true;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>{$_("lobby.return_to_game")}</span>
|
<CircleArrowOutUpLeft class="mr-2" />
|
||||||
<Gamepad2 class="ml-2" />
|
<span>{$_("lobby.leave_game")}</span>
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
|
||||||
|
<!-- Return to game Button -->
|
||||||
|
{#if sessionStore.getState().gameState !== GameState.Lobby}
|
||||||
|
<Button
|
||||||
|
color="none"
|
||||||
|
class="m-2 ml-auto border-2 border-gray-500 dark:border-gray-300 hover:bg-gray-500 dark:hover:bg-gray-300 hover:text-white dark:hover:text-black rounded-full text-gray-500 dark:text-gray-300"
|
||||||
|
on:click={() => {
|
||||||
|
toggleLobbyOverlay();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{$_("lobby.return_to_game")}</span>
|
||||||
|
<Gamepad2 class="ml-2" />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Game Status -->
|
<!-- Game Status -->
|
||||||
<div class="text-center p-6 w-full">
|
<div class="md:mt-[-65px] text-center p-6 w-full">
|
||||||
<span
|
<span
|
||||||
>{$_("game_status.game_status")}
|
>{$_("game_status.game_status")}
|
||||||
<Badge color="dark">
|
<Badge color="dark">
|
||||||
@ -199,6 +234,29 @@
|
|||||||
<TableSearch bind:inputValue={searchQuery} placeholder={$_("lobby.search_player")} />
|
<TableSearch bind:inputValue={searchQuery} placeholder={$_("lobby.search_player")} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Card deck selection -->
|
||||||
|
{#if sessionStore.getState().gameState == GameState.Lobby}
|
||||||
|
<div class="flex w-full justify-center my-10">
|
||||||
|
<Card style="color: unset;">
|
||||||
|
<span class="text-xl">{$_("lobby.selected_card_deck")}</span>
|
||||||
|
<span class="opacity-80 mb-[-15px]">{CardDecks.find((e) => e.id == $sessionStore.cardDeckId)?.name}</span>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<CardDeckShowcase
|
||||||
|
cardComponent={CardDecks.find((e) => e.id == $sessionStore.cardDeckId)?.cardComponent}
|
||||||
|
cardDeckId={CardDecks.find((e) => e.id == $sessionStore.cardDeckId)?.id ?? 0}
|
||||||
|
{cardHeight}
|
||||||
|
{cardWidth}
|
||||||
|
centerDistancePx={centerDistancePx * 3}
|
||||||
|
{maxRotationDeg}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if sessionStore.getPlayerPermissions().isHost}
|
||||||
|
<Button on:click={() => (showCardDeckModal = true)}>{$_("lobby.change_card_deck")}</Button>
|
||||||
|
{/if}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Players Table -->
|
<!-- Players Table -->
|
||||||
<Table striped hoverable noborder class="mb-16">
|
<Table striped hoverable noborder class="mb-16">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PlayerObj } from "../../stores/sessionStore";
|
import type { PlayerObj } from "../../stores/sessionStore";
|
||||||
import { derived, get } from "svelte/store";
|
import { derived, get } from "svelte/store";
|
||||||
import ClassicCard from "../../components/Cards/ClassicCard.svelte";
|
|
||||||
import CardDisplay from "../../components/Game/CardDisplay.svelte";
|
import CardDisplay from "../../components/Game/CardDisplay.svelte";
|
||||||
import { sessionStore } from "../../stores/sessionStore";
|
import { sessionStore } from "../../stores/sessionStore";
|
||||||
import OpponentDisplay from "../../components/Game/OpponentDisplay.svelte";
|
import OpponentDisplay from "../../components/Game/OpponentDisplay.svelte";
|
||||||
|
import { CardDecks } from "../../stores/cardDeck";
|
||||||
|
|
||||||
let maxRotationDeg = 20;
|
export let maxRotationDeg: number;
|
||||||
let centerDistancePx = 200;
|
export let centerDistancePx: number;
|
||||||
let cardWidth = 100;
|
export let cardWidth: number;
|
||||||
let cardHeight = 150;
|
export let cardHeight: number;
|
||||||
let cardComponent = ClassicCard;
|
let cardComponent = CardDecks.find((e) => e.id == $sessionStore.cardDeckId)?.cardComponent;
|
||||||
|
|
||||||
let opponents = derived(sessionStore.store, ($store) => $store.players.filter((e) => e.PlayerId != sessionStore.getUserId()));
|
let opponents = derived(sessionStore.store, ($store) => $store.players.filter((e) => e.PlayerId != sessionStore.getUserId()));
|
||||||
let playerActive = derived(sessionStore.store, ($store) => ($store.playerStates[$store.userId ?? ""] ?? "").Active ?? false);
|
let playerActive = derived(sessionStore.store, ($store) => ($store.playerStates[$store.userId ?? ""] ?? "").Active ?? false);
|
||||||
|
Reference in New Issue
Block a user