Merge branch 'main' into dev

This commit is contained in:
Alberto Xamin 2022-12-29 13:36:24 +00:00
commit a6edde725a
14 changed files with 187 additions and 81 deletions

View File

@ -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"
}

View File

@ -26,12 +26,3 @@ jobs:
- name: Test with pytest - name: Test with pytest
run: | run: |
python -m pytest -p no:warnings 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 }}

View File

@ -1,5 +1,5 @@
# build Vue frontend # build Vue frontend
FROM node:lts-alpine as builder FROM node:16-alpine as builder
COPY ./frontend . COPY ./frontend .
RUN npm install RUN npm install
RUN npm run build RUN npm run build

View File

@ -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 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): if player.character.check(player.game, chars.WillyTheKid):
player.has_played_bang = False 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 True
return False return False
@ -350,7 +350,7 @@ class Gatling(Card):
def play_card(self, player, against, _with=None): def play_card(self, player, against, _with=None):
super().play_card(player, against=against) super().play_card(player, against=against)
player.game.attack_others(player) player.game.attack_others(player, card_name=self.name)
return True return True
@ -385,7 +385,7 @@ class Mancato(Card):
data=f'_special_calamity|{player.name}|{self.name}|{against}') data=f'_special_calamity|{player.name}|{self.name}|{against}')
player.bang_used += 1 player.bang_used += 1
player.has_played_bang = True if not player.game.check_event(ceh.Sparatoria) else 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 True
return False return False

View File

@ -25,7 +25,7 @@ class Pugno(Card):
def play_card(self, player, against, _with=None): def play_card(self, player, against, _with=None):
if against != None: if against != None:
super().play_card(player, against=against) super().play_card(player, against=against)
player.game.attack(player, against) player.game.attack(player, against, card_name=self.name)
return True return True
return False return False
@ -106,7 +106,7 @@ class SpringField(Card):
if against != None and _with != None: if against != None and _with != None:
player.game.deck.scrap(_with) player.game.deck.scrap(_with)
super().play_card(player, against=against) super().play_card(player, against=against)
player.game.attack(player, against) player.game.attack(player, against, card_name=self.name)
return True return True
return False return False
@ -367,7 +367,7 @@ class Pepperbox(Bang):
if self.can_be_used_now: if self.can_be_used_now:
if against != None: if against != None:
Card.play_card(self, player, against=against) Card.play_card(self, player, against=against)
player.game.attack(player, against) player.game.attack(player, against, card_name=self.name)
return True return True
return False return False
else: else:
@ -392,7 +392,7 @@ class FucileDaCaccia(Card):
if self.can_be_used_now: if self.can_be_used_now:
if against != None: if against != None:
super().play_card(player, against=against) super().play_card(player, against=against)
player.game.attack(player, against) player.game.attack(player, against, card_name=self.name)
return True return True
return False return False
else: else:

View File

@ -303,7 +303,7 @@ class Game:
self.players[i].notify_self() self.players[i].notify_self()
self.notify_event_card() 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 self.attack_in_progress = True
attacker.pending_action = pl.PendingAction.WAIT attacker.pending_action = pl.PendingAction.WAIT
attacker.notify_self() attacker.notify_self()
@ -311,7 +311,7 @@ class Game:
self.ready_count = 0 self.ready_count = 0
for p in self.get_alive_players(): for p in self.get_alive_players():
if p != attacker: if p != attacker:
if p.get_banged(attacker=attacker): if p.get_banged(attacker=attacker, card_name=card_name):
self.waiting_for += 1 self.waiting_for += 1
p.notify_self() p.notify_self()
if self.waiting_for == 0: if self.waiting_for == 0:
@ -339,8 +339,8 @@ class Game:
if self.pending_winners and not self.someone_won: if self.pending_winners and not self.someone_won:
return self.announces_winners() return self.announces_winners()
def attack(self, attacker: pl.Player, target_username:str, double:bool=False): 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): if self.get_player_named(target_username).get_banged(attacker=attacker, double=double, card_name=card_name):
self.ready_count = 0 self.ready_count = 0
self.waiting_for = 1 self.waiting_for = 1
attacker.pending_action = pl.PendingAction.WAIT attacker.pending_action = pl.PendingAction.WAIT

View File

