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

View File

@ -86,6 +86,10 @@ img.card:hover {
box-shadow: 0 0 10px lime; box-shadow: 0 0 10px lime;
} }
.supply-pile.selected {
box-shadow: 0 0 10px lime;
}
.chat-window { .chat-window {
border: 1px solid black; border: 1px solid black;
width: 300px; width: 300px;
@ -431,6 +435,15 @@ img.card:hover {
color: white; color: white;
visibility: hidden; visibility: hidden;
grid-template-rows: 5fr auto; grid-template-rows: 5fr auto;
transition: top 0.1s ease-in;
}
.dialog.supply {
top: 80%;
}
.dialog.hand {
top: 50%;
} }
.dialog img { .dialog img {
@ -462,23 +475,47 @@ img.card:hover {
var resolve_request; var resolve_request;
var enable_hand_selection = false; var enable_hand_selection = false;
var enable_supply_selection = false;
var toggle_hand_selection = function(index) { var toggle_hand_selection = function(index) {
document.querySelectorAll(".player-hand img.card")[index].classList.toggle("selected"); 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) { function dialog_confirm(ev) {
var selected = []; if (resolve_request.request.type == "GainCard") {
var selected = document.querySelector(".supply-pile.selected");
document.querySelectorAll(".player-hand img.card.selected").forEach((c) => { if (selected) {
selected.push(parseInt(c.dataset.index)); selected = parseInt(selected.dataset.index);
c.classList.remove("selected"); }
});
document.querySelectorAll(".supply-pile.selected").forEach((c) => {
var msg = { type: "HandCardsChosen", choice: selected }; c.classList.remove("selected");
webSocket.send(JSON.stringify(msg)); });
enable_hand_selection = false;
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"; document.querySelector("#dialog").style.visibility = "hidden";
dialog.classList.remove("hand");
dialog.classList.remove("supply");
} }
var mouseenter = function(ev) { var mouseenter = function(ev) {
@ -552,6 +589,13 @@ img.card:hover {
ev.dataTransfer.setData("text", JSON.stringify(data)); 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) { var doubleclick = function(ev) {
let msg = { let msg = {
type: "BuyCard", type: "BuyCard",
@ -570,6 +614,7 @@ img.card:hover {
"data-index": vnode.attrs.index, "data-index": vnode.attrs.index,
ondragstart: dragStart, ondragstart: dragStart,
draggable: true, draggable: true,
onclick: click,
ondblclick: doubleclick, ondblclick: doubleclick,
}, },
m("img", { m("img", {
@ -722,7 +767,6 @@ img.card:hover {
} }
} }
return { return {
view: function(vnode) { view: function(vnode) {
return m(".player-hand", return m(".player-hand",
@ -1024,9 +1068,26 @@ img.card:hover {
var handle_resolve_request = function(request) { var handle_resolve_request = function(request) {
var dlg = document.querySelector("#dialog"); 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) { var handle_gameover = function(msg) {
@ -1077,7 +1138,7 @@ img.card:hover {
} else if (msg.type == "PlayerId") { } else if (msg.type == "PlayerId") {
my_player_id = msg.id; my_player_id = msg.id;
} else if (msg.type == "ResolveRequest") { } else if (msg.type == "ResolveRequest") {
handle_resolve_request(msg.request); handle_resolve_request(msg);
} else { } else {
console.log("event?"); console.log("event?");
console.log(event.data); console.log(event.data);