Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions build/entitlements.mac.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Required for Steam overlay injection -->
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>

<!-- Standard Electron entitlements -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "beaming",
"author": "Kyle Florence",
"description": "A browser-based puzzle game that involves directing beams through a hexagonal grid.",
"version": "0.4.4",
"version": "0.5.0",
"license": "CC BY-NC 4.0",
"main": "src/electron/main.js",
"type": "module",
Expand Down
2 changes: 2 additions & 0 deletions src/components/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Gutter } from './gutter'
import { Phosphor } from './iconlib.js'
import { Icon, Icons } from './icon.js'
import { Game } from './game.js'
import { Achievements } from '../keys.js'

const elements = Object.freeze({
cancel: document.getElementById('editor-cancel'),
Expand Down Expand Up @@ -448,6 +449,7 @@ export class Editor {

#updateConfiguration (state) {
elements.configuration.value = JSON.stringify(state, null, 2)
window.electron?.steam.unlockAchievement(Achievements.Edit)
this.#updatePlayUrl()
}

Expand Down
6 changes: 5 additions & 1 deletion src/components/items/beam.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Step, StepState } from '../step'
import { Collision, CollisionMergeWith } from '../collision'
import { Cache } from '../cache'
import { Tile } from './tile.js'
import { Achievements } from '../../keys.js'

export class Beam extends Item {
done = false
Expand Down Expand Up @@ -321,7 +322,10 @@ export class Beam extends Item {
} else if (!isSameDirection) {
// For a collision with self, the update at point of impact will occur on the next update loop. This results in
// a better visualization of the collision which will result in an infinite looping animation.
setTimeout(() => this.#update(stepIndex), puzzle.getBeamsUpdateDelay())
setTimeout(() => {
window.electron?.steam.unlockAchievement(Achievements.Infinity)
this.#update(stepIndex)
}, puzzle.getBeamsUpdateDelay())
}
}

Expand Down
20 changes: 20 additions & 0 deletions src/components/puzzle.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ import { Tile } from './items/tile.js'
import { Storage } from './storage.js'
import { Puzzles } from '../puzzles/index.js'
import { debug } from './debug.js'
import { Achievements } from '../keys.js'

const electron = window.electron

const elements = Object.freeze({
canvas: document.getElementById('puzzle-canvas-wrapper'),
Expand Down Expand Up @@ -715,6 +718,8 @@ export class Puzzle {
return
}

const id = this.state.getId()

this.solved = true

this.updateSelectedTile(undefined)
Expand All @@ -723,6 +728,8 @@ export class Puzzle {
// Store the solution in cache
this.state.setSolution(this.layout.tiles.filter(this.#mask.tileFilter))

const solutions = State.addSolution(id, this.state.getSolution())

const p = document.createElement('p')
p.classList.add(Puzzle.ClassNames.Solved)
p.textContent = 'Puzzle solved!'
Expand All @@ -735,6 +742,19 @@ export class Puzzle {

document.body.classList.add(Puzzle.Events.Solved)
emitEvent(Puzzle.Events.Solved)

if (electron) {
electron.steam.unlockAchievement(Achievements.FirstSolve)

if (solutions.length > 5) {
electron.steam.unlockAchievement(Achievements.Hex)
}

// TODO should probably come up with a more flexible system for this rather than hard coding it
if (this.state.getId() === '001') {
electron.steam.unlockAchievement(Achievements.Puzzle001)
}
}
}

#onStateUpdate (event) {
Expand Down
2 changes: 1 addition & 1 deletion src/components/settings.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import './settings/cache.js'
import './settings/debug.js'
import './settings/profile.js'
import './settings/troubleshooting.js'
25 changes: 18 additions & 7 deletions src/components/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,18 @@ export class State {
this.#deltas.filter((delta, index) => index <= deltaIndex).forEach((delta) => this.#apply(delta))
}

static add (id) {
const ids = Array.from(new Set([...State.getIds(), id]))
Storage.set(State.key(State.CacheKeys.Ids), JSON.stringify(ids))
return ids
}

static addSolution (id, solution) {
const solutions = Array.from(new Set([...State.getSolutions(), solution.join(':')]))
Storage.set(State.key(id ?? State.getId(), State.CacheKeys.Solutions), JSON.stringify(solutions))
return solutions
}

static clearCache (id) {
Storage.delete(id === undefined ? id : State.key(id))
}
Expand Down Expand Up @@ -406,6 +418,10 @@ export class State {
return params.get(State.CacheKeys.Parent)?.split(',') ?? []
}

static getSolutions (id) {
return JSON.parse(Storage.get(State.key(id ?? State.getId(), State.CacheKeys.Solutions)) ?? '[]')
}

static resolve (id) {
let values = []

Expand Down Expand Up @@ -512,7 +528,8 @@ export class State {
Id: 'id',
Ids: 'ids',
Locked: 'locked',
Parent: 'parent'
Parent: 'parent',
Solutions: 'solutions'
})

static ContextKeys = Object.freeze({
Expand Down Expand Up @@ -542,12 +559,6 @@ export class State {

static toString = classToString('State')

static add (id) {
const ids = Array.from(new Set([...State.getIds(), id]))
Storage.set(State.key(State.CacheKeys.Ids), JSON.stringify(ids))
return ids
}

static remove (id, context = State.getContext()) {
const ids = State.getIds(context)
const index = ids.indexOf(id)
Expand Down
1 change: 1 addition & 0 deletions src/electron/channels.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export default Object.freeze({
debug: 'debug',
quit: 'quit',
resizeWindow: 'resize-window',
steamAchievementUnlock: 'steam-achievement-unlock',
storeDelete: 'store-delete',
storeGet: 'store-get',
storeSet: 'store-set',
Expand Down
14 changes: 12 additions & 2 deletions src/electron/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ function createWindow () {

window = new BrowserWindow(options)

if (store.get(Keys.enableSteamOverlay) === true) {
steam.setupOverlay(window)
}

if (windowType === Values.maximized) {
window.maximize()
}
Expand Down Expand Up @@ -115,6 +119,12 @@ ipcMain.on(channels.resizeWindow, (event, value, settings) => {
}
})

ipcMain.handle(channels.steamAchievementUnlock, (event, name) => {
steam.unlockAchievement(name).catch((reason) => {
console.error('Failed to unlock achievement', reason)
})
})

ipcMain.handle(channels.storeDelete, (event, key) => {
if (key === undefined) {
store.clear()
Expand All @@ -136,10 +146,10 @@ ipcMain.handle(channels.storeSet, (event, key, value) => {
})

app.whenReady().then(() => {
createWindow()

steam.setup()

createWindow()

app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
Expand Down
7 changes: 7 additions & 0 deletions src/electron/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import { Keys } from '../keys.js'
const electron = 'electron'
const localStorage = window.localStorage

const steam = {
unlockAchievement: async function (name) {
return ipcRenderer.invoke(channels.steamAchievementUnlock, name)
}
}

const store = {
delete: async function (key) {
return ipcRenderer.invoke(channels.storeDelete, key)
Expand Down Expand Up @@ -62,5 +68,6 @@ contextBridge.exposeInMainWorld(electron, {
onWindowResized,
quit,
resizeWindow,
steam,
store
})
8 changes: 8 additions & 0 deletions src/electron/settings/window.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,11 @@ electron.onWindowResized((bounds) => {
windowWidth.value = bounds.width
}
})

const $steamOverlay = document.getElementById('settings-steam-overlay')
$steamOverlay.checked = localStorage.getItem(Keys.enableSteamOverlay) === 'true'

$steamOverlay.addEventListener('change', () => {
localStorage.setItem(Keys.enableSteamOverlay, $steamOverlay.checked)
window.electron?.store.set(Keys.enableSteamOverlay, $steamOverlay.checked)
})
27 changes: 27 additions & 0 deletions src/electron/steam.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { SteamworksSDK } from 'steamworks-ffi-node'
import { AchievementNames } from '../keys.js'

export default class Steam {
#interval
#sdk

appId = 4172230
overlayEnabled = false

constructor () {
this.#sdk = SteamworksSDK.getInstance()
Expand All @@ -15,9 +17,20 @@ export default class Steam {
this.#interval = setInterval(this.#sdk.runCallbacks.bind(this.#sdk), 1000)
}

this.overlayEnabled = this.#sdk.isOverlayAvailable()

console.debug(Steam.toString('setup'), this.#sdk.getStatus())
}

setupOverlay (window) {
if (!this.overlayEnabled) {
return
}

this.#sdk.addElectronSteamOverlay(window)
console.debug(Steam.toString('addOverlay'), 'Steam overlay added')
}

teardown () {
if (this.#interval === undefined) {
return
Expand All @@ -28,6 +41,20 @@ export default class Steam {
this.#sdk.shutdown()
}

async unlockAchievement (name) {
if (!AchievementNames.includes(name)) {
throw new Error(`Invalid achievement name: ${name}`)
}

let unlocked = await this.#sdk.achievements.isAchievementUnlocked(name)
if (!unlocked) {
unlocked = await this.#sdk.achievements.unlockAchievement(name)
console.debug(Steam.toString('unlockAchievement'), name, unlocked)
}

return unlocked
}

static toString () {
return '[' + ['Steam', ...arguments].join(':') + ']'
}
Expand Down
9 changes: 7 additions & 2 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ <h1 class="flex-left">Settings</h1>
<button id="settings-profile-create">New Profile</button>
</div>
</fieldset>
<fieldset id="settings-window">
<fieldset class="electron-only" id="settings-window">
<legend>Window</legend>
<div class="settings">
<input id="settings-window-fullscreen" name="settings-window" type="radio" value="fullscreen" />
Expand All @@ -438,6 +438,8 @@ <h1 class="flex-left">Settings</h1>
<input class="number" id="settings-window-height" type="number" />
<label for="settings-window-height">Height</label>
</div>
<input id="settings-steam-overlay" type="checkbox" />
<label for="settings-steam-overlay">Enable Steam overlay (experimental, requires restart)</label>
</div>
</fieldset>
<fieldset id="settings-troubleshooting">
Expand All @@ -446,7 +448,10 @@ <h1 class="flex-left">Settings</h1>
Found a bug? When reporting please include the share URL (<i class="ph-bold ph-share-network"></i>) along
with your report. For some extra information on screen, you can enable debug mode below.
</p>
<input id="settings-debug" type="checkbox" /><label for="settings-debug">Enable debug mode</label>
<div class="settings">
<input id="settings-debug" type="checkbox" />
<label for="settings-debug">Enable debug mode</label>
</div>
</fieldset>
<fieldset id="settings-cache">
<legend>Cache</legend>
Expand Down
13 changes: 13 additions & 0 deletions src/keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,21 @@ function key () {
return Array.from(arguments).join(':')
}

// The values should correspond to the values defined in Steam Admin
// https://partner.steamgames.com/apps/achievements/4172230
export const Achievements = Object.freeze({
FirstSolve: 'ACH_FIRST',
Edit: 'ACH_EDIT',
Hex: 'ACH_HEX',
Infinity: 'ACH_INFINITY',
Puzzle001: 'ACH_PUZZLE_001'
})

export const AchievementNames = Object.values(Achievements)

export const Keys = Object.freeze({
debug: 'debug',
enableSteamOverlay: 'enable-steam-overlay',
window: key(settings, window),
windowHeight: key(settings, window, 'height'),
windowWidth: key(settings, window, 'width')
Expand Down
4 changes: 2 additions & 2 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -952,11 +952,11 @@ dialog ul {
margin-right: 1em;
}

#settings-window {
.electron-only {
display: none;
}

.electron #settings-window {
.electron .electron-only {
display: block;
}

Expand Down
Loading