@ -57,6 +57,7 @@ class Player:
self.on_failed_response_cb = None self.on_failed_response_cb = None
self.event_type: str = None self.event_type: str = None
self.expected_response = [] self.expected_response = []
self.attacking_card = None
self.attacker: Player = None self.attacker: Player = None
self.target_p: str = None self.target_p: str = None
self.is_drawing = False self.is_drawing = False
@ -960,8 +961,9 @@ class Player:
self.on_failed_response_cb = self.take_no_damage_response self.on_failed_response_cb = self.take_no_damage_response
self.notify_self() 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.attacker = attacker
self.attacking_card = card_name
print(f'attacker -> {attacker}') print(f'attacker -> {attacker}')
self.mancato_needed = 1 if not double else 2 self.mancato_needed = 1 if not double else 2
if card_index != None: if card_index != None:
@ -1016,6 +1018,7 @@ class Player:
def get_indians(self, attacker): def get_indians(self, attacker):
self.attacker = 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 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: 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') print('Cant defend')
@ -1033,6 +1036,7 @@ class Player:
def get_dueled(self, attacker): def get_dueled(self, attacker):
self.attacker = 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): 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') print('Cant defend')
self.take_damage_response() self.take_damage_response()
@ -1086,6 +1090,7 @@ class Player:
self.heal_if_needed() self.heal_if_needed()
self.mancato_needed = 0 self.mancato_needed = 0
self.expected_response = [] self.expected_response = []
self.attacking_card = None
self.event_type = '' self.event_type = ''
self.notify_self() self.notify_self()
self.attacker = None self.attacker = None
@ -1096,6 +1101,7 @@ class Player:
self.dmg_card_index = -1 self.dmg_card_index = -1
self.mancato_needed = 0 self.mancato_needed = 0
self.expected_response = [] self.expected_response = []
self.attacking_card = None
self.event_type = '' self.event_type = ''
self.notify_self() self.notify_self()
self.attacker = None self.attacker = None

View File

@ -567,8 +567,11 @@ def chat_message(sid, msg, pl=None):
else: else:
sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} COMMAND NOT FOUND'}) sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} COMMAND NOT FOUND'})
else: else:
color = sid.encode('utf-8').hex()[-3:] # get a color from sid
sio.emit('chat_message', room=ses.game.name, data={'color': f'#{color}','text':f'[{ses.name}]: {msg}'}) 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: if not ses.game.is_replay:
Metrics.send_metric('chat_message', points=[1], tags=[f'game:{ses.game.name.replace(" ","_")}']) Metrics.send_metric('chat_message', points=[1], tags=[f'game:{ses.game.name.replace(" ","_")}'])

View File

