Compare commits

..

8 Commits

Author SHA1 Message Date
4d688ed625 Add .github/workflows/build.yaml
All checks were successful
Build / Build NPM Project (push) Successful in 25s
Build / Tag Version (push) Successful in 3s
2023-12-06 21:47:00 +00:00
295de3ee01 Update README.md 2023-12-05 16:40:36 +00:00
0212ab13a2 Framed screenshot better 2023-11-23 16:17:47 +00:00
456267e200 Update README.md 2023-08-14 21:27:45 +00:00
ad9703eb8f Added screenshot 2023-08-14 17:26:54 -04:00
2c2d2588fc Update README.md 2023-08-14 20:34:46 +00:00
d310cbb1a2 Add LICENSE 2023-08-14 20:34:07 +00:00
2f9d51b68c Update README.md 2023-08-14 20:32:44 +00:00
25 changed files with 2885 additions and 10698 deletions

37
.github/workflows/build.yaml vendored Normal file
View File

@ -0,0 +1,37 @@
name: Build
run-name: Build
on:
push:
jobs:
build:
name: Build NPM Project
runs-on: ubuntu-latest
container: node
steps:
- name: Clone Repository
uses: ztimson/actions/clone@develop
- name: Install Dependencies
run: npm i
- name: Build Project
run: npm run build
tag:
name: Tag Version
needs: build
runs-on: ubuntu-latest
container: node
steps:
- name: Clone Repository
uses: ztimson/actions/clone@develop
- name: Get Version Number
run: echo "VERSION=$(cat package.json | grep version | grep -Eo ':.+' | grep -Eo '[[:alnum:]\.\/\-]+')" >> $GITHUB_ENV
- name: Tag Version
uses: ztimson/actions/tag@develop
with:
tag: ${{env.VERSION}}

11
LICENSE Normal file
View File

