package game import ( "fmt" "slices" "strings" "muhq.space/muhqs-game/go/log" "muhq.space/muhqs-game/go/utils" ) type Permanent interface { Card() *Card Tile() *Tile SetTile(*Tile) // Damage returnes the marked damage on the permanent. Damage() int ContainingPerm() Permanent Pile() []Permanent Controller() *Player changeController(*Player) Owner() *Player Effects() []string HasEffect(string) bool XEffect(string) (int, error) Marks(PermanentMark) int adjustMarks(PermanentMark, int) IsDestroyed() bool String() string CurrentlyAvailActions() []Action AvailActions() []Action IsAvailableTile(*Tile) bool // Attackable reports if a permanent is attackable. Attackable() bool // RangeProtected reports if a permanent is attackable in ranged combat. RangeProtected() bool fight(p Permanent) adjustDamage(damage int) addTmpEffect(tmpEffect string) removeTmpEffect(tmpEffect string) setContainingPerm(p Permanent) addPermanentToPile(p Permanent) clearPile() removePermanentFromPile(p Permanent) onPile(containing Permanent) onUnpile(containing Permanent) onDrop(containing Permanent) } type permanentBase struct { card *Card tile *Tile damage int pile []Permanent containingPerm Permanent controller *Player owner *Player tmpEffects []string marks map[PermanentMark]int } func newPermanentBase(card *Card, tile *Tile, owner *Player) permanentBase { if card == nil || tile == nil || owner == nil { log.Panicf("invalid argument for newPermanentBase(%p, %p, %p\n)", card, tile, owner) } if tile != nil && tile.Permanent != nil { log.Panicf("Tile at %v already occupied by %v", tile.Position, tile.Permanent) } return permanentBase{ card: card, tile: tile, controller: owner, owner: owner, marks: make(map[PermanentMark]int), } } func (p *permanentBase) Card() *Card { return p.card } func (p *permanentBase) Tile() *Tile { return p.tile } func (p *permanentBase) SetTile(t *Tile) { p.tile = t } func (p *permanentBase) Damage() int { return p.damage } func (p *permanentBase) ContainingPerm() Permanent { return p.containingPerm } func (p *permanentBase) Pile() []Permanent { return p.pile } func (p *permanentBase) Controller() *Player { return p.controller } func (p *permanentBase) Owner() *Player { return p.owner } func (p *permanentBase) changeController(c *Player) { p.controller = c } func FmtPermanent(p Permanent) string { typeIndicator := strings.ToUpper(p.Card().Type.String()[:1]) containing := p.ContainingPerm() if containing == nil { if p.Tile() == nil { return p.Card().Name } return fmt.Sprintf("%s%v", typeIndicator, p.Tile().Position) } for i, piled := range containing.Pile() { if piled == p { return fmt.Sprintf("%s%v[%d]", typeIndicator, containing.Tile().Position, i+1) } } log.Panicf("Permanent piled at %v not found in the pile", TileOrContainingPermTile(p).Position) return "" } func FindPermanent(s *LocalState, desc string) Permanent { var pos Position _, err := fmt.Sscanf(desc[1:strings.Index(desc, ")")], POSITION_FMT, &pos.X, &pos.Y) if err != nil { log.Panicf("desc %q not a valid permanent description", desc) } p := s.Map().TileAt(pos).Permanent return p } func DealDamage(src, dest Permanent, damage int) { actualDamage := damage armor, err := dest.XEffect("armor") if err == nil { piercing, err := src.XEffect("piercing") if err == nil { piercing = 0 } reduction := max(armor-piercing, 0) actualDamage = max(actualDamage-reduction, 0) } if actualDamage > 0 { dest.adjustDamage(actualDamage) src.Controller().gameState.fireEvent( Event{eventType: damageDealt, affected: []any{dest}, sources: []any{src}}) } } func (p *permanentBase) adjustDamage(damage int) { p.damage += damage if p.damage < 0 { p.damage = 0 } } func (p *permanentBase) fight(Permanent) {} func (p *permanentBase) addTmpEffect(tmpEffect string) { p.tmpEffects = append(p.tmpEffects, tmpEffect) } func (p *permanentBase) removeTmpEffect(tmpEffect string) { p.tmpEffects = utils.RemoveFromUnorderedSlice[string](p.tmpEffects, tmpEffect) } // Effects returnes a slice containing all static effects of a permanent. // The new slice returned contains the card and temporary effetcs. func (p *permanentBase) Effects() []string { effects := make([]string, 0, len(p.card.getEffects())+len(p.tmpEffects)) return append(append(effects, p.card.getEffects()...), p.tmpEffects...) } func (p *permanentBase) HasEffect(effect string) bool { return slices.Contains(p.tmpEffects, effect) || p.card.hasEffect(effect) } func (p *permanentBase) XEffect(effect string) (int, error) { found := false x := 0 xE, err := p.card.getXEffect(effect) if err == nil { x = xE.x found = true } if x < 0 { log.Panic("initial negativ x effect") } for _, e := range p.tmpEffects { log.Debug("tmp xEffect", "effect", effect, "tmpEffect", e) if !strings.Contains(e, effect) { continue } tmpX, err := parseXEffect(e) if err != nil { continue } found = true // TODO: implement layers if tmpX.set { x = tmpX.x } else { x += tmpX.x } } if found { return x, nil } else { return 0, err } } func (p *permanentBase) Marks(mark PermanentMark) int { if amount, found := p.marks[mark]; found { return amount } return 0 } func (p *permanentBase) adjustMarks(mark PermanentMark, amount int) { p.marks[mark] += amount } func (p *permanentBase) CurrentlyAvailActions() []Action { return nil } func (p *permanentBase) AvailActions() []Action { return nil } func (p *permanentBase) IsAvailableTile(tile *Tile) bool { return tile.IsAvailableForCard(p.card) } // Attackable reports if a permanent is attackable. // Only permanents represented on the map are attackable. func (p *permanentBase) Attackable() bool { return p.Tile() != nil } // RangeProtected reports if the permanent is attackable in range combat. // If the permanent has no Tile then it is not represented on the map and is not attackable at all. // House tiles protect from ranged combat. func (p *permanentBase) RangeProtected() bool { return !p.Attackable() || p.Tile().Type == TileTypes.House } func (p *permanentBase) setContainingPerm(containing Permanent) { p.containingPerm = containing } func (p *permanentBase) addPermanentToPile(a Permanent) { p.pile = append(p.pile, a) } func (p *permanentBase) clearPile() { p.pile = nil } func (p *permanentBase) removePermanentFromPile(rm Permanent) { p.pile = utils.RemoveFromUnorderedSlice[Permanent](p.pile, rm) } func (b *permanentBase) onPile(containing Permanent) { b.Card().Impl.onPile(containing) } func (b *permanentBase) onUnpile(containing Permanent) { b.Card().Impl.onUnpile(containing) } func (b *permanentBase) onDrop(dropped Permanent) { b.onUnpile(dropped.ContainingPerm()) b.Card().Impl.onDrop(dropped) } func addPermanentToPile(containing, p Permanent) { p.setContainingPerm(containing) containing.addPermanentToPile(p) p.onPile(containing) } func removePermanentFromPile(containing, rm Permanent) { containing.removePermanentFromPile(rm) rm.onUnpile(containing) rm.setContainingPerm(nil) } func movePermanent(p Permanent, t *Tile) { if !p.IsAvailableTile(t) { log.Panicf("moving %v to not available tile %v", p, t) } leaveTileOrPile(p) enterTileOrPile(p, t) } func enterTile(p Permanent, t *Tile) { p.SetTile(t) t.entering(p) p.Card().Impl.onEntering(t) } func enterTileOrPile(p Permanent, t *Tile) { if t.Permanent == nil { enterTile(p, t) } else { addPermanentToPile(t.Permanent, p) p.SetTile(nil) } } func leaveTileOrPile(p Permanent) { if cont := p.ContainingPerm(); cont != nil { removePermanentFromPile(cont, p) } else { p.Card().Impl.onLeaving(p.Tile()) p.Tile().leaving(p) p.SetTile(nil) } } func TileOrContainingPermTile(p Permanent) *Tile { tile := p.Tile() if tile == nil { tile = p.ContainingPerm().Tile() } return tile } func AdjacentTiles(p Permanent) []*Tile { m := p.Controller().gameState.Map() return TilesInRange(m, p, 1) }