@ -1,12 +1,16 @@
<template> <template>
<div class="chat"> <div class="chat" :style="`${collapsed?'min-width:0':''}`">
<h4 v-if="spectators > 0">{{$tc("chat.spectators", spectators)}}</h4> <div class="chat-header">
<div style="display:flex;align-items: center;max-height: 20pt;">
<h3>{{$t("chat.chat")}}</h3> <h3>{{$t("chat.chat")}}</h3>
<transition-group name="message" tag="div" id="chatbox"> <button class="btn" @click="collapsed = !collapsed" style="max-height:20pt;">{{collapsed?">>":"X"}}</button>
<!-- <div id="chatbox"> --> </div>
<p style="margin:1pt;" class="chat-message" v-for="(msg, i) in messages" v-bind:key="`${i}-c`" :style="`color:${msg.color}`">{{msg.text}}</p> <h4 v-if="spectators > 0">{{$tc("chat.spectators", spectators)}}</h4>
</div>
<div class="cont">
<transition-group name="message" tag="div" id="chatbox" :style="`${collapsed?'display:none':''}`">
<p style="margin:1pt;" class="chat-message" v-for="(msg, i) in messages" v-bind:key="`${i}-c`" :style="`color:${msg.color};background:${msg.bgcolor}${msg.bgcolor?';border-left: medium solid '+msg.color+';padding-left:2pt;':''}`">{{msg.text}}</p>
<p class="end" key="end" style="color:#0000">.</p> <p class="end" key="end" style="color:#0000">.</p>
<!-- </div> -->
</transition-group> </transition-group>
<div v-if="commandSuggestion.length > 0"> <div v-if="commandSuggestion.length > 0">
<p style="margin:1pt 15pt;cursor:pointer;" class="chat-message" v-for="(msg, i) in commandSuggestion" v-bind:key="`${i}-c`" :style="`color:orange`" <p style="margin:1pt 15pt;cursor:pointer;" class="chat-message" v-for="(msg, i) in commandSuggestion" v-bind:key="`${i}-c`" :style="`color:orange`"
@ -17,6 +21,10 @@
<input id="submit-message" type="submit" class="btn" :value="$t('submit')"/> <input id="submit-message" type="submit" class="btn" :value="$t('submit')"/>
</form> </form>
</div> </div>
<transition-group name="message" tag="div" id="toast-chatbox">
<p style="margin:1pt;" class="chat-message" v-for="msg in toasts" v-bind:key="`${msg.text}-c`" :style="`width:fit-content;color:${msg.color};background:${msg.bgcolor}${msg.bgcolor?';border-left: medium solid '+msg.color+';padding-left:2pt;padding-right:4pt;':''}`">{{msg.text}}</p>
</transition-group>
</div>
</template> </template>
<script> <script>
@ -34,9 +42,11 @@ export default {
}, },
data: () => ({ data: () => ({
messages: [], messages: [],
toasts: [],
text: '', text: '',
spectators: 0, spectators: 0,
commands: [{cmd:'/debug', help:'Toggles the debug mode'}], commands: [{cmd:'/debug', help:'Toggles the debug mode'}],
collapsed: false,
}), }),
computed: { computed: {
commandSuggestion() { commandSuggestion() {
@ -52,8 +62,10 @@ export default {
// console.log(msg) // console.log(msg)
if ((typeof msg === "string" && msg.indexOf('_') === 0) || (msg.color != null && msg.text.indexOf('_') === 0)) { if ((typeof msg === "string" && msg.indexOf('_') === 0) || (msg.color != null && msg.text.indexOf('_') === 0)) {
let t_color = null let t_color = null
let bg_color = null
if (msg.color != null) { if (msg.color != null) {
t_color = msg.color t_color = msg.color
bg_color = msg.bgcolor
msg = msg.text msg = msg.text
} }
let params = msg.split('|') let params = msg.split('|')
@ -75,7 +87,7 @@ export default {
} }
} }
if (t_color != null) { 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 { } else {
this.messages.push({text:this.$t(`chat.${type}`, params)}); this.messages.push({text:this.$t(`chat.${type}`, params)});
} }
@ -95,6 +107,10 @@ export default {
} else { // a chat message } else { // a chat message
(new Audio(message_sfx)).play(); (new Audio(message_sfx)).play();
this.messages.push(msg); 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"); let container = this.$el.querySelector("#chatbox");
container.scrollTop = container.scrollHeight; container.scrollTop = container.scrollHeight;
@ -149,7 +165,7 @@ input {
.std-text { .std-text {
color: var(--font-color); color: var(--font-color);
} }
.chat { .chat, .cont {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@ -167,7 +183,10 @@ input {
transform: translateX(30px); transform: translateX(30px);
} }
@media only screen and (min-width:1000px) { @media only screen and (min-width:1000px) {
.chat { .chat-header {
margin-left: 10pt;
}
.chat, .cont {
height: 90vh; height: 90vh;
margin-left: 10pt; margin-left: 10pt;
} }
@ -176,6 +195,12 @@ input {
margin-right: -5pt; margin-right: -5pt;
} }
} }
#toast-chatbox {
position: fixed;
bottom: 30pt;
left: 0;
background: --var(--bg-color);
}
@media only screen and (max-width:1000px) { @media only screen and (max-width:1000px) {
#msg-form { #msg-form {
flex-direction: column; flex-direction: column;

View File

@ -38,6 +38,8 @@
<p v-if="players.length < 3" class="center-stuff" style="min-height: 19px;">{{$t('minimum_players')}}</p> <p v-if="players.length < 3" class="center-stuff" style="min-height: 19px;">{{$t('minimum_players')}}</p>
<p v-else style="min-height: 19px;"> </p> <p v-else style="min-height: 19px;"> </p>
</div> </div>
<div style="position:relative;">
<div v-if="showTurnFlow" id="turn-indicator" :class="{reversed:turnReversed}"/>
<transition-group name="list" tag="div" class="players-table"> <transition-group name="list" tag="div" class="players-table">
<Card v-if="startGameCard" key="_start_game_" :donotlocalize="true" :card="startGameCard" @click.native="startGame"/> <Card v-if="startGameCard" key="_start_game_" :donotlocalize="true" :card="startGameCard" @click.native="startGame"/>
<div v-for="p in playersTable" v-bind:key="p.card.name" style="position:relative;"> <div v-for="p in playersTable" v-bind:key="p.card.name" style="position:relative;">
@ -69,6 +71,7 @@
</div> </div>
</div> </div>
</transition-group> </transition-group>
</div>
<div v-if="started"> <div v-if="started">
<deck :endTurnAction="()=>{wantsToEndTurn = true}"/> <deck :endTurnAction="()=>{wantsToEndTurn = true}"/>
<player :isEndingTurn="wantsToEndTurn" :cancelEndingTurn="()=>{wantsToEndTurn = false}" :chooseCardFromPlayer="choose" :cancelChooseCardFromPlayer="()=>{hasToChoose=false}"/> <player :isEndingTurn="wantsToEndTurn" :cancelEndingTurn="()=>{wantsToEndTurn = false}" :chooseCardFromPlayer="choose" :cancelChooseCardFromPlayer="()=>{hasToChoose=false}"/>
@ -129,6 +132,9 @@ export default {
is_competitive: false, is_competitive: false,
disconnect_bot: false, disconnect_bot: false,
debug_mode: false, debug_mode: false,
showTurnFlow: false,
turnReversed: false,
turn: -1,
}), }),
sockets: { sockets: {
room(data) { room(data) {
@ -347,8 +353,19 @@ export default {
privateRoom(old, _new) { privateRoom(old, _new) {
if (this.isRoomOwner && old !== _new) if (this.isRoomOwner && old !== _new)
this.$socket.emit('private') 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() { mounted() {
if (Vue.config.devtools) if (Vue.config.devtools)
console.log('mounted lobby') console.log('mounted lobby')
@ -409,6 +426,39 @@ export default {
justify-content: space-evenly; justify-content: space-evenly;
margin-bottom: 12pt; 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 { .lobby {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -115,6 +115,7 @@ export default {
can_target_sheriff: true, can_target_sheriff: true,
show_role: false, show_role: false,
attacker: undefined, attacker: undefined,
attacking_card: undefined,
notifycard: null, notifycard: null,
desc: '', desc: '',
scrapHand: [], scrapHand: [],
@ -175,6 +176,7 @@ export default {
this.sight = self.sight this.sight = self.sight
this.sight_extra = self.sight_extra this.sight_extra = self.sight_extra
this.attacker = self.attacker this.attacker = self.attacker
this.attacking_card = self.attacking_card
this.mancato_needed = self.mancato_needed this.mancato_needed = self.mancato_needed
this.is_ghost = self.is_ghost this.is_ghost = self.is_ghost
if (this.pending_action == 5 && self.target_p) { if (this.pending_action == 5 && self.target_p) {
@ -215,7 +217,8 @@ export default {
}, },
computed:{ computed:{
respondText() { 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() { showScrapScreen() {
return this.isEndingTurn && !this.canEndTurn && this.is_my_turn; return this.isEndingTurn && !this.canEndTurn && this.is_my_turn;

View File

@ -653,7 +653,7 @@
}, },
"Jacky Murieta": { "Jacky Murieta": {
"name": "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": { "Josh McCloud": {
"name": "Josh McCloud", "name": "Josh McCloud",

View File

@ -653,7 +653,7 @@
}, },
"Jacky Murieta": { "Jacky Murieta": {
"name": "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": { "Josh McCloud": {
"name": "Josh McCloud", "name": "Josh McCloud",

View File

@ -3516,9 +3516,10 @@ eventemitter3@^4.0.0:
version "4.0.7" version "4.0.7"
resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz"
events@^3.0.0: "events@^3.0.0":
version "3.3.0" "integrity" "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" "resolved" "https://registry.npmjs.org/events/-/events-3.3.0.tgz"
"version" "3.3.0"
eventsource@^1.0.7: eventsource@^1.0.7:
version "1.1.1" version "1.1.1"