first add
This commit is contained in:
commit
5e108532d8
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
-r
|
0
chatWithDSLocal.md
Normal file
0
chatWithDSLocal.md
Normal file
6
err.txt
Normal file
6
err.txt
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
> mobile-game@1.0.0 build
|
||||
> tsc
|
||||
|
||||
src/core/GameBox.ts(35,29): error TS2339: Property 'element' does not exist on type '{ aspectRatio: number; backgroundColor: string; borderColor: string; }'.
|
||||
src/index.ts(8,3): error TS2353: Object literal may only specify known properties, and 'element' does not exist in type '{ aspectRatio: number; backgroundColor: string; borderColor: string; }'.
|
5
game.js
Normal file
5
game.js
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
import { GameBox } from './core/GameBox.js';
|
||||
|
||||
const game = new GameBox();
|
||||
game.init();
|
32
index.html
Normal file
32
index.html
Normal file
@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0,
|
||||
maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||
<title>Mobile Game</title>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
#gameContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="gameContainer"></div>
|
||||
<script type="module" src="/dist/index.js"></script>
|
||||
</body>
|
||||
</html>
|
4864
package-lock.json
generated
Normal file
4864
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
package.json
Normal file
26
package.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "mobile-game",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "",
|
||||
"main": "matrix-demo.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "tsc",
|
||||
"dev": "browser-sync start --server --directory --files '**/*.css, **/*.html, **/*.js, **/*.ts' --no-notify"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.12.0",
|
||||
"nodemon": "^3.1.0",
|
||||
"serve": "^14.0.0",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.3",
|
||||
"webpack": "^5.97.1",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"minigame-api-typings": "^2.4.0",
|
||||
"wechat-miniprogram-webpack-plugin": "^1.0.0"
|
||||
}
|
||||
}
|
15
project.config.json
Normal file
15
project.config.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"miniprogramRoot": "./",
|
||||
"appid": "your-appid",
|
||||
"projectname": "mobile-game",
|
||||
"description": "My mobile game",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"es6": true,
|
||||
"postcss": true,
|
||||
"minified": true
|
||||
},
|
||||
"compileType": "miniprogram",
|
||||
"libVersion": "2.30.0",
|
||||
"condition": {}
|
||||
}
|
6
src/config.ts
Normal file
6
src/config.ts
Normal file
@ -0,0 +1,6 @@
|
||||
// src/config.ts
|
||||
export const GameConfig = {
|
||||
aspectRatio: 2 / 1,
|
||||
backgroundColor: '#f2e4a9',
|
||||
borderColor: '#23a5a5'
|
||||
};
|
50
src/core/ControlBall.ts
Normal file
50
src/core/ControlBall.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { Vector2 } from './Vector2.js';
|
||||
import { ITouchData } from './types/TouchData.js';
|
||||
|
||||
export class ControlBall {
|
||||
private position: Vector2;
|
||||
private velocity: Vector2 = new Vector2();
|
||||
private radius: number;
|
||||
private maxSpeed: number;
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
|
||||
constructor(
|
||||
canvas: HTMLCanvasElement,
|
||||
private gameWidth: number,
|
||||
private gameHeight: number,
|
||||
private baseSpeed: number
|
||||
) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d')!;
|
||||
this.radius = Math.min(gameWidth, gameHeight) * 0.05;
|
||||
this.maxSpeed = baseSpeed * 1.2;
|
||||
this.position = new Vector2(gameWidth / 2, gameHeight / 2);
|
||||
}
|
||||
|
||||
update(touchData: ITouchData): void {
|
||||
// 计算速度
|
||||
const speed = this.baseSpeed * touchData.speedFactor;
|
||||
this.velocity = touchData.normalizedPosition.scale(speed);
|
||||
|
||||
// 更新位置
|
||||
this.position = this.position.add(this.velocity);
|
||||
|
||||
// 边界检测
|
||||
this.position.x = Math.max(this.radius, Math.min(this.gameWidth - this.radius, this.position.x));
|
||||
this.position.y = Math.max(this.radius, Math.min(this.gameHeight - this.radius, this.position.y));
|
||||
}
|
||||
|
||||
draw(): void {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2);
|
||||
this.ctx.fillStyle = '#ff6b6b';
|
||||
this.ctx.fill();
|
||||
this.ctx.closePath();
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.position = new Vector2(this.gameWidth / 2, this.gameHeight / 2);
|
||||
this.velocity = new Vector2();
|
||||
}
|
||||
}
|
298
src/core/GameBox.ts
Normal file
298
src/core/GameBox.ts
Normal file
@ -0,0 +1,298 @@
|
||||
|
||||
// src/core/GameBox.ts
|
||||
export class GameBox {
|
||||
public destroy(): void {
|
||||
// 移除事件监听器
|
||||
window.removeEventListener('resize', () => this.updateSize());
|
||||
window.removeEventListener('orientationchange', () => {
|
||||
setTimeout(() => this.updateSize(), 100);
|
||||
});
|
||||
|
||||
// 移除 DOM 元素
|
||||
if (this.element && this.element.parentNode) {
|
||||
this.element.parentNode.removeChild(this.element);
|
||||
}
|
||||
}
|
||||
public aspectRatio: number;
|
||||
public backgroundColor: string;
|
||||
public borderColor: string;
|
||||
|
||||
private container: HTMLElement;
|
||||
private element: HTMLElement;
|
||||
private ballElement: HTMLElement;
|
||||
private touchRingElement: HTMLElement;
|
||||
//
|
||||
constructor(config: {
|
||||
aspectRatio: number,
|
||||
backgroundColor: string,
|
||||
borderColor: string,
|
||||
element: HTMLElement
|
||||
}) {
|
||||
this.aspectRatio = config.aspectRatio;
|
||||
this.backgroundColor = config.backgroundColor;
|
||||
this.borderColor = config.borderColor;
|
||||
|
||||
|
||||
this.container = config.element;
|
||||
this.element = this.createBoxElement();
|
||||
this.ballElement = this.createBall();
|
||||
this.touchRingElement = this.createTouchRing();
|
||||
|
||||
this.container.style.display = 'flex';
|
||||
this.container.style.justifyContent = 'center';
|
||||
this.container.style.alignItems = 'center';
|
||||
this.container.style.width = '100vw';
|
||||
this.container.style.height = '100vh';
|
||||
this.container.style.overflow = 'hidden';
|
||||
|
||||
this.bindEvents();
|
||||
this.updateSize();
|
||||
}
|
||||
|
||||
// 删除 createContainer 方法
|
||||
|
||||
|
||||
private createBoxElement(): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
element.className = 'game-box';
|
||||
element.style.boxSizing = 'border-box';
|
||||
element.style.backgroundSize = 'auto 100%';
|
||||
element.style.backgroundRepeat = 'no-repeat';
|
||||
element.style.backgroundPosition = 'center center';
|
||||
element.style.display = 'flex';
|
||||
element.style.justifyContent = 'center';
|
||||
element.style.alignItems = 'center';
|
||||
|
||||
this.applyBoxStyle(element);
|
||||
this.container.appendChild(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
private applyBoxStyle(element: HTMLElement): void {
|
||||
// Create grid background with random colors
|
||||
const gridSize = 200; // Size of each grid square (doubled from previous)
|
||||
const gridColor = 'rgba(0, 0, 0, 0.1)';
|
||||
|
||||
// Create canvas for random grid colors
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
canvas.width = 2000;
|
||||
canvas.height = 2000;
|
||||
|
||||
// Create gradient grid pattern
|
||||
const baseColor = 200; // Base gray level
|
||||
const colorStep = 40; // Increased color step for larger grids
|
||||
|
||||
for (let x = 0; x < canvas.width; x += gridSize) {
|
||||
for (let y = 0; y < canvas.height; y += gridSize) {
|
||||
// Calculate gradient based on grid position
|
||||
const xCycle = Math.floor((x / gridSize) % 5);
|
||||
const yCycle = Math.floor((y / gridSize) % 5);
|
||||
const colorValue = baseColor - (xCycle + yCycle) * colorStep;
|
||||
|
||||
// Ensure color stays within valid range
|
||||
const finalColor = Math.max(0, Math.min(255, colorValue));
|
||||
|
||||
ctx.fillStyle = `rgba(${finalColor},${finalColor},${finalColor},1)`;
|
||||
ctx.fillRect(x, y, gridSize, gridSize);
|
||||
|
||||
// Add subtle border between cells
|
||||
ctx.strokeStyle = 'rgba(0,0,0,0.1)';
|
||||
ctx.strokeRect(x, y, gridSize, gridSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Add grid lines and coordinates
|
||||
const gridImage = canvas.toDataURL();
|
||||
element.style.background = `
|
||||
url('${gridImage}'),
|
||||
linear-gradient(to right, ${gridColor} 1px, transparent 1px),
|
||||
linear-gradient(to bottom, ${gridColor} 1px, transparent 1px),
|
||||
${this.backgroundColor}
|
||||
`;
|
||||
element.style.backgroundSize = `${gridSize}px ${gridSize}px`;
|
||||
element.style.border = `1px solid ${this.borderColor}`;
|
||||
|
||||
// Add coordinate labels
|
||||
this.addGridCoordinates(element);
|
||||
}
|
||||
|
||||
private addGridCoordinates(element: HTMLElement): void {
|
||||
const gridSize = 200;
|
||||
const width = 2000; // Use canvas width for consistent coordinates
|
||||
const height = 2000; // Use canvas height for consistent coordinates
|
||||
|
||||
// X-axis labels
|
||||
for (let x = 0; x < width; x += gridSize) {
|
||||
const label = document.createElement('div');
|
||||
label.style.position = 'absolute';
|
||||
label.style.left = `${x}px`;
|
||||
label.style.bottom = '-20px';
|
||||
label.style.color = this.borderColor;
|
||||
label.style.fontSize = '12px';
|
||||
label.innerText = `${x / gridSize}`;
|
||||
element.appendChild(label);
|
||||
}
|
||||
|
||||
// Y-axis labels
|
||||
for (let y = 0; y < height; y += gridSize) {
|
||||
const label = document.createElement('div');
|
||||
label.style.position = 'absolute';
|
||||
label.style.top = `${y}px`;
|
||||
label.style.left = '-20px';
|
||||
label.style.color = this.borderColor;
|
||||
label.style.fontSize = '12px';
|
||||
label.innerText = `${y / gridSize}`;
|
||||
element.appendChild(label);
|
||||
}
|
||||
}
|
||||
|
||||
private createBall(): HTMLElement {
|
||||
const ball = document.createElement('div');
|
||||
ball.style.position = 'absolute';
|
||||
ball.style.width = '40px';
|
||||
ball.style.height = '40px';
|
||||
ball.style.borderRadius = '50%';
|
||||
ball.style.backgroundColor = 'red';
|
||||
ball.style.transform = 'translate(-50%, -50%)';
|
||||
ball.style.left = '50%';
|
||||
ball.style.top = '50%';
|
||||
|
||||
this.element.appendChild(ball);
|
||||
return ball;
|
||||
}
|
||||
|
||||
private createTouchRing(): HTMLElement {
|
||||
const ring = document.createElement('div');
|
||||
ring.style.position = 'absolute';
|
||||
ring.style.width = '100px';
|
||||
ring.style.height = '100px';
|
||||
ring.style.borderRadius = '50%';
|
||||
ring.style.border = '2px solid rgba(255,255,255,0.5)';
|
||||
ring.style.left = '20%';
|
||||
ring.style.bottom = '30%';
|
||||
ring.style.background = 'rgba(245, 222, 179, 0.5)'; // wheat color with 50% opacity
|
||||
ring.style.transform = 'translate(-50%, 50%)';
|
||||
ring.style.pointerEvents = 'auto';
|
||||
this.element.appendChild(ring);
|
||||
return ring;
|
||||
}
|
||||
|
||||
private bindEvents(): void {
|
||||
window.addEventListener('resize', () => this.updateSize());
|
||||
window.addEventListener('orientationchange', () => {
|
||||
setTimeout(() => this.updateSize(), 100);
|
||||
});
|
||||
|
||||
// Add touch event listeners
|
||||
this.touchRingElement.addEventListener('touchstart', this.handleTouchStart.bind(this));
|
||||
this.touchRingElement.addEventListener('touchmove', this.handleTouchMove.bind(this));
|
||||
this.touchRingElement.addEventListener('touchend', this.handleTouchEnd.bind(this));
|
||||
}
|
||||
|
||||
private handleTouchStart(event: TouchEvent): void {
|
||||
event.preventDefault();
|
||||
this.isTouching = true;
|
||||
this.updateBallPosition(event.touches[0]);
|
||||
// Start continuous movement
|
||||
this.moveInterval = window.setInterval(() => {
|
||||
if (this.isTouching) {
|
||||
this.lastBackgroundPosition.x += this.currentDirection.x;
|
||||
this.lastBackgroundPosition.y += this.currentDirection.y;
|
||||
this.element.style.backgroundPosition = `${this.lastBackgroundPosition.x}% ${this.lastBackgroundPosition.y}%`;
|
||||
}
|
||||
}, 16); // ~60fps
|
||||
}
|
||||
|
||||
private handleTouchMove(event: TouchEvent): void {
|
||||
event.preventDefault();
|
||||
this.updateBallPosition(event.touches[0]);
|
||||
}
|
||||
|
||||
private handleTouchEnd(): void {
|
||||
this.isTouching = false;
|
||||
if (this.moveInterval) {
|
||||
window.clearInterval(this.moveInterval);
|
||||
this.moveInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
private lastBackgroundPosition = { x: 50, y: 50 }; // Track background position
|
||||
private isTouching = false;
|
||||
private currentDirection = { x: 0, y: 0 };
|
||||
private moveInterval: number | null = null;
|
||||
|
||||
private updateBallPosition(touch: Touch): void {
|
||||
const rect = this.touchRingElement.getBoundingClientRect();
|
||||
const ringCenterX = rect.left + rect.width / 2;
|
||||
const ringCenterY = rect.top + rect.height / 2;
|
||||
const ringRadius = rect.width / 2;
|
||||
|
||||
// Get touch position relative to ring center
|
||||
const touchX = touch.clientX - ringCenterX;
|
||||
const touchY = touch.clientY - ringCenterY;
|
||||
|
||||
// Calculate distance from center
|
||||
const distance = Math.sqrt(touchX * touchX + touchY * touchY);
|
||||
const normalizedDistance = Math.min(distance / ringRadius, 1);
|
||||
|
||||
// Calculate speed factor
|
||||
let speedFactor = 0;
|
||||
if (normalizedDistance > 0.5 && normalizedDistance <= 1) {
|
||||
// Map distance from 0.5-1 to 0.8-1.2 in 5% increments
|
||||
const steps = (normalizedDistance - 0.5) / 0.1;
|
||||
speedFactor = 0.8 + Math.floor(steps) * 0.05;
|
||||
}
|
||||
|
||||
// Calculate direction vector
|
||||
const directionX = touchX / distance;
|
||||
const directionY = touchY / distance;
|
||||
|
||||
// Normalize direction vector to maintain equal speed in both axes
|
||||
const magnitude = Math.sqrt(directionX * directionX + directionY * directionY);
|
||||
const normalizedX = directionX / magnitude;
|
||||
const normalizedY = directionY / magnitude;
|
||||
|
||||
// Update current direction for continuous movement
|
||||
this.currentDirection = {
|
||||
x: -normalizedX * speedFactor * 0.5, // Keep horizontal speed unchanged
|
||||
y: -normalizedY * speedFactor * 1.0 // Double vertical speed
|
||||
};
|
||||
|
||||
// Update background position based on last position
|
||||
this.lastBackgroundPosition.x += this.currentDirection.x;
|
||||
this.lastBackgroundPosition.y += this.currentDirection.y;
|
||||
this.element.style.backgroundPosition = `${this.lastBackgroundPosition.x}% ${this.lastBackgroundPosition.y}%`;
|
||||
}
|
||||
|
||||
public updateSize(): void {
|
||||
const visualWidth = window.innerWidth;
|
||||
const visualHeight = window.innerHeight;
|
||||
const windowRatio = visualWidth / visualHeight;
|
||||
|
||||
let boxWidth, boxHeight;
|
||||
|
||||
if (windowRatio > this.aspectRatio) {
|
||||
// Window is wider than aspect ratio - fit to height
|
||||
boxHeight = visualHeight;
|
||||
boxWidth = boxHeight * this.aspectRatio;
|
||||
} else {
|
||||
// Window is taller than aspect ratio - fit to width
|
||||
boxWidth = visualWidth;
|
||||
boxHeight = boxWidth / this.aspectRatio;
|
||||
}
|
||||
|
||||
// Set box dimensions
|
||||
this.element.style.width = `${boxWidth}px`;
|
||||
this.element.style.height = `${boxHeight}px`;
|
||||
this.element.style.margin = 'auto'; // Use flexbox centering
|
||||
|
||||
// Update touch ring position
|
||||
// this.touchRingElement.style.left = '20px';
|
||||
// this.touchRingElement.style.bottom = '20px';
|
||||
}
|
||||
|
||||
public setBackground(imageUrl: string): void {
|
||||
this.element.style.backgroundImage = `url('${imageUrl}')`;
|
||||
}
|
||||
}
|
32
src/core/GameInput.ts
Normal file
32
src/core/GameInput.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { ITouchData } from './types/TouchData.js';
|
||||
import { Vector2 } from './Vector2.js';
|
||||
import { Direction } from './types/Direction.js';
|
||||
|
||||
// 声明自定义事件类型
|
||||
declare global {
|
||||
interface HTMLElementEventMap {
|
||||
'touchChange': CustomEvent<ITouchData>;
|
||||
}
|
||||
}
|
||||
|
||||
export class GameInput {
|
||||
private currentInput: ITouchData = {
|
||||
direction: Direction.None,
|
||||
speedFactor: 1,
|
||||
normalizedPosition: new Vector2(0, 0)
|
||||
};
|
||||
|
||||
constructor(private touchRing: HTMLElement) {
|
||||
this.initEvents();
|
||||
}
|
||||
|
||||
private initEvents(): void {
|
||||
this.touchRing.addEventListener('touchChange', (event: CustomEvent<ITouchData>) => {
|
||||
this.currentInput = event.detail;
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentInput(): ITouchData {
|
||||
return this.currentInput;
|
||||
}
|
||||
}
|
18
src/core/TouchBox.ts
Normal file
18
src/core/TouchBox.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Vector2 } from './Vector2.js';
|
||||
export enum Direction {
|
||||
Up = 'up',
|
||||
Down = 'down',
|
||||
Left = 'left',
|
||||
Right = 'right',
|
||||
UpLeft = 'up-left',
|
||||
UpRight = 'up-right',
|
||||
DownLeft = 'down-left',
|
||||
DownRight = 'down-right',
|
||||
None = 'none'
|
||||
}
|
||||
|
||||
export interface ITouchData {
|
||||
direction: Direction;
|
||||
speedFactor: number; // 0.8 - 1.2
|
||||
normalizedPosition: Vector2; // 归一化后的位置
|
||||
}
|
107
src/core/TouchRing.ts
Normal file
107
src/core/TouchRing.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { Vector2 } from './Vector2';
|
||||
import { ITouchData, Direction } from './types/TouchData';
|
||||
|
||||
export class TouchRing {
|
||||
private element!: HTMLDivElement; // 使用明确赋值断言
|
||||
private radius: number;
|
||||
private center: Vector2;
|
||||
private isTouching = false;
|
||||
|
||||
constructor(private parent: HTMLElement, private size: number) {
|
||||
this.element = document.createElement('div');
|
||||
this.radius = size / 2;
|
||||
this.center = new Vector2(this.radius, this.radius);
|
||||
this.initElement();
|
||||
this.initEvents();
|
||||
}
|
||||
|
||||
private initElement(): void {
|
||||
|
||||
this.element.style.width = `${this.size}px`;
|
||||
this.element.style.height = `${this.size}px`;
|
||||
this.element.style.borderRadius = '50%';
|
||||
this.element.style.backgroundColor = '#5ab5da';
|
||||
this.element.style.opacity = '0.7';
|
||||
this.element.style.position = 'absolute';
|
||||
this.element.style.left = '5%';
|
||||
this.element.style.bottom = '5%';
|
||||
this.element.style.touchAction = 'none';
|
||||
this.parent.appendChild(this.element);
|
||||
}
|
||||
|
||||
private initEvents(): void {
|
||||
this.element.addEventListener('touchstart', this.handleTouchStart.bind(this));
|
||||
this.element.addEventListener('touchmove', this.handleTouchMove.bind(this));
|
||||
this.element.addEventListener('touchend', this.handleTouchEnd.bind(this));
|
||||
}
|
||||
|
||||
private handleTouchStart(event: TouchEvent): void {
|
||||
event.preventDefault();
|
||||
this.isTouching = true;
|
||||
}
|
||||
|
||||
private handleTouchMove(event: TouchEvent): void {
|
||||
if (!this.isTouching) return;
|
||||
event.preventDefault();
|
||||
|
||||
const touch = event.touches[0];
|
||||
const rect = this.element.getBoundingClientRect();
|
||||
const touchPos = new Vector2(
|
||||
touch.clientX - rect.left,
|
||||
touch.clientY - rect.top
|
||||
);
|
||||
|
||||
const touchData = this.calculateTouchData(touchPos);
|
||||
this.dispatchTouchEvent(touchData);
|
||||
}
|
||||
|
||||
private handleTouchEnd(): void {
|
||||
this.isTouching = false;
|
||||
this.dispatchTouchEvent({
|
||||
direction: Direction.None,
|
||||
speedFactor: 1,
|
||||
normalizedPosition: new Vector2()
|
||||
});
|
||||
}
|
||||
|
||||
private calculateTouchData(touchPos: Vector2): ITouchData {
|
||||
const offset = touchPos.subtract(this.center);
|
||||
const distance = offset.length();
|
||||
const clampedDistance = Math.min(distance, this.radius);
|
||||
|
||||
// 计算速度因子
|
||||
const speedFactor = 0.8 + (clampedDistance / this.radius) * 0.4;
|
||||
|
||||
// 计算方向
|
||||
const angle = Math.atan2(offset.y, offset.x);
|
||||
const direction = this.getDirectionFromAngle(angle);
|
||||
|
||||
// 归一化位置
|
||||
const normalizedPosition = offset.scale(1 / this.radius);
|
||||
|
||||
return {
|
||||
direction,
|
||||
speedFactor,
|
||||
normalizedPosition
|
||||
};
|
||||
}
|
||||
|
||||
private getDirectionFromAngle(angle: number): Direction {
|
||||
const pi = Math.PI;
|
||||
if (angle >= -pi / 8 && angle < pi / 8) return Direction.Right;
|
||||
if (angle >= pi / 8 && angle < 3 * pi / 8) return Direction.UpRight;
|
||||
if (angle >= 3 * pi / 8 && angle < 5 * pi / 8) return Direction.Up;
|
||||
if (angle >= 5 * pi / 8 && angle < 7 * pi / 8) return Direction.UpLeft;
|
||||
if (angle >= 7 * pi / 8 || angle < -7 * pi / 8) return Direction.Left;
|
||||
if (angle >= -7 * pi / 8 && angle < -5 * pi / 8) return Direction.DownLeft;
|
||||
if (angle >= -5 * pi / 8 && angle < -3 * pi / 8) return Direction.Down;
|
||||
if (angle >= -3 * pi / 8 && angle < -pi / 8) return Direction.DownRight;
|
||||
return Direction.None;
|
||||
}
|
||||
|
||||
private dispatchTouchEvent(data: ITouchData): void {
|
||||
const event = new CustomEvent('touchChange', { detail: data });
|
||||
this.element.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
45
src/core/Vector2.ts
Normal file
45
src/core/Vector2.ts
Normal file
@ -0,0 +1,45 @@
|
||||
export class Vector2 {
|
||||
constructor(public x: number = 0, public y: number = 0) { }
|
||||
|
||||
// 向量加法
|
||||
add(v: Vector2): Vector2 {
|
||||
return new Vector2(this.x + v.x, this.y + v.y);
|
||||
}
|
||||
|
||||
// 向量减法
|
||||
subtract(v: Vector2): Vector2 {
|
||||
return new Vector2(this.x - v.x, this.y - v.y);
|
||||
}
|
||||
|
||||
// 向量缩放
|
||||
scale(scalar: number): Vector2 {
|
||||
return new Vector2(this.x * scalar, this.y * scalar);
|
||||
}
|
||||
|
||||
// 向量归一化
|
||||
normalize(): Vector2 {
|
||||
const len = this.length();
|
||||
return len > 0 ? this.scale(1 / len) : new Vector2();
|
||||
}
|
||||
|
||||
// 向量长度
|
||||
length(): number {
|
||||
return Math.sqrt(this.x * this.x + this.y * this.y);
|
||||
}
|
||||
|
||||
// 计算两点间距离
|
||||
distanceTo(v: Vector2): number {
|
||||
return this.subtract(v).length();
|
||||
}
|
||||
|
||||
// 克隆向量
|
||||
clone(): Vector2 {
|
||||
return new Vector2(this.x, this.y);
|
||||
}
|
||||
|
||||
// 限制向量长度
|
||||
clampLength(maxLength: number): Vector2 {
|
||||
const len = this.length();
|
||||
return len > maxLength ? this.normalize().scale(maxLength) : this.clone();
|
||||
}
|
||||
}
|
14
src/core/types/Direction.ts
Normal file
14
src/core/types/Direction.ts
Normal file
@ -0,0 +1,14 @@
|
||||
|
||||
export enum Direction {
|
||||
None = 'none',
|
||||
Up = 'up',
|
||||
Down = 'down',
|
||||
Left = 'left',
|
||||
Right = 'right',
|
||||
UpLeft = 'up-left',
|
||||
UpRight = 'up-right',
|
||||
DownLeft = 'down-left',
|
||||
DownRight = 'down-right'
|
||||
}
|
||||
|
||||
export type DirectionType = keyof typeof Direction;
|
18
src/core/types/TouchData.ts
Normal file
18
src/core/types/TouchData.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Vector2 } from '../Vector2.js';
|
||||
export enum Direction {
|
||||
Up = 'up',
|
||||
Down = 'down',
|
||||
Left = 'left',
|
||||
Right = 'right',
|
||||
UpLeft = 'up-left',
|
||||
UpRight = 'up-right',
|
||||
DownLeft = 'down-left',
|
||||
DownRight = 'down-right',
|
||||
None = 'none'
|
||||
}
|
||||
|
||||
export interface ITouchData {
|
||||
direction: Direction;
|
||||
speedFactor: number; // 0.8 - 1.2
|
||||
normalizedPosition: Vector2; // 归一化后的位置
|
||||
}
|
14
src/index.ts
Normal file
14
src/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { GameBox } from './core/GameBox.js';
|
||||
|
||||
const gameContainer = document.getElementById('gameContainer') || document.body;
|
||||
const gameBox = new GameBox({
|
||||
aspectRatio: 16 / 9,
|
||||
backgroundColor: '#ffffff',
|
||||
borderColor: '#000000',
|
||||
element: gameContainer
|
||||
});
|
||||
|
||||
// 处理窗口关闭
|
||||
window.addEventListener('beforeunload', () => {
|
||||
gameBox.destroy();
|
||||
});
|
5
src/types/game.d.ts
vendored
Normal file
5
src/types/game.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
declare interface IGameBoxConfig {
|
||||
aspectRatio: number;
|
||||
backgroundColor: string;
|
||||
borderColor: string;
|
||||
}
|
9
src/utils/ScreenUtiles.ts
Normal file
9
src/utils/ScreenUtiles.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export class ScreenAdapter {
|
||||
static get viewportRatio(): number {
|
||||
return window.innerWidth / window.innerHeight;
|
||||
}
|
||||
|
||||
static isLandscape(): boolean {
|
||||
return window.matchMedia("(orientation: landscape)").matches;
|
||||
}
|
||||
}
|
30
tsconfig.json
Normal file
30
tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"module": "ESNext",
|
||||
"outDir": "./dist",
|
||||
"rootDirs": [
|
||||
"./src",
|
||||
"./dist"
|
||||
],
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"*": [
|
||||
"./src/*",
|
||||
"./node_modules/*"
|
||||
]
|
||||
},
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
27
webpack.config.js
Normal file
27
webpack.config.js
Normal file
@ -0,0 +1,27 @@
|
||||
|
||||
const path = require('path');
|
||||
const WechatMiniprogramWebpackPlugin = require('wechat-miniprogram-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
entry: './game.js',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'game.js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js']
|
||||
},
|
||||
plugins: [
|
||||
new WechatMiniprogramWebpackPlugin()
|
||||
],
|
||||
mode: 'production'
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user