use crate::prelude::*;
const MAX_COOLDOWN: Duration = Duration::from_secs(100);
fn default_cooldown() -> Duration { Duration::from_millis(4000) }
fn default_circle_scale() -> f64 { 1. }
const COOLDOWN_STROKE_WIDTH: f64 = 1.512;
const COOLDOWN_EXTRA_RADIUS: f64 =
0.5 * (SELECT_STROKE_WIDTH + COOLDOWN_STROKE_WIDTH) +
DEFAULT_EDGE_WIDTH;
const DEFAULT_LABEL_FONT_SIZE: f64 = 8.;
#[derive(Debug,Serialize,Deserialize)]
pub struct Spec {
image: Box<dyn PieceSpec>,
#[serde(default)] labels: SpecLabels,
#[serde(default)] label: TextOptionsSpec,
occult: Option<OccultSpec>,
#[serde(default="default_circle_scale")] circle_scale: f64,
#[serde(default="default_cooldown")]
#[serde(with="humantime_serde")] cooldown: Duration,
itemname: Option<String>,
#[serde(default)] desc: Option<String>,
}
#[derive(Debug,Default,Clone,Serialize,Deserialize)]
pub struct OccultSpec {
#[serde(default)] label: String,
}
#[derive(Debug,Clone,Serialize,Deserialize)]
#[serde(untagged)]
pub enum SpecLabels {
Texts(IndexVec<FaceId, String>),
OneTo(u8),
RangeInclusive([u8; 2]),
}
use SpecLabels as SL;
impl Default for SpecLabels {
fn default() -> Self { Self::Texts(default()) }
}
#[derive(Debug,Serialize,Deserialize)]
struct Die {
nfaces: RawFaceId,
desc: String,
itemname: String,
labels: IndexVec<FaceId, String>, image: Arc<dyn InertPieceTrait>, text_options: TextOptions,
surround_outline: CircleOutline,
cooldown_radius: f64,
cooldown_time: Duration,
}
#[derive(Debug,Serialize,Deserialize)]
struct State {
cooldown_expires: Option<FutureInstant>,
}
#[typetag::serde(name="Die")]
impl PieceXData for State {
fn dummy() -> Self { State { cooldown_expires: None } }
}
#[derive(Serialize, Debug)]
struct OverlayTemplateContext<'c> {
label_text: &'c str,
label_start_element: Html,
cooldown_active: bool,
radius: f64,
remprop: f64,
path_d: &'c str,
cd_elid: &'c str,
total_ms: f64,
}
#[typetag::serde(name="Die")]
impl PieceSpec for Spec {
#[throws(SpecError)]
fn load(&self, PLA { gpc,ig,depth,.. }: PLA) -> SpecLoaded {
gpc.rotateable = false;
let SpecLoadedInert { p: image, occultable: img_occultable } =
self.image.load_inert(ig, depth)?;
let mut nfaces: Option<(RawFaceId, &'static str)> = None;
let mut set_nfaces = |n, why: &'static str| {
if let Some((already, already_why)) = nfaces {
if already != n {
throw!(SpecError::WrongNumberOfFaces {
got: n,
got_why: why.into(),
exp: already,
exp_why: already_why.into(),
})
}
}
nfaces = Some((n, why));
Ok::<_,SpecError>(())
};
match image.nfaces() {
0 => throw!(SpecError::ZeroFaces),
1 => { },
n => set_nfaces(n, "image")?,
}
let range_labels = |a: RawFaceId, b: RawFaceId| {
if a >= b { throw!(SpecError::InvalidRange(a.into(), b.into())) }
let l = (a..=b).map(|i| i.to_string()).collect();
Ok::<IndexVec<FaceId,String>,SpecError>(l)
};
let labels = match self.labels {
SL::Texts(ref l) => l.clone(),
SL::OneTo(n) => range_labels(1,n)?,
SL::RangeInclusive([a,b]) => range_labels(a,b)?,
};
let labels = if labels.len() > 0 {
let n = labels.len();
let n = n.try_into().map_err(|_| SpecError::FarTooManyFaces(n))?;
set_nfaces(n, "labels")?;
labels
} else {
index_vec!["".into()]
};
let text_options = self.label.resolve(DEFAULT_LABEL_FONT_SIZE)?;
if_let!{ Some((nfaces,_)) = nfaces;
else throw!(SpecError::MultipleFacesRequired) };
let radius = image.bbox_approx()?.size()?.len()? * 0.5;
let radius = radius * if image.shape() == Some(Shape::Circle) {
0.5f64 .sqrt()
} else {
1.0
};
let radius = if (0.5 .. 1.5).contains(&self.circle_scale) {
radius * self.circle_scale
} else {
throw!(SpecError::InvalidSizeScale)
};
let surround_outline = CircleOutline { diam: radius * 2. };
let cooldown_radius = radius + COOLDOWN_EXTRA_RADIUS;
let cooldown_time = {
let t = self.cooldown;
if t <= MAX_COOLDOWN { t }
else { throw!(SpecError::TimeoutTooLarge { got: t, max: MAX_COOLDOWN }) }
};
let itemname = self.itemname.clone().unwrap_or_else(
|| format!("die.{}.{}", nfaces, image.itemname()));
let desc = self.desc.clone().unwrap_or_default();
let initial_state = {
State { cooldown_expires: cooldown_start_value(cooldown_time)? }
};
gpc.xdata_init(initial_state)?;
let occ_label = |occ: &OccultSpec| -> String {
if occ.label == "" && labels.iter().any(|l| l != "") {
"?".into()
} else {
occ.label.clone()
}
};
let occultable = match (img_occultable, &self.occult) {
(None, None) => None,
(None, Some(occ)) => {
let SpecLoadedInert { p: image, occultable: image_occ_reload } =
self.image.load_inert(ig, depth)?;
if image_occ_reload.is_some() {
throw!(internal_logic_error(
format!("reloading image {:?} occ varies", &self)
));
}
if image.nfaces() != 1 {
throw!(SpecError::UnoccultableButRichImageForOccultation)
}
let occ_label = occ_label(occ);
Some((image.into(), occ_label))
},
(Some((_, image_occ_image)), occ) => {
let default_occ = default();
let occ = occ.as_ref().unwrap_or(&default_occ);
let occ_label = occ_label(occ);
Some((image_occ_image, occ_label))
},
}.map(|(occ_image, occ_label)| {
let occ_ilk = LOI::Distinct;
let our_occ_image = Arc::new(Die {
nfaces, cooldown_time, cooldown_radius, surround_outline,
text_options: text_options.clone(),
itemname: itemname.clone(),
desc: desc.clone(),
image: occ_image,
labels: index_vec![occ_label],
}) as _;
Ok::<_,SpecError>((occ_ilk, our_occ_image))
}).transpose()?;
let die = Die {
nfaces, cooldown_time, cooldown_radius, surround_outline,
itemname, labels, desc, text_options,
image: image.into()
};
let special = PieceSpecialProperties {
rendering: Some(SpecialClientRendering::Die{}),
..default()
};
SpecLoaded {
p: Box::new(die) as _,
occultable,
special,
}
}
}
#[throws(IE)]
pub fn cooldown_start_value(cooldown_time: Duration) -> Option<FutureInstant> {
let t = cooldown_time.try_into().map_err(IE::from)?;
Some(t)
}
impl Die {
#[throws(IE)]
pub fn cooldown_remaining(&self, state: &State) -> Duration {
let expires = &state.cooldown_expires;
if_let!{ Some(FutureInstant(then)) = *expires; else return Ok(default()) };
let now = config().global_clock.now();
if now > then { return default() }
let remaining = then - now;
if remaining > self.cooldown_time {
throw!(internal_logic_error(format!(
"die {:?}: cooldown time remaining {:?} > total {:?}!, resetting",
&self.itemname,
remaining, self.cooldown_time
)))
}
remaining
}
#[throws(IE)]
pub fn cooldown_remprop(&self, state: &State) -> f64 {
self.cooldown_remaining(state)?.as_secs_f64()
/
self.cooldown_time .as_secs_f64()
}
#[throws(IE)]
pub fn cooldown_running(&self, state: &State) -> bool {
self.cooldown_remaining(state)? != Duration::default()
}
#[throws(ApiPieceOpError)]
pub fn check_permit_flip_roll(&self, state: &State) {
if self.cooldown_running(state)? {
throw!(Inapplicable::DieCooldown)
} else {
()
}
}
#[throws(IE)]
pub fn cooldown_cleanup(&self, state: &mut State) {
if ! self.cooldown_running(state)? {
state.cooldown_expires = None;
}
}
#[throws(IE)]
pub fn cooldown_cleanup_hook(&self, gpieces: &mut GPieces, piece: PieceId) {
let state = gpieces
.byid_mut(piece).context("load hook")?
.xdata.get_mut_exp()?;
self.cooldown_cleanup(state)?;
}
}
#[dyn_upcast]
impl OutlineTrait for Die {
#[throws(IE)]
fn surround_path(&self) -> Html {
self.surround_outline.outline_path(1.0)?
}
delegate! {
to self.surround_outline {
fn bbox_approx(&self) -> Result<Rect, IE>;
fn shape(&self) -> Option<Shape>;
}
}
delegate! {
to self.image {
fn outline_path(&self, scale: f64) -> Result<Html, IE>;
fn thresh_dragraise(&self) -> Result<Option<Coord>, IE>;
}
}
}
#[dyn_upcast]
impl PieceBaseTrait for Die {
fn nfaces(&self) -> RawFaceId { self.nfaces }
fn itemname(&self) -> &str { &self.itemname }
}
#[typetag::serde(name="Die")]
impl PieceTrait for Die {
#[throws(IE)]
fn describe_html(&self, gpc: &GPiece, _: &GOccults) -> Html {
let nfaces = self.nfaces();
let showing = {
let label = &self.labels[gpc.face];
let idesc = || self.image.describe_html(gpc.face);
let ldesc = || Html::from_txt(label);
if label == "" {
hformat!("now showing {}", idesc()?)
} else if self.labels.iter().filter(|&l| l == label).count() == 1 {
hformat!("now showing {}", ldesc())
} else {
hformat!("now showing {}, {}", idesc()?, ldesc())
}
};
if self.desc == "" {
hformat!("a d{} ({})", nfaces, showing)
} else {
hformat!("{} (d{}; {})", self.desc, nfaces, showing)
}
}
#[throws(ApiPieceOpError)]
fn ui_permit_flip(&self, gpc: &GPiece) -> bool {
let state: &State = gpc.xdata.get_exp()?;
let () = self.check_permit_flip_roll(state)?;
true
}
#[throws(InternalError)]
fn add_ui_operations(&self, _: ShowUnocculted, upd: &mut Vec<UoDescription>,
_gs: &GameState, _gpc: &GPiece) {
upd.push(UoDescription {
kind: UoKind::Piece,
def_key: 'r',
opname: "roll".to_string(),
desc: Html::lit("Roll").into(),
wrc: WRC::UpdateSvg,
});
}
#[throws(ApiPieceOpError)]
fn ui_operation(&self, _: ShowUnocculted, args: ApiPieceOpArgs<'_>,
opname: &str, wrc: WhatResponseToClientOp)
-> OpOutcomeThunk {
let ApiPieceOpArgs { gs,piece,player,ioccults,ipc,.. } = args;
let gpc = gs.pieces.byid_mut(piece)?;
let gpl = gs.players.byid(player)?;
let state = gpc.xdata.get_mut_exp()?;
match (opname, wrc) {
("roll", wrc@ WRC::UpdateSvg) => {
let () = self.check_permit_flip_roll(state)?;
state.cooldown_expires = cooldown_start_value(self.cooldown_time)?;
gpc.face = config().game_rng.range(0 .. self.nfaces).into();
let logents = log_did_to_piece(
ioccults,&gs.occults,gpl,gpc,ipc,
"rolled"
)?;
((
wrc,
PieceUpdateOp::Modify(()),
logents,
).into(), default()).into()
},
_ => throw!(Ia::BadUiOperation)
}
}
#[throws(IE)]
fn svg_piece(&self, f: &mut Html, gpc: &GPiece, _: &GameState,
vpid: VisiblePieceId) {
self.svg(f, vpid, gpc.face, &gpc.xdata)?
}
#[throws(IE)]
fn held_change_hook(&self,
_: &InstanceRef,
_gplayers: &GPlayers,
_ipieces: &IPieces,
_goccults: &GOccults,
gpieces: &mut GPieces,
piece: PieceId,
_was_held: Option<PlayerId>)
-> OpHookThunk {
self.cooldown_cleanup_hook(gpieces, piece)?;
default()
}
#[throws(IE)]
fn save_reloaded_hook(&self, piece: PieceId, gs: &mut GameState
, _: &InstanceRef) {
self.cooldown_cleanup_hook(&mut gs.pieces, piece)?;
}
#[throws(IE)]
fn loaded_hook_preview(&self, gpc: &mut GPiece) {
let state: &mut State = gpc.xdata.get_mut_exp()?;
state.cooldown_expires = Some(FutureInstant::try_from(
Duration::from_secs_f64(
self.cooldown_time.as_secs_f64() * 0.25
)
)?)
}
#[throws(IE)]
fn bbox_preview(&self) -> Rect {
let r: Coord = cast(self.cooldown_radius.ceil())
.ok_or(CoordinateOverflow)?;
let br = PosC::new(r,r);
let tl = (-br)?;
Rect{ corners: [tl,br] }
}
}
#[typetag::serde(name="Die")]
impl InertPieceTrait for Die {
#[throws(IE)]
fn svg(&self, f: &mut Html, vpid: VisiblePieceId, face: FaceId,
xdata: &PieceXDataState ) {
let state: &State = xdata.get_exp()?;
let iface = if self.image.nfaces() == 1 { default() } else { face };
self.image.svg(f, vpid, iface, xdata)?;
let label = self.labels.get(face).map(|s| &**s).unwrap_or_else(
|| self.labels.get(0).map(|s| &**s) .unwrap_or_default());
let remprop = self.cooldown_remprop(state)?;
let cooldown_active = remprop != 0.;
let (path_d, cd_elid) = if cooldown_active {
let mut path_d = String::new();
die_cooldown_path(&mut path_d, self.cooldown_radius, remprop)?;
(path_d, format!("def.{}.die.cd", vpid))
} else {
default()
};
let label_start_element = self.text_options.start_element();
let tc = OverlayTemplateContext {
label_text: label,
label_start_element,
cooldown_active,
radius: self.cooldown_radius,
path_d: &path_d,
cd_elid: &cd_elid,
total_ms: self.cooldown_time.as_secs_f64() * 1000.,
remprop,
};
write!(f.as_html_string_mut(), "{}",
nwtemplates::render("die-overlay.tera", &tc)?)?;
}
#[throws(IE)]
fn describe_html(&self, face: FaceId) -> Html {
let label = &self.labels[face];
let idesc = || self.image.describe_html(face);
let ldesc = || Html::from_txt(label);
if label == "" {
hformat!("a d{} ({})", self.nfaces, idesc()?)
} else {
hformat!("a d{} ({}, {})", self.nfaces, idesc()?, ldesc())
}
}
}