Skip to content

Commit

Permalink
Che bato/game card icons (#31)
Browse files Browse the repository at this point in the history
- Implementation of GameCards Power, HP, Damage, sentinel, shield,
experience and upgrades.
  • Loading branch information
CheBato authored Dec 26, 2024
1 parent 66e1f11 commit 9f0d536
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 12 deletions.
46 changes: 42 additions & 4 deletions src/app/_components/Gameboard/_subcomponents/UnitsBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,48 @@ const UnitsBoard: React.FC<IUnitsBoardProps> = ({
};

//------------------------CONTEXT------------------------//
/**
* Takes an array of cards which can be main units or upgrades,
* and returns a new array of only upgrades,
*/
const findUpgrades = (cards: ICardData[]): Record<string, ICardData[]> => {
// Group upgrades by their parentCardId
const upgradesByParentId: Record<string, ICardData[]> = {};

cards.forEach((card) => {
if (card.parentCardId) {
// If this card is actually an upgrade that references a parent ID
if (!upgradesByParentId[card.parentCardId]) {
upgradesByParentId[card.parentCardId] = [];
}
upgradesByParentId[card.parentCardId].push(card);
}
});
return upgradesByParentId
}
/**
* Takes an array of cards which can be main units or upgrades,
* and returns a new array of only main cards,
* each including a subcards array with any matching upgrades.
*/
const attachUpgrades = (cards: ICardData[], upgradesByParentId: Record<string, ICardData[]>): ICardData[] => {
// For each "main" card (has NO parentCardId), attach the subcards
const mainCards = cards
.filter((card) => !card.parentCardId)
.map((card) => ({
...card,
subcards: card.uuid ? upgradesByParentId[card.uuid] ?? [] : [], // attach any upgrades belonging to this card
}));
return mainCards;
}
const { gameState, connectedPlayer, getOpponent } = useGame();

const playerUnits = gameState?.players[connectedPlayer].cardPiles[arena];
const opponentUnits = gameState?.players[getOpponent(connectedPlayer)].cardPiles[arena];
const rawPlayerUnits = gameState?.players[connectedPlayer].cardPiles[arena];
const rawOpponentUnits = gameState?.players[getOpponent(connectedPlayer)].cardPiles[arena];
const allCardsInArena = [...rawPlayerUnits, ...rawOpponentUnits];
const upgradeMapping = findUpgrades(allCardsInArena);
const playerUnits = attachUpgrades(rawPlayerUnits, upgradeMapping);
const opponentUnits = attachUpgrades(rawOpponentUnits, upgradeMapping);

return (
<Box sx={mainBoxStyle}>
Expand All @@ -55,7 +93,7 @@ const UnitsBoard: React.FC<IUnitsBoardProps> = ({
<Grid sx={opponentGridStyle}>
{opponentUnits.map((card: ICardData) => (
<Box key={card.id} sx={{ flex: "0 0 auto" }}>
<GameCard card={card} size="square" variant={'gameboard'}/>
<GameCard card={card} subcards={card.subcards} size="square" variant={'gameboard'}/>
</Box>
))}
</Grid>
Expand All @@ -64,7 +102,7 @@ const UnitsBoard: React.FC<IUnitsBoardProps> = ({
<Grid sx={playerGridStyle}>
{playerUnits.map((card: ICardData) => (
<Box key={card.id} sx={{ flex: "0 0 auto" }}>
<GameCard card={card} size="square" variant={'gameboard'}/>
<GameCard card={card} subcards={card.subcards} size="square" variant={'gameboard'}/>
</Box>
))}
</Grid>
Expand Down
7 changes: 7 additions & 0 deletions src/app/_components/_sharedcomponents/Cards/CardTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ interface ICardSetId {
number: number;
}

type IAspect = 'aggression' | 'command' | 'cunning' | 'heroism' | 'vigilance' | 'villainy';

export interface ICardData {
uuid: string;
parentCardId?: string,
id?: number;
name?: string;
selected?: boolean;
Expand All @@ -21,6 +24,9 @@ export interface ICardData {
damage?: number;
setId: ICardSetId;
type: string;
subcards?: ICardData[];
aspects?: IAspect[];
sentinel?: boolean;
}
export interface IServerCardData {
count: number;
Expand All @@ -32,6 +38,7 @@ export interface IGameCardProps {
onClick?: () => void;
variant?: "lobby" | "gameboard";
disabled?: boolean;
subcards?: ICardData[];
}

export interface ILeaderBaseCardProps {
Expand Down
205 changes: 197 additions & 8 deletions src/app/_components/_sharedcomponents/Cards/GameCard/GameCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import {
Typography,
Box,
} from "@mui/material";
import Grid from "@mui/material/Grid2";
import Image from "next/image";
import { IGameCardProps, ICardData, IServerCardData } from "@/app/_components/_sharedcomponents/Cards/CardTypes";
import { useGame } from "@/app/_contexts/Game.context";
import { s3CardImageURL } from "@/app/_utils/s3Utils";
import {s3CardImageURL, s3TokenImageURL} from "@/app/_utils/s3Utils";

// Type guard to check if the card is ICardData
const isICardData = (card: ICardData | IServerCardData): card is ICardData => {
Expand All @@ -19,6 +20,7 @@ const GameCard: React.FC<IGameCardProps> = ({
card,
size = "standard",
onClick,
subcards = [],
variant,
disabled = false,
}) => {
Expand All @@ -37,14 +39,53 @@ const GameCard: React.FC<IGameCardProps> = ({
}
};

// upgrade on click
/*const upgradeClickFunction = (card:ICardData) => {
if(card.selectable){
sendGameMessage(["cardClicked", card.uuid]);
}
}*/
const handleClick = onClick ?? defaultClickFunction;
const cardBorderColor = (card: ICardData) => {
if (card.selected) return "yellow";
if (card.selectable) return "limegreen";
if (card.exhausted) return "gray";
return "";
}
// helper function to get the correct aspects for the upgrade cards
const cardUpgradebackground = (card: ICardData) => {
if (!card.aspects){
return null
}

// Check if Villainy or Heroism are the sole aspects
if (card.aspects.includes("villainy") && card.aspects.length === 1) {
return "upgrade-black.png";
}
if (card.aspects.includes("heroism") && card.aspects.length === 1) {
return "upgrade-white.png";
}
// Check other aspects
switch (true) {
case card.aspects.includes("aggression"):
return "upgrade-red.png";
case card.aspects.includes("command"):
return "upgrade-green.png";
case card.aspects.includes("cunning"):
return "upgrade-yellow.png";
case card.aspects.includes("vigilance"):
return "upgrade-blue.png";
default:
// Fallback for unexpected cases
return "upgrade-grey.png";
}
};

// Filter subcards into Shields and other upgrades
const shieldCards = subcards.filter((subcard) => subcard.name === 'Shield');
const otherUpgradeCards = subcards.filter((subcard) => subcard.name !== 'Shield');

// Styles
const styles = {
cardStyles: {
borderRadius: ".38em",
Expand All @@ -62,6 +103,11 @@ const GameCard: React.FC<IGameCardProps> = ({
// For "standard" or other sizes:
height: size === "standard" ? "10rem" : "8rem",
width: size === "standard" ? "7.18rem" : "8rem",
border: `2px solid ${cardBorderColor(cardData)}`,
/*...(cardData.exhausted &&{
transform: 'rotate(4deg)',
transition: 'transform 0.3s ease',
})*/
}
),
},
Expand All @@ -72,7 +118,7 @@ const GameCard: React.FC<IGameCardProps> = ({
position: "relative",
textAlign: "center",
whiteSpace: "normal",
backgroundColor: "transparent",
backgroundColor: variant === "lobby" ? "transparent" : "black",
backgroundImage: `url(${s3CardImageURL(cardData)})`,
backgroundSize: size === "standard" ? "contain" : "cover",
backgroundPosition: size === "standard" ? "center" : "top",
Expand All @@ -88,7 +134,8 @@ const GameCard: React.FC<IGameCardProps> = ({
fontSize: "1.3em",
margin: 0,
},
iconLayer:{

counterIconLayer:{
position: "absolute",
width: "100%",
display: "flex",
Expand All @@ -100,14 +147,114 @@ const GameCard: React.FC<IGameCardProps> = ({
backgroundImage: `url(/counterIcon.svg)`,
alignItems: "center",
justifyContent: "center",

},
powerIconLayer:{
position: "absolute",
width: "2rem",
display: "flex",
height: "2.5rem",
bottom: "0px",
backgroundPosition: "left",
backgroundSize: "contain",
backgroundRepeat: "no-repeat",
backgroundImage: `url(${s3TokenImageURL('power-badge')})`,
alignItems: "center",
justifyContent: "center",
},
healthIconLayer:{
position: "absolute",
width: "2rem",
display: "flex",
height: "2.5rem",
bottom: "0px",
right: "0px",
backgroundPosition: "right",
backgroundSize: "contain",
backgroundRepeat: "no-repeat",
backgroundImage: `url(${s3TokenImageURL('hp-badge')})`,
alignItems: "center",
justifyContent: "center",
},
damageIconLayer:{
position: "absolute",
width: "6.5rem",
display: "flex",
height: "2.5rem",
bottom: "0px",
right: "18px",
background: "linear-gradient(90deg, rgba(255, 0, 0, 0) 47.44%, rgba(255, 0, 0, 0.911111) 75.61%, #FF0000 102.56%)",
alignItems: "center",
justifyContent: "center",
},
shieldIconLayer:{
position: "relative",
width: "2rem",
display: "flex",
height: "2.5rem",
top:"0px",
right: "0px",
backgroundPosition: "right",
backgroundSize: "contain",
backgroundRepeat: "no-repeat",
backgroundImage: `url(${s3TokenImageURL('shield-token')})`,
alignItems: "center",
justifyContent: "center",
},
shieldContainerStyle: {
position:"absolute",
top:"0px",
width: "100%",
justifyContent: "right",
alignItems: "center",
columnGap: "4px"
},
upgradeIconLayer:{
position: "relative",
width: "100%",
display: "flex",
height: "30px",
bottom:"0px",
right: "0px",
backgroundPosition: "right",
backgroundSize: "contain",
backgroundRepeat: "no-repeat",
alignItems: "center",
justifyContent: "center",
backgroundColor: "transparent",
},
damageNumberStyle:{
fontSize: variant === 'lobby' ? "2rem" : "1.9rem",
fontWeight: "700",
position: "absolute",
right:"16px",
},
numberStyle:{
fontSize: "2rem",
fontSize: variant === 'lobby' ? "2rem" : "1.9rem",
fontWeight: "700",
},
upgradeNameStyle:{
fontSize: "11px",
marginTop: "2px",
fontWeight: "800",
color: "black"
},
sentinelStyle:{
position: "absolute",
width: "2rem",
display: "flex",
height: "2.5rem",
top:"36px",
right: "0px",
backgroundPosition: "right",
backgroundSize: "contain",
backgroundRepeat: "no-repeat",
backgroundImage: `url(/SentinelToken.png)`,
alignItems: "center",
justifyContent: "center",
}
}
return (
<>
<MuiCard sx={styles.cardStyles}

onClick={disabled ? undefined : handleClick}
Expand All @@ -116,11 +263,40 @@ const GameCard: React.FC<IGameCardProps> = ({
<CardContent sx={styles.cardContentStyle}>
<Box sx={{ display: 'flex', flexDirection: 'column', height: "100%"}}>
</Box>
{variant === "lobby" && (
<Box sx={styles.iconLayer}>
{variant === "lobby" ? (
<Box sx={styles.counterIconLayer}>
<Typography sx={styles.numberStyle}>{cardCounter}</Typography>
</Box>
)}
) : variant === "gameboard" ? (
<>
<Grid direction="row" container sx={styles.shieldContainerStyle}>
{shieldCards.map((shieldCard) => (
<>
<Box
key={shieldCard.uuid}
sx={styles.shieldIconLayer}
/>
</>
))}
</Grid>
{cardData.sentinel && (
<Box sx={styles.sentinelStyle}/>
)}
<Box sx={styles.powerIconLayer}>
<Typography sx={{...styles.numberStyle,marginRight:"2px"}}>{cardData.power}</Typography>
</Box>
{Number(cardData.damage) > 0 && (
<Box sx={styles.damageIconLayer}>
<Typography sx={styles.damageNumberStyle}>
{cardData.damage}
</Typography>
</Box>
)}
<Box sx={styles.healthIconLayer}>
<Typography sx={{...styles.numberStyle,marginLeft:"2px"}}>{cardData.hp}</Typography>
</Box>
</>
) : null}
</CardContent>
) : (
<CardContent sx={styles.cardContentStyle}>
Expand All @@ -143,6 +319,19 @@ const GameCard: React.FC<IGameCardProps> = ({
</CardContent>
)}
</MuiCard>
{otherUpgradeCards.map((subcard, index) => (
<Box
key={subcard.uuid}
sx={{...styles.upgradeIconLayer,
backgroundImage: `url(${(cardUpgradebackground(subcard))})`,
bottom: `${index * 7 + 2}px`,
}}
// onClick={() => upgradeClickFunction(subcard)}
>
<Typography sx={styles.upgradeNameStyle}>{subcard.name}</Typography>
</Box>
))}
</>
);
};

Expand Down
4 changes: 4 additions & 0 deletions src/app/_utils/s3Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ export const s3CardImageURL = (card: ICardData) => {
return s3ImageURL(`cards/${card.setId.set}/${cardNumber}.webp`);
};

export const s3TokenImageURL = (token_name: string) =>{
return s3ImageURL(`game/${token_name}.webp`);
}


// Helper function to update a card's id
export const updateIdsWithMapping = (data: IDeckData, mapping: IMapping): IDeckData => {
Expand Down

0 comments on commit 9f0d536

Please sign in to comment.