diff --git a/packages/client/components/game.astro b/packages/client/components/game.astro index de272407a5..4abaac99f3 100644 --- a/packages/client/components/game.astro +++ b/packages/client/components/game.astro @@ -3,6 +3,7 @@
+ diff --git a/packages/client/data/sprites.json b/packages/client/data/sprites.json index 35dc0d0f08..7795c79106 100755 --- a/packages/client/data/sprites.json +++ b/packages/client/data/sprites.json @@ -502,6 +502,7 @@ }, "offsetY": -24 }, + { "id": "mobs/angel", "width": 32, "height": 32, "offsetX": -8, "offsetY": -13 }, { "id": "mobs/card", "width": 32, "height": 32, "offsetX": -8, "offsetY": -13 }, { "id": "mobs/card2", "width": 32, "height": 32, "offsetX": -8, "offsetY": -13 }, @@ -1164,6 +1165,27 @@ "offsetX": 0, "offsetY": 0 }, + { + "id": "objects/sprucetree", + "width": 64, + "height": 80, + "animations": { + "idle": { + "length": 1, + "row": 0 + }, + "shake": { + "length": 5, + "row": 1 + }, + "stump": { + "length": 1, + "row": 2 + } + }, + "offsetX": -24, + "offsetY": -56 + }, { "id": "mobs/clam", "width": 32, diff --git a/packages/client/public/img/sprites/objects/sprucetree.png b/packages/client/public/img/sprites/objects/sprucetree.png new file mode 100644 index 0000000000..62422357d7 Binary files /dev/null and b/packages/client/public/img/sprites/objects/sprucetree.png differ diff --git a/packages/client/public/img/tilesets/tilesheet-2.png b/packages/client/public/img/tilesets/tilesheet-2.png index 291c184d00..e876e86931 100644 Binary files a/packages/client/public/img/tilesets/tilesheet-2.png and b/packages/client/public/img/tilesets/tilesheet-2.png differ diff --git a/packages/client/scss/game/impl/_container.scss b/packages/client/scss/game/impl/_container.scss index 350696854c..1a376908ec 100644 --- a/packages/client/scss/game/impl/_container.scss +++ b/packages/client/scss/game/impl/_container.scss @@ -15,6 +15,7 @@ #background, #entities, +#entities-fore, #foreground, #cursor, #text-canvas, diff --git a/packages/client/src/controllers/entities.ts b/packages/client/src/controllers/entities.ts index ff25c98d9f..234c367ea9 100644 --- a/packages/client/src/controllers/entities.ts +++ b/packages/client/src/controllers/entities.ts @@ -7,6 +7,7 @@ import Pet from '../entity/character/pet/pet'; import Player from '../entity/character/player/player'; import Projectile from '../entity/objects/projectile'; import Effect from '../entity/objects/effect'; +import Tree from '../entity/objects/tree'; import { Modules } from '@kaetram/common/network'; @@ -127,13 +128,17 @@ export default class EntitiesController { prefix = 'effectentity'; break; } + + case Modules.EntityType.Tree: { + entity = this.createTree(info as EntityData); + + prefix = 'objects'; + break; + } } // Something went wrong creating the entity. - if (!entity) { - console.log(info); - return log.error(`Failed to create entity ${info.instance}`); - } + if (!entity) return log.error(`Failed to create entity ${info.instance}`); let sprite = entity.sprite || this.game.sprites.get(`${prefix}/${info.key}`); @@ -336,6 +341,16 @@ export default class EntitiesController { return new Effect(info.instance); } + /** + * Creates a new tree object based on the info provided. + * @param info Contains the key and instance of the tree. + * @returns A new tree object. + */ + + private createTree(info: EntityData): Entity { + return new Tree(info.instance); + } + /** * Checks if the instance provided is the same as the main player. * @param instance The instance we are checking. @@ -446,6 +461,9 @@ export default class EntitiesController { */ public registerPosition(entity: Entity): void { + // Tree entities are registered as colliding on the rendering grid. + if (entity.isTree()) this.game.map.grid[entity.gridY][entity.gridX] = 2; + this.grids.addToRenderingGrid(entity); } diff --git a/packages/client/src/controllers/input.ts b/packages/client/src/controllers/input.ts index 1ceb8c6a07..f87f04ba3b 100644 --- a/packages/client/src/controllers/input.ts +++ b/packages/client/src/controllers/input.ts @@ -560,6 +560,12 @@ export default class InputController { break; } + case Modules.EntityType.Tree: { + this.setCursor(this.cursors.axe); + this.hovering = Modules.Hovering.Tree; + break; + } + case Modules.EntityType.Player: { if (this.game.pvp) { this.setCursor(this.getAttackCursor()); @@ -750,7 +756,7 @@ export default class InputController { */ private isTargetable(entity: Entity): boolean { - return this.isAttackable(entity) || entity.isNPC() || entity.isChest(); + return this.isAttackable(entity) || entity.isNPC() || entity.isChest() || entity.isTree(); } /** diff --git a/packages/client/src/entity/character/player/handler.ts b/packages/client/src/entity/character/player/handler.ts index 664293a7ce..026d4df01e 100644 --- a/packages/client/src/entity/character/player/handler.ts +++ b/packages/client/src/entity/character/player/handler.ts @@ -44,10 +44,11 @@ export default class Handler extends CharacterHandler { // Prevent calculating pathing when we target a mob that is within range. if (this.character.canAttackTarget() && !this.character.trading) return []; - let isObject = this.map.isObject(x, y); + let isObject = this.map.isObject(x, y), + isTree = this.character.target?.isTree(); // Ignore requests into colliding tiles but allow targetable objects. - if (this.map.isColliding(x, y) && !isObject) return []; + if (this.map.isColliding(x, y) && !isObject && !isTree) return []; // Sends the packet to the server with the request. this.game.socket.send(Packets.Movement, { @@ -74,6 +75,8 @@ export default class Handler extends CharacterHandler { ignores.push({ x: x + 1, y }, { x: x - 1, y }, { x, y: y + 1 }, { x, y: y - 1 }); } + if (isTree) ignores.push({ x, y }); + return this.game.findPath(this.character, x, y, ignores, cursor); } diff --git a/packages/client/src/entity/entity.ts b/packages/client/src/entity/entity.ts index 48b53c1583..940b00173f 100644 --- a/packages/client/src/entity/entity.ts +++ b/packages/client/src/entity/entity.ts @@ -225,7 +225,8 @@ export default abstract class Entity { name: string, speed = this.sprite.idleSpeed, count = 1, - onEndCount?: () => void + onEndCount?: () => void, + withStop = false ): void { // Prevent setting animation if no sprite or it's the same animation. if (this.animation?.name === name) return; @@ -234,7 +235,7 @@ export default abstract class Entity { let { length, row, width, height } = this.sprite.animations[name]; // Create a new animation instance to prevent pointer issues. - this.animation = new Animation(name, length, row, width, height); + this.animation = new Animation(name, length, row, width, height, withStop); // Restart the attack animation if it's already playing. if (name.startsWith('atk')) this.animation.reset(); @@ -432,6 +433,14 @@ export default abstract class Entity { return this.type === Modules.EntityType.Object; } + /** + * @returns Whether or not the entity is a tree type. + */ + + public isTree(): boolean { + return this.type === Modules.EntityType.Tree; + } + /** * Default implementation for `isModerator()` * @returns False by default. diff --git a/packages/client/src/entity/objects/effect.ts b/packages/client/src/entity/objects/effect.ts index 9558ab6169..95d18447e8 100644 --- a/packages/client/src/entity/objects/effect.ts +++ b/packages/client/src/entity/objects/effect.ts @@ -6,4 +6,16 @@ export default class Effect extends Entity { public constructor(instance: string) { super(instance, Modules.EntityType.Effect); } + + public override idle(): void { + this.setAnimation( + 'idle', + 150, + 1, + () => { + // + }, + true + ); + } } diff --git a/packages/client/src/entity/objects/tree.ts b/packages/client/src/entity/objects/tree.ts new file mode 100644 index 0000000000..3ff10f2129 --- /dev/null +++ b/packages/client/src/entity/objects/tree.ts @@ -0,0 +1,17 @@ +import Entity from '../entity'; + +import { Modules } from '@kaetram/common/network'; + +export default class Tree extends Entity { + public cut = false; + + public constructor(instance: string) { + super(instance, Modules.EntityType.Tree); + } + + public override idle(): void { + this.setAnimation(this.cut ? 'stump' : 'idle', 150, 1, () => { + // + }); + } +} diff --git a/packages/client/src/entity/sprite.ts b/packages/client/src/entity/sprite.ts index fac2b300c6..c934041b57 100644 --- a/packages/client/src/entity/sprite.ts +++ b/packages/client/src/entity/sprite.ts @@ -103,7 +103,7 @@ export default class Sprite { public loadHurtSprite(): void { // Hurt sprite already exists or there's not hurt effect, no need to load. - if (this.hurtSprite || !this.hasHurtSprite()) return; + if (this.hurtSprite || !this.hasHurtSprite() || !this.loaded) return; this.hurtSprite = Utils.getHurtSprite(this); } @@ -115,7 +115,7 @@ export default class Sprite { public loadSilhouetteSprite(): void { // Silhouette sprite already exists or there's no silhouette, no need to load. - if (this.silhouetteSprite || !this.hasSilhouette() || isMobile()) return; + if (this.silhouetteSprite || !this.hasSilhouette() || isMobile() || !this.loaded) return; this.silhouetteSprite = Utils.getSilhouetteSprite(this); } @@ -197,7 +197,7 @@ export default class Sprite { public hasSilhouette(): boolean { let type = this.getType(); - return type === 'mobs' || type === 'player' || type === 'npcs'; + return type === 'mobs' || type === 'player' || type === 'npcs' || type === 'objects'; } /** diff --git a/packages/client/src/menu/guilds.ts b/packages/client/src/menu/guilds.ts index 42869c2e93..9f40ad404f 100644 --- a/packages/client/src/menu/guilds.ts +++ b/packages/client/src/menu/guilds.ts @@ -20,7 +20,6 @@ export default class Guilds extends Menu { // Container for creating a new guild. private create: HTMLElement = document.querySelector('#guilds-create')!; - private createError: HTMLElement = document.querySelector('#guilds-create-error')!; private backButton: HTMLElement = document.querySelector('#guilds-back-button')!; @@ -904,6 +903,6 @@ export default class Guilds extends Menu { */ private setError(text = ''): void { - this.createError.innerHTML = text; + this.createError.innerHTML = Util.parseMessage(Util.formatNotification(text)); } } diff --git a/packages/client/src/renderer/renderer.ts b/packages/client/src/renderer/renderer.ts index 0b47924b37..1fbcb812b9 100644 --- a/packages/client/src/renderer/renderer.ts +++ b/packages/client/src/renderer/renderer.ts @@ -56,6 +56,7 @@ export default class Renderer { protected overlay = document.querySelector('#overlay')!; protected textCanvas = document.querySelector('#text-canvas')!; protected entities = document.querySelector('#entities')!; + protected entitiesFore = document.querySelector('#entities-fore')!; protected cursor = document.querySelector('#cursor')!; protected entitiesMask = document.querySelector('#entities-mask')!; @@ -66,12 +67,14 @@ export default class Renderer { this.overlay, this.textCanvas, this.entities, + this.entitiesFore, this.cursor, this.entitiesMask ]; // Create the contexts based on the canvases. protected entitiesContext: CanvasRenderingContext2D = this.entities.getContext('2d')!; + protected entitiesForeContext: CanvasRenderingContext2D = this.entitiesFore.getContext('2d')!; protected overlayContext: CanvasRenderingContext2D = this.overlay.getContext('2d')!; protected textContext: CanvasRenderingContext2D = this.textCanvas.getContext('2d')!; protected cursorContext: CanvasRenderingContext2D = this.cursor.getContext('2d')!; @@ -79,18 +82,20 @@ export default class Renderer { protected allContexts = [ this.entitiesContext, + this.entitiesForeContext, + this.entitiesMaskContext, this.overlayContext, this.textContext, - this.cursorContext, - this.entitiesMaskContext + this.cursorContext ]; // We split contexts into two arrays, one for tilemap rendering and one for the rest. protected contexts = [ this.entitiesContext, + this.entitiesForeContext, + this.entitiesMaskContext, this.textContext, - this.overlayContext, - this.entitiesMaskContext + this.overlayContext ]; // Zooming buttons @@ -385,6 +390,7 @@ export default class Renderer { if (this.game.player.dead) return; this.setCameraView(this.entitiesContext); + this.setCameraView(this.entitiesForeContext); this.forEachVisibleEntity((entity: Entity) => { // Skip entities that aren't properly loaded or are invisible. @@ -393,7 +399,7 @@ export default class Renderer { this.drawEntity(entity); }); - this.entitiesMaskContext.globalAlpha = 0.3; + this.entitiesMaskContext.globalAlpha = 0.2; this.entitiesMaskContext.drawImage(this.entities, 0, 0); } @@ -570,36 +576,37 @@ export default class Renderer { dx = ~~(entity.x * this.camera.zoomFactor), dy = ~~(entity.y * this.camera.zoomFactor), flipX = dx + this.actualTileSize, - flipY = dy + entity.sprite.height; + flipY = dy + entity.sprite.height, + context = entity.isTree() ? this.entitiesForeContext : this.entitiesContext; - this.entitiesContext.save(); + context.save(); // Update the entity fading onto the context. - if (entity.fading) this.entitiesContext.globalAlpha = entity.fadingAlpha; + if (entity.fading) context.globalAlpha = entity.fadingAlpha; // Handle flipping since we use the same sprite for right/left. if (entity.spriteFlipX) { - this.entitiesContext.translate(flipX, dy); - this.entitiesContext.scale(-1, 1); + context.translate(flipX, dy); + context.scale(-1, 1); } else if (entity.spriteFlipY) { - this.entitiesContext.translate(dx, flipY); - this.entitiesContext.scale(1, -1); - } else this.entitiesContext.translate(dx, dy); + context.translate(dx, flipY); + context.scale(1, -1); + } else context.translate(dx, dy); // Scale the entity to the current zoom factor. - this.entitiesContext.scale(this.camera.zoomFactor, this.camera.zoomFactor); + context.scale(this.camera.zoomFactor, this.camera.zoomFactor); // Scale the entity again if it has a custom scaling associated with it. - if (entity.customScale) this.entitiesContext.scale(entity.customScale, entity.customScale); + if (entity.customScale) context.scale(entity.customScale, entity.customScale); // Rotate using the entity's angle. - if (entity.angle !== 0) this.entitiesContext.rotate(entity.angle); + if (entity.angle !== 0) context.rotate(entity.angle); // Draw the entity shadowf if (entity.hasShadow()) { let shadowSprite = this.game.sprites.get('shadow')!; - this.entitiesContext.drawImage( + context.drawImage( shadowSprite.image, 0, 0, @@ -612,7 +619,7 @@ export default class Renderer { ); } - this.entitiesContext.drawImage( + context.drawImage( entity.getSprite().image, frame!.x, frame!.y, @@ -626,10 +633,12 @@ export default class Renderer { this.drawEntityFore(entity); - this.entitiesContext.restore(); + context.restore(); this.drawHealth(entity as Character); + if (!entity.isPlayer() && !entity.isMob() && !entity.isNPC() && !entity.isItem()) return; + if (!this.game.overlays.hasOverlay()) if (this.game.player.instance === entity.instance && this.camera.isCentered()) this.drawPlayerName(entity as Player); @@ -924,8 +933,6 @@ export default class Renderer { */ private drawName(entity: Character | Item): void { - if (entity.isPet() || entity.isProjectile()) return; - let x = entity.x + 8, // Default offsets y = entity.y - 5, colour = 'white', diff --git a/packages/client/src/utils/util.ts b/packages/client/src/utils/util.ts index 11631f4234..b72ccca266 100644 --- a/packages/client/src/utils/util.ts +++ b/packages/client/src/utils/util.ts @@ -389,7 +389,7 @@ export default { for (let i = 0; i < cloneData.data.length; i += 4) { // Non-empty pixels are skipped. - if (cloneData.data[i + 3] !== 0) continue; + if (cloneData.data[i + 3] > 24) continue; // Extract the x and y coordinates of the pixel. let x = (i / 4) % sprite.image.width, diff --git a/packages/common/network/modules.ts b/packages/common/network/modules.ts index aba97fb0c2..f0029aa773 100644 --- a/packages/common/network/modules.ts +++ b/packages/common/network/modules.ts @@ -39,7 +39,8 @@ export enum EntityType { Object, Pet, LootBag, - Effect + Effect, + Tree } export enum Interfaces { @@ -181,7 +182,8 @@ export enum Hovering { Item, NPC, Chest, - Object + Object, + Tree } export enum AudioTypes { diff --git a/packages/common/util/utils.ts b/packages/common/util/utils.ts index e480ad23c9..c8e977e8b2 100644 --- a/packages/common/util/utils.ts +++ b/packages/common/util/utils.ts @@ -27,7 +27,7 @@ export default { */ createInstance(identifier = 0): string { - return `${identifier}${this.randomInt(1000, 100_000)}${++this.counter}`; + return `${identifier}-${this.randomInt(1000, 100_000)}${++this.counter}`; }, /** @@ -137,7 +137,7 @@ export default { */ getEntityType(instance: string): number { - return parseInt(instance.slice(0, 1)); + return parseInt(instance.split('-')[0]); }, /** diff --git a/packages/server/data/trees.json b/packages/server/data/trees.json index 0d445d5df6..5b87d6ec8e 100644 --- a/packages/server/data/trees.json +++ b/packages/server/data/trees.json @@ -188,5 +188,12 @@ "experience": 350, "difficulty": 240, "item": "bloodwoodlogs" + }, + + "sprucetree": { + "levelRequirement": 45, + "experience": 400, + "difficulty": 250, + "item": "logs" } } diff --git a/packages/server/src/controllers/entities.ts b/packages/server/src/controllers/entities.ts index c8430a82dc..cd8da4ba46 100644 --- a/packages/server/src/controllers/entities.ts +++ b/packages/server/src/controllers/entities.ts @@ -1,6 +1,7 @@ import mobData from '../../data/mobs.json'; import itemData from '../../data/items.json'; import npcData from '../../data/npcs.json'; +import treeData from '../../data/trees.json'; import NPC from '../game/entity/npc/npc'; import Item from '../game/entity/objects/item'; import Chest from '../game/entity/objects/chest'; @@ -10,6 +11,7 @@ import Projectile from '../game/entity/objects/projectile'; import LootBag from '../game/entity/objects/lootbag'; import Pet from '../game/entity/character/pet/pet'; import Effect from '../game/entity/objects/effect'; +import Tree from '../game/entity/objects/tree'; import log from '@kaetram/common/util/log'; import { Modules } from '@kaetram/common/network'; @@ -40,6 +42,7 @@ export default class Entities { private pets: { [instance: string]: Pet } = {}; private lootBags: { [instance: string]: LootBag } = {}; private effects: { [instance: string]: Effect } = {}; + private trees: { [instance: string]: Tree } = {}; public constructor(private world: World) { this.map = world.map; @@ -71,10 +74,17 @@ export default class Entities { case Modules.EntityType.Mob: { return this.spawnMob(key, position.x, position.y); } + + case Modules.EntityType.Tree: { + return this.spawnTree(key, position.x, position.y); + } } }); - log.info(`Spawned ${Object.keys(this.entities).length} entities!`); + log.info(`Spawned ${Object.keys(this.items).length} items!`); + log.info(`Spawned ${Object.keys(this.npcs).length} NPCs!`); + log.info(`Spawned ${Object.keys(this.mobs).length} mobs!`); + log.info(`Spawned ${Object.keys(this.trees).length} trees!`); // Spawns the static chests throughout the world. @@ -154,6 +164,24 @@ export default class Entities { return mob; } + /** + * Spawns a tree in the world and adds it to the world. + * @param key The key of the tree, used to determine its sprite. + * @param x The x grid coordinate of the tree spawn. + * @param y The y grid coordinate of the tree spawn. + * @returns A new tree object. + */ + + public spawnTree(key: string, x: number, y: number): Tree { + let tree = new Tree(key, x, y); + + this.add(tree); + + this.trees[tree.instance] = tree; + + return tree; + } + /** * Spawns a chest in the world at the given position. * @param items The items that the chest might drop. @@ -587,6 +615,7 @@ export default class Entities { if (key in itemData) return Modules.EntityType.Item; if (key in npcData) return Modules.EntityType.NPC; if (key in mobData) return Modules.EntityType.Mob; + if (key in treeData) return Modules.EntityType.Tree; return -1; } diff --git a/packages/server/src/game/entity/objects/tree.ts b/packages/server/src/game/entity/objects/tree.ts new file mode 100644 index 0000000000..7b7c4cd288 --- /dev/null +++ b/packages/server/src/game/entity/objects/tree.ts @@ -0,0 +1,10 @@ +import Entity from '../entity'; + +import Utils from '@kaetram/common/util/utils'; +import { Modules } from '@kaetram/common/network'; + +export default class Tree extends Entity { + public constructor(key: string, x: number, y: number) { + super(Utils.createInstance(Modules.EntityType.Tree), key, x, y); + } +}