├── src
├── vite-env.d.ts
├── game
│ ├── Wall.ts
│ ├── Floor.ts
│ ├── index.ts
│ ├── tests
│ │ ├── map.spec.ts
│ │ ├── cargo.spec.ts
│ │ └── game.spec.ts
│ ├── collisionDetection.ts
│ ├── position.ts
│ ├── player.ts
│ ├── map.ts
│ ├── game.ts
│ ├── placePoint.ts
│ ├── move.ts
│ ├── cargo.ts
│ └── gameData.ts
├── assets
│ ├── wall.png
│ ├── cargo.png
│ ├── empty.png
│ ├── floor.png
│ ├── keeper.png
│ ├── target.png
│ ├── cargo_on_target.png
│ ├── keeper_on_target.png
│ └── vue.svg
├── utils
│ └── id.ts
├── style.css
├── main.ts
├── components
│ ├── Wall.vue
│ ├── Empty.vue
│ ├── Floor.vue
│ ├── mapEdit
│ │ ├── Cargos.vue
│ │ ├── PlacePoints.vue
│ │ ├── EditElement.vue
│ │ ├── Keeper.vue
│ │ ├── Cargo.vue
│ │ ├── PlacePoint.vue
│ │ ├── MapDataDisplay.vue
│ │ ├── EditElementView.vue
│ │ └── MapBlock.vue
│ ├── Cargos.vue
│ ├── PlacePoints.vue
│ ├── PlacePoint.vue
│ ├── Cargo.vue
│ ├── Map.vue
│ └── Player.vue
├── composables
│ ├── mapEdit
│ │ ├── collectMapBlock.ts
│ │ ├── editElement.ts
│ │ ├── tests
│ │ │ ├── cargo.spec.ts
│ │ │ └── placePoint.spec.ts
│ │ ├── tile.ts
│ │ ├── keeper.ts
│ │ ├── cargo.ts
│ │ ├── placePoint.ts
│ │ └── map.ts
│ └── position.ts
├── App.vue
├── router
│ └── index.ts
└── view
│ ├── Game.vue
│ └── MapEdit.vue
├── .vscode
└── extensions.json
├── postcss.config.js
├── README.md
├── vite.config.ts
├── tailwind.config.js
├── tsconfig.node.json
├── .gitignore
├── index.html
├── package.json
├── tsconfig.json
├── public
└── vite.svg
└── pnpm-lock.yaml
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/game/Wall.ts:
--------------------------------------------------------------------------------
1 | export class Wall {
2 | public name: string = "Wall";
3 | }
4 |
--------------------------------------------------------------------------------
/src/game/Floor.ts:
--------------------------------------------------------------------------------
1 | export class Floor {
2 | public name: string = "Floor";
3 | }
4 |
--------------------------------------------------------------------------------
/src/assets/wall.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cuixiaorui/sokoban-vue3/HEAD/src/assets/wall.png
--------------------------------------------------------------------------------
/src/utils/id.ts:
--------------------------------------------------------------------------------
1 | let id = 0;
2 |
3 | export function generateId() {
4 | return id++;
5 | }
6 |
--------------------------------------------------------------------------------
/src/assets/cargo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cuixiaorui/sokoban-vue3/HEAD/src/assets/cargo.png
--------------------------------------------------------------------------------
/src/assets/empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cuixiaorui/sokoban-vue3/HEAD/src/assets/empty.png
--------------------------------------------------------------------------------
/src/assets/floor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cuixiaorui/sokoban-vue3/HEAD/src/assets/floor.png
--------------------------------------------------------------------------------
/src/assets/keeper.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cuixiaorui/sokoban-vue3/HEAD/src/assets/keeper.png
--------------------------------------------------------------------------------
/src/assets/target.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cuixiaorui/sokoban-vue3/HEAD/src/assets/target.png
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
3 | }
4 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/src/assets/cargo_on_target.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cuixiaorui/sokoban-vue3/HEAD/src/assets/cargo_on_target.png
--------------------------------------------------------------------------------
/src/assets/keeper_on_target.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cuixiaorui/sokoban-vue3/HEAD/src/assets/keeper_on_target.png
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 |
6 | body{
7 | background: black;
8 | }
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # sokoban-vue3
2 | sokoban game by vue3
3 |
4 |
5 |
6 | 1. 代码大全
7 | 2. 代码整洁之道
8 | 3. 重构
9 |
10 |
11 | ## 功能
12 |
13 | -[ ] 撤回
14 |
15 |
--------------------------------------------------------------------------------
/src/game/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./game";
2 | export * from "./player";
3 | export * from "./map"
4 | export * from "./cargo"
5 | export * from "./placePoint"
6 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 | import "./style.css";
3 | import App from "./App.vue";
4 | import { router } from "./router";
5 |
6 | createApp(App).use(router).mount("#app");
7 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [vue()],
7 | })
8 |
--------------------------------------------------------------------------------
/src/components/Wall.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
![]()
4 |
5 |
6 |
7 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: [
4 | "./index.html",
5 | "./src/**/*.{vue,js,ts,jsx,tsx}",
6 | ],
7 | theme: {
8 | extend: {},
9 | },
10 | plugins: [],
11 | }
--------------------------------------------------------------------------------
/src/components/Empty.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
![]()
4 |
5 |
6 |
7 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/components/Floor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
![]()
4 |
5 |
6 |
7 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/mapEdit/Cargos.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/src/game/tests/map.spec.ts:
--------------------------------------------------------------------------------
1 | import { it, expect, describe } from "vitest";
2 | import { createMap, getMap, setupMap } from "../map";
3 | import { Floor } from "../Floor";
4 | import { Wall } from "../Wall";
5 |
6 | describe("Map", () => {
7 | it("should init map ", () => {
8 | setupMap(createMap([[1, 2]]));
9 |
10 | expect(getMap().data).toEqual([[new Wall(), new Floor()]]);
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + Vue + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/components/Cargos.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/composables/mapEdit/collectMapBlock.ts:
--------------------------------------------------------------------------------
1 | let isCollect = false;
2 |
3 | export function useCollectMapBlock() {
4 | function start() {
5 | isCollect = true;
6 | }
7 |
8 | function stop() {
9 | isCollect = false;
10 | }
11 |
12 | function collect(fn: () => void) {
13 | if (!isCollect) return;
14 | fn && fn();
15 | }
16 |
17 | return {
18 | start,
19 | stop,
20 | collect,
21 | };
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/mapEdit/PlacePoints.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/composables/mapEdit/editElement.ts:
--------------------------------------------------------------------------------
1 | import { ref } from "vue";
2 | import { TileType } from "./tile";
3 |
4 | export interface EditElement {
5 | title: string;
6 | type: TileType | "cargo" | "placePoint" | "keeper";
7 | imgSrc: string
8 | }
9 |
10 |
11 | export const currentSelectedEditElement = ref();
12 |
13 | export function setCurrentSelectedElement(element: EditElement) {
14 | currentSelectedEditElement.value = element;
15 | }
16 |
--------------------------------------------------------------------------------
/src/assets/vue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/PlacePoints.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/composables/mapEdit/tests/cargo.spec.ts:
--------------------------------------------------------------------------------
1 | import { it, expect, describe, beforeEach } from "vitest";
2 | import { useCargo } from "../cargo";
3 |
4 | describe("map edit cargo", () => {
5 | beforeEach(() => {
6 | const { reset } = useCargo();
7 |
8 | reset();
9 | });
10 | it("should remove cargo", () => {
11 | const { removeCargo, addCargo, cargos } = useCargo();
12 |
13 | const cargo = addCargo(1, 1);
14 |
15 | removeCargo(cargo);
16 |
17 | expect(cargos.length).toBe(0);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/game/collisionDetection.ts:
--------------------------------------------------------------------------------
1 | import { getCargoByPosition } from "./cargo";
2 | import { getElementByPosition } from "./map";
3 | import { type Position } from "./position";
4 |
5 | export function collisionWall(position: Position) {
6 | const element = getElementByPosition(position);
7 |
8 | if (element.name === "Wall") {
9 | return true;
10 | }
11 |
12 | return false;
13 | }
14 |
15 | export function collisionCargo(position: Position) {
16 | const cargo = getCargoByPosition(position);
17 |
18 | return !!cargo;
19 | }
20 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
28 |
--------------------------------------------------------------------------------
/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHashHistory } from "vue-router";
2 | import Game from "../view/Game.vue";
3 | import MapEdit from "../view/MapEdit.vue";
4 |
5 | export const router = createRouter({
6 | history: createWebHashHistory(),
7 | routes: [
8 | {
9 | path: "/",
10 | redirect: "Game",
11 | },
12 |
13 | {
14 | name: "Game",
15 | path: "/game",
16 | component: Game,
17 | },
18 | {
19 | path: "/mapEdit",
20 | component: MapEdit,
21 | },
22 | ],
23 | });
24 |
--------------------------------------------------------------------------------
/src/composables/position.ts:
--------------------------------------------------------------------------------
1 | import { computed } from "vue";
2 |
3 | const STEP = 32;
4 |
5 | interface Position {
6 | x: number;
7 | y: number;
8 | }
9 |
10 | export function usePosition(position: Position) {
11 | const top = computed(() => {
12 | return position.y * STEP;
13 | });
14 |
15 | const left = computed(() => {
16 | return position.x * STEP;
17 | });
18 |
19 | const positionStyle = computed(() => {
20 | return [`top:${top.value}px`, `left:${left.value}px`];
21 | });
22 |
23 | return {
24 | positionStyle,
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/PlacePoint.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
![]()
4 |
5 |
6 |
7 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/composables/mapEdit/tests/placePoint.spec.ts:
--------------------------------------------------------------------------------
1 | import { it, expect, describe, beforeEach } from "vitest";
2 | import { usePlacePoint } from "../placePoint";
3 |
4 | describe("map edit placePoint", () => {
5 | beforeEach(() => {
6 | const { reset } = usePlacePoint();
7 |
8 | reset();
9 | });
10 |
11 | it("should remove the place point", () => {
12 | const { removePlacePoint, addPlacePoint, placePoints } = usePlacePoint();
13 |
14 | const placePoint = addPlacePoint(1, 1);
15 |
16 | removePlacePoint(placePoint);
17 |
18 | expect(placePoints.length).toBe(0);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/components/mapEdit/EditElement.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
![]()
4 |
{{ editElement.title }}
5 |
6 |
7 |
8 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sokoban-vue3",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vue-tsc && vite build",
9 | "preview": "vite preview",
10 | "test": "vitest"
11 | },
12 | "dependencies": {
13 | "vue": "^3.3.4",
14 | "vue-router": "^4.2.4"
15 | },
16 | "devDependencies": {
17 | "@vitejs/plugin-vue": "^4.2.3",
18 | "autoprefixer": "^10.4.15",
19 | "postcss": "^8.4.28",
20 | "tailwindcss": "^3.3.3",
21 | "typescript": "^5.0.2",
22 | "vite": "^4.4.5",
23 | "vitest": "^0.34.2",
24 | "vue-tsc": "^1.8.5"
25 | }
26 | }
--------------------------------------------------------------------------------
/src/game/position.ts:
--------------------------------------------------------------------------------
1 | export interface Position {
2 | x: number;
3 | y: number;
4 | }
5 |
6 | export function calcLeftPosition(position: Position) {
7 | return {
8 | x: position.x - 1,
9 | y: position.y,
10 | };
11 | }
12 |
13 | export function calcRightPosition(position: Position) {
14 | return {
15 | x: position.x + 1,
16 | y: position.y,
17 | };
18 | }
19 |
20 | export function calcUpPosition(position: Position) {
21 | return {
22 | x: position.x,
23 | y: position.y - 1,
24 | };
25 | }
26 | export function calcDownPosition(position: Position) {
27 | return {
28 | x: position.x,
29 | y: position.y + 1,
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/Cargo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
![]()
7 |
8 |
9 |
10 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/components/mapEdit/Keeper.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
![]()
9 |
10 |
11 |
12 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/components/mapEdit/Cargo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
![]()
4 |
5 |
6 |
7 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/composables/mapEdit/tile.ts:
--------------------------------------------------------------------------------
1 | import floorImg from "../../assets/floor.png";
2 | import WallImg from "../../assets/wall.png";
3 | import { type EditElement } from "./editElement";
4 |
5 | export enum TileType {
6 | wall = 1,
7 | floor = 2,
8 | }
9 |
10 | interface TileEditElement extends EditElement {
11 | type: TileType;
12 | }
13 |
14 | export const tileEditElements: TileEditElement[] = [
15 | {
16 | imgSrc: floorImg,
17 | title: "地板",
18 | type: TileType.floor,
19 | },
20 | {
21 | imgSrc: WallImg,
22 | title: "墙",
23 | type: TileType.wall,
24 | },
25 | ];
26 |
27 | export function isTile(element: EditElement): element is TileEditElement {
28 | return element.type === TileType.floor || element.type === TileType.wall;
29 | }
30 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "preserve",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/src/composables/mapEdit/keeper.ts:
--------------------------------------------------------------------------------
1 | import { reactive, ref } from "vue";
2 | import { type EditElement } from "./editElement";
3 | import keeperImgSrc from "../../assets/keeper.png";
4 |
5 | export interface Keeper {
6 | x: number;
7 | y: number;
8 | }
9 |
10 | export const keeperEditElement: EditElement = {
11 | title: "玩家",
12 | type: "keeper",
13 | imgSrc: keeperImgSrc,
14 | };
15 |
16 | const isShowKeeper = ref(false);
17 | const keeper = reactive({
18 | x: 0,
19 | y: 0,
20 | });
21 |
22 | export function useKeeper() {
23 | function showKeeper() {
24 | isShowKeeper.value = true;
25 | }
26 |
27 | function hideKeeper() {
28 | isShowKeeper.value = false;
29 | }
30 | return {
31 | keeper,
32 | isShowKeeper,
33 | showKeeper,
34 | hideKeeper,
35 | };
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/mapEdit/PlacePoint.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
![]()
4 |
5 |
6 |
7 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/view/Game.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
31 |
--------------------------------------------------------------------------------
/src/game/player.ts:
--------------------------------------------------------------------------------
1 | import { Direction, move } from "./move";
2 |
3 | export interface Player {
4 | x: number;
5 | y: number;
6 | }
7 |
8 | let _player: Player;
9 | export function setupPlayer(player: Player) {
10 | _player = player;
11 | }
12 |
13 | export function createPlayer({ x, y }: { x: number; y: number }): Player {
14 | return {
15 | x,
16 | y,
17 | };
18 | }
19 |
20 | export function moveLeft() {
21 | move(Direction.left);
22 | }
23 |
24 | export function moveRight() {
25 | move(Direction.right);
26 | }
27 |
28 | export function moveUp() {
29 | move(Direction.up);
30 | }
31 |
32 | export function moveDown() {
33 | move(Direction.down);
34 | }
35 |
36 | export function getPlayer() {
37 | return _player;
38 | }
39 |
40 | export function initPlayer(player: Player) {
41 | _player = player;
42 | }
43 |
44 | export function updatePlayer({ x, y }: { x: number; y: number }) {
45 | _player.x = x;
46 | _player.y = y;
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/Map.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
31 |
32 |
41 |
--------------------------------------------------------------------------------
/src/game/tests/cargo.spec.ts:
--------------------------------------------------------------------------------
1 | import { it, expect, describe } from "vitest";
2 | import { createPlacePoints, setupPlacePoints } from "../placePoint";
3 | import {
4 | handleHitPlacePoint,
5 | createCargos,
6 | setupCargos,
7 | } from "../cargo";
8 |
9 | describe("cargo", () => {
10 | it("should on target place point when hit place point", () => {
11 | setupPlacePoints(createPlacePoints([{ x: 1, y: 1 }]));
12 | const cargos = createCargos([{ x: 1, y: 1 }]);
13 | setupCargos(createCargos([{ x: 1, y: 1 }]));
14 | const cargo = cargos[0]
15 | handleHitPlacePoint(cargo);
16 |
17 | expect(cargo.onTargetPoint).toBeTruthy();
18 | });
19 |
20 | it("should reset on target point statue", () => {
21 | setupPlacePoints(createPlacePoints([{ x: 1, y: 1 }]));
22 | const cargos = createCargos([{ x: 1, y: 1 }]);
23 | setupCargos(createCargos([{ x: 1, y: 1 }]));
24 |
25 | const cargo = cargos[0]
26 | handleHitPlacePoint(cargo);
27 | cargo.x += 1;
28 | handleHitPlacePoint(cargo);
29 |
30 | expect(cargo.onTargetPoint).toBeFalsy();
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/src/game/map.ts:
--------------------------------------------------------------------------------
1 | import { Floor } from "./Floor";
2 | import { Wall } from "./Wall";
3 | import { type Position } from "./position";
4 |
5 | export type Element = Floor | Wall;
6 |
7 | export interface Map {
8 | data: Element[][];
9 | }
10 |
11 | let _map: Map;
12 | export function setupMap(map: Map) {
13 | _map = map;
14 | }
15 |
16 | export function createMap(rawMap: number[][]): Map {
17 | return {
18 | data: convertRawMap(rawMap),
19 | };
20 | }
21 |
22 | export function getMap() {
23 | return _map;
24 | }
25 | export function updateMap(rawMap: number[][]) { _map.data = convertRawMap(rawMap);
26 | } export function getElementByPosition(position: Position) {
27 | return _map.data[position.y][position.x];
28 | }
29 |
30 | // 0. 空白
31 | // 1. 墙
32 | // 2. 地板
33 | function convertRawMap(rawMap: number[][]) {
34 | const newMap: Element[][] = [];
35 | rawMap.forEach((row, i) => {
36 | newMap[i] = [];
37 | row.forEach((col, j) => {
38 | if (col === 1) {
39 | newMap[i][j] = new Wall();
40 | } else if (col === 2) {
41 | newMap[i][j] = new Floor();
42 | }
43 | }); });
44 |
45 | return newMap;
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/mapEdit/MapDataDisplay.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/composables/mapEdit/cargo.ts:
--------------------------------------------------------------------------------
1 | import { reactive } from "vue";
2 | import { generateId } from "../../utils/id";
3 | import { type EditElement } from "./editElement";
4 | import cargoSrc from "../../assets/cargo.png";
5 |
6 | export interface Cargo {
7 | id: number;
8 | x: number;
9 | y: number;
10 | }
11 |
12 | export const cargoEditElement: EditElement = {
13 | type: "cargo",
14 | title: "箱子",
15 | imgSrc: cargoSrc,
16 | };
17 |
18 | const cargos = reactive([]);
19 |
20 | export function useCargo() {
21 | function createCargo(x: number, y: number) {
22 | return {
23 | x,
24 | y,
25 | id: generateId(),
26 | };
27 | }
28 |
29 | function addCargo(x: number, y: number) {
30 | const cargo = createCargo(x, y);
31 | cargos.push(cargo);
32 | return cargo;
33 | }
34 |
35 | function removeCargo(cargo: Cargo) {
36 |
37 | const cargoIndex = cargos.findIndex((c) => c.id === cargo.id);
38 |
39 | if (cargoIndex !== -1) cargos.splice(cargoIndex, 1);
40 | }
41 |
42 | function reset () {
43 | cargos.length = 0
44 | }
45 |
46 | return {
47 | cargos,
48 | reset,
49 | addCargo,
50 | removeCargo,
51 | };
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/Player.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
![]()
4 |
5 |
6 |
7 |
56 |
--------------------------------------------------------------------------------
/src/composables/mapEdit/placePoint.ts:
--------------------------------------------------------------------------------
1 | import { reactive } from "vue";
2 | import { generateId } from "../../utils/id";
3 | import { type EditElement } from "./editElement";
4 | import placePointImgSrc from "../../assets/target.png";
5 |
6 | export interface PlacePoint {
7 | x: number;
8 | y: number;
9 | id: number;
10 | }
11 |
12 | export const placePointEditElement: EditElement = {
13 | type: "placePoint",
14 | title: "放置点",
15 | imgSrc: placePointImgSrc,
16 | };
17 |
18 | const placePoints = reactive([]);
19 |
20 | export function usePlacePoint() {
21 | function createPlacePoint(x: number, y: number) {
22 | return {
23 | x,
24 | y,
25 | id: generateId(),
26 | };
27 | }
28 | function addPlacePoint(x: number, y: number) {
29 | const placePoint = createPlacePoint(x, y);
30 | placePoints.push(placePoint);
31 |
32 | return placePoint;
33 | }
34 |
35 | function removePlacePoint(placePoint: PlacePoint) {
36 | const index = placePoints.findIndex((p) => p.id === placePoint.id);
37 |
38 | if (index !== -1) placePoints.splice(index, 1);
39 | }
40 |
41 | function reset() {
42 | placePoints.length = 0;
43 | }
44 |
45 | return {
46 | reset,
47 | removePlacePoint,
48 | addPlacePoint,
49 | placePoints,
50 | };
51 | }
52 |
--------------------------------------------------------------------------------
/src/game/game.ts:
--------------------------------------------------------------------------------
1 | import { isAllCargoOnTarget, updateCargos } from "./cargo";
2 | import { updateMap } from "./map";
3 | import { updatePlacePoints } from "./placePoint";
4 | import { updatePlayer } from "./player";
5 | import { gameDatas } from "./gameData";
6 |
7 | export interface Game {
8 | currentLevel: number;
9 | isWin: boolean;
10 | }
11 |
12 | let _game: Game;
13 | export function setupGame(game: Game) {
14 | _game = game;
15 | }
16 |
17 | export function createGame(config: { level: number }): Game {
18 | return {
19 | currentLevel: config.level,
20 | isWin: false,
21 | };
22 | }
23 |
24 | export function getGame() {
25 | return _game;
26 | }
27 |
28 | export function startGame() {
29 | updateGame();
30 | }
31 |
32 | export function startNextLevel() {
33 | _game.currentLevel += 1;
34 | resetGame();
35 | updateGame();
36 | }
37 |
38 | function resetGame() {
39 | _game.isWin = false;
40 | }
41 |
42 | function updateGame() {
43 | const { player, map, cargos, placePoints } =
44 | gameDatas[_game.currentLevel - 1];
45 |
46 | // playerConfig
47 | // 上面的这些数据 都是 config
48 | updatePlayer(player);
49 | updateMap(map);
50 | updateCargos(cargos);
51 | updatePlacePoints(placePoints);
52 | }
53 |
54 | export function judgeGameWin() {
55 | _game.isWin = isAllCargoOnTarget();
56 | }
57 |
--------------------------------------------------------------------------------
/src/game/placePoint.ts:
--------------------------------------------------------------------------------
1 | import { generateId } from "../utils/id";
2 | import { Position } from "./position";
3 |
4 | export interface PlacePoint {
5 | x: number;
6 | y: number;
7 | id: number;
8 | }
9 |
10 | let _placePoints: PlacePoint[] = []
11 |
12 | export function setupPlacePoints(placePoint: PlacePoint[]) {
13 | _placePoints = placePoint;
14 | }
15 |
16 | export function createPlacePoints(
17 | config: { x: number; y: number }[]
18 | ): PlacePoint[] {
19 | return config.map(({ x, y }) => {
20 | return createPlacePoint(x, y);
21 | });
22 | }
23 |
24 | function createPlacePoint(x: number, y: number): PlacePoint {
25 | return {
26 | x,
27 | y,
28 | id: generateId(),
29 | };
30 | }
31 |
32 | export function updatePlacePoints(rawPlacePoints: { x: number; y: number }[]) {
33 | cleanAllPlacePoints();
34 |
35 | // add new placePoint
36 | rawPlacePoints.forEach(({ x, y }) => {
37 | _placePoints.push(createPlacePoint(x, y));
38 | });
39 | }
40 |
41 | export function getPointByPosition(position: Position) {
42 | return _placePoints.find((point) => {
43 | return point.x === position.x && point.y === position.y;
44 | });
45 | }
46 |
47 | function cleanAllPlacePoints() {
48 | _placePoints.length = 0;
49 | }
50 |
51 | export function getPlacePoints() {
52 | return _placePoints;
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/mapEdit/EditElementView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
currentEditElement: {{ selectedEditElement }}
15 |
16 |
17 |
18 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/game/move.ts:
--------------------------------------------------------------------------------
1 | import { getCargoByPosition, handleHitPlacePoint } from "./cargo";
2 | import { judgeGameWin } from "./game";
3 | import { getPlayer } from "./player";
4 | import { collisionCargo, collisionWall } from "./collisionDetection";
5 | import {
6 | Position,
7 | calcDownPosition,
8 | calcLeftPosition,
9 | calcRightPosition,
10 | calcUpPosition,
11 | } from "./position";
12 |
13 | export enum Direction {
14 | left = "left",
15 | right = "right",
16 | up = "up",
17 | down = "down",
18 | }
19 |
20 | const map: Record<
21 | string,
22 | {
23 | calcPositionFn: (position: Position) => Position;
24 | dirPropName: "x" | "y";
25 | dir: -1 | 1;
26 | }
27 | > = {
28 | left: { calcPositionFn: calcLeftPosition, dirPropName: "x", dir: -1 },
29 | right: { calcPositionFn: calcRightPosition, dirPropName: "x", dir: 1 },
30 | up: { calcPositionFn: calcUpPosition, dirPropName: "y", dir: -1 },
31 | down: { calcPositionFn: calcDownPosition, dirPropName: "y", dir: 1 },
32 | };
33 |
34 | export function move(direction: Direction) {
35 | // 1. 箱子推到放置点上
36 | // 2. 箱子检测是不是碰到了箱子
37 | const player = getPlayer();
38 | const { calcPositionFn, dirPropName, dir } = map[direction];
39 | if (collisionWall(calcPositionFn(player))) return;
40 |
41 | const cargo = getCargoByPosition(calcPositionFn(player));
42 |
43 | if (cargo) {
44 | // 看看是不是墙
45 | if (collisionWall(calcPositionFn(cargo))) {
46 | return;
47 | }
48 |
49 | // 看看是不是箱子
50 | if (collisionCargo(calcPositionFn(cargo))) {
51 | return;
52 | }
53 |
54 | cargo[dirPropName] += 1 * dir;
55 |
56 | handleHitPlacePoint(cargo);
57 |
58 | judgeGameWin();
59 | }
60 |
61 | player[dirPropName] += 1 * dir;
62 | }
63 |
--------------------------------------------------------------------------------
/src/game/cargo.ts:
--------------------------------------------------------------------------------
1 | import { type PlacePoint, getPointByPosition } from "./placePoint";
2 | import { type Position } from "./position";
3 | import { generateId } from "../utils/id";
4 |
5 | export interface Cargo {
6 | x: number;
7 | y: number;
8 | id: number;
9 | onTargetPoint?: PlacePoint | undefined;
10 | }
11 |
12 | let _cargos: Cargo[] = [];
13 | export function setupCargos(cargos: Cargo[]) {
14 | _cargos = cargos;
15 | }
16 |
17 | export function createCargos(rawCargos: { x: number; y: number }[]): Cargo[] {
18 | return rawCargos.map((rawCargo) => {
19 | return createCargo(rawCargo.x, rawCargo.y);
20 | });
21 | }
22 |
23 | function createCargo(x: number, y: number): Cargo {
24 | return {
25 | x,
26 | y,
27 | id: generateId(),
28 | };
29 | }
30 |
31 | export function updateCargos(rawCargos: { x: number; y: number }[]) {
32 | cleanAllCargos();
33 |
34 | // add new cargos
35 | rawCargos.forEach((rawCargo) => {
36 | _cargos.push(createCargo(rawCargo.x, rawCargo.y));
37 | });
38 | }
39 |
40 | function cleanAllCargos() {
41 | _cargos.length = 0;
42 | }
43 |
44 | export function handleHitPlacePoint(cargo: Cargo) {
45 | reset(cargo);
46 |
47 | const point = getPointByPosition(cargo);
48 |
49 | if (point) {
50 | cargo.onTargetPoint = point;
51 | }
52 | }
53 |
54 | function reset(cargo: Cargo) {
55 | if (cargo.onTargetPoint) {
56 | cargo.onTargetPoint = undefined;
57 | }
58 | }
59 |
60 | export function getCargos() {
61 | return _cargos;
62 | }
63 |
64 | export function getCargoByPosition(position: Position) {
65 | return _cargos.find((c) => {
66 | return c.x === position.x && c.y === position.y;
67 | });
68 | }
69 |
70 | export function isAllCargoOnTarget() {
71 | return _cargos.every((cargo) => {
72 | return !!cargo.onTargetPoint;
73 | });
74 | }
--------------------------------------------------------------------------------
/src/view/MapEdit.vue:
--------------------------------------------------------------------------------
1 |
2 |
32 |
33 |
34 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/src/composables/mapEdit/map.ts:
--------------------------------------------------------------------------------
1 | import { reactive } from "vue";
2 | import { TileType } from "./tile";
3 | import { generateId } from "../../utils/id";
4 |
5 | type MapType = TileType | undefined;
6 | export interface MapBlock {
7 | type: MapType;
8 | x: number;
9 | y: number;
10 | id: number;
11 | }
12 |
13 | interface Map {
14 | row: number;
15 | col: number;
16 | data: MapBlock[][];
17 | }
18 |
19 | function createMapBlock(x: number, y: number, type?: MapType): MapBlock {
20 | return {
21 | type,
22 | x,
23 | y,
24 | id: generateId(),
25 | };
26 | }
27 |
28 | const map = reactive