This commit is contained in:
Alex 2024-06-08 13:42:50 +08:00
parent 2de49bd925
commit 320a2826bf
13 changed files with 188 additions and 473 deletions

View file

@ -7,7 +7,6 @@ import {
} from "./settings";
import { SoloToolkitView, VIEW_TYPE } from "./view";
import { soloToolkitExtension } from "./extension";
import { MapgenModal } from "./modal";
export interface PluginApp extends App {
isMobile?: boolean;
@ -39,14 +38,6 @@ export default class SoloToolkitPlugin extends Plugin {
callback: () => this.openView(),
});
this.addCommand({
id: "generate-tomb",
name: "Generate a tomb",
callback: () => {
new MapgenModal(this.app).open();
},
});
this.addSettingTab(new SoloToolkitSettingTab(this.app, this));
}

View file

@ -1,36 +0,0 @@
import { App, ButtonComponent, Modal } from "obsidian";
import { drawMap } from "../utils/mapgen/draw";
import { TombGenerator } from "../utils/mapgen/tomb";
export class MapgenModal extends Modal {
map = new TombGenerator();
constructor(app: App) {
super(app);
}
onOpen() {
const { contentEl } = this;
const canvas = contentEl.createEl("canvas");
canvas.width = 1152;
canvas.height = 1152;
canvas.style.width = "528px";
canvas.style.height = "528px";
canvas.style.marginLeft = "auto";
new ButtonComponent(contentEl).setButtonText("Generate").onClick(() => {
this.map = new TombGenerator();
const ctx = canvas.getContext("2d");
if (ctx) drawMap(ctx, this.map);
});
const ctx = canvas.getContext("2d");
if (ctx) drawMap(ctx, this.map);
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}

View file

@ -4,4 +4,5 @@
@import "./styles/dice";
@import "./styles/word";
@import "./styles/oracle";
@import "./styles/map";
@import "./styles/plugin";

View file

@ -10,7 +10,7 @@
.deck-size {
min-width: 50px;
margin-left: var(--size-2-2);
padding-left: var(--size-2-2);
line-height: var(--input-height);
text-align: right;
}

31
src/styles/map.css Normal file
View file

@ -0,0 +1,31 @@
.srt-container {
.map-buttons {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: var(--size-2-2);
white-space: nowrap;
}
.map-results {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
gap: var(--size-4-4);
width: 100%;
overflow: hidden;
.map-result {
}
}
&.srt-desktop-layout {
.map-results {
flex-direction: column-reverse;
margin-bottom: var(--size-4-6);
}
}
}

View file

@ -3,4 +3,5 @@ export * from "./deck";
export * from "./tarot";
export * from "./word";
export * from "./oracle";
export * from "./map";
export * from "./helpers";

15
src/utils/map.ts Normal file
View file

@ -0,0 +1,15 @@
import { RoomGenerator } from "./mapgen/room";
import { MapBlueprint } from "./mapgen/shared";
// import { generateCave } from "./mapgen/cave";
export const generateMap = (type: string): MapBlueprint => {
switch (type) {
case "Room":
return new RoomGenerator().map;
case "Cave":
// return generateCave();
throw `Unknown map type: ${type}`;
default:
throw `Unknown map type: ${type}`;
}
};

View file

@ -1,13 +1,24 @@
import { Map, MAP_WIDTH, MAP_HEIGHT } from "./shared";
import { MapBlueprint } from "./shared";
import { random } from "../dice";
const TILE_WIDTH = 48;
const TILE_HEIGHT = 48;
let mapWidth = 0;
let mapHeight = 0;
let tileWidth = 0;
let tileHeight = 0;
const LINE_WALL = 3;
const LINE_FLOOR = 0.25;
const LINE_JAG = 2;
const drawTile = (opts: {
const at = (map: MapBlueprint, x: number, y: number): number => {
if (x < 0 || x >= mapWidth || y < 0 || y >= mapHeight) {
return 0;
} else {
return map[x][y];
}
};
const drawFloor = (opts: {
ctx: CanvasRenderingContext2D;
x: number;
y: number;
@ -15,7 +26,7 @@ const drawTile = (opts: {
const { ctx, x, y } = opts;
ctx.fillStyle = "#ffffffee";
ctx.fillRect(x * TILE_WIDTH, y * TILE_HEIGHT, TILE_WIDTH, TILE_HEIGHT);
ctx.fillRect(x * tileWidth, y * tileHeight, tileWidth, tileHeight);
};
const drawHorizontalLine = (opts: {
@ -29,15 +40,15 @@ const drawHorizontalLine = (opts: {
const ctx = opts.ctx;
const x1 = (opts.x1 < opts.x2 ? opts.x1 : opts.x2) * TILE_WIDTH;
const x2 = (opts.x1 < opts.x2 ? opts.x2 : opts.x1) * TILE_WIDTH;
const y = opts.y * TILE_HEIGHT;
const x1 = (opts.x1 < opts.x2 ? opts.x1 : opts.x2) * tileWidth;
const x2 = (opts.x1 < opts.x2 ? opts.x2 : opts.x1) * tileWidth;
const y = opts.y * tileHeight;
ctx.lineWidth = opts.width;
ctx.beginPath();
ctx.moveTo(x1, y);
const step = TILE_WIDTH;
const step = tileWidth;
const hstep = step / 2;
for (let i = x1; i < x2; i += step) {
@ -63,15 +74,15 @@ const drawVerticalLine = (opts: {
const ctx = opts.ctx;
const x = opts.x * TILE_WIDTH;
const y1 = (opts.y1 < opts.y2 ? opts.y1 : opts.y2) * TILE_HEIGHT;
const y2 = (opts.y1 < opts.y2 ? opts.y2 : opts.y1) * TILE_HEIGHT;
const x = opts.x * tileWidth;
const y1 = (opts.y1 < opts.y2 ? opts.y1 : opts.y2) * tileHeight;
const y2 = (opts.y1 < opts.y2 ? opts.y2 : opts.y1) * tileHeight;
ctx.lineWidth = opts.width;
ctx.beginPath();
ctx.moveTo(x, y1);
const step = TILE_HEIGHT;
const step = tileHeight;
const hstep = step / 2;
for (let i = y1; i < y2; i += step) {
@ -86,13 +97,18 @@ const drawVerticalLine = (opts: {
ctx.stroke();
};
export const drawMap = (ctx: CanvasRenderingContext2D, map: Map) => {
export const drawMap = (ctx: CanvasRenderingContext2D, map: MapBlueprint) => {
mapWidth = map.length;
mapHeight = map[0].length;
tileWidth = Math.floor(ctx.canvas.width / mapWidth);
tileHeight = Math.floor(ctx.canvas.height / mapHeight);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
for (let x = 0; x < MAP_WIDTH; x++) {
for (let y = 0; y < MAP_HEIGHT; y++) {
if (map.at(x, y) === 1) {
drawTile({
for (let x = 0; x < mapWidth; x++) {
for (let y = 0; y < mapHeight; y++) {
if (at(map, x, y) === 1) {
drawFloor({
ctx,
x,
y,
@ -101,11 +117,11 @@ export const drawMap = (ctx: CanvasRenderingContext2D, map: Map) => {
}
}
for (let x = 0; x < MAP_WIDTH; x++) {
for (let y = 0; y < MAP_HEIGHT; y++) {
if (map.at(x, y) === 1) {
for (let x = 0; x < mapWidth; x++) {
for (let y = 0; y < mapHeight; y++) {
if (at(map, x, y) === 1) {
// Left wall
if (map.at(x - 1, y) === 0) {
if (at(map, x - 1, y) === 0) {
drawVerticalLine({
ctx,
x: x,
@ -114,8 +130,8 @@ export const drawMap = (ctx: CanvasRenderingContext2D, map: Map) => {
width: LINE_WALL,
});
}
// Right wall & floor
if (map.at(x + 1, y) === 0) {
// Right wall & floor line
if (at(map, x + 1, y) === 0) {
drawVerticalLine({
ctx,
x: x + 1,
@ -133,7 +149,7 @@ export const drawMap = (ctx: CanvasRenderingContext2D, map: Map) => {
});
}
// Up wall
if (map.at(x, y - 1) === 0) {
if (at(map, x, y - 1) === 0) {
drawHorizontalLine({
ctx,
x1: x,
@ -142,8 +158,8 @@ export const drawMap = (ctx: CanvasRenderingContext2D, map: Map) => {
width: LINE_WALL,
});
}
// Down wall & floor
if (map.at(x, y + 1) === 0) {
// Down wall & floor line
if (at(map, x, y + 1) === 0) {
drawHorizontalLine({
ctx,
x1: x,

65
src/utils/mapgen/room.ts Normal file
View file

@ -0,0 +1,65 @@
import { random } from "../dice";
import { shuffle } from "../helpers";
import { Cell, MapBlueprint, ROOM_HEIGHT, ROOM_WIDTH } from "./shared";
export class RoomGenerator {
map: MapBlueprint = [];
roomWidth = random(4, 8);
roomHeight = random(4, 8);
constructor() {
this.prepareMap();
this.createRoom();
}
private prepareMap() {
this.map = [];
for (let i = 0; i < ROOM_WIDTH; i++) {
this.map.push([]);
for (let j = 0; j < ROOM_HEIGHT; j++) {
this.map[i].push(0);
}
}
}
private createRoom() {
const x = Math.floor(ROOM_WIDTH / 2 - this.roomWidth / 2);
const y = Math.floor(ROOM_HEIGHT / 2 - this.roomHeight / 2);
const doorPlaces: Cell[] = [];
for (let i = x; i < x + this.roomWidth; i++) {
for (let j = y; j < y + this.roomHeight; j++) {
this.map[i][j] = 1;
// North
if (j === y && i > x && i < x + this.roomWidth - 1) {
doorPlaces.push({
x: i,
y: j - 1,
});
}
// South
if (
j === y + this.roomHeight - 1 &&
i > x &&
i < x + this.roomWidth - 1
) {
doorPlaces.push({
x: i,
y: j + 1,
});
}
}
}
shuffle(doorPlaces);
const doors = random(1, 5);
for (let i = 0; i < doors; i++) {
const cell = doorPlaces.pop();
if (cell) {
this.map[cell.x][cell.y] = 1;
}
}
}
}

View file

@ -1,5 +1,5 @@
export const MAP_WIDTH = 24;
export const MAP_HEIGHT = 24;
export const ROOM_WIDTH = 12;
export const ROOM_HEIGHT = 12;
export type Cell = {
x: number;
@ -18,6 +18,8 @@ export type Connection = {
idb: number;
};
export type MapBlueprint = number[][];
export abstract class Map {
abstract at(x: number, y: number): number;
}

View file

@ -1,223 +0,0 @@
import { random } from "../dice";
import { Map, Room, Cell, MAP_HEIGHT, MAP_WIDTH } from "./shared";
const SPLITS = 4;
export class TombGenerator implements Map {
roomCount: number = random(4, 8);
map: number[][] = [];
leafs: Room[] = [];
rooms: Room[] = [];
constructor() {
this.prepareMap();
this.createLeafs();
this.createRooms();
// this.connectRooms();
}
at(x: number, y: number): number {
if (x < 0 || x >= MAP_WIDTH || y < 0 || y >= MAP_HEIGHT) {
return 0;
} else {
return this.map[x][y];
}
}
private prepareMap() {
this.map = [];
for (let i = 0; i < MAP_WIDTH; i++) {
this.map.push([]);
for (let j = 0; j < MAP_HEIGHT; j++) {
this.map[i].push(0);
}
}
}
private createLeafs() {
this.leafs.push({ x: 0, y: 0, w: MAP_WIDTH, h: MAP_HEIGHT });
for (let i = 0; i < SPLITS; i++) {
const newLeafs: Room[] = [];
for (const index in this.leafs) {
const leaf = this.leafs[index];
const splitHorizontally = random(1, 2) === 1;
if (splitHorizontally && leaf.h >= 10) {
const splitAt = Math.floor(leaf.h / 2) + random(-1, 1);
newLeafs.push({
x: leaf.x,
y: leaf.y,
w: leaf.w,
h: splitAt,
});
newLeafs.push({
x: leaf.x,
y: leaf.y + splitAt,
w: leaf.w,
h: leaf.h - splitAt,
});
} else if (leaf.w >= 10) {
const splitAt = Math.floor(leaf.w / 2) + random(-1, 1);
newLeafs.push({
x: leaf.x,
y: leaf.y,
w: splitAt,
h: leaf.h,
});
newLeafs.push({
x: leaf.x + splitAt,
y: leaf.y,
w: leaf.w - splitAt,
h: leaf.h,
});
} else {
newLeafs.push({
x: leaf.x,
y: leaf.y,
w: leaf.w,
h: leaf.h,
});
}
}
this.leafs = newLeafs;
// const indexes: number[] = Object.keys(newLeafs)
// .map((i) => Number(i))
// .sort()
// .reverse();
// for (const index of indexes) {
// const newLeaf = newLeafs[index];
// if (newLeaf) {
// this.leafs.splice(Number(index), 1, ...newLeaf);
// }
// }
}
}
private createRooms() {
for (const leaf of this.leafs) {
for (let i = leaf.x; i < leaf.x + leaf.w - 1; i++) {
for (let j = leaf.y; j < leaf.y + leaf.h - 1; j++) {
this.map[i][j] = 1;
}
}
}
// let retryCount = 0;
// const maxRetry = 100;
// for (let i = 0; i < this.roomCount; i++) {
// const w = random(3, 5);
// const h = random(3, 5);
// const x = random(1, MAP_WIDTH - w - 2);
// const y = random(1, MAP_HEIGHT - h - 2);
// if (this.isRoomOverlapsOther({ x, y, w, h })) {
// if (retryCount > maxRetry) {
// return;
// } else {
// retryCount++;
// i--;
// continue;
// }
// } else {
// retryCount = 0;
// }
// this.rooms.push({ x, y, w, h });
// for (let cx = 0; cx < w; cx++) {
// for (let cy = 0; cy < h; cy++) {
// this.map[x + cx][y + cy] = 1;
// }
// }
// }
}
private connectRooms() {
let cellToConnectA = { x: 0, y: 0 };
let cellToConnectB = { x: 0, y: 0 };
let distance = MAP_WIDTH * MAP_HEIGHT;
let connectionHash = "";
const connectionHashes: string[] = []; // TODO: should be a tree or a linked list
const getConnectionHash = (roomA: Room, roomB: Room) =>
[this.rooms.indexOf(roomA), this.rooms.indexOf(roomB)].sort().join(".");
for (const roomA of this.rooms) {
distance = MAP_WIDTH * MAP_HEIGHT;
for (const roomB of this.rooms) {
if (roomA === roomB) continue;
const hash = getConnectionHash(roomA, roomB);
if (connectionHashes.includes(hash)) continue;
const cellA = {
x: random(roomA.x + 1, roomA.x + roomA.w - 2),
y: random(roomA.y + 1, roomA.y + roomA.h - 2),
};
const cellB = {
x: random(roomB.x + 1, roomB.x + roomB.w - 2),
y: random(roomB.y + 1, roomB.y + roomB.h - 2),
};
const roomDistance = Math.sqrt(
Math.pow(cellB.x - cellA.x, 2) + Math.pow(cellB.y - cellA.y, 2)
);
if (roomDistance < distance) {
cellToConnectA = cellA;
cellToConnectB = cellB;
connectionHash = hash;
distance = roomDistance;
}
}
connectionHashes.push(connectionHash);
this.connectCells(cellToConnectA, cellToConnectB);
}
}
private connectCells(cellA: Cell, cellB: Cell) {
let x = cellA.x;
let y = cellA.y;
let dx = cellB.x - x;
let dy = cellB.y - y;
let sx = 0;
let sy = 0;
let goHorizontal = Math.abs(dx) > Math.abs(dy) ? true : false;
while (x !== cellB.x || y !== cellB.y) {
dx = cellB.x - x;
dy = cellB.y - y;
sx = goHorizontal ? (dx > 0 ? 1 : -1) : 0;
sy = goHorizontal ? 0 : dy > 0 ? 1 : -1;
x += sx;
y += sy;
this.map[x][y] = 1;
if (goHorizontal && x == cellB.x) {
goHorizontal = false;
} else if (!goHorizontal && y === cellB.y) {
goHorizontal = true;
}
}
}
private isRoomOverlapsOther(roomA: Room): boolean {
for (const roomB of this.rooms) {
if (
roomA.x - 1 < roomB.x + roomB.w &&
roomA.x + roomA.w + 2 > roomB.x &&
roomA.y - 1 < roomB.y + roomB.h &&
roomA.y + roomA.h + 1 > roomB.y
) {
return true;
}
}
return false;
}
}

View file

@ -1,149 +0,0 @@
import { random } from "../dice";
import { Room, Cell, MAP_HEIGHT, MAP_WIDTH } from "./shared";
export class TombGenerator {
roomCount: number = random(4, 8);
map: number[][] = [];
rooms: Room[] = [];
constructor() {
this.prepareMap();
this.createRooms();
this.connectRooms();
}
at(x: number, y: number): number {
if (x < 0 || x >= MAP_WIDTH || y < 0 || y >= MAP_HEIGHT) {
return 0;
} else {
return this.map[x][y];
}
}
private prepareMap() {
this.map = [];
for (let i = 0; i < MAP_WIDTH; i++) {
this.map.push([]);
for (let j = 0; j < MAP_HEIGHT; j++) {
this.map[i].push(0);
}
}
}
private createRooms() {
let retryCount = 0;
const maxRetry = 100;
for (let i = 0; i < this.roomCount; i++) {
const w = random(3, 5);
const h = random(3, 5);
const x = random(1, MAP_WIDTH - w - 2);
const y = random(1, MAP_HEIGHT - h - 2);
if (this.isRoomOverlapsOther({ x, y, w, h })) {
if (retryCount > maxRetry) {
return;
} else {
retryCount++;
i--;
continue;
}
} else {
retryCount = 0;
}
this.rooms.push({ x, y, w, h });
for (let cx = 0; cx < w; cx++) {
for (let cy = 0; cy < h; cy++) {
this.map[x + cx][y + cy] = 1;
}
}
}
}
private connectRooms() {
let cellToConnectA = { x: 0, y: 0 };
let cellToConnectB = { x: 0, y: 0 };
let distance = MAP_WIDTH * MAP_HEIGHT;
let connectionHash = "";
const connectionHashes: string[] = []; // TODO: should be a tree or a linked list
const getConnectionHash = (roomA: Room, roomB: Room) =>
[this.rooms.indexOf(roomA), this.rooms.indexOf(roomB)].sort().join(".");
for (const roomA of this.rooms) {
distance = MAP_WIDTH * MAP_HEIGHT;
for (const roomB of this.rooms) {
if (roomA === roomB) continue;
const hash = getConnectionHash(roomA, roomB);
if (connectionHashes.includes(hash)) continue;
const cellA = {
x: random(roomA.x + 1, roomA.x + roomA.w - 2),
y: random(roomA.y + 1, roomA.y + roomA.h - 2),
};
const cellB = {
x: random(roomB.x + 1, roomB.x + roomB.w - 2),
y: random(roomB.y + 1, roomB.y + roomB.h - 2),
};
const roomDistance = Math.sqrt(
Math.pow(cellB.x - cellA.x, 2) + Math.pow(cellB.y - cellA.y, 2)
);
if (roomDistance < distance) {
cellToConnectA = cellA;
cellToConnectB = cellB;
connectionHash = hash;
distance = roomDistance;
}
}
connectionHashes.push(connectionHash);
this.connectCells(cellToConnectA, cellToConnectB);
}
}
private connectCells(cellA: Cell, cellB: Cell) {
let x = cellA.x;
let y = cellA.y;
let dx = cellB.x - x;
let dy = cellB.y - y;
let sx = 0;
let sy = 0;
let goHorizontal = Math.abs(dx) > Math.abs(dy) ? true : false;
while (x !== cellB.x || y !== cellB.y) {
dx = cellB.x - x;
dy = cellB.y - y;
sx = goHorizontal ? (dx > 0 ? 1 : -1) : 0;
sy = goHorizontal ? 0 : dy > 0 ? 1 : -1;
x += sx;
y += sy;
this.map[x][y] = 1;
if (goHorizontal && x == cellB.x) {
goHorizontal = false;
} else if (!goHorizontal && y === cellB.y) {
goHorizontal = true;
}
}
}
private isRoomOverlapsOther(roomA: Room): boolean {
for (const roomB of this.rooms) {
if (
roomA.x - 1 < roomB.x + roomB.w &&
roomA.x + roomA.w + 2 > roomB.x &&
roomA.y - 1 < roomB.y + roomB.h &&
roomA.y + roomA.h + 1 > roomB.y
) {
return true;
}
}
return false;
}
}

View file

@ -1,10 +1,14 @@
import { ButtonComponent } from "obsidian";
import { SoloToolkitView as View } from "./index";
import { PluginApp } from "../main";
import { clickToCopy } from "../utils";
import { clickToCopy, generateMap } from "../utils";
import { MapBlueprint } from "src/utils/mapgen/shared";
import { drawMap } from "src/utils/mapgen/draw";
const MAX_REMEMBER_SIZE = 20;
export class MapView {
view: View;
maps: MapBlueprint[] = [];
mapBtnsEl: HTMLElement;
mapResultsEl: HTMLElement;
@ -19,9 +23,8 @@ export class MapView {
this.mapBtnsEl = this.view.tabViewEl.createDiv("map-buttons");
this.mapBtnsEl.empty();
this.createMapBtn("Tomb");
this.createMapBtn("Room");
this.createMapBtn("Cave");
this.createMapBtn("Town");
if (!this.view.isMobile) {
this.mapResultsEl = this.view.tabViewEl.createDiv("map-results");
@ -35,17 +38,22 @@ export class MapView {
this.mapResultsEl.empty();
}
addResult(type: string, value: string, immediate = false) {
addResult(map: MapBlueprint, immediate = false) {
const elClass = ["map-result"];
if (immediate) elClass.push("nofade");
const el = this.mapResultsEl.createEl("a", { cls: elClass.join(" ") });
el.onclick = clickToCopy(value);
const typeEl = el.createSpan("map-result-type");
typeEl.setText(type);
// TODO:
// el.onclick = clickToCopy(value);
const valueEl = el.createSpan("map-result-value");
valueEl.setText(value);
const canvasEl = el.createEl("canvas");
canvasEl.width = 1000;
canvasEl.height = 1000;
canvasEl.style.width = "300px";
canvasEl.style.height = "300px";
const ctx = canvasEl.getContext("2d");
if (ctx) drawMap(ctx, map);
}
createMapBtn(type: string) {
@ -53,25 +61,18 @@ export class MapView {
.setButtonText(type)
.setTooltip(`Generate a ${type.toLowerCase()}`)
.onClick(() => {
const app = this.view.app as PluginApp;
if (app?.commands?.executeCommandById) {
app.commands.executeCommandById(
`solo-rpg-toolkit:generate-${type.toLowerCase()}`
);
}
// const value = generateMap(type);
// this.maps.push([type, value]);
// this.addResult(type, value);
const value = generateMap(type);
this.maps.push(value);
this.addResult(value);
});
}
repopulateResults() {
// while (this.maps.length > MAX_REMEMBER_SIZE) {
// this.maps.shift();
// }
// for (const map of this.maps) {
// const [type, value] = map;
// this.addResult(type, value, true);
// }
while (this.maps.length > MAX_REMEMBER_SIZE) {
this.maps.shift();
}
for (const map of this.maps) {
this.addResult(map, true);
}
}
}