story: tab-mode
This commit is contained in:
parent
7fc90eba74
commit
f3f4c43e81
|
|
@ -32,6 +32,7 @@ use crate::{
|
||||||
Alignment, HunterMut, HunterRef, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT,
|
Alignment, HunterMut, HunterRef, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT,
|
||||||
Powerful, PreviousGuardianAction, Role, RoleTitle,
|
Powerful, PreviousGuardianAction, Role, RoleTitle,
|
||||||
},
|
},
|
||||||
|
team::Team,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Result<T> = core::result::Result<T, GameError>;
|
type Result<T> = core::result::Result<T, GameError>;
|
||||||
|
|
@ -242,6 +243,16 @@ impl Character {
|
||||||
!self.is_wolf()
|
!self.is_wolf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn team(&self) -> Team {
|
||||||
|
if let Alignment::Traitor = self.alignment() {
|
||||||
|
return Team::AnyEvil;
|
||||||
|
}
|
||||||
|
if self.is_wolf() {
|
||||||
|
return Team::Wolves;
|
||||||
|
}
|
||||||
|
Team::Village
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn known_elder(&self) -> bool {
|
pub const fn known_elder(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self.role,
|
self.role,
|
||||||
|
|
@ -600,8 +611,15 @@ impl Character {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Role::MasonLeader { .. } => {
|
Role::MasonLeader { .. } => {
|
||||||
log::error!(
|
log::debug!(
|
||||||
"night_action_prompts got to MasonLeader, should be handled before the living check"
|
"night_action_prompts got to MasonLeader;
|
||||||
|
mason leader alive: {}; night: {night}; current prompt titles: {}",
|
||||||
|
self.alive(),
|
||||||
|
prompts
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.title().to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Role::Empath { cursed: false } => prompts.push(ActionPrompt::Empath {
|
Role::Empath { cursed: false } => prompts.push(ActionPrompt::Empath {
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,30 @@ pub enum DiedTo {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiedTo {
|
impl DiedTo {
|
||||||
|
pub const fn day(&self) -> Option<NonZeroU8> {
|
||||||
|
match self {
|
||||||
|
DiedTo::Execution { day } => Some(*day),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub const fn night(&self) -> Option<u8> {
|
||||||
|
Some(match self {
|
||||||
|
DiedTo::Execution { .. } => return None,
|
||||||
|
|
||||||
|
DiedTo::MapleWolf { night, .. }
|
||||||
|
| DiedTo::MapleWolfStarved { night }
|
||||||
|
| DiedTo::Militia { night, .. }
|
||||||
|
| DiedTo::Wolfpack { night, .. }
|
||||||
|
| DiedTo::AlphaWolf { night, .. }
|
||||||
|
| DiedTo::Shapeshift { night, .. }
|
||||||
|
| DiedTo::Hunter { night, .. }
|
||||||
|
| DiedTo::GuardianProtecting { night, .. }
|
||||||
|
| DiedTo::PyreMasterLynchMob { night, .. }
|
||||||
|
| DiedTo::PyreMaster { night, .. } => night.get(),
|
||||||
|
|
||||||
|
DiedTo::LoneWolf { night, .. } | DiedTo::MasonLeaderRecruitFail { night, .. } => *night,
|
||||||
|
})
|
||||||
|
}
|
||||||
pub fn next_night(&self) -> Option<DiedTo> {
|
pub fn next_night(&self) -> Option<DiedTo> {
|
||||||
let mut next = self.clone();
|
let mut next = self.clone();
|
||||||
match &mut next {
|
match &mut next {
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,7 @@ impl Game {
|
||||||
GameActions::NightDetails(NightDetails::new(
|
GameActions::NightDetails(NightDetails::new(
|
||||||
&night.used_actions(),
|
&night.used_actions(),
|
||||||
recorded_changes,
|
recorded_changes,
|
||||||
|
self.village(),
|
||||||
)),
|
)),
|
||||||
)?;
|
)?;
|
||||||
self.state = GameState::Day {
|
self.state = GameState::Day {
|
||||||
|
|
|
||||||
|
|
@ -45,13 +45,17 @@ pub struct NightDetails {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NightDetails {
|
impl NightDetails {
|
||||||
pub fn new(choices: &[(ActionPrompt, ActionResult)], changes: Box<[NightChange]>) -> Self {
|
pub fn new(
|
||||||
|
choices: &[(ActionPrompt, ActionResult)],
|
||||||
|
changes: Box<[NightChange]>,
|
||||||
|
village: &Village,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
changes,
|
changes,
|
||||||
choices: choices
|
choices: choices
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
.filter_map(|(prompt, result)| NightChoice::new(prompt, result))
|
.filter_map(|(prompt, result)| NightChoice::new(prompt, result, village))
|
||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -64,9 +68,9 @@ pub struct NightChoice {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NightChoice {
|
impl NightChoice {
|
||||||
pub fn new(prompt: ActionPrompt, result: ActionResult) -> Option<Self> {
|
pub fn new(prompt: ActionPrompt, result: ActionResult, village: &Village) -> Option<Self> {
|
||||||
Some(Self {
|
Some(Self {
|
||||||
prompt: StoryActionPrompt::new(prompt)?,
|
prompt: StoryActionPrompt::new(prompt, village)?,
|
||||||
result: StoryActionResult::new(result),
|
result: StoryActionResult::new(result),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -185,6 +189,7 @@ pub enum StoryActionPrompt {
|
||||||
chosen: CharacterId,
|
chosen: CharacterId,
|
||||||
},
|
},
|
||||||
WolfPackKill {
|
WolfPackKill {
|
||||||
|
killing_wolf: CharacterId,
|
||||||
chosen: CharacterId,
|
chosen: CharacterId,
|
||||||
},
|
},
|
||||||
Shapeshifter {
|
Shapeshifter {
|
||||||
|
|
@ -215,7 +220,7 @@ pub enum StoryActionPrompt {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StoryActionPrompt {
|
impl StoryActionPrompt {
|
||||||
pub fn new(prompt: ActionPrompt) -> Option<Self> {
|
pub fn new(prompt: ActionPrompt, village: &Village) -> Option<Self> {
|
||||||
Some(match prompt {
|
Some(match prompt {
|
||||||
ActionPrompt::BeholderWakes { character_id } => Self::BeholderWakes {
|
ActionPrompt::BeholderWakes { character_id } => Self::BeholderWakes {
|
||||||
character_id: character_id.character_id,
|
character_id: character_id.character_id,
|
||||||
|
|
@ -370,7 +375,10 @@ impl StoryActionPrompt {
|
||||||
ActionPrompt::WolfPackKill {
|
ActionPrompt::WolfPackKill {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
} => Self::WolfPackKill { chosen: marked },
|
} => Self::WolfPackKill {
|
||||||
|
chosen: marked,
|
||||||
|
killing_wolf: village.killing_wolf().map(|c| c.character_id())?,
|
||||||
|
},
|
||||||
ActionPrompt::Shapeshifter { character_id } => Self::Shapeshifter {
|
ActionPrompt::Shapeshifter { character_id } => Self::Shapeshifter {
|
||||||
character_id: character_id.character_id,
|
character_id: character_id.character_id,
|
||||||
},
|
},
|
||||||
|
|
@ -433,8 +441,12 @@ impl StoryActionPrompt {
|
||||||
|
|
||||||
pub const fn character_id(&self) -> Option<CharacterId> {
|
pub const fn character_id(&self) -> Option<CharacterId> {
|
||||||
match self {
|
match self {
|
||||||
StoryActionPrompt::MasonsWake { .. } | StoryActionPrompt::WolfPackKill { .. } => None,
|
StoryActionPrompt::MasonsWake { .. } => None,
|
||||||
StoryActionPrompt::Seer { character_id, .. }
|
StoryActionPrompt::WolfPackKill {
|
||||||
|
killing_wolf: character_id,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| StoryActionPrompt::Seer { character_id, .. }
|
||||||
| StoryActionPrompt::Protector { character_id, .. }
|
| StoryActionPrompt::Protector { character_id, .. }
|
||||||
| StoryActionPrompt::Arcanist { character_id, .. }
|
| StoryActionPrompt::Arcanist { character_id, .. }
|
||||||
| StoryActionPrompt::Gravedigger { character_id, .. }
|
| StoryActionPrompt::Gravedigger { character_id, .. }
|
||||||
|
|
|
||||||
|
|
@ -784,6 +784,34 @@ clients {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-basis: content;
|
flex-basis: content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.story {
|
||||||
|
padding-bottom: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width : 799px) {
|
||||||
|
.story-characters {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 3px;
|
||||||
|
row-gap: 5px;
|
||||||
|
justify-content: space-between;
|
||||||
|
overflow-x: scroll;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width : 800px) {
|
||||||
|
.story-characters {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 3px;
|
||||||
|
row-gap: 5px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width : 1900px) {
|
@media only screen and (min-width : 1900px) {
|
||||||
|
|
@ -1321,7 +1349,7 @@ select {
|
||||||
background-color: $village_color;
|
background-color: $village_color;
|
||||||
border: 1px solid $village_border;
|
border: 1px solid $village_border;
|
||||||
|
|
||||||
&:hover {
|
&.hover:hover {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: $village_border;
|
background-color: $village_border;
|
||||||
}
|
}
|
||||||
|
|
@ -1330,7 +1358,7 @@ select {
|
||||||
border: 1px solid $village_border_faint;
|
border: 1px solid $village_border_faint;
|
||||||
background-color: $village_color_faint;
|
background-color: $village_color_faint;
|
||||||
|
|
||||||
&:hover {
|
&.hover:hover {
|
||||||
background-color: $village_border_faint;
|
background-color: $village_border_faint;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1345,7 +1373,7 @@ select {
|
||||||
background-color: $wolves_color;
|
background-color: $wolves_color;
|
||||||
border: 1px solid $wolves_border;
|
border: 1px solid $wolves_border;
|
||||||
|
|
||||||
&:hover {
|
&.hover:hover {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: $wolves_border;
|
background-color: $wolves_border;
|
||||||
}
|
}
|
||||||
|
|
@ -1354,7 +1382,7 @@ select {
|
||||||
border: 1px solid $wolves_border_faint;
|
border: 1px solid $wolves_border_faint;
|
||||||
background-color: $wolves_color_faint;
|
background-color: $wolves_color_faint;
|
||||||
|
|
||||||
&:hover {
|
&.hover:hover {
|
||||||
background-color: $wolves_border_faint;
|
background-color: $wolves_border_faint;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1369,7 +1397,7 @@ select {
|
||||||
background-color: $intel_color;
|
background-color: $intel_color;
|
||||||
border: 1px solid $intel_border;
|
border: 1px solid $intel_border;
|
||||||
|
|
||||||
&:hover {
|
&.hover:hover {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: $intel_border;
|
background-color: $intel_border;
|
||||||
}
|
}
|
||||||
|
|
@ -1378,7 +1406,7 @@ select {
|
||||||
border: 1px solid $intel_border_faint;
|
border: 1px solid $intel_border_faint;
|
||||||
background-color: $intel_color_faint;
|
background-color: $intel_color_faint;
|
||||||
|
|
||||||
&:hover {
|
&.hover:hover {
|
||||||
background-color: $intel_border_faint;
|
background-color: $intel_border_faint;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1393,7 +1421,7 @@ select {
|
||||||
background-color: $defensive_color;
|
background-color: $defensive_color;
|
||||||
border: 1px solid $defensive_border;
|
border: 1px solid $defensive_border;
|
||||||
|
|
||||||
&:hover {
|
&.hover:hover {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: $defensive_border;
|
background-color: $defensive_border;
|
||||||
}
|
}
|
||||||
|
|
@ -1402,7 +1430,7 @@ select {
|
||||||
border: 1px solid $defensive_border_faint;
|
border: 1px solid $defensive_border_faint;
|
||||||
background-color: $defensive_color_faint;
|
background-color: $defensive_color_faint;
|
||||||
|
|
||||||
&:hover {
|
&.hover:hover {
|
||||||
background-color: $defensive_border_faint;
|
background-color: $defensive_border_faint;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1417,7 +1445,7 @@ select {
|
||||||
background-color: $offensive_color;
|
background-color: $offensive_color;
|
||||||
border: 1px solid $offensive_border;
|
border: 1px solid $offensive_border;
|
||||||
|
|
||||||
&:hover {
|
&.hover:hover {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: $offensive_border;
|
background-color: $offensive_border;
|
||||||
}
|
}
|
||||||
|
|
@ -1426,7 +1454,7 @@ select {
|
||||||
border: 1px solid $offensive_border_faint;
|
border: 1px solid $offensive_border_faint;
|
||||||
background-color: $offensive_color_faint;
|
background-color: $offensive_color_faint;
|
||||||
|
|
||||||
&:hover {
|
&.hover:hover {
|
||||||
background-color: $offensive_border_faint;
|
background-color: $offensive_border_faint;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1441,7 +1469,7 @@ select {
|
||||||
background-color: $starts_as_villager_color;
|
background-color: $starts_as_villager_color;
|
||||||
border: 1px solid $starts_as_villager_border;
|
border: 1px solid $starts_as_villager_border;
|
||||||
|
|
||||||
&:hover {
|
&.hover:hover {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: $starts_as_villager_border;
|
background-color: $starts_as_villager_border;
|
||||||
}
|
}
|
||||||
|
|
@ -1450,7 +1478,7 @@ select {
|
||||||
border: 1px solid $starts_as_villager_border_faint;
|
border: 1px solid $starts_as_villager_border_faint;
|
||||||
background-color: $starts_as_villager_color_faint;
|
background-color: $starts_as_villager_color_faint;
|
||||||
|
|
||||||
&:hover {
|
&.hover:hover {
|
||||||
background-color: $starts_as_villager_border_faint;
|
background-color: $starts_as_villager_border_faint;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1465,7 +1493,7 @@ select {
|
||||||
background-color: $traitor_color;
|
background-color: $traitor_color;
|
||||||
border: 1px solid $traitor_border;
|
border: 1px solid $traitor_border;
|
||||||
|
|
||||||
&:hover {
|
&.hover:hover {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: $traitor_border;
|
background-color: $traitor_border;
|
||||||
}
|
}
|
||||||
|
|
@ -1474,7 +1502,7 @@ select {
|
||||||
border: 1px solid $traitor_border_faint;
|
border: 1px solid $traitor_border_faint;
|
||||||
background-color: $traitor_color_faint;
|
background-color: $traitor_color_faint;
|
||||||
|
|
||||||
&:hover {
|
&.hover:hover {
|
||||||
background-color: $traitor_border_faint;
|
background-color: $traitor_border_faint;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1484,7 +1512,7 @@ select {
|
||||||
background-color: $drunk_color;
|
background-color: $drunk_color;
|
||||||
border: 1px solid $drunk_border;
|
border: 1px solid $drunk_border;
|
||||||
|
|
||||||
&:hover {
|
&.hover:hover {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: $drunk_border;
|
background-color: $drunk_border;
|
||||||
}
|
}
|
||||||
|
|
@ -1493,7 +1521,7 @@ select {
|
||||||
border: 1px solid $drunk_border_faint;
|
border: 1px solid $drunk_border_faint;
|
||||||
background-color: $drunk_color_faint;
|
background-color: $drunk_color_faint;
|
||||||
|
|
||||||
&:hover {
|
&.hover:hover {
|
||||||
background-color: $drunk_border_faint;
|
background-color: $drunk_border_faint;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1543,6 +1571,10 @@ select {
|
||||||
}
|
}
|
||||||
|
|
||||||
.setup-screen {
|
.setup-screen {
|
||||||
|
.inactive {
|
||||||
|
filter: brightness(0%);
|
||||||
|
}
|
||||||
|
|
||||||
margin-top: 2%;
|
margin-top: 2%;
|
||||||
font-size: 1.5vw;
|
font-size: 1.5vw;
|
||||||
|
|
||||||
|
|
@ -1693,11 +1725,6 @@ li.choice {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.inactive {
|
|
||||||
// filter: grayscale(100%) brightness(30%);
|
|
||||||
filter: brightness(0%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.qrcode {
|
.qrcode {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -1938,50 +1965,66 @@ li.choice {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
align-items: baseline;
|
align-items: center;
|
||||||
align-content: baseline;
|
align-content: baseline;
|
||||||
justify-content: baseline;
|
|
||||||
justify-items: baseline;
|
justify-items: baseline;
|
||||||
|
gap: 1ch;
|
||||||
|
|
||||||
padding-top: 5px;
|
padding-top: 2px;
|
||||||
padding-bottom: 5px;
|
padding-bottom: 2px;
|
||||||
padding-left: 5px;
|
padding-left: 2px;
|
||||||
padding-right: 10px;
|
padding-right: 2px;
|
||||||
|
|
||||||
&:has(.killer) {
|
.inactive {
|
||||||
border: 1px solid rgba(212, 85, 0, 0.5);
|
filter: grayscale(100%);
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:has(.powerful) {
|
// img {
|
||||||
border: 1px solid rgba(0, 173, 193, 0.5);
|
// vertical-align: sub;
|
||||||
}
|
// }
|
||||||
|
|
||||||
&:has(.inactive) {
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
vertical-align: sub;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.execution {
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alignment-eq {
|
.alignment-eq,
|
||||||
img {
|
.roleblock-span {
|
||||||
vertical-align: sub;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1ch;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-span {
|
||||||
|
height: max-content;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
word-wrap: normal;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
border: 1px solid $intel_border;
|
.dead {
|
||||||
background-color: color.change($intel_color, $alpha: 0.1);
|
text-decoration: line-through;
|
||||||
padding-top: 5px;
|
font-style: italic;
|
||||||
padding-bottom: 5px;
|
}
|
||||||
|
|
||||||
padding-left: 10px;
|
&:hover::after {
|
||||||
padding-right: 10px;
|
content: attr(role);
|
||||||
|
overflow-y: hidden;
|
||||||
|
position: absolute;
|
||||||
|
margin-top: 60px;
|
||||||
|
color: white;
|
||||||
|
background-color: black;
|
||||||
|
border: 1px solid white;
|
||||||
|
padding: 3px;
|
||||||
|
z-index: 4;
|
||||||
|
align-self: center;
|
||||||
|
justify-self: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.character-span {
|
.character-span {
|
||||||
|
|
@ -1990,7 +2033,12 @@ li.choice {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 1ch;
|
||||||
|
|
||||||
|
.dead {
|
||||||
|
text-decoration: line-through;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
.number {
|
.number {
|
||||||
color: rgba(255, 255, 0, 0.7);
|
color: rgba(255, 255, 0, 0.7);
|
||||||
|
|
@ -2699,6 +2747,29 @@ dialog::backdrop {
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
// gap: 3px;
|
||||||
|
margin: 0;
|
||||||
|
align-self: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.tab-button:not(.selected) {
|
||||||
|
flex-grow: 1;
|
||||||
|
// color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button {
|
||||||
|
color: white;
|
||||||
|
flex-grow: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
text-shadow: 2px 2px black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.story {
|
.story {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -2707,13 +2778,14 @@ dialog::backdrop {
|
||||||
// width: 100vw;
|
// width: 100vw;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
row-gap: 5px;
|
row-gap: 5px;
|
||||||
margin: 5vh 10vw 0px 10vw;
|
margin: 5vh 5vw 0px 5vw;
|
||||||
|
|
||||||
.character-headline {
|
.character-headline {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
.icon-spacer {
|
.icon-spacer {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
|
@ -2729,12 +2801,47 @@ dialog::backdrop {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.character-details {
|
|
||||||
display: none;
|
|
||||||
|
|
||||||
&.shown {
|
.actions {
|
||||||
display: flex;
|
width: 100%;
|
||||||
|
// min-height: 30vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.no-content {
|
||||||
|
filter: grayscale(60%);
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
// backdrop-filter: brightness(20%);
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-headline {
|
||||||
|
display: none;
|
||||||
|
// margin-left: 0.5cm;
|
||||||
|
// font-size: 1.5em;
|
||||||
|
// font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action,
|
||||||
|
.change {
|
||||||
|
font-size: 1.25em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1ch;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-details {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
border-top: none;
|
border-top: none;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -2760,11 +2867,7 @@ dialog::backdrop {
|
||||||
}
|
}
|
||||||
|
|
||||||
.details {
|
.details {
|
||||||
display: none;
|
display: flex;
|
||||||
|
|
||||||
&.shown {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
|
|
@ -2805,3 +2908,59 @@ dialog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wolves-highlight {
|
||||||
|
color: $wolves_color;
|
||||||
|
|
||||||
|
.number {
|
||||||
|
color: $wolves_border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.village-highlight {
|
||||||
|
color: $village_color;
|
||||||
|
|
||||||
|
.number {
|
||||||
|
color: $village_border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.intel-highlight {
|
||||||
|
color: $intel_color;
|
||||||
|
|
||||||
|
.number {
|
||||||
|
color: $intel_border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.defensive-highlight {
|
||||||
|
color: $defensive_color;
|
||||||
|
|
||||||
|
.number {
|
||||||
|
color: $defensive_border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.offensive-highlight {
|
||||||
|
color: $offensive_color;
|
||||||
|
|
||||||
|
.number {
|
||||||
|
color: $offensive_border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.starts-as-villager-highlight {
|
||||||
|
color: $starts_as_villager_color;
|
||||||
|
|
||||||
|
.number {
|
||||||
|
color: $starts_as_villager_border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.traitor-highlight {
|
||||||
|
color: $traitor_color;
|
||||||
|
|
||||||
|
.number {
|
||||||
|
color: $traitor_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (C) 2025 Emilis Bliūdžius
|
// Copyright (C) 2026 Emilis Bliūdžius
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
// it under the terms of the GNU Affero General Public License as
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
|
@ -12,6 +12,22 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use werewolves_proto::{character::Character, game::SetupRole, team::Team};
|
||||||
|
|
||||||
pub trait Class {
|
pub trait Class {
|
||||||
fn class(&self) -> Option<&'static str>;
|
fn class(&self) -> Option<&'static str>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Class for Character {
|
||||||
|
fn class(&self) -> Option<&'static str> {
|
||||||
|
if let Team::AnyEvil = self.team() {
|
||||||
|
return Some("traitor");
|
||||||
|
}
|
||||||
|
Some(
|
||||||
|
Into::<SetupRole>::into(self.role_title())
|
||||||
|
.category()
|
||||||
|
.class(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (C) 2025 Emilis Bliūdžius
|
// Copyright (C) 2026 Emilis Bliūdžius
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
// it under the terms of the GNU Affero General Public License as
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
|
@ -29,15 +29,13 @@ pub struct AlignmentSpanProps {
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn AlignmentSpan(AlignmentSpanProps { alignment }: &AlignmentSpanProps) -> Html {
|
pub fn AlignmentSpan(AlignmentSpanProps { alignment }: &AlignmentSpanProps) -> Html {
|
||||||
let class = match alignment {
|
let class = match alignment {
|
||||||
role::Alignment::Village => "village",
|
role::Alignment::Village => "village-highlight",
|
||||||
role::Alignment::Wolves => "wolves",
|
role::Alignment::Wolves => "wolves-highlight",
|
||||||
role::Alignment::Traitor => "traitor",
|
role::Alignment::Traitor => "traitor-highlight",
|
||||||
};
|
};
|
||||||
html! {
|
html! {
|
||||||
<span class={classes!("attribute-span", "faint", class)}>
|
<span class={classes!("attribute-span", class)}>
|
||||||
<div>
|
<Icon source={alignment.icon()} icon_type={IconType::Fit}/>
|
||||||
<Icon source={alignment.icon()} icon_type={IconType::Small}/>
|
|
||||||
</div>
|
|
||||||
{alignment.to_string()}
|
{alignment.to_string()}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,20 +29,14 @@ pub fn AlignmentComparisonSpan(
|
||||||
match comparison {
|
match comparison {
|
||||||
AlignmentEq::Same => html! {
|
AlignmentEq::Same => html! {
|
||||||
<span class="alignment-eq">
|
<span class="alignment-eq">
|
||||||
<div><Icon source={IconSource::Village} icon_type={IconType::Small}/></div>
|
<Icon source={IconSource::Equal} icon_type={IconType::Fit}/>
|
||||||
<div><Icon source={IconSource::Village} icon_type={IconType::Small}/></div>
|
|
||||||
{"the same"}
|
{"the same"}
|
||||||
<div><Icon source={IconSource::Wolves} icon_type={IconType::Small}/></div>
|
|
||||||
<div><Icon source={IconSource::Wolves} icon_type={IconType::Small}/></div>
|
|
||||||
</span>
|
</span>
|
||||||
},
|
},
|
||||||
AlignmentEq::Different => html! {
|
AlignmentEq::Different => html! {
|
||||||
<span class="alignment-eq">
|
<span class="alignment-eq">
|
||||||
<div><Icon source={IconSource::Village} icon_type={IconType::Small}/></div>
|
<Icon source={IconSource::NotEqual} icon_type={IconType::Fit}/>
|
||||||
<div><Icon source={IconSource::Wolves} icon_type={IconType::Small}/></div>
|
|
||||||
{"different"}
|
{"different"}
|
||||||
<div><Icon source={IconSource::Wolves} icon_type={IconType::Small}/></div>
|
|
||||||
<div><Icon source={IconSource::Village} icon_type={IconType::Small}/></div>
|
|
||||||
</span>
|
</span>
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,29 +25,10 @@ pub struct DiedToSpanProps {
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn DiedToSpan(DiedToSpanProps { died_to }: &DiedToSpanProps) -> Html {
|
pub fn DiedToSpan(DiedToSpanProps { died_to }: &DiedToSpanProps) -> Html {
|
||||||
let class = match died_to {
|
|
||||||
DiedToTitle::Execution => "execution",
|
|
||||||
DiedToTitle::MapleWolfStarved | DiedToTitle::MapleWolf => {
|
|
||||||
SetupRoleTitle::MapleWolf.category().class()
|
|
||||||
}
|
|
||||||
DiedToTitle::Militia => SetupRoleTitle::Militia.category().class(),
|
|
||||||
DiedToTitle::LoneWolf
|
|
||||||
| DiedToTitle::AlphaWolf
|
|
||||||
| DiedToTitle::Shapeshift
|
|
||||||
| DiedToTitle::Wolfpack => SetupRoleTitle::Werewolf.category().class(),
|
|
||||||
DiedToTitle::Hunter => SetupRoleTitle::Hunter.category().class(),
|
|
||||||
DiedToTitle::GuardianProtecting => SetupRoleTitle::Guardian.category().class(),
|
|
||||||
DiedToTitle::PyreMasterLynchMob | DiedToTitle::PyreMaster => {
|
|
||||||
SetupRoleTitle::PyreMaster.category().class()
|
|
||||||
}
|
|
||||||
DiedToTitle::MasonLeaderRecruitFail => SetupRoleTitle::MasonLeader.category().class(),
|
|
||||||
};
|
|
||||||
let icon = died_to.icon().unwrap_or(IconSource::Skull);
|
let icon = died_to.icon().unwrap_or(IconSource::Skull);
|
||||||
html! {
|
html! {
|
||||||
<span class={classes!("attribute-span", "faint", class)}>
|
<span class={classes!("attribute-span",)}>
|
||||||
<div>
|
<Icon source={icon} icon_type={IconType::Fit}/>
|
||||||
<Icon source={icon} icon_type={IconType::Small}/>
|
|
||||||
</div>
|
|
||||||
{died_to.to_string().to_case(Case::Title)}
|
{died_to.to_string().to_case(Case::Title)}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,12 @@ pub fn KillerSpan(KillerSpanProps { killer }: &KillerSpanProps) -> Html {
|
||||||
Killer::NotKiller => "inactive",
|
Killer::NotKiller => "inactive",
|
||||||
};
|
};
|
||||||
html! {
|
html! {
|
||||||
<span class={classes!("attribute-span", "faint")}>
|
<span class={classes!("attribute-span")}>
|
||||||
<div class={classes!(class)}>
|
<Icon
|
||||||
<Icon source={killer.icon()} icon_type={IconType::Small}/>
|
source={killer.icon()}
|
||||||
</div>
|
icon_type={IconType::Fit}
|
||||||
|
classes={classes!(class)}
|
||||||
|
/>
|
||||||
{killer.to_string()}
|
{killer.to_string()}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,12 @@ pub fn PowerfulSpan(PowerfulSpanProps { powerful }: &PowerfulSpanProps) -> Html
|
||||||
Powerful::NotPowerful => "inactive",
|
Powerful::NotPowerful => "inactive",
|
||||||
};
|
};
|
||||||
html! {
|
html! {
|
||||||
<span class={classes!("attribute-span", "faint")}>
|
<span class={classes!("attribute-span")}>
|
||||||
<div class={classes!(class)}>
|
<Icon
|
||||||
<Icon source={powerful.icon()} icon_type={IconType::Small}/>
|
source={powerful.icon()}
|
||||||
</div>
|
icon_type={IconType::Fit}
|
||||||
|
classes={classes!(class)}
|
||||||
|
/>
|
||||||
{powerful.to_string()}
|
{powerful.to_string()}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
use core::ops::Not;
|
||||||
|
|
||||||
// Copyright (C) 2025 Emilis Bliūdžius
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
|
@ -13,10 +15,17 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
use werewolves_proto::{character::Character, game::SetupRole, message::CharacterIdentity};
|
use werewolves_proto::{
|
||||||
|
character::Character,
|
||||||
|
game::{GameTime, SetupRole},
|
||||||
|
message::CharacterIdentity,
|
||||||
|
};
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
use crate::components::{Icon, IconSource, IconType, PartialAssociatedIcon};
|
use crate::{
|
||||||
|
class::Class,
|
||||||
|
components::{Icon, IconSource, IconType, PartialAssociatedIcon},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
pub struct CharacterCardProps {
|
pub struct CharacterCardProps {
|
||||||
|
|
@ -89,3 +98,47 @@ pub fn CharacterTargetCard(
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
pub struct CharacterHighlightProps {
|
||||||
|
pub char: Character,
|
||||||
|
pub time: GameTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn CharacterHighlight(
|
||||||
|
CharacterHighlightProps { char, time }: &CharacterHighlightProps,
|
||||||
|
) -> Html {
|
||||||
|
let class = char.class().map(|c| format!("{c}-highlight"));
|
||||||
|
let icon = char
|
||||||
|
.role_title()
|
||||||
|
.icon()
|
||||||
|
.map(|source| {
|
||||||
|
html! {
|
||||||
|
<Icon source={source} icon_type={IconType::Fit}/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(html! {
|
||||||
|
<div class="icon-spacer"/>
|
||||||
|
});
|
||||||
|
let dead = char.died_to().and_then(|died_to| {
|
||||||
|
let night = died_to.night();
|
||||||
|
let day = died_to.day();
|
||||||
|
match (time, day, night) {
|
||||||
|
(GameTime::Day { number }, Some(day), None) => number.get() > day.get(),
|
||||||
|
(GameTime::Night { number }, None, Some(night)) => *number > night,
|
||||||
|
(GameTime::Day { number }, None, Some(night)) => number.get() > night,
|
||||||
|
(GameTime::Night { number }, Some(day), None) => *number >= day.get(),
|
||||||
|
(_, None, None) | (_, Some(_), Some(_)) => true,
|
||||||
|
}
|
||||||
|
.then_some("dead")
|
||||||
|
});
|
||||||
|
let role_text = char.role_title().to_string().to_case(Case::Title);
|
||||||
|
html! {
|
||||||
|
<span class={classes!("highlight-span", class)} role={role_text}>
|
||||||
|
{icon}
|
||||||
|
<span class={classes!("number")}><b>{char.number().get()}</b></span>
|
||||||
|
<span class={classes!("name", dead)}>{char.name()}</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
// Copyright (C) 2026 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use convert_case::{Case, Casing};
|
||||||
|
use werewolves_proto::{game::SetupRole, role::RoleTitle};
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::{Icon, IconSource, IconType, PartialAssociatedIcon};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
pub struct RoleSpanProps {
|
||||||
|
pub role: RoleTitle,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn RoleSpan(RoleSpanProps { role }: &RoleSpanProps) -> Html {
|
||||||
|
let class = Into::<SetupRole>::into(*role).category().class();
|
||||||
|
let icon = role
|
||||||
|
.icon()
|
||||||
|
.map(|icon| {
|
||||||
|
html! {
|
||||||
|
<Icon source={icon} icon_type={IconType::Small}/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(html! {<div class="icon-spacer"/>});
|
||||||
|
let role_name = role.to_string().to_case(Case::Title);
|
||||||
|
html! {
|
||||||
|
<span class={classes!("role-span", class, "faint")}>
|
||||||
|
{icon}
|
||||||
|
<span class="role-name">{role_name}</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
pub struct RoleblockProps {
|
||||||
|
#[prop_or_default]
|
||||||
|
pub children: Html,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn Roleblock(RoleblockProps { children }: &RoleblockProps) -> Html {
|
||||||
|
let content = if *children == html! {} {
|
||||||
|
html! {<span>{"Drunk"}</span>}
|
||||||
|
} else {
|
||||||
|
children.clone()
|
||||||
|
};
|
||||||
|
html! {
|
||||||
|
<span class={classes!("roleblock-span")}>
|
||||||
|
<Icon source={IconSource::Roleblock} icon_type={IconType::Fit}/>
|
||||||
|
{content}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,85 +13,152 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use core::ops::Not;
|
||||||
use std::{collections::HashMap, rc::Rc};
|
use std::{collections::HashMap, rc::Rc};
|
||||||
|
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
|
aura::AuraTitle,
|
||||||
character::{Character, CharacterId},
|
character::{Character, CharacterId},
|
||||||
|
diedto::DiedTo,
|
||||||
game::{
|
game::{
|
||||||
GameTime, SetupRole,
|
GameTime, SetupRole,
|
||||||
night::changes::NightChange,
|
night::changes::NightChange,
|
||||||
story::{NightChoice, StoryActionPrompt},
|
story::{NightChoice, StoryActionPrompt, StoryActionResult},
|
||||||
},
|
},
|
||||||
|
player::Protection,
|
||||||
|
role::{RoleBlock, RoleTitle},
|
||||||
};
|
};
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
use crate::components::{Icon, IconSource, IconType, Identity, PartialAssociatedIcon};
|
use crate::{
|
||||||
|
class::Class,
|
||||||
|
components::{
|
||||||
|
AuraSpan, Button, CharacterHighlight, Icon, IconSource, IconType, Identity, IdentitySpan,
|
||||||
|
PartialAssociatedIcon, RoleSpan, Roleblock,
|
||||||
|
attributes::{
|
||||||
|
AlignmentComparisonSpan, AlignmentSpan, DiedToSpan, KillerSpan, PowerfulSpan,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
pub struct CharacterStoryProps {
|
pub struct CharacterStoryProps {
|
||||||
|
pub all_characters: Rc<[Character]>,
|
||||||
pub character: Character,
|
pub character: Character,
|
||||||
pub actions: Box<[(GameTime, Box<[NightChange]>, Box<[NightChoice]>)]>,
|
pub actions: Box<[(GameTime, Box<[NightChange]>, Box<[NightChoice]>)]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn CharacterStory(CharacterStoryProps { character, actions }: &CharacterStoryProps) -> Html {
|
pub fn CharacterStory(
|
||||||
let mut by_time = actions.clone();
|
CharacterStoryProps {
|
||||||
|
all_characters,
|
||||||
|
character,
|
||||||
|
actions,
|
||||||
|
}: &CharacterStoryProps,
|
||||||
|
) -> Html {
|
||||||
|
let class = character.class();
|
||||||
|
let mut by_time = actions
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, changes, choices)| !(changes.is_empty() && choices.is_empty()))
|
||||||
|
.cloned()
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
by_time.sort_by_key(|s| s.0);
|
by_time.sort_by_key(|s| s.0);
|
||||||
let by_time = by_time
|
let open_time = use_state(|| by_time.iter().next().map(|(t, _, _)| *t));
|
||||||
|
if let Some(current_open_time) = open_time.as_ref()
|
||||||
|
&& let Some(last) = by_time.last().map(|c| c.0)
|
||||||
|
{
|
||||||
|
if last < *current_open_time || !by_time.iter().any(|(t, _, _)| t == current_open_time) {
|
||||||
|
open_time.set(Some(last));
|
||||||
|
} else if let Some(first) = by_time.first().map(|c| c.0)
|
||||||
|
&& first > *current_open_time
|
||||||
|
{
|
||||||
|
open_time.set(Some(first));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let tabs = by_time
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(time, changes, choices)| {
|
.map(|(time, _, _)| {
|
||||||
|
let time_text = match time {
|
||||||
|
GameTime::Day { number } => format!("day {number}"),
|
||||||
|
GameTime::Night { number } => format!("night {number}"),
|
||||||
|
};
|
||||||
|
let on_click = {
|
||||||
|
let open_time = open_time.setter();
|
||||||
|
Callback::from(move |_| open_time.set(Some(time)))
|
||||||
|
};
|
||||||
|
let selected = open_time
|
||||||
|
.as_ref()
|
||||||
|
.map(|open| *open == time)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let faint = selected.not().then_some("faint");
|
||||||
|
let selected = selected.then_some("selected");
|
||||||
html! {
|
html! {
|
||||||
<CharacterStoryTime
|
<button
|
||||||
character={character.clone()}
|
onclick={on_click}
|
||||||
time={time}
|
class={classes!("tab-button", class, faint, selected, "hover")}
|
||||||
changes={changes}
|
>
|
||||||
choices={choices}
|
{time_text}
|
||||||
/>
|
</button>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Html>();
|
.collect::<Html>();
|
||||||
|
|
||||||
|
let story_content = open_time
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|time| actions.iter().find(|(t, _, _)| t == time))
|
||||||
|
.map(|(time, changes, choices)| {
|
||||||
|
let choices = choices
|
||||||
|
.iter()
|
||||||
|
.map(|c| {
|
||||||
|
html! {
|
||||||
|
<Choice
|
||||||
|
all_characters={all_characters.clone()}
|
||||||
|
character={character.clone()}
|
||||||
|
choice={c.clone()}
|
||||||
|
all_choices_that_night={choices.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Html>();
|
||||||
|
let changes = changes
|
||||||
|
.iter()
|
||||||
|
.map(|c| {
|
||||||
|
html! {
|
||||||
|
<Change all_characters={all_characters.clone()} change={c.clone()} time={*time}/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Html>();
|
||||||
|
let changes = (changes == html! {}).not().then_some(html! {
|
||||||
|
<>
|
||||||
|
<span class="sub-headline">{"changes"}</span>
|
||||||
|
<div>
|
||||||
|
{changes}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
});
|
||||||
|
let choices = (choices == html! {}).not().then_some(html! {
|
||||||
|
<>
|
||||||
|
<span class="sub-headline">{"choices"}</span>
|
||||||
|
<div>
|
||||||
|
{choices}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
});
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
{choices}
|
||||||
|
{changes}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
});
|
||||||
html! {
|
html! {
|
||||||
<div class="character-story">
|
<div class="character-story">
|
||||||
<CharacterStoryContainer character={character.clone()}>
|
<div class="tabs">
|
||||||
{by_time}
|
{tabs}
|
||||||
</CharacterStoryContainer>
|
</div>
|
||||||
</div>
|
<div class="story-content">
|
||||||
}
|
{story_content}
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
|
||||||
struct CharacterStoryTimeProps {
|
|
||||||
pub character: Character,
|
|
||||||
pub time: GameTime,
|
|
||||||
pub changes: Box<[NightChange]>,
|
|
||||||
pub choices: Box<[NightChoice]>,
|
|
||||||
}
|
|
||||||
#[function_component]
|
|
||||||
fn CharacterStoryTime(
|
|
||||||
CharacterStoryTimeProps {
|
|
||||||
character,
|
|
||||||
time,
|
|
||||||
changes,
|
|
||||||
choices,
|
|
||||||
}: &CharacterStoryTimeProps,
|
|
||||||
) -> Html {
|
|
||||||
let open = use_state(|| false);
|
|
||||||
let time_text = match time {
|
|
||||||
GameTime::Day { number } => format!("day {number}"),
|
|
||||||
GameTime::Night { number } => format!("night {number}"),
|
|
||||||
};
|
|
||||||
let shown = open.then_some("shown");
|
|
||||||
let on_click = {
|
|
||||||
let open = open.clone();
|
|
||||||
Callback::from(move |_| open.set(!*open))
|
|
||||||
};
|
|
||||||
html! {
|
|
||||||
<div class="story-time">
|
|
||||||
<span class={classes!("time")} onclick={on_click}>
|
|
||||||
{time_text}
|
|
||||||
</span>
|
|
||||||
<div class={classes!("details", shown)}>
|
|
||||||
{"hello"}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
@ -102,6 +169,8 @@ struct ChoiceProps {
|
||||||
all_characters: Rc<[Character]>,
|
all_characters: Rc<[Character]>,
|
||||||
character: Character,
|
character: Character,
|
||||||
choice: NightChoice,
|
choice: NightChoice,
|
||||||
|
all_choices_that_night: Box<[NightChoice]>,
|
||||||
|
time: GameTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
|
|
@ -110,45 +179,258 @@ fn Choice(
|
||||||
all_characters,
|
all_characters,
|
||||||
character,
|
character,
|
||||||
choice,
|
choice,
|
||||||
|
all_choices_that_night,
|
||||||
|
time,
|
||||||
}: &ChoiceProps,
|
}: &ChoiceProps,
|
||||||
) -> Html {
|
) -> Html {
|
||||||
let generate_prompt = |chosen: CharacterId| -> Html { todo!() };
|
let generate_prompt = |chosen: CharacterId, wording: &'static str| -> Option<Html> {
|
||||||
|
all_characters
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.character_id() == chosen)
|
||||||
|
.map(|chosen| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<CharacterHighlight
|
||||||
|
char={character.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
<span>{wording}</span>
|
||||||
|
<CharacterHighlight
|
||||||
|
char={chosen.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
let prompt = match &choice.prompt {
|
let prompt = match &choice.prompt {
|
||||||
StoryActionPrompt::Guardian {
|
StoryActionPrompt::Guardian {
|
||||||
chosen, guarding, ..
|
chosen, guarding, ..
|
||||||
} => todo!(),
|
} => generate_prompt(*chosen, if *guarding { "guarded" } else { "protected" }),
|
||||||
StoryActionPrompt::Seer { chosen, .. } => todo!(),
|
StoryActionPrompt::Seer { chosen, .. } => generate_prompt(*chosen, "checked"),
|
||||||
StoryActionPrompt::Protector { chosen, .. } => todo!(),
|
StoryActionPrompt::Protector { chosen, .. } => generate_prompt(*chosen, "protected"),
|
||||||
StoryActionPrompt::Arcanist { chosen, .. } => todo!(),
|
StoryActionPrompt::Arcanist {
|
||||||
StoryActionPrompt::Gravedigger { chosen, .. } => todo!(),
|
chosen: (target1, target2),
|
||||||
StoryActionPrompt::Hunter { chosen, .. } => todo!(),
|
..
|
||||||
StoryActionPrompt::Militia { chosen, .. } => todo!(),
|
} => all_characters
|
||||||
StoryActionPrompt::MapleWolf { chosen, .. } => todo!(),
|
.iter()
|
||||||
StoryActionPrompt::Adjudicator { chosen, .. } => todo!(),
|
.find(|c| c.character_id() == *target1)
|
||||||
StoryActionPrompt::PowerSeer { chosen, .. } => todo!(),
|
.and_then(|t1| {
|
||||||
StoryActionPrompt::Mortician { chosen, .. } => todo!(),
|
all_characters
|
||||||
StoryActionPrompt::Beholder { chosen, .. } => todo!(),
|
.iter()
|
||||||
StoryActionPrompt::MasonsWake { leader, masons } => todo!(),
|
.find(|c| c.character_id() == *target2)
|
||||||
StoryActionPrompt::MasonLeaderRecruit { chosen, .. } => todo!(),
|
.map(|t2| (t1, t2))
|
||||||
StoryActionPrompt::Empath { chosen, .. } => todo!(),
|
})
|
||||||
StoryActionPrompt::Vindicator { chosen, .. } => todo!(),
|
.map(|(t1, t2)| {
|
||||||
StoryActionPrompt::PyreMaster { chosen, .. } => todo!(),
|
html! {
|
||||||
StoryActionPrompt::WolfPackKill { chosen } => todo!(),
|
<>
|
||||||
StoryActionPrompt::Shapeshifter { .. } => todo!(),
|
<CharacterHighlight
|
||||||
StoryActionPrompt::AlphaWolf { chosen, .. } => todo!(),
|
char={character.clone()}
|
||||||
StoryActionPrompt::DireWolf { chosen, .. } => todo!(),
|
time={*time}
|
||||||
StoryActionPrompt::LoneWolfKill { chosen, .. } => todo!(),
|
/>
|
||||||
StoryActionPrompt::Insomniac { .. } => todo!(),
|
<span>{"checked"}</span>
|
||||||
StoryActionPrompt::Bloodletter { chosen, .. } => todo!(),
|
<CharacterHighlight
|
||||||
StoryActionPrompt::BeholderWakes { character_id } => todo!(),
|
char={t1.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
<span>{"and"}</span>
|
||||||
|
<CharacterHighlight
|
||||||
|
char={t2.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
StoryActionPrompt::Gravedigger { chosen, .. } => generate_prompt(*chosen, "dug"),
|
||||||
|
StoryActionPrompt::Hunter { chosen, .. } => generate_prompt(*chosen, "set a trap for"),
|
||||||
|
StoryActionPrompt::Militia { chosen, .. } => generate_prompt(*chosen, "shot"),
|
||||||
|
StoryActionPrompt::MapleWolf { chosen, .. } => generate_prompt(*chosen, "ate"),
|
||||||
|
StoryActionPrompt::Adjudicator { chosen, .. } => generate_prompt(*chosen, "checked"),
|
||||||
|
StoryActionPrompt::PowerSeer { chosen, .. } => generate_prompt(*chosen, "checked"),
|
||||||
|
StoryActionPrompt::Mortician { chosen, .. } => generate_prompt(*chosen, "examined"),
|
||||||
|
StoryActionPrompt::Beholder { chosen, .. } => generate_prompt(*chosen, "beheld"),
|
||||||
|
StoryActionPrompt::MasonLeaderRecruit { chosen, .. } => {
|
||||||
|
generate_prompt(*chosen, "attempted to recruit")
|
||||||
|
}
|
||||||
|
StoryActionPrompt::WolfPackKill { chosen, .. } => {
|
||||||
|
generate_prompt(*chosen, "took the wolfpack kill and attacked")
|
||||||
|
}
|
||||||
|
StoryActionPrompt::MasonsWake { leader, .. } => all_characters
|
||||||
|
.iter()
|
||||||
|
.find(|l| l.character_id() == *leader)
|
||||||
|
.map(|leader| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<CharacterHighlight
|
||||||
|
char={character.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
<span>{"woke with the masons of"}</span>
|
||||||
|
<CharacterHighlight
|
||||||
|
char={leader.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
StoryActionPrompt::Empath { chosen, .. } => generate_prompt(*chosen, "checked"),
|
||||||
|
StoryActionPrompt::Vindicator { chosen, .. } => generate_prompt(*chosen, "protected"),
|
||||||
|
StoryActionPrompt::PyreMaster { chosen, .. } => generate_prompt(*chosen, "burned"),
|
||||||
|
StoryActionPrompt::Shapeshifter { .. } => all_choices_that_night
|
||||||
|
.iter()
|
||||||
|
.find_map(|c| match c.prompt {
|
||||||
|
StoryActionPrompt::WolfPackKill { chosen, .. } => Some(chosen),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.and_then(|target| all_characters.iter().find(|c| c.character_id() == target))
|
||||||
|
.map(|target| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<CharacterHighlight
|
||||||
|
char={character.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
<span>{"chose to shift into"}</span>
|
||||||
|
<CharacterHighlight
|
||||||
|
char={target.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
StoryActionPrompt::Insomniac { .. } => Some(html! {
|
||||||
|
<>
|
||||||
|
<CharacterHighlight
|
||||||
|
char={character.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
<span>{"woke in the night due to visits from: "}</span>
|
||||||
|
</>
|
||||||
|
}),
|
||||||
|
StoryActionPrompt::AlphaWolf { chosen, .. } => generate_prompt(*chosen, "killed"),
|
||||||
|
StoryActionPrompt::DireWolf { chosen, .. } => {
|
||||||
|
generate_prompt(*chosen, "roleblocked visitors to")
|
||||||
|
}
|
||||||
|
StoryActionPrompt::LoneWolfKill { chosen, .. } => generate_prompt(*chosen, "killed"),
|
||||||
|
StoryActionPrompt::Bloodletter { chosen, .. } => generate_prompt(*chosen, "bloodlet"),
|
||||||
|
StoryActionPrompt::BeholderWakes { character_id } => {
|
||||||
|
generate_prompt(*character_id, "woke again to see")
|
||||||
|
}
|
||||||
};
|
};
|
||||||
html! {}
|
if prompt.is_none() {
|
||||||
|
return html! {};
|
||||||
|
}
|
||||||
|
let result = choice.result.as_ref().map(|result| match result {
|
||||||
|
StoryActionResult::RoleBlocked => html! {
|
||||||
|
<>
|
||||||
|
<span>{"but was"}</span>
|
||||||
|
<Roleblock>{"Roleblocked"}</Roleblock>
|
||||||
|
</>
|
||||||
|
},
|
||||||
|
StoryActionResult::Seer(alignment) => html! {
|
||||||
|
<>
|
||||||
|
<span>{"and saw"}</span>
|
||||||
|
<AlignmentSpan alignment={*alignment}/>
|
||||||
|
</>
|
||||||
|
},
|
||||||
|
StoryActionResult::PowerSeer { powerful } => html! {
|
||||||
|
<>
|
||||||
|
<span>{"and saw"}</span>
|
||||||
|
<PowerfulSpan powerful={*powerful}/>
|
||||||
|
</>
|
||||||
|
},
|
||||||
|
StoryActionResult::Adjudicator { killer } => html! {
|
||||||
|
<>
|
||||||
|
<span>{"and saw"}</span>
|
||||||
|
<KillerSpan killer={*killer}/>
|
||||||
|
</>
|
||||||
|
},
|
||||||
|
StoryActionResult::Arcanist(alignment_eq) => html! {
|
||||||
|
<>
|
||||||
|
<span>{"and saw them as"}</span>
|
||||||
|
<AlignmentComparisonSpan comparison={*alignment_eq}/>
|
||||||
|
</>
|
||||||
|
},
|
||||||
|
StoryActionResult::GraveDigger(None) => html! {
|
||||||
|
<span>{"but found an empty grave"}</span>
|
||||||
|
},
|
||||||
|
StoryActionResult::GraveDigger(Some(role_title)) => html! {
|
||||||
|
<>
|
||||||
|
<span>{"as"}</span>
|
||||||
|
<RoleSpan role={*role_title}/>
|
||||||
|
</>
|
||||||
|
},
|
||||||
|
StoryActionResult::Mortician(died_to_title) => html! {
|
||||||
|
<>
|
||||||
|
<span>{"and found"}</span>
|
||||||
|
<DiedToSpan died_to={*died_to_title}/>
|
||||||
|
<span>{"to be the cause of death"}</span>
|
||||||
|
</>
|
||||||
|
},
|
||||||
|
StoryActionResult::Insomniac { visits } => {
|
||||||
|
let mut visit_chars = vec![];
|
||||||
|
for visit in visits {
|
||||||
|
match all_characters.iter().find(|c| c.character_id() == *visit) {
|
||||||
|
Some(char) => visit_chars.push(char),
|
||||||
|
None => {
|
||||||
|
log::warn!("visit chars missing {visit}");
|
||||||
|
return html! {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visit_chars
|
||||||
|
.into_iter()
|
||||||
|
.map(|visitor| {
|
||||||
|
html! {
|
||||||
|
<CharacterHighlight
|
||||||
|
char={visitor.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Html>()
|
||||||
|
}
|
||||||
|
StoryActionResult::Empath { scapegoat: true } => {
|
||||||
|
html! {<span>{"and found their scapegoat"}</span>}
|
||||||
|
}
|
||||||
|
StoryActionResult::Empath { scapegoat: false } => {
|
||||||
|
html! {<span>{"who was not a scapegoat"}</span>}
|
||||||
|
}
|
||||||
|
StoryActionResult::BeholderSawNothing => html! {
|
||||||
|
<>
|
||||||
|
{"and saw"}
|
||||||
|
<em>{"nothing"}</em>
|
||||||
|
</>
|
||||||
|
},
|
||||||
|
StoryActionResult::BeholderSawEverything => html! {
|
||||||
|
<>
|
||||||
|
{"and saw"}
|
||||||
|
<em>{"everything"}</em>
|
||||||
|
</>
|
||||||
|
},
|
||||||
|
StoryActionResult::ShiftFailed => html! {
|
||||||
|
{"however, their shift failed"}
|
||||||
|
},
|
||||||
|
StoryActionResult::Drunk => html! {
|
||||||
|
<>
|
||||||
|
<span>{"but got"}</span>
|
||||||
|
<AuraSpan aura={AuraTitle::Drunk} />
|
||||||
|
</>
|
||||||
|
},
|
||||||
|
});
|
||||||
|
html! {
|
||||||
|
<div class="action">
|
||||||
|
{prompt}
|
||||||
|
{result}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
struct ChangeProps {
|
struct ChangeProps {
|
||||||
all_characters: Rc<[Character]>,
|
all_characters: Rc<[Character]>,
|
||||||
change: NightChange,
|
change: NightChange,
|
||||||
|
time: GameTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
|
|
@ -156,53 +438,236 @@ fn Change(
|
||||||
ChangeProps {
|
ChangeProps {
|
||||||
all_characters,
|
all_characters,
|
||||||
change,
|
change,
|
||||||
|
time,
|
||||||
}: &ChangeProps,
|
}: &ChangeProps,
|
||||||
) -> Html {
|
) -> Html {
|
||||||
match change {
|
match change {
|
||||||
NightChange::RoleChange(role_title, ..) => todo!(),
|
NightChange::RoleChange(_, role_title) => Some(html! {
|
||||||
NightChange::Kill { target, died_to } => todo!(),
|
<>
|
||||||
|
<span>{"role changed to"}</span>
|
||||||
|
<RoleSpan role={*role_title}/>
|
||||||
|
</>
|
||||||
|
}),
|
||||||
|
NightChange::Kill { died_to, .. } => Some(html! {
|
||||||
|
<>
|
||||||
|
<span>{"died to"}</span>
|
||||||
|
<DiedToSpan died_to={died_to.title()}/>
|
||||||
|
</>
|
||||||
|
}),
|
||||||
NightChange::RoleBlock {
|
NightChange::RoleBlock {
|
||||||
source,
|
source,
|
||||||
target,
|
block_type: RoleBlock::Direwolf,
|
||||||
block_type,
|
..
|
||||||
} => todo!(),
|
} => all_characters
|
||||||
NightChange::Shapeshift { source, into } => todo!(),
|
.iter()
|
||||||
NightChange::Protection { target, protection } => todo!(),
|
.find(|s| s.character_id() == *source)
|
||||||
NightChange::ElderReveal { .. } => todo!(),
|
.map(|source| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<span>{"had visitors role blocked by"}</span>
|
||||||
|
<CharacterHighlight
|
||||||
|
char={source.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
NightChange::Shapeshift { source, .. } => all_characters
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.character_id() == *source)
|
||||||
|
.map(|source| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
{"shapeshifted by"}
|
||||||
|
<CharacterHighlight
|
||||||
|
char={source.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
{"and became a werewolf"}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
NightChange::ElderReveal { .. } => Some(html! {
|
||||||
|
<>
|
||||||
|
<span>{"learned they are the"}</span>
|
||||||
|
<RoleSpan role={RoleTitle::Elder}/>
|
||||||
|
</>
|
||||||
|
}),
|
||||||
NightChange::MasonRecruit {
|
NightChange::MasonRecruit {
|
||||||
mason_leader,
|
mason_leader,
|
||||||
recruiting,
|
recruiting,
|
||||||
} => todo!(),
|
} => all_characters
|
||||||
NightChange::ApplyAura { source, aura, .. } => todo!(),
|
.iter()
|
||||||
NightChange::LostAura { aura, .. } => todo!(),
|
.find(|c| c.character_id() == *mason_leader)
|
||||||
NightChange::EmpathFoundScapegoat { .. } | NightChange::HunterTarget { .. } => {
|
.and_then(|mason| {
|
||||||
return html! {};
|
all_characters
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.character_id() == *recruiting)
|
||||||
|
.map(|recruiting| (mason, recruiting))
|
||||||
|
})
|
||||||
|
.map(|(mason, recruiting)| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<CharacterHighlight
|
||||||
|
char={mason.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
<span>{"recruited"}</span>
|
||||||
|
<CharacterHighlight
|
||||||
|
char={recruiting.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
<span>{"into the masons"}</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
NightChange::ApplyAura { source, aura, .. } => {
|
||||||
|
let from = all_characters
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.character_id() == *source)
|
||||||
|
.map(|source| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<span>{"from"}</span>
|
||||||
|
<CharacterHighlight
|
||||||
|
char={source.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Some(html! {
|
||||||
|
<>
|
||||||
|
<span>{"received the"}</span>
|
||||||
|
<AuraSpan aura={aura.title()}/>
|
||||||
|
<span>{"aura"}</span>
|
||||||
|
{from}
|
||||||
|
</>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
NightChange::LostAura { aura, .. } => Some(html! {
|
||||||
|
<>
|
||||||
|
<span>{"lost the"}</span>
|
||||||
|
<AuraSpan aura={aura.title()}/>
|
||||||
|
<span>{"aura"}</span>
|
||||||
|
</>
|
||||||
|
}),
|
||||||
|
NightChange::EmpathFoundScapegoat { scapegoat, .. } => all_characters
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.character_id() == *scapegoat)
|
||||||
|
.map(|scapegoat| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<span>{"took on the scapegoat's curse from"}</span>
|
||||||
|
<CharacterHighlight
|
||||||
|
char={scapegoat.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
NightChange::HunterTarget { source, .. } => all_characters
|
||||||
|
.iter()
|
||||||
|
.find(|c| c.character_id() == *source)
|
||||||
|
.map(|source| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
{"had the hunter's mark placed on them by"}
|
||||||
|
<CharacterHighlight
|
||||||
|
char={source.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
NightChange::Protection { target, protection } => {
|
||||||
|
let prot = match protection {
|
||||||
|
Protection::Guardian {
|
||||||
|
source,
|
||||||
|
guarding: true,
|
||||||
|
} => all_characters
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.character_id() == *source)
|
||||||
|
.map(|source| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<span>{"was guarded by"}</span>
|
||||||
|
<CharacterHighlight
|
||||||
|
char={source.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Protection::Guardian {
|
||||||
|
source,
|
||||||
|
guarding: false,
|
||||||
|
}
|
||||||
|
| Protection::Protector { source }
|
||||||
|
| Protection::Vindicator { source } => all_characters
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.character_id() == *source)
|
||||||
|
.map(|source| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<span>{"was protected by"}</span>
|
||||||
|
<CharacterHighlight
|
||||||
|
char={source.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
all_characters
|
||||||
|
.iter()
|
||||||
|
.find(|t| t.character_id() == *target)
|
||||||
|
.and_then(|target| prot.map(|prot| (target, prot)))
|
||||||
|
.map(|(target, prot)| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<CharacterHighlight
|
||||||
|
char={target.clone()}
|
||||||
|
time={*time}
|
||||||
|
/>
|
||||||
|
{prot}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
html! {
|
.map(|change| {
|
||||||
<div class="change">
|
html! {
|
||||||
</div>
|
<div class="change">
|
||||||
}
|
{change}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
struct CharacterStoryContainerProps {
|
pub struct CharacterStoryButtonProps {
|
||||||
pub character: Character,
|
pub character: Character,
|
||||||
#[prop_or_default]
|
pub choices_by_time: Box<[(GameTime, Box<[NightChange]>, Box<[NightChoice]>)]>,
|
||||||
pub children: Html,
|
pub on_click: Callback<(
|
||||||
|
Character,
|
||||||
|
Box<[(GameTime, Box<[NightChange]>, Box<[NightChoice]>)]>,
|
||||||
|
)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
fn CharacterStoryContainer(
|
pub fn CharacterStoryButton(
|
||||||
CharacterStoryContainerProps {
|
CharacterStoryButtonProps {
|
||||||
character,
|
character,
|
||||||
children,
|
choices_by_time,
|
||||||
}: &CharacterStoryContainerProps,
|
on_click,
|
||||||
|
}: &CharacterStoryButtonProps,
|
||||||
) -> Html {
|
) -> Html {
|
||||||
let open = use_state(|| true);
|
let clickable = !choices_by_time
|
||||||
let role_class = Into::<SetupRole>::into(character.role_title())
|
.iter()
|
||||||
.category()
|
.all(|(_, c1, c2)| c1.is_empty() && c2.is_empty());
|
||||||
.class();
|
let role_class = character.class();
|
||||||
let icon = character
|
let icon = character
|
||||||
.role_title()
|
.role_title()
|
||||||
.icon()
|
.icon()
|
||||||
|
|
@ -225,21 +690,21 @@ fn CharacterStoryContainer(
|
||||||
<div class="icon-spacer"/>
|
<div class="icon-spacer"/>
|
||||||
});
|
});
|
||||||
|
|
||||||
let on_click = {
|
let on_click = if clickable {
|
||||||
let open = open.clone();
|
let cb = on_click.clone();
|
||||||
Callback::from(move |_| open.set(!*open))
|
let char = character.clone();
|
||||||
|
let act = choices_by_time.clone();
|
||||||
|
Callback::from(move |_| cb.emit((char.clone(), act.clone())))
|
||||||
|
} else {
|
||||||
|
Callback::noop()
|
||||||
};
|
};
|
||||||
let shown = open.then_some("shown");
|
let inactive = clickable.not().then_some("no-content");
|
||||||
|
let hover = clickable.then_some("hover");
|
||||||
html! {
|
html! {
|
||||||
<div class="character-story-card">
|
<div class={classes!("character-headline", role_class, "faint", inactive, hover)} onclick={on_click}>
|
||||||
<div class={classes!("character-headline", role_class, "faint")} onclick={on_click}>
|
{icon}
|
||||||
{icon}
|
<Identity ident={character.identity().into_public()}/>
|
||||||
<Identity ident={character.identity().into_public()}/>
|
{dead_icon}
|
||||||
{dead_icon}
|
|
||||||
</div>
|
|
||||||
<div class={classes!("character-details", role_class, "faint", shown)}>
|
|
||||||
{children.clone()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ pub fn generate_story() -> GameStory {
|
||||||
empath,
|
empath,
|
||||||
scapegoat,
|
scapegoat,
|
||||||
hunter,
|
hunter,
|
||||||
|
mason_leader,
|
||||||
) = (
|
) = (
|
||||||
(SetupRole::Werewolf, players_iter.next().unwrap()),
|
(SetupRole::Werewolf, players_iter.next().unwrap()),
|
||||||
(SetupRole::DireWolf, players_iter.next().unwrap()),
|
(SetupRole::DireWolf, players_iter.next().unwrap()),
|
||||||
|
|
@ -59,6 +60,12 @@ pub fn generate_story() -> GameStory {
|
||||||
players_iter.next().unwrap(),
|
players_iter.next().unwrap(),
|
||||||
),
|
),
|
||||||
(SetupRole::Hunter, players_iter.next().unwrap()),
|
(SetupRole::Hunter, players_iter.next().unwrap()),
|
||||||
|
(
|
||||||
|
SetupRole::MasonLeader {
|
||||||
|
recruits_available: NonZeroU8::new(3).unwrap(),
|
||||||
|
},
|
||||||
|
players_iter.next().unwrap(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
let mut settings = GameSettings::empty();
|
let mut settings = GameSettings::empty();
|
||||||
settings.add_and_assign(werewolf.0, werewolf.1);
|
settings.add_and_assign(werewolf.0, werewolf.1);
|
||||||
|
|
@ -79,6 +86,7 @@ pub fn generate_story() -> GameStory {
|
||||||
settings.add_and_assign(empath.0, empath.1);
|
settings.add_and_assign(empath.0, empath.1);
|
||||||
settings.add_and_assign(scapegoat.0, scapegoat.1);
|
settings.add_and_assign(scapegoat.0, scapegoat.1);
|
||||||
settings.add_and_assign(hunter.0, hunter.1);
|
settings.add_and_assign(hunter.0, hunter.1);
|
||||||
|
settings.add_and_assign(mason_leader.0, mason_leader.1);
|
||||||
settings.fill_remaining_slots_with_villagers(players.len());
|
settings.fill_remaining_slots_with_villagers(players.len());
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
let (
|
let (
|
||||||
|
|
@ -100,6 +108,7 @@ pub fn generate_story() -> GameStory {
|
||||||
empath,
|
empath,
|
||||||
scapegoat,
|
scapegoat,
|
||||||
hunter,
|
hunter,
|
||||||
|
mason_leader,
|
||||||
) = (
|
) = (
|
||||||
werewolf.1,
|
werewolf.1,
|
||||||
dire_wolf.1,
|
dire_wolf.1,
|
||||||
|
|
@ -119,6 +128,7 @@ pub fn generate_story() -> GameStory {
|
||||||
empath.1,
|
empath.1,
|
||||||
scapegoat.1,
|
scapegoat.1,
|
||||||
hunter.1,
|
hunter.1,
|
||||||
|
mason_leader.1,
|
||||||
);
|
);
|
||||||
let mut game = Game::new(&players, settings).unwrap();
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
game.r#continue().r#continue();
|
game.r#continue().r#continue();
|
||||||
|
|
@ -223,6 +233,13 @@ pub fn generate_story() -> GameStory {
|
||||||
assert!(!game.r#continue().empath());
|
assert!(!game.r#continue().empath());
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().masons_leader_recruit();
|
||||||
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().masons_wake();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next().title().insomniac();
|
game.next().title().insomniac();
|
||||||
game.r#continue().insomniac();
|
game.r#continue().insomniac();
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
@ -303,6 +320,13 @@ pub fn generate_story() -> GameStory {
|
||||||
assert!(game.r#continue().empath());
|
assert!(game.r#continue().empath());
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().masons_leader_recruit();
|
||||||
|
game.mark(game.character_by_player_id(scapegoat).character_id());
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().masons_wake();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next().title().insomniac();
|
game.next().title().insomniac();
|
||||||
game.r#continue().insomniac();
|
game.r#continue().insomniac();
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
@ -379,6 +403,13 @@ pub fn generate_story() -> GameStory {
|
||||||
assert_eq!(game.r#continue().mortician(), DiedToTitle::Wolfpack);
|
assert_eq!(game.r#continue().mortician(), DiedToTitle::Wolfpack);
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().masons_leader_recruit();
|
||||||
|
game.mark(game.character_by_player_id(shapeshifter).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().masons_wake();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next().title().insomniac();
|
game.next().title().insomniac();
|
||||||
game.r#continue().insomniac();
|
game.r#continue().insomniac();
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
@ -435,6 +466,9 @@ pub fn generate_story() -> GameStory {
|
||||||
);
|
);
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().masons_wake();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next().title().insomniac();
|
game.next().title().insomniac();
|
||||||
game.r#continue().insomniac();
|
game.r#continue().insomniac();
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ use crate::components::{
|
||||||
attributes::{
|
attributes::{
|
||||||
AlignmentComparisonSpan, AlignmentSpan, CategorySpan, DiedToSpan, KillerSpan, PowerfulSpan,
|
AlignmentComparisonSpan, AlignmentSpan, CategorySpan, DiedToSpan, KillerSpan, PowerfulSpan,
|
||||||
},
|
},
|
||||||
story::{CharacterStory, StoryError},
|
story::{CharacterStory, CharacterStoryButton, StoryError},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
|
@ -94,603 +94,54 @@ pub fn Story(StoryProps { story }: &StoryProps) -> Html {
|
||||||
})
|
})
|
||||||
.collect::<Box<[_]>>();
|
.collect::<Box<[_]>>();
|
||||||
|
|
||||||
|
let selected_char = use_state::<
|
||||||
|
Option<(
|
||||||
|
Character,
|
||||||
|
Box<[(GameTime, Box<[NightChange]>, Box<[NightChoice]>)]>,
|
||||||
|
)>,
|
||||||
|
_,
|
||||||
|
>(|| actions_by_character.first().cloned());
|
||||||
|
let on_char_pick = {
|
||||||
|
let selected_char = selected_char.setter();
|
||||||
|
Callback::from(move |(char, actions)| selected_char.set(Some((char, actions))))
|
||||||
|
};
|
||||||
let chars = actions_by_character
|
let chars = actions_by_character
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(char, actions)| {
|
.map(|(char, actions)| {
|
||||||
html! {
|
html! {
|
||||||
<CharacterStory character={char} actions={actions} />
|
<CharacterStoryButton character={char} choices_by_time={actions} on_click={on_char_pick.clone()} />
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Html>();
|
.collect::<Html>();
|
||||||
|
|
||||||
|
let all_characters = village.characters().into_iter().collect::<Rc<_>>();
|
||||||
|
let actions = selected_char.as_ref().map(|(char, actions)| {
|
||||||
|
let class = Into::<SetupRole>::into(char.role_title())
|
||||||
|
.category()
|
||||||
|
.class();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<CharacterCard char={char.clone()} dead={char.died_to().is_some()} faint=true/>
|
||||||
|
<div class={classes!("character-details", class, "faint")}>
|
||||||
|
<CharacterStory
|
||||||
|
character={char.clone()}
|
||||||
|
actions={actions.clone()}
|
||||||
|
all_characters={all_characters}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div class="story">
|
<div class="story">
|
||||||
{chars}
|
<div class="story-characters">
|
||||||
|
{chars}
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[function_component]
|
|
||||||
// pub fn OldStory(StoryProps { story }: &StoryProps) -> Html {
|
|
||||||
// let final_characters =
|
|
||||||
// story
|
|
||||||
// .final_village()
|
|
||||||
// .unwrap_or_else(|_| story.starting_village.clone())
|
|
||||||
// .characters()
|
|
||||||
// .into_iter()
|
|
||||||
// .map(|c| {
|
|
||||||
// let dead =c.alive().not();
|
|
||||||
// html! {
|
|
||||||
// <>
|
|
||||||
// <CharacterCard faint=true char={c} dead={dead}/>
|
|
||||||
// </>
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .collect::<Html>();
|
|
||||||
// let bits = story
|
|
||||||
// .iter()
|
|
||||||
// .map(|(time, changes)| {
|
|
||||||
// let characters = story
|
|
||||||
// .village_at(match time {
|
|
||||||
// GameTime::Day { .. } => {
|
|
||||||
// time
|
|
||||||
// },
|
|
||||||
// GameTime::Night { .. } => {
|
|
||||||
// time.previous().unwrap_or(time)
|
|
||||||
// },
|
|
||||||
// }).ok().flatten()
|
|
||||||
// .map(|v| Rc::new(v.characters().into_iter()
|
|
||||||
// .map(|c| (c.character_id(), c))
|
|
||||||
// .collect::<HashMap<CharacterId, Character>>()))
|
|
||||||
// .unwrap_or_else(||
|
|
||||||
// Rc::new(story.starting_village
|
|
||||||
// .characters().into_iter()
|
|
||||||
// .map(|c| (c.character_id(), c))
|
|
||||||
// .collect::<HashMap<_, _>>()));
|
|
||||||
// let changes = match changes {
|
|
||||||
// GameActions::DayDetails(day_changes) => {
|
|
||||||
// let execute_list =
|
|
||||||
// day_changes
|
|
||||||
// .iter()
|
|
||||||
// .map(|c| match c {
|
|
||||||
// DayDetail::Execute(target) => *target,
|
|
||||||
// })
|
|
||||||
// .filter_map(|c| story.starting_village.character_by_id(c).ok())
|
|
||||||
// .map(|c| {
|
|
||||||
// html! {
|
|
||||||
// <CharacterCard faint=true char={c.clone()}/>
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .collect::<Html>();
|
|
||||||
|
|
||||||
// day_changes.is_empty().not().then_some(html! {
|
|
||||||
// <div class="day">
|
|
||||||
// <h3>{"village executed"}</h3>
|
|
||||||
// <div class="executed">
|
|
||||||
// {execute_list}
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// GameActions::NightDetails(details) => details.choices.is_empty().not().then_some({
|
|
||||||
// let choices = details
|
|
||||||
// .choices
|
|
||||||
// .iter()
|
|
||||||
// .map(|c| {
|
|
||||||
// html! {
|
|
||||||
// <StoryNightChoice choice={c.clone()} characters={characters.clone()}/>
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .collect::<Html>();
|
|
||||||
|
|
||||||
// let changes = details
|
|
||||||
// .changes
|
|
||||||
// .iter()
|
|
||||||
// .map(|c| {
|
|
||||||
// html! {
|
|
||||||
// <li class="change">
|
|
||||||
// <StoryNightChange
|
|
||||||
// change={c.clone()}
|
|
||||||
// characters={characters.clone()}
|
|
||||||
// />
|
|
||||||
// </li>
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .collect::<Html>();
|
|
||||||
|
|
||||||
// html! {
|
|
||||||
// <div class="night">
|
|
||||||
// <label>{"choices"}</label>
|
|
||||||
// <ul class="choices">
|
|
||||||
// {choices}
|
|
||||||
// </ul>
|
|
||||||
// <label>{"changes"}</label>
|
|
||||||
// <ul class="changes">
|
|
||||||
// {changes}
|
|
||||||
// </ul>
|
|
||||||
// </div>
|
|
||||||
// }
|
|
||||||
// }),
|
|
||||||
// };
|
|
||||||
|
|
||||||
// changes
|
|
||||||
// .map(|changes| {
|
|
||||||
// html! {
|
|
||||||
// <div class="time-period">
|
|
||||||
// <h1>{"on "}{time.to_string()}{"..."}</h1>
|
|
||||||
// {changes}
|
|
||||||
// </div>
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .unwrap_or_default()
|
|
||||||
// })
|
|
||||||
// .collect::<Html>();
|
|
||||||
// html! {
|
|
||||||
// <div class="story">
|
|
||||||
// <div class="cast">
|
|
||||||
// {final_characters}
|
|
||||||
// </div>
|
|
||||||
// {bits}
|
|
||||||
// </div>
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[derive(Debug, Clone, PartialEq, Properties)]
|
|
||||||
// struct StoryNightChangeProps {
|
|
||||||
// change: NightChange,
|
|
||||||
// characters: Rc<HashMap<CharacterId, Character>>,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[function_component]
|
|
||||||
// fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightChangeProps) -> Html {
|
|
||||||
// match change {
|
|
||||||
// NightChange::LostAura { character, aura } => characters.get(character).map(|character| html!{
|
|
||||||
// <>
|
|
||||||
// <CharacterCard faint=true char={character.clone()}/>
|
|
||||||
// {"lost the"}
|
|
||||||
// <AuraSpan aura={aura.title()}/>
|
|
||||||
// {"aura"}
|
|
||||||
// </>
|
|
||||||
// }).unwrap_or_default(),
|
|
||||||
// NightChange::ApplyAura { source, target, aura } => characters.get(source).and_then(|source| characters.get(target).map(|target| (source, target))).map(|(source, target)| {
|
|
||||||
// html!{
|
|
||||||
// <>
|
|
||||||
// <CharacterCard faint=true char={target.clone()}/>
|
|
||||||
// <span class="story-text">{"gained the"}</span>
|
|
||||||
// <AuraSpan aura={aura.title()}/>
|
|
||||||
// <span class="story-text">{"aura from"}</span>
|
|
||||||
// <CharacterCard faint=true char={source.clone()}/>
|
|
||||||
// </>
|
|
||||||
// }
|
|
||||||
// }).unwrap_or_default(),
|
|
||||||
// NightChange::RoleChange(character_id, role_title) => characters
|
|
||||||
// .get(character_id)
|
|
||||||
// .map(|char| {
|
|
||||||
// let mut new_char = char.clone();
|
|
||||||
// let _ = new_char.role_change(*role_title, GameTime::Night { number: 0 });
|
|
||||||
|
|
||||||
// html! {
|
|
||||||
// <>
|
|
||||||
// <CharacterCard faint=true char={char.clone()}/>
|
|
||||||
// <span class="story-text">{"is now"}</span>
|
|
||||||
// <CharacterCard faint=true char={new_char.clone()}/>
|
|
||||||
// </>
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .unwrap_or_default(),
|
|
||||||
|
|
||||||
// NightChange::Kill { target, died_to } => {
|
|
||||||
|
|
||||||
// characters
|
|
||||||
// .get(target)
|
|
||||||
// .map(|target| {
|
|
||||||
// html! {
|
|
||||||
// <>
|
|
||||||
// <Icon source={IconSource::Skull} icon_type={IconType::Small}/>
|
|
||||||
// <CharacterCard faint=true char={target.clone()}/>
|
|
||||||
// <span class="story-text">{"died to"}</span>
|
|
||||||
// <DiedToSpan died_to={died_to.title()}/>
|
|
||||||
// </>
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .unwrap_or_default()
|
|
||||||
// },
|
|
||||||
// NightChange::RoleBlock { source, target, .. } => characters
|
|
||||||
// .get(source)
|
|
||||||
// .and_then(|s| characters.get(target).map(|t| (s, t)))
|
|
||||||
// .map(|(source, target)| {
|
|
||||||
// html! {
|
|
||||||
// <>
|
|
||||||
// <CharacterCard faint=true char={source.clone()}/>
|
|
||||||
// <span class="story-text">{"role blocked"}</span>
|
|
||||||
// <CharacterCard faint=true char={target.clone()}/>
|
|
||||||
// </>
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .unwrap_or_default(),
|
|
||||||
// NightChange::Shapeshift { source, into } => characters
|
|
||||||
// .get(source)
|
|
||||||
// .and_then(|s| characters.get(into).map(|i| (s, i)))
|
|
||||||
// .map(|(source, into)| {
|
|
||||||
// html! {
|
|
||||||
// <>
|
|
||||||
// <CharacterCard faint=true char={source.clone()}/>
|
|
||||||
// <span class="story-text">{"shapeshifted into"}</span>
|
|
||||||
// <CharacterCard faint=true char={into.clone()}/>
|
|
||||||
// </>
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .unwrap_or_default(),
|
|
||||||
|
|
||||||
// NightChange::ElderReveal { elder } => characters
|
|
||||||
// .get(elder)
|
|
||||||
// .map(|elder| {
|
|
||||||
// html! {
|
|
||||||
// <>
|
|
||||||
// <CharacterCard faint=true char={elder.clone()}/>
|
|
||||||
// <span class="story-text">{"learned they are the Elder"}</span>
|
|
||||||
// </>
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .unwrap_or_default(),
|
|
||||||
// NightChange::EmpathFoundScapegoat { empath, scapegoat } => characters
|
|
||||||
// .get(empath)
|
|
||||||
// .and_then(|e| characters.get(scapegoat).map(|s| (e, s)))
|
|
||||||
// .map(|(empath, scapegoat)| {
|
|
||||||
// html! {
|
|
||||||
// <>
|
|
||||||
// <CharacterCard faint=true char={empath.clone()}/>
|
|
||||||
// <span class="story-text">{"found the scapegoat in"}</span>
|
|
||||||
// <CharacterCard faint=true char={scapegoat.clone()}/>
|
|
||||||
// <span class="story-text">{"and took on their curse"}</span>
|
|
||||||
// </>
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .unwrap_or_default(),
|
|
||||||
|
|
||||||
// NightChange::HunterTarget { .. }
|
|
||||||
// | NightChange::MasonRecruit { .. }
|
|
||||||
// | NightChange::Protection { .. } => html! {}, // sorted in prompt side
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[derive(Debug, Clone, PartialEq, Properties)]
|
|
||||||
// struct StoryNightResultProps {
|
|
||||||
// result: StoryActionResult,
|
|
||||||
// characters: Rc<HashMap<CharacterId, Character>>,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[function_component]
|
|
||||||
// fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightResultProps) -> Html {
|
|
||||||
// match result {
|
|
||||||
// StoryActionResult::ShiftFailed => html!{
|
|
||||||
// <span class="story-text">{"but it failed"}</span>
|
|
||||||
// },
|
|
||||||
// StoryActionResult::Drunk => html! {
|
|
||||||
// <>
|
|
||||||
// <span class="story-text">{"but got "}</span>
|
|
||||||
// <AuraSpan aura={AuraTitle::Drunk}/>
|
|
||||||
// <span class="story-text">{" instead"}</span>
|
|
||||||
// </>
|
|
||||||
// },
|
|
||||||
// StoryActionResult::BeholderSawEverything => html!{
|
|
||||||
// <span class="story-text">{"and saw everything 👁️"}</span>
|
|
||||||
// },
|
|
||||||
// StoryActionResult::BeholderSawNothing => html!{
|
|
||||||
// <span class="story-text">{"but saw nothing"}</span>
|
|
||||||
// },
|
|
||||||
// StoryActionResult::RoleBlocked => html! {
|
|
||||||
// <span class="story-text">{"but was role blocked"}</span>
|
|
||||||
// },
|
|
||||||
// StoryActionResult::Seer(alignment) => {
|
|
||||||
// html! {
|
|
||||||
// <span>
|
|
||||||
// <span class="story-text">{"and saw"}</span>
|
|
||||||
// <AlignmentSpan alignment={*alignment}/>
|
|
||||||
// </span>
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// StoryActionResult::PowerSeer { powerful } => {
|
|
||||||
// html! {
|
|
||||||
// <span>
|
|
||||||
// <span class="story-text">{"and discovered they are"}</span>
|
|
||||||
// <PowerfulSpan powerful={*powerful}/>
|
|
||||||
// </span>
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// StoryActionResult::Adjudicator { killer } => html! {
|
|
||||||
// <span>
|
|
||||||
// <span class="story-text">{"and saw"}</span>
|
|
||||||
// <KillerSpan killer={*killer}/>
|
|
||||||
// </span>
|
|
||||||
// },
|
|
||||||
// StoryActionResult::Arcanist(same) => html! {
|
|
||||||
// <span>
|
|
||||||
// <span class="story-text">{"and saw"}</span>
|
|
||||||
// <AlignmentComparisonSpan comparison={*same}/>
|
|
||||||
// </span>
|
|
||||||
// },
|
|
||||||
// StoryActionResult::GraveDigger(None) => html! {
|
|
||||||
// <span class="story-text">
|
|
||||||
// {"found an empty grave"}
|
|
||||||
// </span>
|
|
||||||
// },
|
|
||||||
// StoryActionResult::GraveDigger(Some(role_title)) => {
|
|
||||||
// let category = Into::<SetupRole>::into(*role_title).category();
|
|
||||||
// html! {
|
|
||||||
// <span>
|
|
||||||
// <span class="story-text">{"found the body of a"}</span>
|
|
||||||
// <CategorySpan category={category} icon={role_title.icon()}>
|
|
||||||
// {role_title.to_string().to_case(Case::Title)}
|
|
||||||
// </CategorySpan>
|
|
||||||
// </span>
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// StoryActionResult::Mortician(died_to_title) => html! {
|
|
||||||
// <>
|
|
||||||
// <span class="story-text">{"and found the cause of death to be"}</span>
|
|
||||||
// <DiedToSpan died_to={*died_to_title}/>
|
|
||||||
// </>
|
|
||||||
// },
|
|
||||||
// StoryActionResult::Insomniac { visits } => {
|
|
||||||
// let visitors = visits
|
|
||||||
// .iter()
|
|
||||||
// .filter_map(|c| characters.get(c))
|
|
||||||
// .map(|c| {
|
|
||||||
// html! {
|
|
||||||
// <CharacterCard faint=true char={c.clone()}/>
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .collect::<Html>();
|
|
||||||
// html! {
|
|
||||||
// {visitors}
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// StoryActionResult::Empath { scapegoat: false } => html! {
|
|
||||||
// <>
|
|
||||||
// <span class="story-text">{"and saw that they are"}</span>
|
|
||||||
// <span class="attribute-span faint">
|
|
||||||
// <div class="inactive">
|
|
||||||
// <Icon source={IconSource::Heart} icon_type={IconType::Small}/>
|
|
||||||
// </div>
|
|
||||||
// {"Not The Scapegoat"}
|
|
||||||
// </span>
|
|
||||||
// </>
|
|
||||||
// },
|
|
||||||
// StoryActionResult::Empath { scapegoat: true } => html! {
|
|
||||||
// <>
|
|
||||||
// <span class="story-text">{"and saw that they are"}</span>
|
|
||||||
// <span class="attribute-span faint wolves">
|
|
||||||
// <div>
|
|
||||||
// <Icon source={IconSource::Heart} icon_type={IconType::Small}/>
|
|
||||||
// </div>
|
|
||||||
// {"The Scapegoat"}
|
|
||||||
// </span>
|
|
||||||
// </>
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[derive(Debug, Clone, PartialEq, Properties)]
|
|
||||||
// struct StoryNightChoiceProps {
|
|
||||||
// choice: NightChoice,
|
|
||||||
// characters: Rc<HashMap<CharacterId, Character>>,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[function_component]
|
|
||||||
// fn StoryNightChoice(StoryNightChoiceProps { choice, characters}: &StoryNightChoiceProps) -> Html {
|
|
||||||
// let generate = |character_id: &CharacterId, chosen: &CharacterId, action: &'static str| {
|
|
||||||
// characters
|
|
||||||
// .get(character_id)
|
|
||||||
// .and_then(|char| characters.get(chosen).map(|c| (char, c)))
|
|
||||||
// .map(|(char, chosen)| {
|
|
||||||
// html! {
|
|
||||||
// <>
|
|
||||||
// <CharacterCard faint=true char={char.clone()}/>
|
|
||||||
// <span class="story-text">{action}</span>
|
|
||||||
// <CharacterCard faint=true char={chosen.clone()}/>
|
|
||||||
// </>
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// };
|
|
||||||
// let choice_body = match &choice.prompt {
|
|
||||||
// StoryActionPrompt::Arcanist {
|
|
||||||
// character_id,
|
|
||||||
// chosen: (chosen1, chosen2),
|
|
||||||
// } => characters
|
|
||||||
// .get(character_id)
|
|
||||||
// .and_then(|arcanist| characters.get(chosen1).map(|c| (arcanist, c)))
|
|
||||||
// .and_then(|(arcanist, chosen1)| {
|
|
||||||
// characters
|
|
||||||
// .get(chosen2)
|
|
||||||
// .map(|chosen2| (arcanist, chosen1, chosen2))
|
|
||||||
// })
|
|
||||||
// .map(|(arcanist, chosen1, chosen2)| {
|
|
||||||
// html! {
|
|
||||||
// <>
|
|
||||||
// <CharacterCard faint=true char={arcanist.clone()}/>
|
|
||||||
// <span class="story-text">{"compared"}</span>
|
|
||||||
// <CharacterCard faint=true char={chosen1.clone()}/>
|
|
||||||
// <span class="story-text">{"and"}</span>
|
|
||||||
// <CharacterCard faint=true char={chosen2.clone()}/>
|
|
||||||
// </>
|
|
||||||
// }
|
|
||||||
// }),
|
|
||||||
// StoryActionPrompt::MasonsWake { leader, masons } => characters.get(leader).map(|leader| {
|
|
||||||
// let masons = masons
|
|
||||||
// .iter()
|
|
||||||
// .filter_map(|m| characters.get(m))
|
|
||||||
// .map(|c| {
|
|
||||||
// html! {
|
|
||||||
// <CharacterCard faint=true char={c.clone()}/>
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .collect::<Html>();
|
|
||||||
// html! {
|
|
||||||
// <>
|
|
||||||
// <CharacterCard faint=true char={leader.clone()}/>
|
|
||||||
// <span class="story-text">{"'s masons"}</span>
|
|
||||||
// {masons}
|
|
||||||
// <span class="story-text">{"convened in secret"}</span>
|
|
||||||
// </>
|
|
||||||
// }
|
|
||||||
// }),
|
|
||||||
// StoryActionPrompt::Bloodletter { character_id, chosen } => generate(character_id, chosen, "spilt wolf blood on"),
|
|
||||||
|
|
||||||
// StoryActionPrompt::BeholderWakes { character_id }=>characters
|
|
||||||
// .get(character_id)
|
|
||||||
// .map(|char| {
|
|
||||||
// html! {
|
|
||||||
// <>
|
|
||||||
// <CharacterCard faint=true char={char.clone()}/>
|
|
||||||
// <span class="story-text">{"woke up and saw"}</span>
|
|
||||||
// </>
|
|
||||||
// }
|
|
||||||
// }),
|
|
||||||
// StoryActionPrompt::Vindicator {
|
|
||||||
// character_id,
|
|
||||||
// chosen,
|
|
||||||
// }
|
|
||||||
// | StoryActionPrompt::Protector {
|
|
||||||
// character_id,
|
|
||||||
// chosen,
|
|
||||||
// } => generate(character_id, chosen, "protected"),
|
|
||||||
// StoryActionPrompt::Gravedigger {
|
|
||||||
// character_id,
|
|
||||||
// chosen,
|
|
||||||
// } => generate(character_id, chosen, "dug up"),
|
|
||||||
// StoryActionPrompt::Adjudicator {
|
|
||||||
// character_id,
|
|
||||||
// chosen,
|
|
||||||
// }
|
|
||||||
// | StoryActionPrompt::PowerSeer {
|
|
||||||
// character_id,
|
|
||||||
// chosen,
|
|
||||||
// }
|
|
||||||
// | StoryActionPrompt::Empath {
|
|
||||||
// character_id,
|
|
||||||
// chosen,
|
|
||||||
// }
|
|
||||||
// | StoryActionPrompt::Seer {
|
|
||||||
// character_id,
|
|
||||||
// chosen,
|
|
||||||
// } => generate(character_id, chosen, "checked"),
|
|
||||||
// StoryActionPrompt::Hunter {
|
|
||||||
// character_id,
|
|
||||||
// chosen,
|
|
||||||
// } => generate(character_id, chosen, "set a trap for"),
|
|
||||||
// StoryActionPrompt::Militia {
|
|
||||||
// character_id,
|
|
||||||
// chosen,
|
|
||||||
// } => generate(character_id, chosen, "shot"),
|
|
||||||
// StoryActionPrompt::MapleWolf {
|
|
||||||
// character_id,
|
|
||||||
// chosen,
|
|
||||||
// } => characters
|
|
||||||
// .get(character_id)
|
|
||||||
// .and_then(|char| characters.get(chosen).map(|c| (char, c)))
|
|
||||||
// .map(|(char, chosen)| {
|
|
||||||
// html! {
|
|
||||||
// <>
|
|
||||||
// <CharacterCard faint=true char={char.clone()}/>
|
|
||||||
// <span class="story-text">{"invited"}</span>
|
|
||||||
// <CharacterCard faint=true char={chosen.clone()}/>
|
|
||||||
// <span class="story-text">{"for dinner"}</span>
|
|
||||||
// </>
|
|
||||||
// }
|
|
||||||
// }),
|
|
||||||
// StoryActionPrompt::Guardian {
|
|
||||||
// character_id,
|
|
||||||
// chosen,
|
|
||||||
// guarding,
|
|
||||||
// } => generate(
|
|
||||||
// character_id,
|
|
||||||
// chosen,
|
|
||||||
// if *guarding { "guarded" } else { "protected" },
|
|
||||||
// ),
|
|
||||||
// StoryActionPrompt::Mortician {
|
|
||||||
// character_id,
|
|
||||||
// chosen,
|
|
||||||
// } => generate(character_id, chosen, "examined"),
|
|
||||||
// StoryActionPrompt::Beholder {
|
|
||||||
// character_id,
|
|
||||||
// chosen,
|
|
||||||
// } => generate(character_id, chosen, "👁️"),
|
|
||||||
|
|
||||||
// StoryActionPrompt::MasonLeaderRecruit {
|
|
||||||
// character_id,
|
|
||||||
// chosen,
|
|
||||||
// } => generate(character_id, chosen, "tried recruiting"),
|
|
||||||
// StoryActionPrompt::PyreMaster {
|
|
||||||
// character_id,
|
|
||||||
// chosen,
|
|
||||||
// } => generate(character_id, chosen, "torched"),
|
|
||||||
// StoryActionPrompt::WolfPackKill { chosen } => {
|
|
||||||
// characters.get(chosen).map(|chosen: &Character| {
|
|
||||||
// html! {
|
|
||||||
// <>
|
|
||||||
// <AlignmentSpan alignment={Alignment::Wolves}/>
|
|
||||||
// <span class="story-text">{"attempted a kill on"}</span>
|
|
||||||
// <CharacterCard faint=true char={chosen.clone()} />
|
|
||||||
// </>
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// StoryActionPrompt::Shapeshifter { character_id } => {
|
|
||||||
// if choice.result.is_none() {
|
|
||||||
// return html!{};
|
|
||||||
// }
|
|
||||||
// characters.get(character_id).map(|shifter| {
|
|
||||||
// html! {
|
|
||||||
// <>
|
|
||||||
// <CharacterCard faint=true char={shifter.clone()} />
|
|
||||||
// <span class="story-text">{"decided to shapeshift into the wolf kill target"}</span>
|
|
||||||
// </>
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// StoryActionPrompt::AlphaWolf {
|
|
||||||
// character_id,
|
|
||||||
// chosen,
|
|
||||||
// } => generate(character_id, chosen, "took a stab at"),
|
|
||||||
// StoryActionPrompt::DireWolf {
|
|
||||||
// character_id,
|
|
||||||
// chosen,
|
|
||||||
// } => generate(character_id, chosen, "roleblocked"),
|
|
||||||
// StoryActionPrompt::LoneWolfKill {
|
|
||||||
// character_id,
|
|
||||||
// chosen,
|
|
||||||
// } => generate(character_id, chosen, "sought vengeance from"),
|
|
||||||
// StoryActionPrompt::Insomniac { character_id } => {
|
|
||||||
// characters.get(character_id).map(|insomniac| {
|
|
||||||
// html! {
|
|
||||||
// <>
|
|
||||||
// <CharacterCard faint=true char={insomniac.clone()} />
|
|
||||||
// <span class="story-text">{"witnessed visits from"}</span>
|
|
||||||
// </>
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// let result = choice.result.as_ref().map(|result| {
|
|
||||||
// html! {
|
|
||||||
// <StoryNightResult result={result.clone()} characters={characters.clone()}/>
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// choice_body
|
|
||||||
// .map(|choice_body| {
|
|
||||||
// html! {
|
|
||||||
// <li class="choice">
|
|
||||||
// <Icon
|
|
||||||
// source={IconSource::ListItem}
|
|
||||||
// icon_type={IconType::Small}
|
|
||||||
// classes={classes!("li-icon")}
|
|
||||||
// />
|
|
||||||
// {choice_body}
|
|
||||||
// {result}
|
|
||||||
// </li>
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .unwrap_or_default()
|
|
||||||
// }
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue