starting perk, settings icons

This commit is contained in:
Renan LE CARO 2025-04-02 10:41:35 +02:00
parent 991e7a1d85
commit 1cf82d6641
12 changed files with 467 additions and 185 deletions

View file

@ -1121,5 +1121,47 @@
"bricks": "bbb______tttttt________tttttt________tttttt______bbtttttt______bbbbbttt______bbbbbb________bbbbbb________bbbbbb______ttbbbbbb______tttttbbb______tttttt________tttttt________tttttt______bbtttttt______bbbbbttt______bbbbbb________bbbbbb________bbbbbb________bbbbbb___________bbb______________",
"svg": null,
"color": ""
},
{
"name": "icon:starting_perks",
"size": 8,
"bricks": "_________y_y_y___________l_l_l_l_________l_l_l_l_________l_l_l_l",
"svg": null,
"color": ""
},
{
"name": "icon:download",
"size": 8,
"bricks": "___yy______yy______yy______yy______yy____yyyyyy___yyyy__gggyyggg",
"svg": null,
"color": ""
},
{
"name": "icon:upload",
"size": 8,
"bricks": "gggyyggg__yyyy___yyyyyy____yy______yy______yy______yy______yy___",
"svg": null,
"color": ""
},
{
"name": "icon:coins",
"size": 8,
"bricks": "__yyyy___yyOOyy_yyOOOOyyyOOOOOOyyOOOOOOyyyOOOOyy_yyOOyy___yyyy__",
"svg": null,
"color": ""
},
{
"name": "icon:particles",
"size": 8,
"bricks": "_y_y_y__________y_yyy_y___yyy___y_yyy__y_____y___y_y__y________y",
"svg": null,
"color": ""
},
{
"name": "icon:reset",
"size": 8,
"bricks": "__rrrr___r____r_r_r__r_rr__rr__rr__rr__rr_r__r_r_r____r___rrrr__",
"svg": null,
"color": ""
}
]
]

View file

@ -1,10 +1,9 @@
* {
font-family:
Courier New,
Courier,
Lucida Sans Typewriter,
Lucida Typewriter,
monospace;
font-family: Courier New,
Courier,
Lucida Sans Typewriter,
Lucida Typewriter,
monospace;
box-sizing: border-box;
}
@ -47,6 +46,7 @@ body {
line-height: 20px;
max-width: calc(100vw - 80px);
overflow: hidden;
&:hover,
&:focus {
background: rgba(0, 0, 0, 0.3);
@ -173,10 +173,12 @@ body:not(.has-alert-open) #popup {
filter: saturate(0);
}
}
&[disabled] {
opacity: 0.2;
}
}
}
}
}
@ -193,6 +195,7 @@ body:not(.has-alert-open) #popup {
margin-left: auto;
margin-right: auto;
}
section {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
@ -379,15 +382,18 @@ h2.histogram-title strong {
width: 32px;
height: 32px;
}
p {
flex-grow: 1;
color: rgba(255, 255, 255, 0.6);
margin: 0 20px;
}
&.used p strong {
color: white;
}
& > span {
flex-grow: 0;
@ -410,6 +416,7 @@ h2.histogram-title strong {
background: red;
}
}
&.used {
opacity: 1;
}

View file

@ -68,9 +68,11 @@ import { hoursSpentPlaying } from "./pure_functions";
import { helpMenuEntry } from "./help";
import { creativeMode } from "./creative";
import { setupTooltips } from "./tooltip";
import {startingPerkMenuButton} from "./startingPerks";
export function play() {
if (applyFullScreenChoice()) return;
export async function play() {
if (await applyFullScreenChoice()) return;
if (gameState.running) return;
gameState.running = true;
gameState.ballStickToPuck = false;
@ -556,8 +558,45 @@ function donationNag(gameState) {
async function openSettingsMenu() {
pause(true);
const actions: AsyncAlertAction<() => void>[] = [];
const actions: AsyncAlertAction<() => void>[] = [
startingPerkMenuButton()
];
const languages= [
{
text: "English",
value: "en",
icon: icons['UK']
},
{
text: "Français",
value: "fr",
icon: icons['France']
}
]
actions.push({
icon:languages.find(l=>l.value===getCurrentLang())?.icon,
text: t("main_menu.language"),
help: t("main_menu.language_help"),
async value() {
const pick = await asyncAlert({
title: t("main_menu.language"),
content: [
t("main_menu.language_help"),
...languages
],
allowClose: true,
});
if (
pick &&
pick !== getCurrentLang() &&
(await confirmRestart(gameState))
) {
setSettingValue("lang", pick);
window.location.reload();
}
},
});
for (const key of Object.keys(options) as OptionId[]) {
if (options[key])
actions.push({
@ -575,33 +614,7 @@ async function openSettingsMenu() {
});
}
actions.push({
text: t("main_menu.reset"),
help: t("main_menu.reset_help"),
async value() {
if (
await asyncAlert({
title: t("main_menu.reset"),
content: [
t("main_menu.reset_instruction"),
{
text: t("main_menu.reset_confirm"),
value: true,
},
{
text: t("main_menu.reset_cancel"),
value: false,
},
],
allowClose: true,
})
) {
localStorage.clear();
window.location.reload();
}
},
});
actions.push({
icon:icons['icon:download'],
text: t("main_menu.download_save_file"),
help: t("main_menu.download_save_file_help"),
async value() {
@ -650,6 +663,7 @@ async function openSettingsMenu() {
});
actions.push({
icon:icons['icon:upload'],
text: t("main_menu.load_save_file"),
help: t("main_menu.load_save_file_help"),
async value() {
@ -733,37 +747,9 @@ async function openSettingsMenu() {
},
});
actions.push({
text: t("main_menu.language"),
help: t("main_menu.language_help"),
async value() {
const pick = await asyncAlert({
title: t("main_menu.language"),
content: [
t("main_menu.language_help"),
{
text: "English",
value: "en",
},
{
text: "Français",
value: "fr",
},
],
allowClose: true,
});
if (
pick &&
pick !== getCurrentLang() &&
(await confirmRestart(gameState))
) {
setSettingValue("lang", pick);
window.location.reload();
}
},
});
actions.push({
icon:icons['icon:coins'],
text: t("main_menu.max_coins", { max: getCurrentMaxCoins() }),
help: t("main_menu.max_coins_help"),
async value() {
@ -772,6 +758,7 @@ async function openSettingsMenu() {
},
});
actions.push({
icon:icons['icon:particles'],
text: t("main_menu.max_particles", { max: getCurrentMaxParticles() }),
help: t("main_menu.max_particles_help"),
async value() {
@ -780,6 +767,34 @@ async function openSettingsMenu() {
},
});
actions.push({
icon:icons['icon:reset'],
text: t("main_menu.reset"),
help: t("main_menu.reset_help"),
async value() {
if (
await asyncAlert({
title: t("main_menu.reset"),
content: [
t("main_menu.reset_instruction"),
{
text: t("main_menu.reset_confirm"),
value: true,
},
{
text: t("main_menu.reset_cancel"),
value: false,
},
],
allowClose: true,
})
) {
localStorage.clear();
window.location.reload();
}
},
});
const cb = await asyncAlert<() => void>({
title: t("main_menu.settings_title"),
content: [t("main_menu.settings_help"), ...actions],
@ -791,7 +806,7 @@ async function openSettingsMenu() {
}
}
function applyFullScreenChoice(): boolean {
async function applyFullScreenChoice() {
try {
if (!(document.fullscreenEnabled || document.webkitFullscreenEnabled)) {
return false;
@ -799,19 +814,19 @@ function applyFullScreenChoice(): boolean {
if (document.fullscreenElement !== null && !isOptionOn("fullscreen")) {
if (document.exitFullscreen) {
document.exitFullscreen();
await document.exitFullscreen();
return true;
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen();
await document.webkitCancelFullScreen();
return true;
}
} else if (isOptionOn("fullscreen") && !document.fullscreenElement) {
const docel = document.documentElement;
if (docel.requestFullscreen) {
docel.requestFullscreen();
await docel.requestFullscreen()
return true;
} else if (docel.webkitRequestFullscreen) {
docel.webkitRequestFullscreen();
await docel.webkitRequestFullscreen();
return true;
}
}
@ -926,6 +941,9 @@ document.addEventListener("keydown", async (e) => {
let pageLoad = new Date();
document.addEventListener("keyup", async (e) => {
const focused = document.querySelector("button:focus");
if (e.key in pressed) {
setKeyPressed(e.key, 0);
@ -950,7 +968,7 @@ document.addEventListener("keyup", async (e) => {
} else if (
e.key.toLowerCase() === "r" &&
!alertsOpen &&
pageLoad > Date.now() + 1000
pageLoad < Date.now() - 500
) {
// When doing ctrl + R in dev to refresh, i don't want to instantly restart a run
if (await confirmRestart(gameState)) {

View file

@ -136,6 +136,7 @@ export function gameOver(title: string, intro: string) {
}).then(() =>
restart({
levelToAvoid: currentLevelInfo(gameState).name,
mode:gameState.mode
}),
);
}

View file

@ -1732,6 +1732,66 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>starting_perks</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>starting_perks_checked</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>starting_perks_help</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>starting_perks_unchecked</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-FR</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>title</name>
<description/>

View file

@ -110,6 +110,10 @@
"main_menu.show_stats_help": "Coins, time, bounces, misses",
"main_menu.sounds": "Game sounds",
"main_menu.sounds_help": "Can slow down some phones.",
"main_menu.starting_perks": "Starting perks",
"main_menu.starting_perks_checked": "When you start a new game, one of those perks will be give to you. You can click the list to exclude some perks from the pool.",
"main_menu.starting_perks_help": "Choose possible starting upgrades",
"main_menu.starting_perks_unchecked": "The perks below are not offered as starting perks, but you can click to add them to the pool. ",
"main_menu.title": "Breakout 71",
"main_menu.unlocks": "Unlocked content",
"main_menu.unlocks_help": "Try perks and levels you unlocked",

View file

@ -110,6 +110,10 @@
"main_menu.show_stats_help": "Pièces, temps, rebonds, ratés",
"main_menu.sounds": "Sons du jeu",
"main_menu.sounds_help": "Ralentis certains téléphones.",
"main_menu.starting_perks": "Avantages de départ",
"main_menu.starting_perks_checked": "Lorsque vous démarrez une nouvelle partie, l'un de ces avantages vous sera attribué. Vous pouvez cliquer sur la liste pour exclure certains avantages de la sélection.",
"main_menu.starting_perks_help": "Choisissez les avantages de départ",
"main_menu.starting_perks_unchecked": "Les avantages ci-dessous ne sont pas proposés comme avantages de départ, mais vous pouvez cliquer pour les ajouter aux avantages de départ possibles.",
"main_menu.title": "Breakout 71",
"main_menu.unlocks": "Contenu débloqué",
"main_menu.unlocks_help": "Essayez les éléments débloqués",

View file

@ -397,18 +397,16 @@ export function render(gameState: GameState) {
ctx.globalCompositeOperation = "source-over";
ctx.globalAlpha = gameState.perks.unbounded ? 0.1 : 1;
let redLeftSide = hasCombo &&!gameState.perks.unbounded&& (gameState.perks.left_is_lava || gameState.perks.trampoline)
let redRightSide = hasCombo &&!gameState.perks.unbounded&& (gameState.perks.right_is_lava || gameState.perks.trampoline)
let redTop = hasCombo && gameState.perks.unbounded<=2 && (gameState.perks.top_is_lava || gameState.perks.trampoline)
if (gameState.offsetXRoundedDown) {
// draw outside of gaming area to avoid capturing borders in recordings
ctx.fillStyle =
hasCombo && gameState.perks.left_is_lava ? "red" : gameState.puckColor;
drawStraightLine(
ctx,
gameState,
(hasCombo &&
gameState.perks.left_is_lava &&
!gameState.perks.unbounded &&
"red") ||
(redLeftSide && "red") ||
"white",
gameState.offsetX - 1,
0,
@ -420,10 +418,7 @@ export function render(gameState: GameState) {
drawStraightLine(
ctx,
gameState,
(hasCombo &&
gameState.perks.right_is_lava &&
!gameState.perks.unbounded &&
"red") ||
(redRightSide && "red") ||
"white",
width - gameState.offsetX + 1,
0,
@ -432,15 +427,11 @@ export function render(gameState: GameState) {
gameState.perks.unbounded ? 0.1 : 1,
);
} else {
ctx.fillStyle = "red";
drawStraightLine(
ctx,
gameState,
(hasCombo &&
gameState.perks.left_is_lava &&
!gameState.perks.unbounded &&
"red") ||
(redLeftSide && "red") ||
"",
0,
0,
@ -452,10 +443,7 @@ export function render(gameState: GameState) {
drawStraightLine(
ctx,
gameState,
(hasCombo &&
gameState.perks.right_is_lava &&
!gameState.perks.unbounded &&
"red") ||
(redRightSide && "red") ||
"",
width - 1,
0,
@ -464,15 +452,14 @@ export function render(gameState: GameState) {
1,
);
}
ctx.globalAlpha = gameState.perks.unbounded > 1 ? 0.1 : 1;
if(redTop)
drawStraightLine(
ctx,
gameState,
(hasCombo && gameState.perks.top_is_lava && "red") || "",
gameState.offsetXRoundedDown,
"red",
gameState.perks.unbounded ? 0 : gameState.offsetXRoundedDown,
1,
width - gameState.offsetXRoundedDown,
gameState.perks.unbounded ? width : width - gameState.offsetXRoundedDown,
1,
1,
);

54
src/startingPerks.ts Normal file
View file

@ -0,0 +1,54 @@
import {asyncAlert} from "./asyncAlert";
import {PerkId, Upgrade} from "./types";
import {t} from "./i18n/i18n";
import {icons, upgrades} from "./loadGameData";
import {getSettingValue, getTotalScore, setSettingValue} from "./settings";
export function startingPerkMenuButton(){
return {
icon:icons['icon:starting_perks'],
text:t('main_menu.starting_perks'),
help:t('main_menu.starting_perks_help'),
async value(){
await openStartingPerksEditor()
}
}
}
function isChecked(u:Upgrade):boolean{
return getSettingValue('start_with_'+u.id, u.giftable)
}
export async function openStartingPerksEditor(){
const ts=getTotalScore()
const avaliable=upgrades.filter(u=>!u.requires && !['instant_upgrade'].includes(u.id) && u.threshold<=ts)
const starting = avaliable.filter(u=>isChecked(u))
const buttons=avaliable
.map(u=> {
const checked = isChecked(u);
return {
icon: u.icon,
text: u.name,
tooltip: u.help(1),
value:u,
disabled:checked && starting.length<2,
checked
}
})
const perk :Upgrade|null|void= await asyncAlert({
title:t('main_menu.starting_perks'),
actionsAsGrid:true,
content:[
t('main_menu.starting_perks_checked'),
...buttons.filter(b=>b.checked),
t('main_menu.starting_perks_unchecked'),
...buttons.filter(b=>!b.checked),
]
})
if(perk){
setSettingValue('start_with_'+perk.id,!isChecked(perk))
openStartingPerksEditor()
}
}

View file

@ -7,12 +7,18 @@ export function setupTooltips() {
return;
}
function updateTooltipPosition(e: MouseEvent) {
tooltip.style.transform =
`translate(${e.clientX}px,${e.clientY + 20}px) ` +
(e.clientX > window.innerWidth / 2 ? " translate(-100%,0)" : "");
}
function closeToolTip(){
tooltip.style.display = "none";
hovering=null
}
let hovering:HTMLElement|null=null
document.body.addEventListener(
"mouseenter",
(e: MouseEvent) => {
@ -21,15 +27,24 @@ export function setupTooltips() {
parent = parent.parentElement;
}
if (parent?.hasAttribute("data-tooltip")) {
tooltip.innerHTML = parent?.getAttribute("data-tooltip");
hovering=parent as HTMLElement
tooltip.innerHTML = hovering.getAttribute("data-tooltip") || '';
tooltip.style.display = "";
updateTooltipPosition(e);
} else {
tooltip.style.display = "none";
closeToolTip()
}
},
true,
);
setInterval(()=>{
if(hovering){
if(!document.body.contains(hovering)){
closeToolTip()
}
}
},200)
document.body.addEventListener(
"mousemove",
(e) => {
@ -42,8 +57,7 @@ export function setupTooltips() {
document.body.addEventListener(
"mouseleave",
(e) => {
// tooltip.style.display = 'none';
},
true,
closeToolTip()
}
);
}