diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..2728e2a --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,27 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node +{ + "name": "BangCodespace", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/javascript-node:0-16", + "features": { + "ghcr.io/devcontainers/features/python:1": { + "version": "3.7.10" + } + }, + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [5001, 8080], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "cd frontend;npm i;cd ../backend;pip install -r requirements.txt" + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 7e26aba..3f93692 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -25,13 +25,4 @@ jobs: if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Test with pytest run: | - python -m pytest -p no:warnings - - - name: Notify discord - uses: th0th/notify-discord@v0.4.1 - if: ${{ always() }} - env: - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} - GITHUB_ACTOR: ${{ github.actor }} - GITHUB_JOB_NAME: "Backend tests" - GITHUB_JOB_STATUS: ${{ job.status }} + python -m pytest -p no:warnings \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index d54c6f1..a9e407f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # build Vue frontend -FROM node:lts-alpine as builder +FROM node:16-alpine as builder COPY ./frontend . RUN npm install RUN npm run build diff --git a/backend/bang/cards.py b/backend/bang/cards.py index 3bdcd95..17970d9 100644 --- a/backend/bang/cards.py +++ b/backend/bang/cards.py @@ -226,7 +226,7 @@ class Bang(Card): player.has_played_bang = True if not player.game.check_event(ceh.Sparatoria) else player.bang_used > 1 if player.character.check(player.game, chars.WillyTheKid): player.has_played_bang = False - player.game.attack(player, against, double=player.character.check(player.game, chars.SlabTheKiller)) + player.game.attack(player, against, double=player.character.check(player.game, chars.SlabTheKiller), card_name=self.name) return True return False @@ -350,7 +350,7 @@ class Gatling(Card): def play_card(self, player, against, _with=None): super().play_card(player, against=against) - player.game.attack_others(player) + player.game.attack_others(player, card_name=self.name) return True @@ -385,7 +385,7 @@ class Mancato(Card): data=f'_special_calamity|{player.name}|{self.name}|{against}') player.bang_used += 1 player.has_played_bang = True if not player.game.check_event(ceh.Sparatoria) else player.bang_used > 1 - player.game.attack(player, against) + player.game.attack(player, against, card_name=self.name) return True return False diff --git a/backend/bang/expansions/dodge_city/cards.py b/backend/bang/expansions/dodge_city/cards.py index b62c0d2..8dd7b49 100644 --- a/backend/bang/expansions/dodge_city/cards.py +++ b/backend/bang/expansions/dodge_city/cards.py @@ -25,7 +25,7 @@ class Pugno(Card): def play_card(self, player, against, _with=None): if against != None: super().play_card(player, against=against) - player.game.attack(player, against) + player.game.attack(player, against, card_name=self.name) return True return False @@ -106,7 +106,7 @@ class SpringField(Card): if against != None and _with != None: player.game.deck.scrap(_with) super().play_card(player, against=against) - player.game.attack(player, against) + player.game.attack(player, against, card_name=self.name) return True return False @@ -367,7 +367,7 @@ class Pepperbox(Bang): if self.can_be_used_now: if against != None: Card.play_card(self, player, against=against) - player.game.attack(player, against) + player.game.attack(player, against, card_name=self.name) return True return False else: @@ -392,7 +392,7 @@ class FucileDaCaccia(Card): if self.can_be_used_now: if against != None: super().play_card(player, against=against) - player.game.attack(player, against) + player.game.attack(player, against, card_name=self.name) return True return False else: diff --git a/backend/bang/game.py b/backend/bang/game.py index 5dfde2e..3548454 100644 --- a/backend/bang/game.py +++ b/backend/bang/game.py @@ -303,7 +303,7 @@ class Game: self.players[i].notify_self() self.notify_event_card() - def attack_others(self, attacker: pl.Player): + def attack_others(self, attacker: pl.Player, card_name:str=None): self.attack_in_progress = True attacker.pending_action = pl.PendingAction.WAIT attacker.notify_self() @@ -311,7 +311,7 @@ class Game: self.ready_count = 0 for p in self.get_alive_players(): if p != attacker: - if p.get_banged(attacker=attacker): + if p.get_banged(attacker=attacker, card_name=card_name): self.waiting_for += 1 p.notify_self() if self.waiting_for == 0: @@ -339,8 +339,8 @@ class Game: if self.pending_winners and not self.someone_won: return self.announces_winners() - def attack(self, attacker: pl.Player, target_username:str, double:bool=False): - if self.get_player_named(target_username).get_banged(attacker=attacker, double=double): + def attack(self, attacker: pl.Player, target_username:str, double:bool=False, card_name:str=None): + if self.get_player_named(target_username).get_banged(attacker=attacker, double=double, card_name=card_name): self.ready_count = 0 self.waiting_for = 1 attacker.pending_action = pl.PendingAction.WAIT diff --git a/backend/bang/players.py b/backend/bang/players.py index 777fc41..8daf297 100644 --- a/backend/bang/players.py +++ b/backend/bang/players.py @@ -57,6 +57,7 @@ class Player: self.on_failed_response_cb = None self.event_type: str = None self.expected_response = [] + self.attacking_card = None self.attacker: Player = None self.target_p: str = None self.is_drawing = False @@ -960,8 +961,9 @@ class Player: self.on_failed_response_cb = self.take_no_damage_response self.notify_self() - def get_banged(self, attacker, double=False, no_dmg=False, card_index=None): + def get_banged(self, attacker, double=False, no_dmg=False, card_index=None, card_name=None): self.attacker = attacker + self.attacking_card = card_name print(f'attacker -> {attacker}') self.mancato_needed = 1 if not double else 2 if card_index != None: @@ -1016,6 +1018,7 @@ class Player: def get_indians(self, attacker): self.attacker = attacker + self.attacking_card = "Indiani!" if self.character.check(self.game, chd.ApacheKid) or len([c for c in self.gold_rush_equipment if isinstance(c, grc.Calumet)]) > 0: return False if not self.game.is_competitive and len([c for c in self.hand if isinstance(c, cs.Bang) or (self.character.check(self.game, chars.CalamityJanet) and isinstance(c, cs.Mancato))]) == 0: print('Cant defend') @@ -1033,6 +1036,7 @@ class Player: def get_dueled(self, attacker): self.attacker = attacker + self.attacking_card = "Duello" if (self.game.check_event(ceh.Sermone) and self.is_my_turn) or (not self.game.is_competitive and len([c for c in self.hand if isinstance(c, cs.Bang) or (self.character.check(self.game, chars.CalamityJanet) and isinstance(c, cs.Mancato))]) == 0): print('Cant defend') self.take_damage_response() @@ -1086,6 +1090,7 @@ class Player: self.heal_if_needed() self.mancato_needed = 0 self.expected_response = [] + self.attacking_card = None self.event_type = '' self.notify_self() self.attacker = None @@ -1096,6 +1101,7 @@ class Player: self.dmg_card_index = -1 self.mancato_needed = 0 self.expected_response = [] + self.attacking_card = None self.event_type = '' self.notify_self() self.attacker = None diff --git a/backend/server.py b/backend/server.py index c4fc2f2..9b43579 100644 --- a/backend/server.py +++ b/backend/server.py @@ -567,8 +567,11 @@ def chat_message(sid, msg, pl=None): else: sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} COMMAND NOT FOUND'}) else: - color = sid.encode('utf-8').hex()[-3:] - sio.emit('chat_message', room=ses.game.name, data={'color': f'#{color}','text':f'[{ses.name}]: {msg}'}) + # get a color from sid + color = sid.encode('utf-8').hex()[0:6] + #bg color will be slightly darker and transparent + bg_color = f'{int(color[0:2],16)-10:02x}{int(color[2:4],16)-10:02x}{int(color[4:6],16)-10:02x}20' + sio.emit('chat_message', room=ses.game.name, data={'color': f'#{color}', 'bgcolor': f'#{bg_color}','text':f'[{ses.name}]: {msg}'}) if not ses.game.is_replay: Metrics.send_metric('chat_message', points=[1], tags=[f'game:{ses.game.name.replace(" ","_")}']) diff --git a/frontend/src/components/Chat.vue b/frontend/src/components/Chat.vue index 484e1de..0757662 100644 --- a/frontend/src/components/Chat.vue +++ b/frontend/src/components/Chat.vue @@ -1,21 +1,29 @@ @@ -34,9 +42,11 @@ export default { }, data: () => ({ messages: [], + toasts: [], text: '', spectators: 0, commands: [{cmd:'/debug', help:'Toggles the debug mode'}], + collapsed: false, }), computed: { commandSuggestion() { @@ -52,8 +62,10 @@ export default { // console.log(msg) if ((typeof msg === "string" && msg.indexOf('_') === 0) || (msg.color != null && msg.text.indexOf('_') === 0)) { let t_color = null + let bg_color = null if (msg.color != null) { t_color = msg.color + bg_color = msg.bgcolor msg = msg.text } let params = msg.split('|') @@ -75,7 +87,7 @@ export default { } } if (t_color != null) { - this.messages.push({color:t_color, text:this.$t(`chat.${type}`, params)}); + this.messages.push({color:t_color, bgcolor: bg_color, text:this.$t(`chat.${type}`, params)}); } else { this.messages.push({text:this.$t(`chat.${type}`, params)}); } @@ -95,6 +107,10 @@ export default { } else { // a chat message (new Audio(message_sfx)).play(); this.messages.push(msg); + if (this.collapsed || window.innerWidth < 1000) { + this.toasts.push(msg); + setTimeout(() => this.toasts.shift(), 5000); + } } let container = this.$el.querySelector("#chatbox"); container.scrollTop = container.scrollHeight; @@ -149,7 +165,7 @@ input { .std-text { color: var(--font-color); } -.chat { +.chat, .cont { display: flex; flex-direction: column; } @@ -167,7 +183,10 @@ input { transform: translateX(30px); } @media only screen and (min-width:1000px) { - .chat { + .chat-header { + margin-left: 10pt; + } + .chat, .cont { height: 90vh; margin-left: 10pt; } @@ -176,6 +195,12 @@ input { margin-right: -5pt; } } +#toast-chatbox { + position: fixed; + bottom: 30pt; + left: 0; + background: --var(--bg-color); +} @media only screen and (max-width:1000px) { #msg-form { flex-direction: column; diff --git a/frontend/src/components/Lobby.vue b/frontend/src/components/Lobby.vue index 0e37daf..41e8ecd 100644 --- a/frontend/src/components/Lobby.vue +++ b/frontend/src/components/Lobby.vue @@ -38,37 +38,40 @@

{{$t('minimum_players')}}

- - -
- - 💵️ - - - ❤️ - 💀 - -
- 👻 +
+
+ + +
+ + 💵️ + + + ❤️ + 💀 + +
+ 👻 +
+ + + + + {{getActionEmoji(p)}} +
+ + +
+
+ 🤖 +
- - - - - {{getActionEmoji(p)}} -
- - -
-
- 🤖 -
-
- + +
@@ -129,6 +132,9 @@ export default { is_competitive: false, disconnect_bot: false, debug_mode: false, + showTurnFlow: false, + turnReversed: false, + turn: -1, }), sockets: { room(data) { @@ -347,7 +353,18 @@ export default { privateRoom(old, _new) { if (this.isRoomOwner && old !== _new) this.$socket.emit('private') - } + }, + players(_, _new) { + let x = _new.findIndex(x => x.is_my_turn); + if (x !== -1 && x !== this.turn) { + this.turnReversed = (x+1 === this.turn) + this.showTurnFlow = true; + setTimeout(() => { + this.showTurnFlow = false + }, 1000); + this.turn = x; + } + }, }, mounted() { if (Vue.config.devtools) @@ -409,6 +426,39 @@ export default { justify-content: space-evenly; margin-bottom: 12pt; } +#turn-indicator{ + position: absolute; + width: 100%; + height: 100%; + background-image: linear-gradient(135deg, #cbcbcb33 25%, transparent 25%), linear-gradient(45deg, #cbcbcb33 25%, transparent 25%); + background-size: 80px 200px; + background-position: 0 100px; + background-position-x: 0; + opacity: 0; + background-repeat: repeat; + animation-name: next-turn-animation; + animation-duration: 1s; + animation-iteration-count: 3; + animation-timing-function: linear; +} +#turn-indicator.reversed { + background-image: linear-gradient(225deg, #cbcbcb33 25%, transparent 25%), linear-gradient(315deg, #cbcbcb33 25%, transparent 25%); +} + +@keyframes next-turn-animation { + 0% { +background-position-x: 0; +opacity: 1; + } + 50% { +background-position-x: 80px; + } + 100% { + opacity: 0; + background-position-x: 160px; + } + +} .lobby { display: flex; flex-direction: column; diff --git a/frontend/src/components/Player.vue b/frontend/src/components/Player.vue index 926b7b1..15d1d8f 100644 --- a/frontend/src/components/Player.vue +++ b/frontend/src/components/Player.vue @@ -115,6 +115,7 @@ export default { can_target_sheriff: true, show_role: false, attacker: undefined, + attacking_card: undefined, notifycard: null, desc: '', scrapHand: [], @@ -175,6 +176,7 @@ export default { this.sight = self.sight this.sight_extra = self.sight_extra this.attacker = self.attacker + this.attacking_card = self.attacking_card this.mancato_needed = self.mancato_needed this.is_ghost = self.is_ghost if (this.pending_action == 5 && self.target_p) { @@ -215,7 +217,8 @@ export default { }, computed:{ respondText() { - return `${this.$t('choose_response')}${this.attacker?(this.$t('choose_response_to')+this.attacker):''}${(this.mancato_needed>1)?(` (${this.$t('choose_response_needed')} ` + this.mancato_needed + ')'):''}` + let attCard = this.attacking_card ? ' ('+this.$t('cards.'+this.attacking_card+'.name')+')' : ''; + return `${this.$t('choose_response')}${this.attacker?(this.$t('choose_response_to')+this.attacker+attCard):''}${(this.mancato_needed>1)?(` (${this.$t('choose_response_needed')} ` + this.mancato_needed + ')'):''}` }, showScrapScreen() { return this.isEndingTurn && !this.canEndTurn && this.is_my_turn; diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index dc14e6f..8f4f8dd 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -653,7 +653,7 @@ }, "Jacky Murieta": { "name": "Jacky Murieta", - "desc": "During his turn he can pay 2 gold nuggets to shoot another BANG!" + "desc": "During his turn he can pay 2 gold nuggets for the ability to shoot another BANG!" }, "Josh McCloud": { "name": "Josh McCloud", diff --git a/frontend/src/i18n/it.json b/frontend/src/i18n/it.json index 96459a8..5029f0f 100644 --- a/frontend/src/i18n/it.json +++ b/frontend/src/i18n/it.json @@ -653,7 +653,7 @@ }, "Jacky Murieta": { "name": "Jacky Murieta", - "desc": "Durante il suo turno può pagare 2 pepite per sparare un bang, può farlo più volte per turno" + "desc": "Durante il suo turno può pagare 2 pepite per poter sparare un altro bang, può farlo più volte per turno" }, "Josh McCloud": { "name": "Josh McCloud", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 78280f3..a7159c9 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3516,9 +3516,10 @@ eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" -events@^3.0.0: - version "3.3.0" - resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" +"events@^3.0.0": + "integrity" "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + "resolved" "https://registry.npmjs.org/events/-/events-3.3.0.tgz" + "version" "3.3.0" eventsource@^1.0.7: version "1.1.1"