1 // Copyright 2020-2021 Ian Jackson and contributors to Otter
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
8 //! - has an image, which is another piece which it displays
9 //! - has special counting behaviour on drag and drop
10 //! - represents a *quanity*
13 // - occultable, to hide the quantity
14 // - can have a back face which is less manipulable (if image has 2 faces)
16 use crate::prelude::*;
17 use crate::*; // to get ambassador_impls, macro resolution trouble
19 const DEFAULT_QTY_FONT_SIZE: f64 = 6.;
21 type Qty = MultigrabQty;
23 #[derive(Debug,Serialize,Deserialize)]
25 image: Box<dyn PieceSpec>,
28 #[serde(default)] label: LabelSpec,
31 #[derive(Debug,Default,Clone,Serialize,Deserialize)]
32 pub struct LabelSpec {
33 pub unit_rel_size: Option<f64>,
35 #[serde(flatten,default)]
36 pub options: TextOptionsSpec,
39 #[derive(Debug,Clone,Serialize,Deserialize)]
42 image: Arc<dyn InertPieceTrait>,
45 label_options: TextOptions,
48 #[derive(Debug,Serialize,Deserialize)]
53 #[typetag::serde(name="Currency")]
54 impl PieceXData for Value { fn dummy() -> Self { Value { qty: 0 } } }
56 #[typetag::serde(name="Currency")]
57 impl PieceSpec for Spec {
59 fn load(&self, PLA { gpc,ig,depth,.. }: PLA) -> SpecLoaded {
60 gpc.rotateable = false;
62 let Spec { ref image, ref currency, qty, label: LabelSpec {
63 options: ref label_options, unit_rel_size,
66 let label_options = label_options.resolve(DEFAULT_QTY_FONT_SIZE)?;
67 let unit_size = label_options.size * unit_rel_size.unwrap_or(1.);
69 let SpecLoadedInert { p: image, occultable: image_occultable } =
70 image.load_inert(ig, depth)?;
72 let itemname = format!("currency-{}", image.itemname());
74 if image.nfaces() != 1 {
75 throw!(SpecError::WrongNumberOfFaces {
76 got: image.nfaces(), got_why: "image".into(),
77 exp: 1, exp_why: "needed".into(),
81 let _value: &mut Value = gpc.xdata_mut(|| Value { qty })?;
82 let image: Arc<dyn InertPieceTrait> = image.into();
84 let occultable = Some({
85 let image = image_occultable
87 .unwrap_or_else(|| image.clone());
93 currency: currency.clone(),
94 itemname: itemname.clone(),
95 label_options: label_options.clone(),
101 let bnote = Banknote {
103 currency: currency.clone(),
104 itemname, label_options, unit_size,
107 gpc.fastsplit = FastSplitId::new_placeholder();
109 let special = PieceSpecialProperties {
113 SpecLoaded { p: Box::new(bnote) as _, occultable, special }
117 impl_via_ambassador!{
119 impl OutlineTrait for Banknote { image }
123 impl PieceBaseTrait for Banknote {
124 fn nfaces(&self) -> RawFaceId { self.image.nfaces() }
125 fn itemname(&self) -> &str { &self.itemname }
128 #[typetag::serde(name="Currency")]
129 impl PieceTrait for Banknote {
131 fn describe_html(&self, gpc: &GPiece, _: &GOccults) -> Html {
132 let value: &Value = gpc.xdata.get_exp()?;
133 self.describe(gpc.face, &value.html())?
137 fn svg_piece(&self, f: &mut Html, gpc: &GPiece, _gs: &GameState,
138 vpid: VisiblePieceId) {
139 let value: &Value = gpc.xdata.get_exp()?;
140 self.render(f, vpid, gpc.face, &gpc.xdata, &value.html())?
143 #[throws(ApiPieceOpError)]
144 fn op_multigrab(&self, _: ApiPieceOpArgs, show: ShowUnocculted,
145 take: MultigrabQty, new_z: ShouldSetZLevel) -> OpOutcomeThunk {
146 let currency = self.currency.clone();
147 OpOutcomeThunk::Reborrow(Box::new(
148 move |ig: &mut InstanceGuard, (player, tpiece)|
150 ig.fastsplit_split(player, tpiece, show, new_z,
151 move |_: &IOccults, _: &GOccults, gpl: &GPlayer,
152 tgpc: &mut GPiece, tipc: &IPiece,
155 let self_: &Banknote = tipc.p.show(show).downcast_piece_fastsplit()?;
157 let tgpc_value: &mut Value = tgpc.xdata.get_mut_exp()?;
158 let remaining = tgpc_value.qty.checked_sub(take)
159 .ok_or(Ia::CurrencyShortfall)?;
161 tgpc_value.qty = take;
162 ngpc.xdata_init(Value { qty: remaining })?;
164 tgpc.held = Some(player);
169 let logents = vec![ LogEntry { html: hformat!(
170 "{} took {} {}{}, leaving {}{}",
171 gpl.nick.to_html(), self_.image.describe_html(tgpc.face)?,
173 remaining, ¤cy,
176 let update = PieceUpdateOp::ModifyQuiet(());
179 (WhatResponseToClientOp::UpdateSvg,
187 fn held_change_hook(&self,
192 gpieces: &mut GPieces,
194 was_held: Option<PlayerId>)
196 let missing_e = || internal_error_bydebug(&(was_held, tpiece));
199 let tgpc = gpieces.get(tpiece).ok_or_else(missing_e)?;
200 let tipc = ipieces.get(tpiece).ok_or_else(missing_e)?;
202 if_let!{ Some(player) = was_held; else return Ok(default()) }
203 if tgpc.held.is_some() { /*wat*/ return default(); }
204 let gpl = gplayers.get(player);
206 // Occultation is not yet supported here. When implementing
207 // occultation, delete this and fix all the things.
208 let show = ShowUnocculted::new_visible();
210 let merge_with = gpieces.iter().filter_map(|(mpiece, mgpc)|{
211 if mpiece == tpiece { throw!() }
212 let mipc = ipieces.get(mpiece)?;
214 if mgpc.occult.passive_occid().is_some() {
215 // We don't do occultation yet. But, anyway, we don't want to
216 // deal with this since it might mean we're totally invisible
217 // to our player! When we do support this, call
218 // Occultation::get_kind ?
221 let show_to_player = show;
223 // Our position is within its bbox
224 if ! mipc.show(show_to_player).abs_bbox(mgpc).ok()?
225 .contains(tgpc.pos) { throw!() }
228 let mself: &Banknote = mipc.p.show(show_to_player)
229 .downcast_piece_fastsplit().ok()?;
232 if mself.currency != tself.currency { throw!() }
233 let currency = &mself.currency;
235 // We are in the ellipse inscribed in its bbox
236 let delta = (tgpc.pos - mgpc.pos).ok()?.promote();
237 let bbox_sz = mipc.show(show_to_player).bbox_approx().ok()?;
238 let dist2: f64 = (0..2).map(|i| {
239 // The bbox may not be centred. We imagine a quarter ellipse
240 // inscribed in each corner, with the centre at the nominal position.
241 let delta = delta.coords[i];
242 let cnr = if delta < 0. { bbox_sz.tl() } else { bbox_sz.br() };
243 let rel = delta / (cnr.coords[i] as f64);
246 if ! (dist2 <= 1.) { throw!() }
248 Some((mpiece,mgpc,currency))
252 Some((mpiece,mgpc,currency)) =
253 merge_with.at_most_one().ok().flatten();
254 else return Ok(default());
257 let tqty = tgpc.xdata_exp::<Value>()?.qty;
258 let mqty = mgpc.xdata_exp::<Value>()?.qty;
259 let new_value = match mqty.checked_add(tqty) {
260 Some(qty) => Value { qty },
261 None => return default(), // arithmetic overflow!
264 let logent = hformat!(
265 "{} deposited {}, giving {}{}",
267 Some(gpl) => gpl.nick.to_html(),
268 None => Html::lit("Departing player").into(),
270 tipc.p.show(show).describe_html(tgpc, goccults)?,
275 let logents = vec![ LogEntry { html: logent } ];
277 OpHookThunk::Reborrow(Box::new(move |igg: &mut InstanceGuard, (_player,)| {
279 let (puo, uu_d) = igg.fastsplit_delete(show, tpiece, &logents)?;
285 // None of these situations ought to happen, really, but the
286 // callback structure means it isn't 100% possible to rule them out.
287 let mgpc = ig.gs.pieces.get_mut(mpiece).ok_or("tpiece vanished")?;
288 let mvalue = mgpc.xdata_mut_exp::<Value>().map_err(|_|"xdata vanished")?;
289 mvalue.qty = mvalue.qty.checked_add(tqty).ok_or("overflow")?;
290 if mvalue.qty != new_value.qty { throw!("modified value") }
291 Ok::<_,&'static str>(())
292 })().unwrap_or_else(|m|{
293 warn!("during dorp-and-merge of currency {tpiece:?} into {mpiece:?}: {m}");
296 vec![Box::new(move |prepub: &mut PrepareUpdatesBuffer| {
297 prepub.piece_update_image(mpiece, &None).unwrap_or_else(
298 |e| error!("currency image update failed: {} {:?}", &e, &e));
299 prepub.piece_update(tpiece, &None, puo.into());
300 prepub.log_updates(logents);
301 prepub.add_unprepared(uu_d);
309 fn html(&self) -> Html { hformat!("{}", self.qty) }
314 fn describe(&self, face: FaceId, qty: &HtmlStr) -> Html {
316 self.image.describe_html(face)?,
321 fn render(&self, f: &mut Html, vpid: VisiblePieceId, face: FaceId,
322 xdata_for_image_only: &PieceXDataState, qty: &HtmlStr) {
323 self.image.svg(f, vpid, face, xdata_for_image_only)?;
326 r##"<{}>{}<tspan font-size="{}">{}</tspan></text>"##,
327 &self.label_options.start_element(), qty,
328 &self.unit_size, &self.currency)?;
332 const OCCULT_QTY: HtmlLit = Html::lit("?");
334 #[typetag::serde(name="Currency")]
335 impl InertPieceTrait for Banknote {
337 fn svg(&self, f: &mut Html, id: VisiblePieceId, face: FaceId,
338 xdata_for_image_only: &PieceXDataState /* use with care! */) {
339 self.render(f, id, face, xdata_for_image_only, &OCCULT_QTY)?;
343 fn describe_html(&self, face: FaceId) -> Html {
344 self.describe(face, &OCCULT_QTY)?