Add workshop and remodel effects
This commit is contained in:
parent
d6e2543b9c
commit
f2dab8a71c
173
src/main.rs
173
src/main.rs
@ -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 => {}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user