Add workshop and remodel effects

This commit is contained in:
Markus Wagner 2021-01-20 20:11:11 +01:00
parent d6e2543b9c
commit f2dab8a71c
2 changed files with 233 additions and 29 deletions

View File

@ -25,7 +25,7 @@ enum ClientMessage {
DrawCard,
Discard { index: usize },
TrashHand { index: usize },
HandCardsChosen { choice: Vec<usize> },
ResolveReply { reply: ResolveReply },
}
#[derive(Serialize)]
@ -62,7 +62,10 @@ enum ServerMessage {
score: Vec<(usize, u32)>,
},
ResolveRequest {
/// Request type and context
request: ResolveRequest,
/// The card that is the source of the request to display to the player
card: String,
},
}
@ -175,11 +178,25 @@ enum ResolvingPlayer {
ActivePlayer,
}
#[derive(Deserialize)]
#[serde(tag = "type")]
enum ResolveReply {
HandCardsChosen { choice: Vec<usize> },
SupplyCardChosen { choice: usize },
}
/// Sent by the server to tell the client what information is requested
/// from the player to resolve the current effect
#[derive(Clone, Serialize)]
#[serde(tag = "type")]
enum ResolveRequest {
ChooseHandCardsToDiscard { player: ResolvingPlayer },
ChooseHandCardsToDiscard {
player: ResolvingPlayer,
},
GainCard {
player: ResolvingPlayer,
filter: CardFilter,
},
}
#[derive(Clone)]
@ -187,10 +204,19 @@ enum Effect {
/// Trigger effect if another card is played while effect is active
/// return true to indicate the event is resolved and should be removed
OnCardPlayed(fn(&mut Game, &Card) -> bool),
Resolving(ResolveRequest, ResolvingEffect),
/// Effect that blocks further processing of the game state until
/// some user input is received that allows to fully resolve the effect
Resolving(String, ResolveRequest, ResolvingEffect),
}
type ResolvingEffect = fn(&mut Game, &ClientMessage);
#[derive(Clone, Serialize)]
//#[serde(tag = "type")]
enum CardFilter {
Any,
MaxCost(u32),
}
type ResolvingEffect = fn(&mut Game, &ResolveReply);
pub struct Game {
effects: Vec<Effect>,
@ -205,7 +231,7 @@ pub struct Game {
/// Any effect from a card that requires further input from players
/// and blocks the game until fully resolved.
resolving_effect: Option<(ResolveRequest, ResolvingEffect)>,
resolving_effect: Option<(String, ResolveRequest, ResolvingEffect)>,
}
impl Game {
@ -254,11 +280,12 @@ impl Game {
types: vec![CardType::Action(|game| {
action!(game, 1);
game.add_effect(Effect::Resolving(
"Cellar".into(),
ResolveRequest::ChooseHandCardsToDiscard {
player: ResolvingPlayer::ActivePlayer,
},
|game, message| {
if let ClientMessage::HandCardsChosen { choice } = message {
if let ResolveReply::HandCardsChosen { choice } = message {
let mut discarded = 0;
for c in choice.iter().sorted_by(|a, b| b.cmp(a)) {
game.players[game.active_player].discard(*c);
@ -321,7 +348,36 @@ impl Game {
Card {
name: "Workshop".into(),
cost: 3,
types: vec![CardType::Action(|_| {})],
types: vec![CardType::Action(|game| {
game.add_effect(Effect::Resolving(
"Workshop".into(),
ResolveRequest::GainCard {
player: ResolvingPlayer::ActivePlayer,
filter: CardFilter::MaxCost(4),
},
|game, message| {
if let ResolveReply::SupplyCardChosen { choice } = message {
if let Some((card, count)) = game.supply.get(*choice) {
if *count < 1 {
return;
}
if card.cost > 4 {
return;
}
}
game.supply.get_mut(*choice).unwrap().1 =
game.supply.get(*choice).unwrap().1 - 1;
let card = game.supply[*choice].0.clone();
game.players[game.active_player]
.discard_pile
.push(card.clone());
game.resolving_effect = None;
}
},
));
})],
},
10,
),
@ -337,7 +393,82 @@ impl Game {
Card {
name: "Remodel".into(),
cost: 4,
types: vec![CardType::Action(|_| {})],
types: vec![CardType::Action(|game| {
game.add_effect(Effect::Resolving(
"Remodel".into(),
ResolveRequest::ChooseHandCardsToDiscard {
player: ResolvingPlayer::ActivePlayer,
},
|game, message| {
if let ResolveReply::HandCardsChosen { choice } = message {
if choice.len() != 1 {
return;
}
let card = game.players[game.active_player]
.hand
.remove(choice[0]);
let cost = card.cost;
game.trash.push(card);
game.add_effect(Effect::Resolving(
"Remodel".into(),
ResolveRequest::GainCard {
player: ResolvingPlayer::ActivePlayer,
filter: CardFilter::MaxCost(cost + 2),
},
|game, message| {
if let ResolveReply::SupplyCardChosen {
choice,
} = message
{
if let Some((card, count)) =
game.supply.get(*choice)
{
if *count < 1 {
return;
}
if let Some((
_,
ResolveRequest::GainCard {
filter:
CardFilter::MaxCost(cost),
..
},
_,
)) = game.resolving_effect
{
if card.cost > cost {
return;
}
game.supply
.get_mut(*choice)
.unwrap()
.1 = game
.supply
.get(*choice)
.unwrap()
.1
- 1;
let card =
game.supply[*choice].0.clone();
game.players[game.active_player]
.discard_pile
.push(card.clone());
game.resolving_effect = None;
}
}
}
},
));
}
},
));
})],
},
10,
),
@ -482,7 +613,7 @@ impl Game {
}
}
Effect::Resolving(_, _) => {}
Effect::Resolving(_, _, _) => {}
}
true
@ -525,8 +656,8 @@ impl Game {
self.effects.push(effect);
}
Effect::Resolving(request, effect) => {
self.resolving_effect = Some((request, effect));
Effect::Resolving(card_name, request, effect) => {
self.resolving_effect = Some((card_name, request, effect));
}
}
}
@ -611,12 +742,24 @@ async fn broadcast_state(game: &Game) {
broadcast(&game, &sm).await;
}
if let Some((request, _)) = &game.resolving_effect {
if let Some((card_name, request, _)) = &game.resolving_effect {
match request {
ResolveRequest::ChooseHandCardsToDiscard { ref player } => match player {
ResolvingPlayer::ActivePlayer => {
let p = game.players.get(game.active_player).unwrap();
let sm = ServerMessage::ResolveRequest {
card: card_name.clone(),
request: request.clone(),
};
send_msg(&game, &p, &sm).await;
}
},
ResolveRequest::GainCard { ref player, .. } => match player {
ResolvingPlayer::ActivePlayer => {
let p = game.players.get(game.active_player).unwrap();
let sm = ServerMessage::ResolveRequest {
card: card_name.clone(),
request: request.clone(),
};
send_msg(&game, &p, &sm).await;
@ -917,14 +1060,14 @@ async fn main() -> Result<(), std::io::Error> {
broadcast_state(&game).await;
}
ClientMessage::HandCardsChosen { .. } => {
ClientMessage::ResolveReply { reply } => {
let mut games = req.state().games.write().await;
let game = games.get_mut(&game_id).unwrap();
if player_number == game.active_player {
match game.resolving_effect {
Some((_, ref effect)) => {
effect(game, &msg);
Some((_, _, ref effect)) => {
effect(game, &reply);
}
None => {}
}

View File

@ -86,6 +86,10 @@ img.card:hover {
box-shadow: 0 0 10px lime;
}
.supply-pile.selected {
box-shadow: 0 0 10px lime;
}
.chat-window {
border: 1px solid black;
width: 300px;
@ -431,6 +435,15 @@ img.card:hover {
color: white;
visibility: hidden;
grid-template-rows: 5fr auto;
transition: top 0.1s ease-in;
}
.dialog.supply {
top: 80%;
}
.dialog.hand {
top: 50%;
}
.dialog img {
@ -462,23 +475,47 @@ img.card:hover {
var resolve_request;
var enable_hand_selection = false;
var enable_supply_selection = false;
var toggle_hand_selection = function(index) {
document.querySelectorAll(".player-hand img.card")[index].classList.toggle("selected");
}
var toggle_supply_selection = function(index) {
document.querySelectorAll(".supply-area .supply-pile")[index].classList.toggle("selected");
}
function dialog_confirm(ev) {
var selected = [];
document.querySelectorAll(".player-hand img.card.selected").forEach((c) => {
selected.push(parseInt(c.dataset.index));
c.classList.remove("selected");
});
var msg = { type: "HandCardsChosen", choice: selected };
webSocket.send(JSON.stringify(msg));
enable_hand_selection = false;
if (resolve_request.request.type == "GainCard") {
var selected = document.querySelector(".supply-pile.selected");
if (selected) {
selected = parseInt(selected.dataset.index);
}
document.querySelectorAll(".supply-pile.selected").forEach((c) => {
c.classList.remove("selected");
});
var reply = { type: "SupplyCardChosen", choice: selected };
var msg = { type: "ResolveReply", reply: reply };
webSocket.send(JSON.stringify(msg));
enable_supply_selection = false;
} else {
var selected = [];
document.querySelectorAll(".player-hand img.card.selected").forEach((c) => {
selected.push(parseInt(c.dataset.index));
c.classList.remove("selected");
});
var reply = { type: "HandCardsChosen", choice: selected };
var msg = { type: "ResolveReply", reply: reply };
webSocket.send(JSON.stringify(msg));
enable_hand_selection = false;
}
document.querySelector("#dialog").style.visibility = "hidden";
dialog.classList.remove("hand");
dialog.classList.remove("supply");
}
var mouseenter = function(ev) {
@ -552,6 +589,13 @@ img.card:hover {
ev.dataTransfer.setData("text", JSON.stringify(data));
}
var click = function(e) {
e.preventDefault();
if (enable_supply_selection) {
toggle_supply_selection(parseInt(e.srcElement.parentElement.dataset.index));
}
}
var doubleclick = function(ev) {
let msg = {
type: "BuyCard",
@ -570,6 +614,7 @@ img.card:hover {
"data-index": vnode.attrs.index,
ondragstart: dragStart,
draggable: true,
onclick: click,
ondblclick: doubleclick,
},
m("img", {
@ -722,7 +767,6 @@ img.card:hover {
}
}
return {
view: function(vnode) {
return m(".player-hand",
@ -1024,9 +1068,26 @@ img.card:hover {
var handle_resolve_request = function(request) {
var dlg = document.querySelector("#dialog");
dlg.style.visibility = "visible";
resolve_request = request;
enable_hand_selection = true;
document.querySelector("#dialog img").src = "/static/images/cards/" + request.card.toLowerCase() + ".jpg";
if (request.request.type == "GainCard") {
let cost = request.request?.filter?.MaxCost;
if (cost) {
document.querySelector("#dialog p").innerHTML = "Choose a card to gain with max cost of " + cost + ".";
} else {
document.querySelector("#dialog p").innerHTML = "Choose a card to gain.";
}
enable_supply_selection = true;
dialog.classList.add("supply");
} else if (request.request.type == "ChooseHandCardsToDiscard") {
document.querySelector("#dialog p").innerHTML = "Choose any number of cards to discard.";
enable_hand_selection = true;
dialog.classList.add("hand");
}
dlg.style.visibility = "visible";
}
var handle_gameover = function(msg) {
@ -1077,7 +1138,7 @@ img.card:hover {
} else if (msg.type == "PlayerId") {
my_player_id = msg.id;
} else if (msg.type == "ResolveRequest") {
handle_resolve_request(msg.request);
handle_resolve_request(msg);
} else {
console.log("event?");
console.log(event.data);