first add

This commit is contained in:
zhangkun9038@dingtalk.com 2025-02-01 12:43:55 +08:00
commit 5e108532d8
23 changed files with 10090 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
dist
-r

4466
chat.md Normal file

File diff suppressed because it is too large Load Diff

0
chatWithDSLocal.md Normal file
View File

6
err.txt Normal file
View 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
View File

@ -0,0 +1,5 @@
import { GameBox } from './core/GameBox.js';
const game = new GameBox();
game.init();

32
index.html Normal file
View 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

File diff suppressed because it is too large Load Diff

26
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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();
}
}

View 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;

View 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
View 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
View File

@ -0,0 +1,5 @@
declare interface IGameBoxConfig {
aspectRatio: number;
backgroundColor: string;
borderColor: string;
}

View 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
View 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
View 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'
};