otter 1.2.1

Otter game system; main data structures Rust crate.
Documentation
// Copyright 2020-2021 Ian Jackson and contributors to Otter
// SPDX-License-Identifier: AGPL-3.0-or-later
// There is NO WARRANTY.

use crate::prelude::*;
use crate::*; // to get ambassador_impls, macro resolution trouble

pub const CORE_DESC    : HtmlLit = Html::lit("a play/pickup deck");
pub const DISABLED_DESC: HtmlLit = Html::lit("a play/pickup deck (disabled)");
pub const COUNTING_DESC: HtmlLit = Html::lit("a play pile (counting)");
pub const ENABLED_DESC : HtmlLit = Html::lit("a pickup deck (enabled)");

#[derive(Debug,Serialize,Deserialize)]
struct Deck {
  shape: GenericSimpleShape<(), RectOutline>,
  label: Option<PieceLabelLoaded>,
  #[serde(default="PosC::zero")]
  stack_pos: Pos,
}

#[derive(Debug,Clone,Copy,Ord,PartialOrd,Eq,PartialEq)]
enum State {
  Disabled,
  Counting,
  Enabled,
}
use State::*;

impl_via_ambassador!{
  #[dyn_upcast]
  impl OutlineTrait for Deck { shape }
}

#[typetag::serde(name="PickupDeck")]
impl PieceSpec for piece_specs::Deck {
  #[throws(SpecError)]
  fn load(&self, PLA { gpc,.. }: PLA) -> SpecLoaded {
    let common = SimpleCommon {
      itemname: None,
      faces: self.faces.clone(),
      edges: self.edges.clone(),
      edge_width: self.edge_width,
    };
    let shape = match self.shape {
      Outline::RectOutline(r) => r,
      _ => throw!(SpecError::UnsupportedShape),
    };
    let shape = GenericSimpleShape::new(
      (),
      shape.clone(),
      "magic-pickupdeck",
      &common)?;
    if shape.count_faces() != 2 {
      throw!(SpE::WrongNumberOfFaces {
        got: shape.count_faces(),
        got_why: "shape".into(),
        exp: 2,
        exp_why: "required".into(),
      });
    }
    gpc.moveable = PieceMoveable::IfWresting;
    gpc.rotateable = false;
    let p = Box::new(Deck {
      shape,
      stack_pos: PosC { coords: self.stack_pos },
      label: self.label.load()?,
    }) as Box<dyn PieceTrait>;
    SpecLoaded {
      p,
      occultable: None,
      special: default(),
    }
  }
}

impl Deck {
  #[throws(IE)]
  fn state(&self, gpc: &GPiece, goccults: &GOccults) -> State {
    match gpc.occult.active_views(goccults)? {
      None                                                       => Disabled,
      Some(OccultationViews { defview: OccK::Visible,..       }) => Counting,
      Some(OccultationViews { defview: OccK::Scrambled /*old*/,.. }) |
      Some(OccultationViews { defview: OccK::Displaced(..),.. }) => Enabled, 
      x => throw!(internal_error_bydebug(&x)),
    }
  }

  #[throws(IE)]
  fn current_face(&self, gpc: &GPiece, goccults: &GOccults) -> FaceId {
    RawFaceId::into(match self.state(gpc, goccults)? {
      Disabled | Counting => 0,
      Enabled             => 1,
    })
  }
}

#[dyn_upcast]
impl PieceBaseTrait for Deck {
  fn nfaces(&self) -> RawFaceId { 1 }

  delegate!{
    to self.shape {
      fn itemname(&self) -> &str;
    }
  }
}

#[typetag::serde]
impl PieceTrait for Deck {
  #[throws(IE)]
  fn svg_piece(&self, f: &mut Html, gpc: &GPiece,
               gs: &GameState, _vpid: VisiblePieceId) {
    let face = self.current_face(gpc, &gs.occults)?;
    self.shape.svg_piece_raw(f, face, &mut |_|Ok::<_,IE>(()))?;
    if_chain! {
      if let Some(label) = &self.label;
      if let Some(count) = gpc.occult.active_total_ppieces(&gs.occults)?;
      then {
        label.svg(f,
                  &self.shape.outline,
                  self.shape.edges.get(0),
                  &hformat!(r#"<tspan {}>{}</tspan>"#,
                            monospace_font(5), count))?;
      }
    }
  }

