diff --git a/TODO.md b/TODO.md index 8f53db3..7c1e903 100644 --- a/TODO.md +++ b/TODO.md @@ -118,3 +118,13 @@ front +- Sounds + + - cancel + - hit stock + - recycle stock + - hover stack gap + - drop card 1 2 3 + - drop foundation + - undo + diff --git a/public/audio/cancel.wav b/public/audio/cancel.wav new file mode 100755 index 0000000..05fffd1 Binary files /dev/null and b/public/audio/cancel.wav differ diff --git a/public/audio/drop.wav b/public/audio/drop.wav new file mode 100755 index 0000000..b4722cf Binary files /dev/null and b/public/audio/drop.wav differ diff --git a/public/audio/hit.wav b/public/audio/hit.wav new file mode 100755 index 0000000..f38cebd Binary files /dev/null and b/public/audio/hit.wav differ diff --git a/public/audio/recycle.wav b/public/audio/recycle.wav new file mode 100755 index 0000000..d570fff Binary files /dev/null and b/public/audio/recycle.wav differ diff --git a/public/audio/undo2.wav b/public/audio/undo2.wav new file mode 100755 index 0000000..c6eeeb2 Binary files /dev/null and b/public/audio/undo2.wav differ diff --git a/src/showcase.ts b/src/showcase.ts index 8bc1ee7..feef498 100644 --- a/src/showcase.ts +++ b/src/showcase.ts @@ -457,6 +457,57 @@ export class Card extends Play { } +export class CardDropTarget extends Play { + + + _on_drop?: DropHook + bind_drop(e?: DropHook) { + this._on_drop = e + } + + _will_hover!: boolean + _will_hover_end!: boolean + + anim!: Anim + + _init() { + + this.anim = this._make(Anim, Vec2.make(0, 0), { name: 'card' }) + this.anim.origin = Vec2.make(88, 120) + + let self = this + this.make(Clickable, Vec2.make(16, 16).sub(this.anim.origin), { + rect: Rect.make(0, 0, 170, 210), + on_hover() { + if (self._on_drop) { + self._will_hover = true + return true + } + return false + }, + on_hover_end() { + self._will_hover_end = true + }, + on_drop() { + if (self._on_drop) { + self._on_drop() + } + } + }) + } + + _update() { + + if (this._will_hover) { + this._will_hover = false + } + + if (this._will_hover_end) { + this._will_hover_end = false + } + + } +} let i = 0 @@ -548,7 +599,7 @@ export class Stack extends Play { } remove_cards(n: number) { - let cards = this.cards.splice(-n) + let cards = this.cards.splice(this.cards.length - n, this.cards.length) this._reposition() return cards } @@ -685,6 +736,7 @@ export class Tableu extends Play { this.fronts.top_card?.bind_drop(() => { self.data.on_front_drop() }) + this.open_drop_target() } remove_fronts(i: number) { @@ -703,6 +755,8 @@ export class Tableu extends Play { this.fronts.top_card?.bind_drop(() => { self.data.on_front_drop() }) + + this.open_drop_target() return cards } @@ -723,6 +777,14 @@ export class Tableu extends Play { }) } + get empty() { + return this.fronts.length === 0 && this.backs.length === 0 + } + + open_drop_target() { + this.drop_target.visible = this.empty + } + flip_back() { let [card] = this.fronts.remove_cards(1) card.flip_back() @@ -733,10 +795,19 @@ export class Tableu extends Play { backs!: Stack fronts!: Stack + drop_target!: CardDropTarget _init() { + + this.drop_target = this.make(CardDropTarget, Vec2.make(0, 0), {}) + this.drop_target.bind_drop(() => { + this.data.on_front_drop() + }) + this.backs = this.make(Stack, Vec2.make(0, 0), { h: 33 }) this.fronts = this.make(Stack, Vec2.make(0, 0), {}) + + this.open_drop_target() } diff --git a/src/solitaire_game.ts b/src/solitaire_game.ts index 16b0113..97ed60b 100644 --- a/src/solitaire_game.ts +++ b/src/solitaire_game.ts @@ -3,11 +3,17 @@ import { Rect, Vec2 } from 'blah' import { Play } from './play' import { Anim } from './anim' import { Clickable } from './game' -import { Stack, Card, Cards, Tableu, DragStack } from './showcase' +import { CardDropTarget, Stack, Card, Cards, Tableu, DragStack } from './showcase' import { Dealer } from './solitaire' import { BackRes, make_solitaire_back } from './solitaire_back' -import { n_seven, GamePov, Card as OCard, Solitaire, SolitairePov, IMove, IMoveType } from 'lsolitaire' -import { HitStock, Recycle, TableuToTableu } from 'lsolitaire' +import { n_four, n_seven, GamePov, Card as OCard, Solitaire, SolitairePov, IMove, IMoveType } from 'lsolitaire' +import { HitStock, Recycle, + TableuToTableu, + WasteToTableu, + WasteToFoundation, + TableuToFoundation, + FoundationToTableu, +} from 'lsolitaire' export type TableuDrag = { tableu: number, @@ -15,10 +21,16 @@ export type TableuDrag = { } export type WasteDrag = 'waste' -export type FoundationDrag = 'foundation' +export type FoundationDrag = { + foundation: number +} export type DragSource = TableuDrag | WasteDrag | FoundationDrag +const isFoundationDragSource = (_: DragSource): _ is FoundationDrag => { + return (typeof _ === 'object' && (_ as FoundationDrag).foundation !== undefined) +} + type RecycleData = { on_recycle: () => void @@ -50,6 +62,7 @@ class RecycleView extends Play { this.make(Clickable, Vec2.make(20, 20), { rect: Rect.make(0, 0, 140, 160), on_hover() { + if (anim._animation !== 'disabled') { anim.play('hover') } @@ -76,7 +89,8 @@ const reverse_forEach = (a: Array, f: (_: A) => void) => { type StockData = { on_hit: () => void, - on_recycle: () => void + on_recycle: () => void, + on_front_drag: (e: Vec2) => void } class Stock extends Play { @@ -105,8 +119,13 @@ class Stock extends Play { add_waste(cards: Array) { + cards.forEach(_ => { + _.bind_drop(undefined) + _.bind_drag(undefined) + }) this.waste.add_cards(cards) cards.forEach((card, i) => card.flip_front()) + this.bind_new_front() } @@ -115,6 +134,18 @@ class Stock extends Play { this.stock.add_cards(cards) } + remove_waste(n: number) { + return this.waste.remove_cards(n) + } + + + bind_new_front() { + this.waste.cards.forEach(_ => _.bind_drag(undefined)) + this.waste.top_card?.bind_drag((e: Vec2) => { + this.data.on_front_drag(e) + }) + } + undo_hit(ocards: Array, owaste: Array) { let waste_to_stock = this.waste.remove_cards(ocards.length) @@ -127,6 +158,7 @@ class Stock extends Play { }) waste_to_stock.forEach(card => card.flip_back()) this.stock.add_cards(waste_to_stock) + this.bind_new_front() } hit(ocards: Array) { @@ -137,10 +169,18 @@ class Stock extends Play { let waste = this.waste.remove_cards(this.waste.cards.length) + waste.forEach(_ => { + _.flip_back() + _.bind_drag(undefined) + }) this.waste_hidden.add_cards(waste) this.waste.add_cards(cards) reverse_forEach(this.waste_hidden.cards, _ => _.send_back()) + + this.bind_new_front() + + Sound.play('hit') } recycle() { @@ -154,6 +194,7 @@ class Stock extends Play { cards.forEach(card => card.send_front()) this.stock.add_cards(cards) + Sound.play('recycle') } @@ -195,6 +236,40 @@ class Stock extends Play { } +type FoundationData = { + on_front_drop: () => void, + on_front_drag: (e: Vec2) => void +} +class Foundation extends Play { + + get data() { + return this._data as FoundationData + } + + foundation!: Stack + drop_target!: CardDropTarget + + + add_cards(cards: Array) { + this.foundation.add_cards(cards) + this.foundation.top_card?.bind_drag(e => this.data.on_front_drag(e)) + } + + remove_cards(n: number) { + return this.foundation.remove_cards(n) + } + + _init() { + + this.drop_target = this.make(CardDropTarget, Vec2.make(0, 0), {}) + this.drop_target.bind_drop(() => { + this.data.on_front_drop() + }) + + this.foundation = this.make(Stack, Vec2.make(0, 0), { h: 0 }) + + } +} export class SolitaireGame extends Play { @@ -205,6 +280,7 @@ export class SolitaireGame extends Play { dealer!: Dealer stock!: Stock tableus!: Array + foundations!: Array dragging?: DragStack drag_source?: DragSource @@ -234,23 +310,7 @@ export class SolitaireGame extends Play { rect: Rect.make(0, 0, 0, 0), on_up() { if (self.dragging && !self.dragging.waiting) { - let cards = self.dragging.lerp_release() - - if (self.drag_source === 'waste') { - } else if (self.drag_source === 'foundation') { - } else { - - let { tableu, i } = self.drag_source! - - self.tableus[tableu].add_fronts(cards) - } - - self.dragging.dispose() - self.dragging = undefined - Sound.play('ding') - cards[0].after_ease(() => { - self.cards.shadow_group = undefined - }) + self._release_cancel_drag() } } }) @@ -273,6 +333,25 @@ export class SolitaireGame extends Play { this.stock = this.make(Stock, Vec2.make(stock_x, stock_y), { on_hit() { self.cmd(HitStock) + }, + on_front_drag(v: Vec2) { + if (self.dragging) { + self.dragging.drag(v) + } else { + if (self.pov.can_drag_waste) { + let cards = self.stock.remove_waste(1) + + self.dragging = self.make(DragStack, Vec2.zero, {}) + self.dragging.cards = cards + self.cards.shadow_group = cards + + self.drag_source = 'waste' + + Sound.play(`drag1`) + + } else { + } + } } }) @@ -300,17 +379,33 @@ export class SolitaireGame extends Play { i: e } + let drag_123 = Math.min(3, Math.floor(e/3) + 1) + Sound.play(`drag${drag_123}`) + } else { } } }, on_front_drop() { if (self.drag_source === 'waste') { - } else if (self.drag_source === 'foundation') { - } else { + self.dragging!.wait_drop() + self.cmd(WasteToTableu, { + to: i + }) + } else if (isFoundationDragSource(self.drag_source!)) { + let { foundation } = self.drag_source! + + self.dragging!.wait_drop() + + self.cmd(FoundationToTableu, { + from: foundation, + to: i, + }) + } else if (self.drag_source) { let { tableu, i: _i } = self.drag_source! + self.dragging!.wait_drop() self.cmd(TableuToTableu, { from: tableu, @@ -318,10 +413,57 @@ export class SolitaireGame extends Play { i: _i }) } - self.dragging!.wait_drop() } })) + let foundation_x = 1790, + foundation_y = 166, + foundation_h = 240 + + this.foundations = n_four.map(i => + this.make(Foundation, Vec2.make(foundation_x, foundation_y + foundation_h * i), { + on_front_drag(v: Vec2) { + if (self.dragging) { + self.dragging.drag(v) + } else { + if (self.pov.can_drag_foundation({ from: i })) { + let cards = self.foundations[i].remove_cards(1) + + self.dragging = self.make(DragStack, Vec2.zero, {}) + self.dragging.cards = cards + self.cards.shadow_group = cards + + self.drag_source = { foundation: i } + + Sound.play(`drag1`) + + } else { + } + } + }, + on_front_drop() { + if (self.drag_source === 'waste') { + self.dragging!.wait_drop() + self.cmd(WasteToFoundation, { + to: i + }) + } else if (isFoundationDragSource(self.drag_source!)) { + } else if (self.drag_source) { + let { tableu, i: _i } = self.drag_source! + + if (_i === 1) { + self.dragging!.wait_drop() + self.cmd(TableuToFoundation, { + from: tableu, + to: i, + i: 1 + }) + } + } + } + })) + + this.dealer = this.make(Dealer, Vec2.zero, { on_shuffle() { self.dealer.cards.forEach(_ => self.cards.release(_)) @@ -335,6 +477,30 @@ export class SolitaireGame extends Play { }) } + _release_cancel_drag() { + let cards = this.dragging!.lerp_release() + + if (this.drag_source === 'waste') { + this.stock.add_waste(cards) + } else if (isFoundationDragSource(this.drag_source!)) { + let { foundation } = this.drag_source + this.foundations[foundation].add_cards(cards) + } else { + + let { tableu, i } = this.drag_source! + + this.tableus[tableu].add_fronts(cards) + } + + this.dragging!.dispose() + this.dragging = undefined + this.drag_source = undefined + Sound.play('cancel') + cards[0].after_ease(() => { + this.cards.shadow_group = undefined + }) + } + _collect_pov() { this._init_pov() @@ -381,6 +547,7 @@ export class SolitaireGame extends Play { } undo(res: IMove) { + Sound.play('undo2') if (res instanceof HitStock) { this.stock.undo_hit(res.data.cards, res.data.waste) this._refresh_recycle() @@ -395,10 +562,34 @@ export class SolitaireGame extends Play { } let cards = this.tableus[to].remove_fronts(i) this.tableus[from].add_fronts(cards) + } else if (res instanceof WasteToTableu) { + + let { to } = res.data + + let cards = this.tableus[to].remove_fronts(1) + + this.stock.add_waste(cards) + } else if (res instanceof TableuToFoundation) { + let { flip } = res.res + let { from, to } = res.data + if (flip) { + this.tableus[from].flip_back() + } + let cards = this.foundations[to].remove_cards(1) + this.tableus[from].add_fronts(cards) + } else if (res instanceof WasteToFoundation) { + let { to } = res.data + let cards = this.foundations[to].remove_cards(1) + this.stock.add_waste(cards) + } else if (res instanceof FoundationToTableu) { + let { from, to } = res.data + let cards = this.tableus[to].remove_fronts(1) + this.foundations[from].add_cards(cards) } } apply(res: IMove) { + let dispose_drag_cards if (res instanceof HitStock) { this.stock.hit(res.data.cards) this._refresh_recycle() @@ -417,9 +608,53 @@ export class SolitaireGame extends Play { this.tableus[from].flip_front(flip) } + dispose_drag_cards = cards + } else if (res instanceof WasteToTableu) { + let { to } = res.data + let cards = this.dragging!.lerp_release() + this.tableus[to].add_fronts(cards) + + this.stock.bind_new_front() + + dispose_drag_cards = cards + } else if (res instanceof TableuToFoundation) { + let { flip } = res.res + let { from, to } = res.data + + let cards = this.dragging!.lerp_release() + this.foundations[to].add_cards(cards) + + if (flip) { + this.tableus[from].flip_front(flip) + } + + dispose_drag_cards = cards + + } else if (res instanceof WasteToFoundation) { + let { to } = res.data + + let cards = this.dragging!.lerp_release() + this.foundations[to].add_cards(cards) + + this.stock.bind_new_front() + + dispose_drag_cards = cards + + } else if (res instanceof FoundationToTableu) { + let { from, to } = res.data + let cards = this.dragging!.lerp_release() + this.tableus[to].add_fronts(cards) + + this.stock.bind_new_front() + + dispose_drag_cards = cards + } + + if (dispose_drag_cards) { this.dragging!.dispose() this.dragging = undefined - cards[0].after_ease(() => { + this.drag_source = undefined + dispose_drag_cards[0].after_ease(() => { this.cards.shadow_group = undefined }) } @@ -431,7 +666,13 @@ export class SolitaireGame extends Play { cant(cmd: IMoveType, data: any) { - console.log('cant', cmd) + if (cmd === TableuToFoundation) { + this._release_cancel_drag() + } else if (cmd === WasteToFoundation) { + this._release_cancel_drag() + } else { + console.log('cant', cmd) + } } cant_undo() { diff --git a/src/sound.ts b/src/sound.ts index 994be49..1d2e03e 100644 --- a/src/sound.ts +++ b/src/sound.ts @@ -5,7 +5,10 @@ function load_audio(path: string): HTMLMediaElement { return res } -let names = ['ding', 'drag', 'drag1', 'drag2', 'drag3'] +let names = [ + 'ding', 'drag', 'drag1', 'drag2', 'drag3', + 'cancel', 'drop', 'hit', 'recycle', 'undo2' +] class Sound {