chiark / gitweb /
baf7559d1300656740845ea368cb4d3190af5e6e
[otter.git] / src / currency.rs
1 // Copyright 2020-2021 Ian Jackson and contributors to Otter
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
4
5 //! Currency
6 //!
7 //! A "Currency" piece
8 //!  - has an image, which is another piece which it displays
9 //!  - has special counting behaviour on drag and drop
10 //!  - represents a *quanity*
11
12 // Future plans
13 //  - occultable, to hide the quantity
14 //  - can have a back face which is less manipulable (if image has 2 faces)
15
16 use crate::prelude::*;
17 use crate::*; // to get ambassador_impls, macro resolution trouble
18
19 const DEFAULT_QTY_FONT_SIZE: f64 = 6.;
20
21 type Qty = MultigrabQty;
22
23 #[derive(Debug,Serialize,Deserialize)]
24 pub struct Spec {
25   image: Box<dyn PieceSpec>,
26   qty: Qty,
27   currency: String,
28   #[serde(default)] label: LabelSpec,
29 }
30
31 #[derive(Debug,Default,Clone,Serialize,Deserialize)]
32 pub struct LabelSpec {
33   pub unit_rel_size: Option<f64>,
34
35   #[serde(flatten,default)]
36   pub options: TextOptionsSpec,
37 }
38
39 #[derive(Debug,Clone,Serialize,Deserialize)]
40 pub struct Banknote {
41   itemname: String,
42   image: Arc<dyn InertPieceTrait>,
43   currency: String,
44   unit_size: f64,
45   label_options: TextOptions,
46 }
47
48 #[derive(Debug,Serialize,Deserialize)]
49 pub struct Value {
50   qty: Qty,
51 }
52
53 #[typetag::serde(name="Currency")]
54 impl PieceXData for Value { fn dummy() -> Self { Value { qty: 0 } } }
55
56 #[typetag::serde(name="Currency")]
57 impl PieceSpec for Spec {
58   #[throws(SpecError)]
59   fn load(&self, PLA { gpc,ig,depth,.. }: PLA) -> SpecLoaded {
60     gpc.rotateable = false;
61
62     let Spec { ref image, ref currency, qty, label: LabelSpec {
63       options: ref label_options, unit_rel_size,
64     } } = *self;
65
66     let label_options = label_options.resolve(DEFAULT_QTY_FONT_SIZE)?;
67     let unit_size = label_options.size * unit_rel_size.unwrap_or(1.);
68
69     let SpecLoadedInert { p: image, occultable: image_occultable } =
70       image.load_inert(ig, depth)?;
71
72     let itemname = format!("currency-{}", image.itemname());
73
74     if image.nfaces() != 1 {
75       throw!(SpecError::WrongNumberOfFaces {
76         got: image.nfaces(), got_why: "image".into(),
77         exp: 1,              exp_why: "needed".into(),
78       });
79     }
80
81     let _value: &mut Value = gpc.xdata_mut(|| Value { qty })?;
82     let image: Arc<dyn InertPieceTrait> = image.into();
83
84     let occultable = Some({
85       let image = image_occultable
86         .map(|(_,o)| o)
87         .unwrap_or_else(|| image.clone());
88
89       (
90         LOI::Distinct,
91         Arc::new(Banknote {
92           image,
93           currency: currency.clone(),
94           itemname: itemname.clone(),
95           label_options: label_options.clone(),
96           unit_size,
97         }) as _
98       )
99     });
100         
101     let bnote = Banknote {
102       image,
103       currency: currency.clone(),
104       itemname, label_options, unit_size,
105     };
106
107     gpc.fastsplit = FastSplitId::new_placeholder();
108
109     let special = PieceSpecialProperties {
110       multigrab: true,
111       ..default()
112     };
113     SpecLoaded { p: Box::new(bnote) as _, occultable, special }
114   }
115 }
116
117 impl_via_ambassador!{
118   #[dyn_upcast]
119   impl OutlineTrait for Banknote { image }
120 }
121
122 #[dyn_upcast]
123 impl PieceBaseTrait for Banknote {
124   fn nfaces(&self) -> RawFaceId { self.image.nfaces() }
125   fn itemname(&self) -> &str { &self.itemname }
126 }
127
128 #[typetag::serde(name="Currency")]
129 impl PieceTrait for Banknote {
130   #[throws(IE)]
131   fn describe_html(&self, gpc: &GPiece, _: &GOccults) -> Html {
132     let value: &Value = gpc.xdata.get_exp()?;
133     self.describe(gpc.face, &value.html())?
134   }
135
136   #[throws(IE)]
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())?
141   }
142
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)|
149   {
150     ig.fastsplit_split(player, tpiece, show, new_z,
151       move |_: &IOccults, _: &GOccults, gpl: &GPlayer,
152             tgpc: &mut GPiece, tipc: &IPiece,
153             ngpc: &mut GPiece|
154   {
155     let self_: &Banknote = tipc.p.show(show).downcast_piece_fastsplit()?;
156
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)?;
160
161     tgpc_value.qty = take;
162     ngpc.xdata_init(Value { qty: remaining })?;
163
164     tgpc.held = Some(player);
165     ngpc.held = None;
166     
167     tgpc.pinned = false;
168
169     let logents = vec![ LogEntry { html: hformat!(
170       "{} took {} {}{}, leaving {}{}",
171       gpl.nick.to_html(), self_.image.describe_html(tgpc.face)?,
172       take, &currency,
173       remaining, &currency,
174     )}];
175
176     let update = PieceUpdateOp::ModifyQuiet(());
177
178     Ok((
179       (WhatResponseToClientOp::UpdateSvg,
180        update,
181        logents).into(),
182       default()
183     ))
184   })}))}
185
186   #[throws(IE)]
187   fn held_change_hook(&self,
188                       _ig: &InstanceRef,
189                       gplayers: &GPlayers,
190                       ipieces: &IPieces,
191                       goccults: &GOccults,
192                       gpieces: &mut GPieces,
193                       tpiece: PieceId,
194                       was_held: Option<PlayerId>)
195                       -> OpHookThunk {
196     let missing_e = || internal_error_bydebug(&(was_held, tpiece));
197
198     let tself = self;
199     let tgpc = gpieces.get(tpiece).ok_or_else(missing_e)?;
200     let tipc = ipieces.get(tpiece).ok_or_else(missing_e)?;
201
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);
205
206     // Occultation is not yet supported here.  When implementing
207     // occultation, delete this and fix all the things.
208     let show = ShowUnocculted::new_visible();
209
210     let merge_with = gpieces.iter().filter_map(|(mpiece, mgpc)|{
211       if mpiece == tpiece { throw!() }
212       let mipc = ipieces.get(mpiece)?;
213
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 ?
219         throw!();
220       }
221       let show_to_player = show;
222
223       // Our position is within its bbox
224       if ! mipc.show(show_to_player).abs_bbox(mgpc).ok()?
225         .contains(tgpc.pos) { throw!() }
226
227       // It's a banknote
228       let mself: &Banknote = mipc.p.show(show_to_player)
229         .downcast_piece_fastsplit().ok()?;
230
231       // Of our currency
232       if mself.currency != tself.currency { throw!() }
233       let currency = &mself.currency;
234
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);
244         rel*rel
245       }).sum();
246       if ! (dist2 <= 1.) { throw!() }
247
248       Some((mpiece,mgpc,currency))
249     });
250
251     if_let!{
252       Some((mpiece,mgpc,currency)) =
253         merge_with.at_most_one().ok().flatten();
254       else return Ok(default());
255     }
256
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!
262     };
263
264     let logent = hformat!(
265       "{} deposited {}, giving {}{}",
266       match gpl {
267         Some(gpl) => gpl.nick.to_html(),
268         None => Html::lit("Departing player").into(),
269       },
270       tipc.p.show(show).describe_html(tgpc, goccults)?,
271       &new_value.html(),
272       currency,
273     );
274
275     let logents = vec![ LogEntry { html: logent } ];
276
277   OpHookThunk::Reborrow(Box::new(move |igg: &mut InstanceGuard, (_player,)| {
278
279     let (puo, uu_d) = igg.fastsplit_delete(show, tpiece, &logents)?;
280     // commitment point
281   Ok((move ||{
282     let ig = &mut **igg;
283
284     let () = (||{
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}");
294     });
295
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);
302     }) as _]
303
304   })()) // <- no ?
305   }))}
306 }
307
308 impl Value {
309   fn html(&self) -> Html { hformat!("{}", self.qty) }
310 }
311
312 impl Banknote {
313   #[throws(IE)]
314   fn describe(&self, face: FaceId, qty: &HtmlStr) -> Html {
315     hformat!("{}, {}{}",
316              self.image.describe_html(face)?,
317              qty, &self.currency)
318   }
319
320   #[throws(IE)]
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)?;
324
325     hwrite!(f,
326             r##"<{}>{}<tspan font-size="{}">{}</tspan></text>"##,
327             &self.label_options.start_element(), qty,
328             &self.unit_size, &self.currency)?;
329   }
330 }
331
332 const OCCULT_QTY: HtmlLit = Html::lit("?");
333
334 #[typetag::serde(name="Currency")]
335 impl InertPieceTrait for Banknote {
336   #[throws(IE)]
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)?;
340   }
341
342   #[throws(IE)]
343   fn describe_html(&self, face: FaceId) -> Html {
344     self.describe(face, &OCCULT_QTY)?
345   }
346 }