  #[throws(IE)]
  fn describe_html(&self, gpc: &GPiece, goccults: &GOccults) -> Html {
    match self.state(gpc, goccults)? {
      Disabled => DISABLED_DESC,
      Counting => COUNTING_DESC,
      Enabled => ENABLED_DESC,
    }.into()
  }

  #[throws(InternalError)]
  fn add_ui_operations(&self, _: ShowUnocculted, upd: &mut Vec<UoDescription>,
                       gs: &GameState, gpc: &GPiece) {
    let state = self.state(gpc, &gs.occults)?;
    if state != Enabled {
      upd.push(UoDescription {
        kind: UoKind::Piece,
        def_key: 'A',
        opname: "activate".to_owned(),
        desc: Html::lit("Enable pickup deck").into(),
        wrc: WRC::Unpredictable,
      });
    }
    if state != Counting {
      upd.push(UoDescription {
        kind: UoKind::Piece,
        def_key: 'C',
        opname: "counting".to_owned(),
        desc: Html::lit("Make into counting play pile").into(),
        wrc: WRC::Unpredictable,
      });
    }
    if state != Disabled {
      upd.push(UoDescription {
        kind: UoKind::Piece,
        def_key: 'S',
        opname: "deactivate".to_owned(),
        desc: Html::lit("Disable pickup deck").into(),
        wrc: WRC::Unpredictable,
      });
    }
  }

  #[throws(ApiPieceOpError)]
  fn ui_operation(&self, vis: ShowUnocculted,
                  a: ApiPieceOpArgs<'_>,
                  opname: &str, wrc: WhatResponseToClientOp)
                  -> OpOutcomeThunk {
    let ApiPieceOpArgs { gs,player,piece,ipieces,ioccults,to_recalculate,.. } = a;
    let gen = &mut gs.gen;
    let gplayers = &mut gs.players;
    let gpieces = &mut gs.pieces;

    let goccults = &mut gs.occults;
    let gpc = gpieces.byid_mut(piece)?;

    let gpl = gplayers.byid_mut(player)?;
    let nick = gpl.nick.to_html();

    trace_dbg!("ui op k entry", &opname);

    let rot_checked = gpc.occulter_check_unrotated(vis)?;
    let old_state = self.state(gpc, goccults)?;
  
    let (new_state, did) = match opname {
      "activate"   => (Enabled,  hformat!("enabled {}",         CORE_DESC)),
      "counting"   => (Counting, hformat!("set {} to counting", CORE_DESC)),
      "deactivate" => (Disabled, hformat!("disabled {}",        CORE_DESC)),
      _ => throw!(Ia::BadUiOperation),
    };
  
    let new_view = match new_state {
      Disabled => None,
      Counting => Some(OccKG::Visible),
      Enabled  => {
        let pos = (|| {
          (gpc.pos + self.stack_pos).ok()?
            .clamped(gs.table_size).ok()
        })().ok_or_else(|| Ia::PosOffTable)?;
        let displace = OccDisplacement::Stack { pos };
        let displace = (displace, gpc.zlevel.z.clone());
                  Some(OccKG::Displaced(displace))
      },
    };
    let region_views = new_view.map(|new_view| {
      let region = self.shape.outline.region(gpc.pos)?;
      let views = UniformOccultationView(new_view).views()?;
      Ok::<_,IE>((region, views))
    })
      .transpose()?;

    let mut xupdates = vec![];

    if new_state == old_state {
      throw!(Ia::BadUiOperation);
    }      

    if old_state != Disabled {
      xupdates.extend(
        remove_occultation(&mut gen.unique_gen(),
                           gplayers, gpieces, goccults, ipieces, ioccults,
                           to_recalculate, piece)?
      );
    }

    let puos = PUOs_Simple_Modify;

    if let Some((region, views)) = region_views {
      trace_dbg!("creating occ");
      xupdates.extend(
        create_occultation(&mut gen.unique_gen(), &mut gs.max_z,
                           gplayers, gpieces, goccults, ipieces, ioccults,
                           to_recalculate, rot_checked,
                           region, piece, views, &puos)?
      );
      trace_dbg!("creating occ done", &xupdates);
    }

    let log = vec![ LogEntry { html: hformat!("{} {}", nick, did) }];

    trace_dbg!("ui op k did main");
    
    (PieceUpdate {
      wrc, log,
      ops: puos.into(),
    }, xupdates.into_unprepared(None)).into()
  }

  fn occultation_notify_hook(&self, piece: PieceId) -> UnpreparedUpdates {
    occultation_notify_update_image(piece)
  }
}