@ -0,0 +1,11 @@
Copyright (c) 2023 Zakary Timson
All Rights Reserved.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,5 +1,72 @@
# TSElectronTemplate
This is a minimalist setup to get TypeScript & Electron working. It includes nothing extra.
<!-- Header -->
<div id="top" align="center">
<br />
<!-- Logo -->
<img src="https://git.zakscode.com/repo-avatars/c3df2b0fe775af37194771569752d82e54c81ba8a5493540bf14ec6bf1eadd9d" alt="Logo" width="200" height="200">
<!-- Title -->
### Desktop Daemon
<!-- Description -->
Virtual Desktop Pets
<!-- Repo badges -->
[![Version](https://img.shields.io/badge/dynamic/json.svg?label=Version&style=for-the-badge&url=https://git.zakscode.com/api/v1/repos/ztimson/desktop-daemon/tags&query=$[0].name)](https://git.zakscode.com/ztimson/desktop-daemon/tags)
[![Pull Requests](https://img.shields.io/badge/dynamic/json.svg?label=Pull%20Requests&style=for-the-badge&url=https://git.zakscode.com/api/v1/repos/ztimson/desktop-daemon&query=open_pr_counter)](https://git.zakscode.com/ztimson/desktop-daemon/pulls)
[![Issues](https://img.shields.io/badge/dynamic/json.svg?label=Issues&style=for-the-badge&url=https://git.zakscode.com/api/v1/repos/ztimson/desktop-daemon&query=open_issues_count)](https://git.zakscode.com/ztimson/desktop-daemon/issues)
<!-- Links -->
---
<div>
<a href="https://git.zakscode.com/ztimson/desktop-daemon/releases" target="_blank">Release Notes</a>
<a href="https://git.zakscode.com/ztimson/desktop-daemon/issues/new?template=.github%2fissue_template%2fbug.md" target="_blank">Report a Bug</a>
<a href="https://git.zakscode.com/ztimson/desktop-daemon/issues/new?template=.github%2fissue_template%2fenhancement.md" target="_blank">Request a Feature</a>
</div>
---
</div>
## Table of Contents
[[_TOC_]]
- [Template](#top)
- [About](#about)
- [Built With](#built-with)
- [Setup](#setup)
- [Development](#development)
- [License](#license)
## About
<img alt="Screenshot" src="./screenshot.gif" width="60%" height="auto">
Desktop Daemon is an experimental game I used to learn sprites. You can create customziable pets which live on your desktop. They can largly be ignored as they play along your taskbar but need attention from time to time.
DD was writen completely froms scratch using TypeScript and Electron.
### Built With
[![Electron](https://img.shields.io/badge/Electron-47848F?style=for-the-badge&logo=electron&logoColor=white)](https://www.electronjs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white)](https://typescriptlang.org/)
## Setup
<details>
<summary>
<h3 id="development" style="display: inline">
Development
</h3>
</summary>
#### Prerequisites
- [Node.js](https://nodejs.org/en/download)
#### Instructions
1. Install the dependencies: `npm install`
2. Start the Angular server: `npm run start`
</details>
## License
Copyright © 2023 Zakary Timson | All Rights Reserved
See the [license](./LICENSE) for more information.

BIN
screenshot.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -19,7 +19,7 @@ export class NPC {
private sprite!: any;
private text: string;
public pos = [0, 0];
public pos = [1000, 0];
public vel = [0, 0];
constructor(private readonly ctx: CanvasRenderingContext2D,

12716
v2/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +0,0 @@
export const FRAME_RATE = 10;
export const GRAVITY = 9.8;

View File

@ -1,23 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Desktop Daemon</title>
<link rel="icon" type="img/png" src="assets/logo.png">
<title>Hello World!</title>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<h1>💖 Hello World!</h1>
<p>Welcome to your Electron application.</p>
</body>
</html>

View File

@ -1,45 +1,54 @@
import {app, BrowserWindow, screen} from 'electron';
import path from 'path';
import { app, BrowserWindow } from 'electron';
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack
// plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on
// whether you're running in development or production).
declare const MAIN_WINDOW_WEBPACK_ENTRY: string;
declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string;
if(require('electron-squirrel-startup')) app.quit();
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) {
// eslint-disable-line global-require
app.quit();
}
const createWindow = (): void => {
const primaryDisplay = screen.getPrimaryDisplay();
const height = 250;
// Create the browser window.
const mainWindow = new BrowserWindow({
x: 0,
y: primaryDisplay.workArea.height - height,
height: height,
width: primaryDisplay.size.width,
frame: false,
transparent: true,
roundedCorners: false,
resizable: false,
fullscreen: false,
alwaysOnTop: true,
icon: path.join(__dirname, "../assets/logo.png"),
title: 'Desktop Daemon',
height: 600,
width: 800,
webPreferences: {
// nodeIntegration: true,
// contextIsolation: false,
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
},
});
// and load the index.html of the app.
mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
// Open the DevTools.
mainWindow.webContents.openDevTools();
};
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.

View File

@ -1,84 +0,0 @@
import {GRAVITY} from './constants';
import {SpriteSheet} from './sprite-sheet';
export type NPCOptions = {
bubbleOffset?: [number, number];
noBounds?: boolean;
noGravity?: boolean;
scale?: number;
}
export class NPC {
private readonly emojis = [
0x1F600, 0x1F601, 0x1F603, 0x1F603, 0x1F604, 0x1F605, 0x1F606,
0x1F607, 0x1F609, 0x1F60A, 0x1F642, 0x1F643, 0x1F355, 0x1F354,
];
private readonly texturePack: any;
private random: number = 0;
private sprite!: any;
private text: string;
public pos = [0, 0];
public vel = [0, 0];
constructor(private readonly ctx: CanvasRenderingContext2D,
public readonly spriteSheetPath: string,
public readonly spriteDefPath: string,
public readonly options: NPCOptions = {},
) {
this.texturePack = new SpriteSheet(this.ctx,
'./assets/sprites/texture-pack/spritesheet.png',
'../assets/sprites/texture-pack/spritesheet.json');
if(this.options.bubbleOffset == null) this.options.bubbleOffset = [0, 0];
if(this.options.scale == null) this.options.scale = 1;
this.sprite = new SpriteSheet(ctx, spriteSheetPath, spriteDefPath);
setInterval(() => {
this.message(String.fromCodePoint(this.emojis[~~(Math.random() * this.emojis.length)]));
}, 10000);
}
animate(name: string, reverse = false) {
if(this.text) {
const bubble = this.texturePack.definitions.sprites['bubble'];
const x = this.pos[0] + this.options.bubbleOffset[0] * (reverse ? -1 : 1);
const y = this.pos[1] + this.options.bubbleOffset[1];
this.texturePack.drawSprite(x, y, 'bubble', reverse);
this.ctx.save();
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
this.ctx.font = '24px serif';
this.ctx.fillText(this.text, x + (bubble.width / 2 * (reverse ? -1 : 1)), y + (bubble.height / 2));
this.ctx.restore();
}
this.sprite.animate(this.pos[0], this.pos[1], name, reverse, this.options.scale);
}
tick() {
if(this.random <= 0) {
const move = Math.random();
if(move < 0.333) this.vel = [0, 0];
else if(move < 0.666) this.vel = [-10, 0];
else this.vel = [10, 0];
this.random = Math.random() * 50;
}
this.random--;
this.pos = [this.pos[0] + this.vel[0], this.pos[1] + this.vel[1]];
if(!this.options.noGravity) this.vel[1] -= GRAVITY;
if(this.pos[1] < 0) this.pos[1] = 0;
if(!this.options.noBounds) {
if(this.pos[0] < 0) this.pos[0] = 0;
if(this.pos[0] > this.ctx.canvas.width) this.pos[0] = this.ctx.canvas.width;
}
this.animate(this.vel[0] == 0 ? 'idle' : 'walk', this.vel[0] < 0);
}
message(text: string, ms = 5000) {
setTimeout(() => this.text = null, ms);
this.text = text;
}
}

View File

@ -0,0 +1,2 @@
// See the Electron documentation for details on how to use preload scripts:
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts

View File

@ -1,28 +1,31 @@
/**
* This file will automatically be loaded by webpack and run in the "renderer" context.
* To learn more about the differences between the "main" and the "renderer" context in
* Electron, visit:
*
* https://electronjs.org/docs/latest/tutorial/process-model
*
* By default, Node.js integration in this file is disabled. When enabling Node.js integration
* in a renderer process, please be aware of potential security implications. You can read
* more about security risks here:
*
* https://electronjs.org/docs/tutorial/security
*
* To enable Node.js integration in this file, open up `main.js` and enable the `nodeIntegration`
* flag:
*
* ```
* // Create the browser window.
* mainWindow = new BrowserWindow({
* width: 800,
* height: 600,
* webPreferences: {
* nodeIntegration: true
* }
* });
* ```
*/
import './index.css';
import {FRAME_RATE} from './constants';
import {NPC} from './npc';
const canvas = <HTMLCanvasElement>document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const screenHeight = window.innerHeight;
const screenWidth = window.innerWidth;
canvas.width = screenWidth;
canvas.height = screenHeight;
function clearScreen() { ctx.clearRect(0, 0, screenWidth, screenHeight); }
const dog = new NPC(ctx, '/static/sprites/shadow-dog/spritesheet.png', '/static/sprites/shadow-dog/spritesheet.json', {
bubbleOffset: [50, 75],
scale: 0.25
});
let frame = 0, once = true;
setInterval(() => {
requestAnimationFrame(() => {
frame++;
clearScreen();
dog.tick();
})
}, 1000 / FRAME_RATE);
console.log('👋 This message is being logged by "renderer.js", included via webpack');

View File

@ -1,87 +0,0 @@
export type SpriteDefinitions = {
width?: number;
height?: number;
animations?: {[key: string]: {y: number, frames: number}};
sprites?: {[key: string]: {x: number, y: number, width?: number, height?: number}};
}
export class SpriteSheet {
private readonly definitions!: SpriteDefinitions;
private readonly spriteSheet!: HTMLImageElement;
private animation = '';
private frame = -1;
constructor(private readonly ctx: CanvasRenderingContext2D,
public readonly spriteSheetPath: string,
public readonly spriteDefPath: string,
) {
this.spriteSheet = new Image();
this.spriteSheet.src = spriteSheetPath;
this.definitions = require(spriteDefPath);
// Backfill coordinates if shorthand is used
if(this.definitions.sprites?.length) {
Object.entries(this.definitions.sprites).forEach(sprite => {
const [name, def] = sprite;
if(def.width == null) {
if(this.definitions.width == null)
throw new Error(`Sprite is missing it's width: ${name}`);
this.definitions.sprites[name].x = this.definitions.width * def.x;
this.definitions.sprites[name].width = this.definitions.width;
}
if(def.height == null) {
if(this.definitions.height == null)
throw new Error(`Sprite is missing it's height: ${name}`);
this.definitions.sprites[name].y = this.definitions.height * def.y;
this.definitions.sprites[name].height = this.definitions.height;
}
});
}
}
animate(x: number, y: number, name: string = this.animation, flip = false, scale = 1) {
const animation = this.definitions.animations[name];
if(!animation) throw new Error(`Animation doesn't exist: ${name}`);
if(name == this.animation) {
this.frame++;
if(this.frame >= animation.frames) this.frame = 0;
} else {
this.animation = name;
this.frame = 0;
}
this.drawFrame(x, y, this.animation, this.frame, flip, scale);
}
drawFrame(x: number, y: number, name: string, frame: number, flip = false, scale = 1, centerX = true, centerY = false) {
const sprite = this.definitions.animations[name];
if(!sprite) throw new Error(`Animation doesn't exist: ${name}`);
this.ctx.save();
this.ctx.translate(centerX ?
this.definitions.width * scale / 2 * (flip ? 1 : -1) :
flip ? this.definitions.width : 0,
centerY ? this.definitions.height / -2 : 0);
this.ctx.scale(flip ? -1 : 1, 1);
this.ctx.drawImage(this.spriteSheet,
frame * this.definitions.width, sprite.y * this.definitions.height, this.definitions.width, this.definitions.height,
x * (flip ? -1 : 1), this.ctx.canvas.height - (this.definitions.height * scale + y),
this.definitions.width * scale, this.definitions.height * scale);
this.ctx.restore();
}
drawSprite(x: number, y: number, name: string, flip = false, scale = 1, centerX = true, centerY = false) {
const sprite = this.definitions.sprites[name];
if(!sprite) throw new Error(`Sprite doesn't exist: ${name}`);
this.ctx.save();
this.ctx.translate(centerX ?
this.definitions.width * scale / 2 * (flip ? 1 : -1) :
flip ? this.definitions.width : 0,
centerY ? this.definitions.height / -2 : 0);
this.ctx.scale(flip ? -1 : 1, 1);
this.ctx.drawImage(this.spriteSheet,
sprite.x, sprite.y, sprite.width, sprite.height,
x * (flip ? -1 : 1), this.ctx.canvas.height - (sprite.height * scale + y),
sprite.width * scale, sprite.height * scale);
this.ctx.restore();
}
}

View File

@ -1,3 +0,0 @@
export function sleep(ms: number) {
return new Promise(res => setTimeout(res, ms));
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1018 B

View File

@ -1,14 +0,0 @@
{
"width": 575,
"height": 523,
"animations": {
"idle": {
"y": 0,
"frames": 6
},
"walk": {
"y": 3,
"frames": 8
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -1,11 +0,0 @@
{
"width": 50,
"height": 50,
"animations": [
{
"name": "idle",
"y": 0,
"frames": 8
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 B

View File

@ -1,58 +0,0 @@
{
"sprites": {
"generic": {
"x": 0,
"y": 0,
"width": 100,
"height": 100
},
"text": {
"x": 100,
"y": 0,
"width": 100,
"height": 100
},
"code": {
"x": 200,
"y": 0,
"width": 100,
"height": 100
},
"zip": {
"x": 300,
"y": 0,
"width": 100,
"height": 100
},
"image": {
"x": 400,
"y": 0,
"width": 100,
"height": 100
},
"audio": {
"x": 500,
"y": 0,
"width": 100,
"height": 100
},
"video": {
"x": 600,
"y": 0,
"width": 100,
"height": 100
},
"pdf": {
"x": 700,
"y": 0,
"width": 100,
"height": 100
},
"bubble": {
"x": 0,
"y": 100,
"width": 100,
"height": 100
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB