Various bug fixes and improvements.
This commit is contained in:
@@ -2,7 +2,6 @@ package game
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// BattleUnit is a pet in play with its attached foods applied. Power
|
// BattleUnit is a pet in play with its attached foods applied. Power
|
||||||
@@ -1165,19 +1164,20 @@ func (g *Game) runBattle() *BattleResult {
|
|||||||
Text: fmt.Sprintf("%s eats an apple (now +%d).", q.unit.Card.Name, q.unit.Bonus)})
|
Text: fmt.Sprintf("%s eats an apple (now +%d).", q.unit.Card.Name, q.unit.Bonus)})
|
||||||
}
|
}
|
||||||
case ActionShuffleApples:
|
case ActionShuffleApples:
|
||||||
// Komodo: only if it's the side's first pet, shuffle apples into
|
// Komodo: only if it's the side's first pet, add apples to the
|
||||||
// random deck positions (deterministic via the battle draw tape).
|
// remaining deck and shuffle the whole thing so the apples and the
|
||||||
|
// existing pets intermix (not just apples dropped in).
|
||||||
if q.effect.Condition == ConditionFirstPet && sides[q.seat].petsPlayed != 1 {
|
if q.effect.Condition == ConditionFirstPet && sides[q.seat].petsPlayed != 1 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s := sides[q.seat]
|
s := sides[q.seat]
|
||||||
for range q.effect.count() {
|
for range q.effect.count() {
|
||||||
pos := g.battleDraw(len(s.stack) + 1)
|
|
||||||
apple := g.newApple()
|
apple := g.newApple()
|
||||||
s.stack = slices.Insert(s.stack, pos, apple)
|
s.stack = append(s.stack, apple)
|
||||||
emit(BattleEvent{Type: "summon", Seat: q.seat, Card: &apple,
|
emit(BattleEvent{Type: "summon", Seat: q.seat, Card: &apple,
|
||||||
Text: fmt.Sprintf("%s shuffles an apple into %s's deck.", q.unit.Card.Name, pname(q.seat))})
|
Text: fmt.Sprintf("%s shuffles an apple into %s's deck.", q.unit.Card.Name, pname(q.seat))})
|
||||||
}
|
}
|
||||||
|
g.battleShuffle(s.stack)
|
||||||
case ActionSpendRocks:
|
case ActionSpendRocks:
|
||||||
// Nurse Shark: spend as many Trumpets as available (up to Count) to
|
// Nurse Shark: spend as many Trumpets as available (up to Count) to
|
||||||
// throw two rocks each.
|
// throw two rocks each.
|
||||||
|
|||||||
@@ -196,8 +196,8 @@ const (
|
|||||||
ActionRevealForApples EffectAction = "revealForApples"
|
ActionRevealForApples EffectAction = "revealForApples"
|
||||||
|
|
||||||
// --- Golden pack, tier 6 ---
|
// --- Golden pack, tier 6 ---
|
||||||
// ActionShuffleApples (play) shuffles Count apples into random positions of
|
// ActionShuffleApples (play) adds Count apples to the owner's remaining
|
||||||
// the owner's remaining deck (Komodo). Gated by ConditionFirstPet.
|
// deck and shuffles the whole deck (Komodo). Gated by ConditionFirstPet.
|
||||||
ActionShuffleApples EffectAction = "shuffleApples"
|
ActionShuffleApples EffectAction = "shuffleApples"
|
||||||
// ActionReactivateBuys (battle prep) re-fires the Buy ability of every pet
|
// ActionReactivateBuys (battle prep) re-fires the Buy ability of every pet
|
||||||
// in the owner's hand (Catfish).
|
// in the owner's hand (Catfish).
|
||||||
|
|||||||
@@ -159,6 +159,15 @@ func (g *Game) battleDraw(n int) int {
|
|||||||
// rollRockDie rolls one rock die: 0, 1, or 2 with equal probability.
|
// rollRockDie rolls one rock die: 0, 1, or 2 with equal probability.
|
||||||
func (g *Game) rollRockDie() int { return g.battleDraw(3) }
|
func (g *Game) rollRockDie() int { return g.battleDraw(3) }
|
||||||
|
|
||||||
|
// battleShuffle does an in-place Fisher-Yates over a battle deck, routing its
|
||||||
|
// randomness through battleDraw like every other in-battle draw (Komodo).
|
||||||
|
func (g *Game) battleShuffle(s []Card) {
|
||||||
|
for i := len(s) - 1; i > 0; i-- {
|
||||||
|
j := g.battleDraw(i + 1)
|
||||||
|
s[i], s[j] = s[j], s[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNotYourTurn = errors.New("not your turn")
|
ErrNotYourTurn = errors.New("not your turn")
|
||||||
ErrWrongPhase = errors.New("action not allowed in this phase")
|
ErrWrongPhase = errors.New("action not allowed in this phase")
|
||||||
|
|||||||
@@ -18,9 +18,12 @@ type PlayerView struct {
|
|||||||
Avocados int `json:"avocados,omitempty"`
|
Avocados int `json:"avocados,omitempty"`
|
||||||
// FirstBuyFree / BuysThisRound (Golden pack) are self-only: they'd reveal
|
// FirstBuyFree / BuysThisRound (Golden pack) are self-only: they'd reveal
|
||||||
// hidden deck facts (a Manta Ray) to opponents.
|
// hidden deck facts (a Manta Ray) to opponents.
|
||||||
FirstBuyFree bool `json:"firstBuyFree,omitempty"`
|
FirstBuyFree bool `json:"firstBuyFree,omitempty"`
|
||||||
BuysThisRound int `json:"buysThisRound,omitempty"`
|
BuysThisRound int `json:"buysThisRound,omitempty"`
|
||||||
Deck []Card `json:"deck,omitempty"` // self only
|
// Trumpets is the count banked for the next battle (Bird of Paradise).
|
||||||
|
// Self-only — shown under the player's own deck.
|
||||||
|
Trumpets int `json:"trumpets,omitempty"`
|
||||||
|
Deck []Card `json:"deck,omitempty"` // self only
|
||||||
}
|
}
|
||||||
|
|
||||||
// View is the full game state as seen by one player.
|
// View is the full game state as seen by one player.
|
||||||
@@ -105,6 +108,7 @@ func (g *Game) ViewFor(playerID string) View {
|
|||||||
pv.Deck = p.Deck
|
pv.Deck = p.Deck
|
||||||
pv.FirstBuyFree = p.FirstBuyFree
|
pv.FirstBuyFree = p.FirstBuyFree
|
||||||
pv.BuysThisRound = p.BuysThisRound
|
pv.BuysThisRound = p.BuysThisRound
|
||||||
|
pv.Trumpets = p.PendingTrumpets
|
||||||
}
|
}
|
||||||
v.Players = append(v.Players, pv)
|
v.Players = append(v.Players, pv)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -541,7 +541,7 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
|
|||||||
<div className={`deck-peek deck-peek-${peek.dir}`} style={style}>
|
<div className={`deck-peek deck-peek-${peek.dir}`} style={style}>
|
||||||
<div className="deck-peek-label">
|
<div className="deck-peek-label">
|
||||||
{peek.seat === youSeat ? 'Your' : 'Opponent’s'} deck · {lineup.length} card
|
{peek.seat === youSeat ? 'Your' : 'Opponent’s'} deck · {lineup.length} card
|
||||||
{lineup.length !== 1 ? 's' : ''} (top first)
|
{lineup.length !== 1 ? 's' : ''} (first on the right)
|
||||||
</div>
|
</div>
|
||||||
<div className="deck-peek-cards">
|
<div className="deck-peek-cards">
|
||||||
{lineup.map((c, i) => (
|
{lineup.map((c, i) => (
|
||||||
|
|||||||
@@ -206,19 +206,19 @@ export function CardView({
|
|||||||
onMouseLeave={preview ? undefined : () => setHover(null)}
|
onMouseLeave={preview ? undefined : () => setHover(null)}
|
||||||
>
|
>
|
||||||
{previewEl}
|
{previewEl}
|
||||||
{/* Power sits in a spiky burst badge in the top-left corner (pets only),
|
<div className="card-top">
|
||||||
with any battle damage marker beside it. It's absolutely placed so it
|
<span className="card-tier">{card.tier ? `T${card.tier}` : '·'}</span>
|
||||||
doesn't eat into the room the name and ability text need. */}
|
{card.suit && <span className={`suit-dot suit-${card.suit}`} title={`${card.suit} suit`} />}
|
||||||
|
</div>
|
||||||
|
{/* Power sits in a spiky burst badge on its own centered row (pets only),
|
||||||
|
with any battle damage marker beside it, so it never overlaps the art,
|
||||||
|
name, or ability text. */}
|
||||||
{card.kind === 'pet' && (
|
{card.kind === 'pet' && (
|
||||||
<div className="card-combat">
|
<div className="card-combat">
|
||||||
<span className={`card-power ${bonus > 0 ? 'is-buffed' : ''}`}>{power}</span>
|
<span className={`card-power ${bonus > 0 ? 'is-buffed' : ''}`}>{power}</span>
|
||||||
{damage > 0 && !dead && <span className="card-damage">−{damage}</span>}
|
{damage > 0 && !dead && <span className="card-damage">−{damage}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="card-top">
|
|
||||||
<span className="card-tier">{card.tier ? `T${card.tier}` : '·'}</span>
|
|
||||||
{card.suit && <span className={`suit-dot suit-${card.suit}`} title={`${card.suit} suit`} />}
|
|
||||||
</div>
|
|
||||||
<div className="card-art" aria-hidden>
|
<div className="card-art" aria-hidden>
|
||||||
{artFor(card.name)}
|
{artFor(card.name)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export function ShopPhase({ view, you, send }: Props) {
|
|||||||
const [useAvocado, setUseAvocado] = useState(false)
|
const [useAvocado, setUseAvocado] = useState(false)
|
||||||
const myTurn = view.turn === view.youSeat && !you.ready
|
const myTurn = view.turn === view.youSeat && !you.ready
|
||||||
const avocados = you.avocados ?? 0
|
const avocados = you.avocados ?? 0
|
||||||
|
const trumpets = you.trumpets ?? 0
|
||||||
const freeBuy = !!you.firstBuyFree // Manta Ray: next buy costs no gold
|
const freeBuy = !!you.firstBuyFree // Manta Ray: next buy costs no gold
|
||||||
const canBuy = myTurn && (you.coins > 0 || avocados > 0 || freeBuy)
|
const canBuy = myTurn && (you.coins > 0 || avocados > 0 || freeBuy)
|
||||||
// Nudge the player to pass once there's nothing left to buy.
|
// Nudge the player to pass once there's nothing left to buy.
|
||||||
@@ -376,6 +377,11 @@ export function ShopPhase({ view, you, send }: Props) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{trumpets > 0 && (
|
||||||
|
<div className="trumpet-indicator" title="Trumpets banked for your next battle">
|
||||||
|
🎺 ×{trumpets}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
|
|||||||
>
|
>
|
||||||
<div className="deck-peek-label">
|
<div className="deck-peek-label">
|
||||||
{oppName}’s deck last round · {lineup.length} card
|
{oppName}’s deck last round · {lineup.length} card
|
||||||
{lineup.length !== 1 ? 's' : ''} (top first)
|
{lineup.length !== 1 ? 's' : ''} (first on the right)
|
||||||
</div>
|
</div>
|
||||||
<div className="deck-peek-cards">
|
<div className="deck-peek-cards">
|
||||||
{lineup.map((c, i) => (
|
{lineup.map((c, i) => (
|
||||||
|
|||||||
+27
-8
@@ -983,9 +983,8 @@ h3 {
|
|||||||
.card-top {
|
.card-top {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
/* The power badge now floats over the top-left corner, so tier + suit sit
|
/* Tier sits at the top-left, suit dot at the top-right. */
|
||||||
together at the top-right. */
|
justify-content: space-between;
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
@@ -993,16 +992,15 @@ h3 {
|
|||||||
min-height: 14px;
|
min-height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Combat stats (power badge + battle damage marker) pinned to the top-left
|
/* Combat stats (power badge + battle damage marker) get their own centered
|
||||||
corner. Absolute so they don't push the name and ability text down. */
|
row just under the tier/suit line, so they never overlap the art or text. */
|
||||||
.card-combat {
|
.card-combat {
|
||||||
position: absolute;
|
|
||||||
top: 5px;
|
|
||||||
left: 5px;
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
margin: 2px 0 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-tier {
|
.card-tier {
|
||||||
@@ -1290,6 +1288,19 @@ h3 {
|
|||||||
inset 0 0 0 1px rgba(246, 201, 78, 0.06);
|
inset 0 0 0 1px rgba(246, 201, 78, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Trumpets banked for the next battle, shown as a pill under the deck. */
|
||||||
|
.trumpet-indicator {
|
||||||
|
margin: 12px auto 0;
|
||||||
|
width: fit-content;
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
padding: 5px 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(246, 201, 78, 0.3);
|
||||||
|
background: rgba(0, 0, 0, 0.28);
|
||||||
|
color: var(--gold);
|
||||||
|
}
|
||||||
|
|
||||||
/* Coins shown as big golden discs above the buy row. */
|
/* Coins shown as big golden discs above the buy row. */
|
||||||
.shop-coins {
|
.shop-coins {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2592,6 +2603,14 @@ h3 {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
max-width: min(60vw, 560px);
|
max-width: min(60vw, 560px);
|
||||||
|
/* Read right-to-left like the arrangement screen: the first (top) card sits
|
||||||
|
on the right and later pets flow left. */
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep each card's own contents left-to-right despite the rtl row. */
|
||||||
|
.deck-peek-cards > * {
|
||||||
|
direction: ltr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Backdrop behind the toolbar deck-peek: any click outside dismisses it. Sits
|
/* Backdrop behind the toolbar deck-peek: any click outside dismisses it. Sits
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export interface PlayerView {
|
|||||||
avocados?: number // set-aside Avocado tokens (Golden pack)
|
avocados?: number // set-aside Avocado tokens (Golden pack)
|
||||||
firstBuyFree?: boolean // Manta Ray: next buy is free (self only)
|
firstBuyFree?: boolean // Manta Ray: next buy is free (self only)
|
||||||
buysThisRound?: number // Blue-Ringed Octopus counter (self only)
|
buysThisRound?: number // Blue-Ringed Octopus counter (self only)
|
||||||
|
trumpets?: number // banked Trumpets for next battle (Bird of Paradise, self only)
|
||||||
deck?: Card[]
|
deck?: Card[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user